Windows toolchain matrix
Windows toolchains and what is verified on each.
---xtc supports multiple Windows toolchains. This document records which compile and test combinations are exercised on Windows hosts.
Native Windows 11 ARM64 verification (2026-06, real ARM64 host)
The following was run on a REAL Windows 11 on ARM64 machine (native,
no x86 emulation; PROCESSOR_ARCHITECTURE=ARM64), not on the older
santorini x64-emulation layer. Four compilers are present:
| Compiler | Version | Target | Native ARM64 |
|---|---|---|---|
| clang.exe | 20.1.7 | aarch64-pc-windows-msvc | yes |
| MSVC cl.exe (VS 17 / 2022) | 19.44.35227 (MSVC 14.44.35207) | ARM64 | yes |
| MSVC cl.exe (VS 18 / 2026) | 19.50.35726 (MSVC 14.50.35717) | ARM64 | yes |
| MinGW-W64 gcc.exe (Strawberry) | 13.2.0 | x86_64-w64-mingw32 | no (x64) |
Note: the Strawberry gcc is an x86_64 cross, not ARM64; it produces x64 PE objects (still a valid Windows portability compile-check, run under the ARM64 host’s x64 emulation).
Windows ARM64 fcontext assembly – RUNTIME VERIFIED
The AArch64 ELF fcontext (src/os/asm/fctx_aarch64_aapcs.S) is guarded
#if defined(__aarch64__) && !defined(_WIN32) and therefore compiles
to an EMPTY object on Windows ARM64 (where _WIN32 is defined). Two
Windows-ARM64 siblings were added and verified on the real host:
src/os/asm/fctx_aarch64_ms_pe.S– GNU-assembler (.S) syntax for Clang’s integrated assembler, guarded#if defined(__aarch64__) && defined(_WIN32). Drops the ELF-only directives (.type/.size/.section .note.GNU-stack).src/os/asm/fctx_aarch64_ms_pe.asm– armasm64 (MASM) syntax for the MSVC toolchain, the sibling offctx_x86_64_ms_pe.asm.
Both implement the identical 160-byte frame (d8-d15, x19-x28, x29, x30) and the identical calling contract, and both DELIBERATELY leave x18 untouched – on Windows ARM64 x18 is the reserved platform (TEB) register and must never be clobbered by application code.
A standalone round-trip harness (transfer-arg threading; callee-saved x19-x28 + d8-d15 survive a switch; multi-resume) was run NATIVELY on the ARM64 host:
| Build path | Assembler | C compiler | Harness result |
|---|---|---|---|
.S + clang inline-asm harness |
clang IAS | clang 20 | PASS |
.asm + churn() harness (VS17) |
armasm64 14.44 | cl 19.44 | PASS |
.asm + churn() harness (VS18) |
armasm64 14.50 | cl 19.50 | PASS |
CROSS: .asm obj + clang harness |
armasm64 14.44 | clang 20 | PASS |
CROSS: .S obj + cl harness |
clang IAS | cl 19.44 | PASS |
The two cross-link cases prove the .S and .asm objects are
ABI-interchangeable: clang’s strict register-pinned harness passes
against the armasm64 object, and MSVC’s harness passes against the
clang object.
The clang harness pins sentinel values directly into x19-x28 and
d8-d15 (single inline-asm block doing load + bl __xtc_jump_fcontext
- readback, so only the fiber switch can preserve them). The MSVC
harness – MSVC has no inline asm for ARM64 – verifies the same
property indirectly but soundly: the scheduler calls a deep recursive
churn()between resumes to spill garbage through the entire callee-saved register file, and the fiber’s long-lived locals must survive.
CAVEAT: this verifies the fcontext ASSEMBLY in isolation. The coro
substrate coro_fctx.c is itself guarded #if !defined(_WIN32), so
the ACTIVE coroutine substrate on Windows remains coro_winfiber.c
(Win32 fibers). The Windows-ARM64 fcontext asm is ready and correct
should the fctx path ever be enabled on Windows, but it is not on the
live Windows coroutine path today.
IOCP / SChannel / trivial TU compile-check – NATIVELY COMPILED
Each file was compiled -c against the in-tree headers (src/inc,
__has_include-optional xtc_config.h) on the real ARM64 host. A
full library link was not attempted (no configure run); the goal was
a clean native compile against the Win32 / SSPI SDK headers.
| File | clang 20 | cl 19.44 (VS17) | cl 19.50 (VS18) | gcc 13.2 (x64) |
|---|---|---|---|---|
src/io/io_iocp.c |
OK | OK | OK | OK |
src/io/tls_schannel.c |
OK | OK | OK | OK |
src/xtc_version.c |
OK | OK | OK | OK |
src/os/os_alloc.c |
OK | OK | OK | OK |
All compiled with zero warnings (clang/gcc -Wall -Wextra; MSVC
/W4). No portability bugs were found in io_iocp.c or
tls_schannel.c on the native ARM64 compile – nothing needed fixing.
MSVC requires /std:c11 /experimental:c11atomics (the headers pull in
C11 _Atomic via os_atomic.h); clang and gcc accept -std=c11
directly. This CONFIRMS the headers are MSVC-clean on ARM64 (Task C).
What is still NOT verified: io_iocp.c and tls_schannel.c have only
been COMPILED, never RUN. The AFD poll path, cancel/re-arm lifetime,
wakeup ordering, and the SChannel handshake state machine all remain
untested at runtime – see the test plan below.
Earlier santorini matrix (x64 emulation layer)
The table below is from the older santorini build host (Windows 11 ARM64 running the x86_64 emulation layer, MSYS2 toolchains).
| Toolchain | Version | Build | Tests built | Tests pass | Notes |
|---|---|---|---|---|---|
| MinGW64 gcc | 16.1.0 | OK | 50 | 233/233 | Default Windows path; full coverage |
| Clang64 | 22.1.4 | OK | 50 | 48/48 | LLVM clang with MinGW runtime; 3 POSIX-only tests don’t compile |
| MSVC cl.exe | 14.50.35717 | OK | smoke | 5/5 | xtc.lib (45 objs incl. ml64 fcontext); standalone smoke test |
IOCP backend: round-2 native overlapped (RUNTIME-VERIFIED 2026-06)
Update 2026-06 (santorini, Windows 10.0.26200 ARM64, MinGW gcc 13.2.0 x86_64 under x64 emulation): the IOCP backend has now been RUNTIME- verified on a real Windows host. Built libxtc.a with MinGW (-DXTC_IO_BACKEND_IOCP=1, real winpthreads/winsock, no compat shim) and ran the IOCP-exercising munit suites + a standalone file-AIO driver:
- PASS: test_loop, test_task, test_timer, test_waker (4/4), test_io_wakeup (3/3), test_io_lifecycle (3/3), test_net (3/3 – the AFD-poll socket path on real TCP sockets), and a standalone file-AIO round-trip driver (write+fsync+read 8192 bytes, peer fiber ran during the I/O).
- Three real bugs found and fixed this round (see below).
- BY CONTRACT (not a bug): test_io_events / test_io_register / test_io_integration register a POSIX pipe fd; a Windows anonymous pipe is not AFD-pollable, so xtc_io_reg_fd returns XTC_E_INTERNAL. On Windows only SOCKETS are pollable via AFD – these tests encode a POSIX readiness assumption and need socket-based variants or a Windows skip. The m4 test_aio failed only because it uses a /tmp/XXXXXX mkstemp template (a POSIX-ism); the standalone driver using GetTempFileNameA exercises the same path and passes.
Bugs found and fixed by running on the host
- Wakeup did not coalesce at POST time: every xtc_io_wakeup did a
PostQueuedCompletionStatus, so N wakeups queued N completions and
GetQueuedCompletionStatusEx only drained
maxper poll, leaving residue that later polls re-reported (W3_coalesce failed). Fixed with an atomic wakeup_pending flag so at most one completion is in flight (the epoll self-pipe / kqueue EVFILT_USER coalesce the same way); the poll clears it on drain. - File AIO hung on a synchronous handle: a file opened with _open (or CreateFile without FILE_FLAG_OVERLAPPED) completes ReadFile/ WriteFile synchronously and posts NO port completion, so the parked fiber waited forever. Fixed by setting FILE_SKIP_COMPLETION_PORT_ON_SUCCESS on association and finishing the op inline when the call returns TRUE (only ERROR_IO_PENDING is tracked for a port completion).
- blocking.c used POSIX pipe(), which does not link on Windows; added a _pipe()-based wrapper (works under both MinGW and MSVC, no compat-shim dependency).
Original round-2 design (now verified)
src/io/io_iocp.c was rewritten from the round-1 WSAEventSelect +
WaitForMultipleObjects readiness emulation (hard-capped at 64 handles)
to a native completion-port design:
- A single
CreateIoCompletionPortis the only wait primitive;GetQueuedCompletionStatusExdequeues completions in batch. The 64-handle cap is GONE – any number of sockets and file AIOs associate with the one port. - Socket readiness uses the AFD poll fast path the round-2 plan
called for:
\Device\Afdis opened withNtCreateFile, associated with the port, and oneIOCTL_AFD_POLL(NtDeviceIoControlFile) is armed per registered socket and re-armed after each completion (level-triggered, matching the epoll/kqueue contract). This is the wepoll/libuv approach. - The cross-thread wakeup is
PostQueuedCompletionStatuswith completion-keyXTC_IOCP_KEY_WAKEUP. - File AIO (
pread/pwrite) is an overlappedReadFile/WriteFilewhose file HANDLE is associated with the port, so its completion is dequeued like any socket event.fsync/fdatasyncreturnXTC_E_NOSYSand are offloaded (FlushFileBuffers is synchronous). - OVERLAPPED ownership rule: an OVERLAPPED belongs to the kernel
from the moment a request is accepted until its completion is
dequeued; it is never freed or reused while in flight.
Deregistering an armed socket issues
NtCancelIoFileExand defers the free to the (canceled) completion’s reap. Registration nodes are separately heap-allocated with stable addresses so the kernel-held back-pointer never dangles across a table grow or swap-remove.
What is verified on the Linux dev host (this round)
- Cross-compiles clean with mingw-w64 gcc 14.3.0
(
nix-shell -p pkgsCross.mingwW64.buildPackages.gcc),-std=c11 -Wall -Wextra, against<winsock2.h>/<windows.h>/<winternl.h>/<ntstatus.h>/<mswsock.h>. - Links into a PE32+ binary against
-lntdll -lws2_32(NtCreateFile,NtDeviceIoControlFile,NtCancelIoFileEx,WSAIoctl(SIO_BASE_HANDLE),CreateIoCompletionPort,GetQueuedCompletionStatusEx,PostQueuedCompletionStatusall resolve).dist/configure.acnow adds-lntdllto the WindowsLIBS;dist/build_msvc.batlinksntdll.lib. io_common.c,io_net.c, andio_iocp.call cross-compile together with the IOCP backend selected.- The Linux build is unaffected:
io_iocp.cis#if defined(XTC_IO_BACKEND_IOCP)only, so it compiles to an empty TU on Linux; the full C munit suite stays green there.
What is NOT verified (must run on santorini / a Windows host)
The backend has NOT executed on Windows. The following must be
validated before calling it production quality (mirrors the
reviewed-but-untested status of src/io/io_aix.c):
- AFD poll correctness. That
IOCTL_AFD_POLLwith theXTC_AFD_POLL_*event mask reports FD_READ/FD_WRITE/FD_ACCEPT/ FD_CLOSE-equivalent readiness for connected TCP, listening, and UDP sockets, and thatSIO_BASE_HANDLEyields the right base socket through any layered service providers. - Re-arm/level-triggered semantics. That re-arming after each
completion does not busy-loop on a persistently-ready socket and
does not drop edges, i.e. that the m2
io_eventscontract (E1..E7) passes through this backend exactly as it does on epoll. - Cancel/lifetime under churn.
mod_fdanddel_fdon an armed poll: that the canceled completion is reaped exactly once, the dead node is freed exactly once, and no OVERLAPPED is freed while the kernel still owns it (run under Application Verifier / Dr. Memory if available). - Wakeup ordering. That
PostQueuedCompletionStatuswakeups coalesce into oneXTC_IO_WAKEUPevent per poll (m2 W3) and do not race the AFD completions – this is the suspected home of thetest_proc::selective_receiveflake (see KNOWN_ISSUES.md). - File AIO round-trip through the port (the MSVC smoke test already exercised the round-1 hEvent variant; it must be re-run against the port-associated variant).
- The full m2 suite (
test_io_register,test_io_events,test_io_wakeup,test_io_integration) over a tcp-socketpair (test/include/io_pipe_compat.h), since IOCP/AFD work on sockets, not anonymous CRT pipes.
Run on santorini via dist/santorini-matrix.sh; there is no
non-interactive Windows CI job yet, so this is a hand-run gate.
Cross-compile commands used (Linux dev host)
nix-shell -p pkgsCross.mingwW64.buildPackages.gcc
x86_64-w64-mingw32-gcc -c -std=c11 -DXTC_IO_BACKEND_IOCP=1 \
-I<config-dir-with-XTC_IO_BACKEND_IOCP> -Isrc/inc \
-Wall -Wextra -o io_iocp.o src/io/io_iocp.c
# link check (against a stub allocator + drain_wakeup):
x86_64-w64-mingw32-gcc -o drv.exe drv.c io_iocp.o -lntdll -lws2_32
MinGW64 (msys2 mingw64)
The reference Windows toolchain. Configure auto-detects the IOCP backend; no extra flags required.
export PATH=/mingw64/bin:/usr/bin:$PATH
cd /c/scratch/xtc
cd dist && autoreconf -i && cd ..
mkdir -p build_mingw && cd build_mingw
../dist/configure --with-tls=none
make -j4
All 50 test binaries build. The two TLS-handshake tests SKIP because TLS is disabled in this configuration. The remaining 48 report 233 munit assertions, 0 failures.
Clang64 (msys2 clang64)
The clang frontend with the MinGW runtime. Same configure path as
MinGW64 with CC=clang:
export PATH=/c/msys64/clang64/bin:/usr/bin:$PATH
pacman -S --needed mingw-w64-clang-x86_64-clang
cd /c/scratch/xtc/build_clang64
CC=clang ../dist/configure --with-tls=none
make -j4
47 of 50 test binaries compile. The three that fail to link are:
test_net_udp– uses POSIX-onlynanosleepsemantics.test_proc_wait_fd– exercises a Linux-specific eventfd path that hasn’t been ported to Windows IOCP.test_slab_shm– usesmmap(MAP_SHARED)cross-process state that has no Windows equivalent in xtc yet.
The 47 that build pass 48/48 (the 2 TLS handshake cases SKIP, same reason as MinGW64). Tracking the three Windows ports in PLAN.md.
MSVC cl.exe
C:\\Program Files\\Microsoft Visual Studio\\18\\Community provides
cl.exe 14.50 and ml64.exe; ...\\17\\Community provides the VS2022
cl. libxtc builds with either via dist\\build_msvc.bat, run inside
an x64 Native Tools environment (or after calling vcvars64.bat):
set XTC_SRC=C:\\scratch\\xtc
call "...\\VC\\Auxiliary\\Build\\vcvars64.bat"
C:\\scratch\\xtc\\dist\\build_msvc.bat
This assembles fctx_x86_64_ms_pe.asm with ml64, compiles every
src/ translation unit with cl (/std:c11 /experimental:c11atomics),
archives xtc.lib, and builds test\\msvc\\smoke.c. The smoke test
passes on both VS2022 (17) and VS2026 (18): version, strerror, the
Win32 clocks, a slab alloc/free round-trip, an lwlock acquire/release,
SEH fault containment, and native IOCP file AIO (an overlapped
ReadFile/WriteFile round-trip through xtc_aio on an OVERLAPPED handle).
What made the MSVC build work:
- The GAS context-switch asm was ported to MASM
(
fctx_x86_64_ms_pe.asm). __threadwas replaced by the portableXTC_THREAD_LOCAL(__declspec(thread)on MSVC).__attribute__((format))and__attribute__((packed))were wrapped in portable macros (XTC_PRINTF_FMT,XTC_PACK_*).- MSVC lacks winpthreads, so
src/inc/compat/provides Win32 shims for the pthread / semaphore / sched / unistd / sys.time surface the code uses, plus a hand-authoredxtc_config.h. os_time.cgained a Win32 branch (QueryPerformanceCounter / GetSystemTimePreciseAsFileTime).
The munit harness is not used for the MSVC test because its
MUNIT_ARRAY_PARAM(argc + 1) expands to a VLA array parameter that
cl rejects; the standalone smoke test covers the Win32-specific
paths instead. Wiring the full munit suite for MSVC (a
harness-only fix) is the remaining MSVC work.
Unix domain sockets on Windows (COMPILED, NOT RUNTIME-VERIFIED)
Windows 10 (build 17063+) supports AF_UNIX with SOCK_STREAM via
<afunix.h>, so src/io/io_net.c no longer returns XTC_E_NOSYS
for the four UDS entry points on Windows. xtc_net_unix_listen /
xtc_net_unix_dial mirror the POSIX path with DeleteFileA standing
in for the unlink of a stale socket path and WSA_INIT_ONCE() for
Winsock startup.
The credential path differs by design: Windows AF_UNIX has NO
ancillary credential channel (no SO_PEERCRED/LOCAL_PEERCRED
analogue), so xtc_net_unix_send_creds is a plain send and
xtc_net_unix_recv_creds returns the received bytes with uid/gid
reported as 0. Callers that need peer identity on Windows must use a
different mechanism (e.g. a named-pipe GetNamedPipeClientProcessId
path, not yet wired).
Verified on the Linux dev host only: cross-compiles clean with
mingw-w64 against <afunix.h> (-Wall -Wextra). Not yet run on a
Windows host – the listen/dial round-trip and the no-creds contract
must be exercised on santorini, and a test_net_unix Windows case
added, before this is trusted.
Reproducing on santorini
Push a tarball (gitignored junk excluded):
cd $XTC_SRC_ROOT
git ls-files | tar cz --files-from=- > /tmp/xtc-snap.tgz
scp /tmp/xtc-snap.tgz santorini:xtc-snap.tgz
Land it under c:\scratch and extract:
ssh santorini 'cmd /c "move %USERPROFILE%\xtc-snap.tgz c:\scratch\"'
ssh santorini 'cmd /c "C:\msys64\usr\bin\bash.exe -lc \
\"mkdir -p /c/scratch/xtc && cd /c/scratch/xtc && \
tar xzf /c/scratch/xtc-snap.tgz\""'
Run the matrix script (see dist/santorini-matrix.sh).