commit 678aa2c2c1d76c07ae26fecbd5b4c2128564012d
parent 0f32ede0778411171ae700103652e8a02c781697
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 28 May 2026 09:34:31 -0700
driver/env: add Windows host TU; extract env selection to mk/env.mk
windows.c is a full Win32 implementation of the driver env surface --
file I/O via CreateFileW + UTF-8/UTF-16 conversion, dual-mapped execmem
via CreateFileMappingW + MapViewOfFile, a CfreeDbgOs vtable backed by
CreateThread/_beginthreadex + event objects + AddVectoredExceptionHandler
+ SuspendThread-based interrupt with inline CONTEXT marshalling for
x86_64 and aarch64, and a FlsAlloc-backed CfreeJitTls preserving the TLV
thunk's first-field contract. Replaces posix.c, posix_dbg.c, and
jit_tls_posix.c on Windows builds; common.c stays shared.
Hoist all HOST_OS/HOST_ARCH detection and driver/env source selection
into mk/env.mk so the main Makefile only consumes a documented set of
outputs (HOST_OS, HOST_ARCH, DRIVER_ENV_OS_CFLAGS, DRIVER_ENV_SRCS,
HOST_SYSROOT_*FLAGS, HOST_LDLIBS). Wire MinGW/MSYS/Cygwin into env.mk;
on Windows it picks windows.c plus the per-arch icache TU and adds
-lpsapi for EnumProcessModules.
Split env_internal.h into a strictly OS-neutral surface (heap/diag
externs + env_flush_icache) and a new env_posix.h that layers on the
POSIX-shared bits (<ucontext.h>, <signal.h>, <sys/stat.h>, exec_dual
registry, os_* hooks, DBG_INTERRUPT_SIGNO, POSIX vtable singletons).
POSIX TUs include env_posix.h; common.c, icache_*.c, and windows.c
include env_internal.h. No more #ifndef _WIN32 anywhere in the headers.
Diffstat:
17 files changed, 1921 insertions(+), 228 deletions(-)
diff --git a/Makefile b/Makefile
@@ -7,23 +7,11 @@ BUILD_DIR ?= build/release
else
BUILD_DIR ?= build
endif
-SYSROOT = $(shell xcrun --show-sdk-path)
-HOST_UNAME := $(shell uname -s)
-
-# Normalize uname -m to one of {x86_64, aarch64, rv64, unknown}. Used by
-# driver/env to select per-arch TUs (icache flush, uctx marshalling,
-# Linux exec runtime-alias hint) -- see "driver/env source selection"
-# below.
-HOST_ARCH_RAW := $(shell uname -m)
-ifneq ($(filter $(HOST_ARCH_RAW),x86_64 amd64),)
-HOST_ARCH := x86_64
-else ifneq ($(filter $(HOST_ARCH_RAW),aarch64 arm64),)
-HOST_ARCH := aarch64
-else ifneq ($(filter $(HOST_ARCH_RAW),riscv64),)
-HOST_ARCH := rv64
-else
-HOST_ARCH := unknown
-endif
+
+# Host detection, driver/env source selection, host SDK -isysroot wiring.
+# Everything that branches on HOST_OS / HOST_ARCH lives in env.mk; the rest
+# of this Makefile only reads the variables it produces.
+include mk/env.mk
.DEFAULT_GOAL := all
@@ -31,7 +19,7 @@ ifeq ($(RELEASE),1)
HOST_OPTFLAGS ?= -O2
HOST_MODE_CPPFLAGS = -DNDEBUG
HOST_MODE_CFLAGS = -ffunction-sections -fdata-sections
-ifeq ($(HOST_UNAME),Darwin)
+ifeq ($(HOST_OS),darwin)
HOST_MODE_LDFLAGS = -Wl,-dead_strip -Wl,-S
else
HOST_MODE_LDFLAGS = -Wl,--gc-sections -Wl,-S
@@ -49,10 +37,9 @@ UBSAN_OPTIONS ?= halt_on_error=1:print_stacktrace=1
export ASAN_OPTIONS UBSAN_OPTIONS
endif
-# -isysroot lives in its own var so stage/bootstrap recipes can override
-# host SDK handling when cfree is used as the compiler.
-HOST_SYSROOT_CFLAGS = -isysroot $(SYSROOT)
-HOST_SYSROOT_LDFLAGS = -isysroot $(SYSROOT)
+# HOST_SYSROOT_{C,LD}FLAGS come from env.mk: a `-isysroot <path>` pair on
+# Darwin, empty elsewhere. Stage/bootstrap recipes can clear these when
+# cfree itself is the compiler.
CFLAGS_COMMON = $(HOST_OPTFLAGS) $(HOST_MODE_CPPFLAGS) $(HOST_MODE_CFLAGS) \
-std=c11 -Wpedantic -Wall -Wextra -Werror
@@ -96,77 +83,12 @@ LIB_CFLAGS = $(FREESTANDING_CFLAGS) $(LIB_VISIBILITY_CFLAGS) -Iinclude -Isrc
# are unreachable from the driver.
#
# driver/env/ holds all hosted OS/libc adapter code and is compiled with
-# DRIVER_ENV_CFLAGS. The OS-specific feature-test macros that the env TUs
-# need before any libc header is included live in DRIVER_ENV_OS_CFLAGS,
-# set under "driver/env source selection" below.
+# DRIVER_ENV_CFLAGS. The per-OS feature-test macros and the exact source
+# list (DRIVER_ENV_OS_CFLAGS / DRIVER_ENV_SRCS) come from mk/env.mk.
DRIVER_CFLAGS = $(FREESTANDING_CFLAGS) -Iinclude -Ilang
DRIVER_ENV_CFLAGS = $(HOST_CFLAGS) -Iinclude -Ilang
TEST_HOST_CFLAGS = $(HOST_CFLAGS) -Iinclude -Ilang
-# driver/env source selection. One file per OS, one per arch (icache),
-# one per (arch, OS) (uctx ucontext marshalling). Removing OS/arch ifdefs
-# from the impl is the explicit point of this layout.
-ifeq ($(HOST_UNAME),Darwin)
-DRIVER_ENV_OS_CFLAGS := -D_XOPEN_SOURCE=600 -D_DARWIN_C_SOURCE=1
-DRIVER_ENV_OS_SRC := driver/env/macos.c
-DRIVER_ENV_HINT_SRC :=
-else ifeq ($(HOST_UNAME),Linux)
-DRIVER_ENV_OS_CFLAGS := -D_GNU_SOURCE=1
-DRIVER_ENV_OS_SRC := driver/env/linux.c
-ifeq ($(HOST_ARCH),x86_64)
-DRIVER_ENV_HINT_SRC := driver/env/linux_exec_hint_x86_64.c
-else
-DRIVER_ENV_HINT_SRC := driver/env/linux_exec_hint_default.c
-endif
-else ifeq ($(HOST_UNAME),FreeBSD)
-DRIVER_ENV_OS_CFLAGS :=
-DRIVER_ENV_OS_SRC := driver/env/freebsd.c
-DRIVER_ENV_HINT_SRC :=
-else
-$(error driver/env: unsupported HOST_UNAME=$(HOST_UNAME); see driver/env/windows.c)
-endif
-
-ifeq ($(HOST_ARCH),x86_64)
-DRIVER_ENV_ICACHE_SRC := driver/env/icache_x86.c
-else ifeq ($(HOST_ARCH),aarch64)
-DRIVER_ENV_ICACHE_SRC := driver/env/icache_arm.c
-else ifeq ($(HOST_ARCH),rv64)
-DRIVER_ENV_ICACHE_SRC := driver/env/icache_riscv.c
-else
-$(error driver/env: unsupported HOST_ARCH=$(HOST_ARCH))
-endif
-
-DRIVER_ENV_UCTX_SRC :=
-ifeq ($(HOST_UNAME),Darwin)
-ifeq ($(HOST_ARCH),aarch64)
-DRIVER_ENV_UCTX_SRC := driver/env/uctx_aarch64_macos.c
-endif
-endif
-ifeq ($(HOST_UNAME),Linux)
-ifeq ($(HOST_ARCH),aarch64)
-DRIVER_ENV_UCTX_SRC := driver/env/uctx_aarch64_linux.c
-endif
-ifeq ($(HOST_ARCH),x86_64)
-DRIVER_ENV_UCTX_SRC := driver/env/uctx_x86_64_linux.c
-endif
-ifeq ($(HOST_ARCH),rv64)
-DRIVER_ENV_UCTX_SRC := driver/env/uctx_rv64_linux.c
-endif
-endif
-ifeq ($(DRIVER_ENV_UCTX_SRC),)
-$(error driver/env: no ucontext marshalling for HOST_UNAME=$(HOST_UNAME) HOST_ARCH=$(HOST_ARCH))
-endif
-
-DRIVER_ENV_SRCS := \
- driver/env/common.c \
- driver/env/posix.c \
- driver/env/posix_dbg.c \
- driver/env/jit_tls_posix.c \
- $(DRIVER_ENV_OS_SRC) \
- $(DRIVER_ENV_HINT_SRC) \
- $(DRIVER_ENV_ICACHE_SRC) \
- $(DRIVER_ENV_UCTX_SRC)
-
include mk/config.mk
# Core lib sources. Optional subsystems, backend directories, object format
@@ -486,7 +408,7 @@ $(LIB_AR): $(LIB_RELOC_OBJ)
$(AR) rcs $@ $(LIB_RELOC_OBJ)
$(BIN): $(DRIVER_OBJS) $(LIB_AR) $(BUILD_CONFIG)
- $(CC) $(HOST_LDFLAGS) -o $@ $(DRIVER_OBJS) $(LIB_AR)
+ $(CC) $(HOST_LDFLAGS) -o $@ $(DRIVER_OBJS) $(LIB_AR) $(HOST_LDLIBS)
$(BUILD_DIR)/lib/%.o: src/%.c Makefile $(BUILD_CONFIG)
@mkdir -p $(dir $@)
diff --git a/driver/env/env_internal.h b/driver/env/env_internal.h
@@ -1,20 +1,23 @@
#ifndef CFREE_DRIVER_ENV_INTERNAL_H
#define CFREE_DRIVER_ENV_INTERNAL_H
-/* Internal header shared by the driver/env/ TUs.
+/* Internal header shared by every driver/env/ TU, regardless of host OS.
*
* Each TU implements one slice of the host environment with zero
- * preprocessor conditionals -- selection is done by the Makefile, which
+ * preprocessor conditionals -- selection is done by mk/env.mk, which
* picks at most one of {macos,linux,freebsd,windows}.c, one of
- * icache_<arch>.c, and one of uctx_<arch>_<os>.c per build.
+ * icache_<arch>.c, and (on POSIX) one of uctx_<arch>_<os>.c per build.
*
- * The shared scaffolding lives in common.c (libc-pure), posix.c
- * (mac/linux/freebsd I/O), posix_dbg.c (pthreads + signals + sigsetjmp),
- * and jit_tls_posix.c (pthread_key TLS). The per-OS file fills in the
- * three hooks that genuinely differ: dual-mapped exec memory, the
- * dbg W^X dance, and a few small bits (mtime ns flavor, dlsym mangling,
- * host_target os/obj).
- */
+ * Scope of this header (must stay OS-neutral):
+ * - includes only of <stddef.h>/<stdint.h> and the libcfree public API
+ * - the libc-pure vtables defined in common.c (g_heap_libc, g_diag_stderr)
+ * - the arch-only icache flush hook (icache_<arch>.c)
+ *
+ * The POSIX-shared surface (file I/O scaffold, exec_dual registry, signal
+ * machinery, the os_* hooks the per-OS POSIX file implements, ucontext
+ * marshalling) lives in env_posix.h. Windows has no POSIX overlap, so
+ * windows.c folds everything into a single TU and only needs this
+ * header. */
#include <stddef.h>
#include <stdint.h>
@@ -26,105 +29,15 @@
#include "../driver.h"
#include "../env.h"
-/* ---- exec_dual registry (defined in posix.c) -----------------------------
- * Apple/Linux/FreeBSD dual-mapping execmem produces a write alias and a
- * runtime alias at distinct virtual addresses. The dbg code_write_begin
- * path needs to translate a runtime address into the corresponding write
- * alias. Single-mapping reservations (write == runtime) are not registered.
- */
-void exec_dual_register(void* write_base, void* runtime_base, size_t size);
-void exec_dual_unregister(void* runtime_base);
-int exec_dual_lookup(void* runtime_addr, size_t n, void** write_out);
-
-/* Per-region bookkeeping carried on CfreeExecMemRegion.token.
- * EXEC reservations carry a token; single-mapping ones leave token NULL. */
-typedef struct ExecMemToken {
- void* write_addr;
- void* runtime_addr;
- size_t size;
-} ExecMemToken;
-
-/* Page size & single-mapping helpers (posix.c). */
-size_t driver_host_page_size(void);
-CfreeStatus execmem_reserve_single(size_t size, CfreeExecMemRegion* out);
-int cfree_to_posix_prot(int prot);
+/* ---- vtable singletons wired into DriverEnv (common.c) -----------------
+ * Defined in common.c and consumed by every host's driver_env_init. */
+extern CfreeHeap g_heap_libc;
+extern CfreeDiagSink g_diag_stderr;
-/* Cache directory storage (posix.c). */
-extern char g_cache_dir[4096];
-
-/* ---- icache (icache_<arch>.c) --------------------------------------------
- * Single arch-specific entry used by execmem_flush_icache and by per-OS
- * dbg_flush_icache impls that don't have a better OS-native primitive.
- */
+/* ---- icache (icache_<arch>.c) ------------------------------------------
+ * Arch-only, OS-neutral. The POSIX dbg path delegates here from
+ * os_dbg_flush_icache; Windows uses FlushInstructionCache directly and
+ * does not consume this. */
void env_flush_icache(void* addr, size_t n);
-/* ---- Linux x86_64 runtime-alias hint (linux_exec_hint_<arch>.c) ---------
- * On x86_64 Linux the runtime alias is placed in the low 2 GiB so that
- * direct call/jmp displacements from text reach without thunks. Other
- * arches return 0/NULL and treat this as a no-op. Selected by Makefile
- * via HOST_ARCH on Linux builds. */
-int env_execmem_runtime_extra_flags(void);
-void* env_execmem_low_runtime_hint(size_t size);
-
-/* ---- ucontext marshalling (uctx_<arch>_<os>.c) ---------------------------
- * Provided per (arch, OS) combo. The signal handler in posix_dbg.c calls
- * these to snapshot register state into a CfreeUnwindFrame and write
- * (potentially session-edited) state back.
- */
-#include <ucontext.h>
-void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f);
-void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc);
-
-/* ---- per-OS hooks (macos.c / linux.c / freebsd.c / windows.c) ------------
- * Each hook has exactly one definition per build, selected by Makefile.
- */
-
-#include <sys/stat.h>
-
-/* Reserve `size` bytes of W^X execute memory. May install a dual mapping
- * (Apple mach_vm_remap, Linux memfd, FreeBSD SHM_ANON) or fall back to the
- * single-mapping path on hosts without a dual primitive. */
-CfreeStatus os_execmem_reserve_exec(size_t size, CfreeExecMemRegion* out);
-
-/* dbg W^X transitions. begin yields a writable address (the alias on
- * dual-mapped, runtime_addr with mprotect on single-mapped). end pairs
- * with begin and may be a no-op (Apple) or mprotect-back-to-RX (Linux).
- * Signatures match the CfreeDbgOs vtable shape for direct wiring. */
-CfreeStatus os_dbg_code_write_begin(void* user, void* runtime_addr, size_t n,
- void** write_out);
-void os_dbg_code_write_end(void* user, void* runtime_addr, size_t n);
-
-/* Instruction-cache flush after dbg writes. Apple aarch64 calls
- * sys_icache_invalidate; others delegate to env_flush_icache. */
-void os_dbg_flush_icache(void* user, void* runtime_addr, size_t n);
-
-/* Read st_mtim/st_mtimespec into nanoseconds. Returns 0 on success. */
-int os_stat_mtime_ns(const struct stat* sb, int64_t* out);
-
-/* Symbol resolver with platform name-mangling rules (Apple strips leading
- * underscore for dlsym; ELF passes through). */
-void* os_dlsym(const char* name);
-
-/* Fill the OS/object-format slots of CfreeTarget for the host. */
-void os_host_target_fill(CfreeTarget* t);
-
-/* ---- vtable singletons wired into DriverEnv ----------------------------
- * Each is defined in exactly one TU; driver_env_init pulls them together.
- */
-extern CfreeHeap g_heap_libc; /* common.c */
-extern CfreeDiagSink g_diag_stderr; /* common.c */
-extern CfreeExecMem g_execmem_posix; /* posix.c — page_size set in init */
-extern CfreeDbgOs g_dbg_os_posix; /* posix_dbg.c */
-extern CfreeJitTls g_jit_tls_posix; /* jit_tls_posix.c */
-
-/* posix_dbg.c exposes a worker-thread check for the signal handler. */
-int posix_dbg_caller_is_worker(void);
-
-/* ---- POSIX dbg state shared with the per-OS code_write paths ------------
- * Used only when the OS impl needs to introspect dbg state. Defined in
- * posix_dbg.c.
- */
-#include <signal.h>
-#define DBG_INTERRUPT_SIGNO SIGUSR2
-
#endif
diff --git a/driver/env/env_posix.h b/driver/env/env_posix.h
@@ -0,0 +1,110 @@
+#ifndef CFREE_DRIVER_ENV_POSIX_H
+#define CFREE_DRIVER_ENV_POSIX_H
+
+/* POSIX-shared internal surface used by the POSIX env TUs:
+ * driver/env/posix.c, posix_dbg.c, jit_tls_posix.c
+ * driver/env/macos.c, linux.c, freebsd.c
+ * driver/env/linux_exec_hint_<arch>.c
+ * driver/env/uctx_<arch>_<os>.c
+ *
+ * Holds the bits that are common to every POSIX-shaped host but absent on
+ * Windows: the exec_dual write/runtime alias registry, the dual-mapping
+ * token bookkeeping, the single-mapping execmem helper, the POSIX-vs-
+ * cfree protection translation, the per-OS hooks the shared POSIX TUs
+ * call into, ucontext marshalling, and the dbg interrupt signo.
+ *
+ * Anything that would compile on Windows belongs in env_internal.h
+ * (which this header pulls in); anything that needs <ucontext.h>,
+ * <signal.h>, or <sys/stat.h>'s POSIX `struct stat` belongs here. */
+
+#include <signal.h>
+#include <sys/stat.h>
+#include <ucontext.h>
+
+#include "env_internal.h"
+
+/* ---- exec_dual registry (defined in posix.c) ---------------------------
+ * Apple/Linux/FreeBSD dual-mapping execmem produces a write alias and a
+ * runtime alias at distinct virtual addresses. The dbg code_write_begin
+ * path needs to translate a runtime address into the corresponding write
+ * alias. Single-mapping reservations (write == runtime) are not registered.
+ */
+void exec_dual_register(void* write_base, void* runtime_base, size_t size);
+void exec_dual_unregister(void* runtime_base);
+int exec_dual_lookup(void* runtime_addr, size_t n, void** write_out);
+
+/* Per-region bookkeeping carried on CfreeExecMemRegion.token.
+ * EXEC reservations carry a token; single-mapping ones leave token NULL. */
+typedef struct ExecMemToken {
+ void* write_addr;
+ void* runtime_addr;
+ size_t size;
+} ExecMemToken;
+
+/* Page size & single-mapping helpers (posix.c). */
+size_t driver_host_page_size(void);
+CfreeStatus execmem_reserve_single(size_t size, CfreeExecMemRegion* out);
+int cfree_to_posix_prot(int prot);
+
+/* Cache directory storage (posix.c). */
+extern char g_cache_dir[4096];
+
+/* ---- Linux x86_64 runtime-alias hint (linux_exec_hint_<arch>.c) ---------
+ * On x86_64 Linux the runtime alias is placed in the low 2 GiB so that
+ * direct call/jmp displacements from text reach without thunks. Other
+ * arches return 0/NULL and treat this as a no-op. Selected by mk/env.mk
+ * via HOST_ARCH on Linux builds. */
+int env_execmem_runtime_extra_flags(void);
+void* env_execmem_low_runtime_hint(size_t size);
+
+/* ---- ucontext marshalling (uctx_<arch>_<os>.c) ---------------------------
+ * Provided per (arch, OS) combo. The signal handler in posix_dbg.c calls
+ * these to snapshot register state into a CfreeUnwindFrame and write
+ * (potentially session-edited) state back. */
+void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f);
+void dbg_frame_to_ucontext(const CfreeUnwindFrame* f, ucontext_t* uc);
+
+/* ---- per-OS hooks (macos.c / linux.c / freebsd.c) ------------------------
+ * Each hook has exactly one definition per POSIX build, selected by
+ * mk/env.mk. */
+
+/* Reserve `size` bytes of W^X execute memory. May install a dual mapping
+ * (Apple mach_vm_remap, Linux memfd, FreeBSD SHM_ANON) or fall back to the
+ * single-mapping path on hosts without a dual primitive. */
+CfreeStatus os_execmem_reserve_exec(size_t size, CfreeExecMemRegion* out);
+
+/* dbg W^X transitions. begin yields a writable address (the alias on
+ * dual-mapped, runtime_addr with mprotect on single-mapped). end pairs
+ * with begin and may be a no-op (Apple) or mprotect-back-to-RX (Linux).
+ * Signatures match the CfreeDbgOs vtable shape for direct wiring. */
+CfreeStatus os_dbg_code_write_begin(void* user, void* runtime_addr, size_t n,
+ void** write_out);
+void os_dbg_code_write_end(void* user, void* runtime_addr, size_t n);
+
+/* Instruction-cache flush after dbg writes. Apple aarch64 calls
+ * sys_icache_invalidate; others delegate to env_flush_icache. */
+void os_dbg_flush_icache(void* user, void* runtime_addr, size_t n);
+
+/* Read st_mtim/st_mtimespec into nanoseconds. Returns 0 on success. */
+int os_stat_mtime_ns(const struct stat* sb, int64_t* out);
+
+/* Symbol resolver with platform name-mangling rules (Apple strips leading
+ * underscore for dlsym; ELF passes through). */
+void* os_dlsym(const char* name);
+
+/* Fill the OS/object-format slots of CfreeTarget for the host. */
+void os_host_target_fill(CfreeTarget* t);
+
+/* ---- vtable singletons wired into DriverEnv on POSIX -------------------- */
+extern CfreeExecMem g_execmem_posix; /* posix.c — page_size set in init */
+extern CfreeDbgOs g_dbg_os_posix; /* posix_dbg.c */
+extern CfreeJitTls g_jit_tls_posix; /* jit_tls_posix.c */
+
+/* posix_dbg.c exposes a worker-thread check for the signal handler. */
+int posix_dbg_caller_is_worker(void);
+
+/* dbg interrupt signal: posix_dbg.c's signal handler treats this signo as
+ * "interrupt from the REPL" (vs SIGTRAP/SEGV/etc, which are faults). */
+#define DBG_INTERRUPT_SIGNO SIGUSR2
+
+#endif
diff --git a/driver/env/freebsd.c b/driver/env/freebsd.c
@@ -17,7 +17,7 @@
#include <sys/stat.h>
#include <unistd.h>
-#include "env_internal.h"
+#include "env_posix.h"
/* ---------------- dual-mapped exec memory ---------------- */
diff --git a/driver/env/jit_tls_posix.c b/driver/env/jit_tls_posix.c
@@ -13,7 +13,7 @@
#include <stdlib.h>
#include <string.h>
-#include "env_internal.h"
+#include "env_posix.h"
typedef struct JitTlsCtx {
void* (*get_block)(void* ctx); /* first; matches tlv_thunk's expectation */
diff --git a/driver/env/linux.c b/driver/env/linux.c
@@ -12,7 +12,7 @@
#include <sys/syscall.h>
#include <unistd.h>
-#include "env_internal.h"
+#include "env_posix.h"
/* ---------------- dual-mapped exec memory (memfd_create) ----------------
* memfd_create gives us an anonymous fd; two mmaps of that fd alias the
diff --git a/driver/env/linux_exec_hint_default.c b/driver/env/linux_exec_hint_default.c
@@ -4,7 +4,7 @@
#include <stddef.h>
-#include "env_internal.h"
+#include "env_posix.h"
int env_execmem_runtime_extra_flags(void) { return 0; }
diff --git a/driver/env/linux_exec_hint_x86_64.c b/driver/env/linux_exec_hint_x86_64.c
@@ -7,7 +7,7 @@
#include <stdint.h>
#include <sys/mman.h>
-#include "env_internal.h"
+#include "env_posix.h"
int env_execmem_runtime_extra_flags(void) { return MAP_32BIT; }
diff --git a/driver/env/macos.c b/driver/env/macos.c
@@ -17,7 +17,7 @@
#include <sys/mman.h>
#include <sys/stat.h>
-#include "env_internal.h"
+#include "env_posix.h"
/* ---------------- dual-mapped exec memory ---------------- */
/* mach_vm_remap creates a second VA pointing at the same physical memory.
diff --git a/driver/env/posix.c b/driver/env/posix.c
@@ -18,7 +18,7 @@
#include <time.h>
#include <unistd.h>
-#include "env_internal.h"
+#include "env_posix.h"
/* ---------------- exec memory: single-mapping core + registry ---------------- */
diff --git a/driver/env/posix_dbg.c b/driver/env/posix_dbg.c
@@ -16,7 +16,7 @@
#include <stdlib.h>
#include <string.h>
-#include "env_internal.h"
+#include "env_posix.h"
/* Single-session process model (one debug target at a time). The signal
* handler reads these from async-signal context; both writes happen in
diff --git a/driver/env/uctx_aarch64_linux.c b/driver/env/uctx_aarch64_linux.c
@@ -3,7 +3,7 @@
#include <stdint.h>
-#include "env_internal.h"
+#include "env_posix.h"
void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
const mcontext_t* mc = &uc->uc_mcontext;
diff --git a/driver/env/uctx_aarch64_macos.c b/driver/env/uctx_aarch64_macos.c
@@ -4,7 +4,7 @@
#include <stdint.h>
-#include "env_internal.h"
+#include "env_posix.h"
void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
const struct __darwin_arm_thread_state64* ss = &uc->uc_mcontext->__ss;
diff --git a/driver/env/uctx_rv64_linux.c b/driver/env/uctx_rv64_linux.c
@@ -6,7 +6,7 @@
#include <stdint.h>
-#include "env_internal.h"
+#include "env_posix.h"
void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
const mcontext_t* mc = &uc->uc_mcontext;
diff --git a/driver/env/uctx_x86_64_linux.c b/driver/env/uctx_x86_64_linux.c
@@ -6,7 +6,7 @@
#include <stdint.h>
#include <string.h>
-#include "env_internal.h"
+#include "env_posix.h"
void dbg_ucontext_to_frame(const ucontext_t* uc, CfreeUnwindFrame* f) {
const greg_t* g = uc->uc_mcontext.gregs;
diff --git a/driver/env/windows.c b/driver/env/windows.c
@@ -1,21 +1,1593 @@
-/* Windows host support is wired through the OS axis but not yet
- * implemented. A real Windows port needs to replace nearly every TU in
- * driver/env/ -- there's no POSIX overlap to share. Sketch:
+/* Windows host environment. Replaces posix.c, posix_dbg.c, and
+ * jit_tls_posix.c on Win32 builds; common.c is reused unchanged. Built
+ * against the Win32 API with MinGW-w64 in mind.
*
- * - common.c is reusable as-is (malloc/free/printf are universal).
- * - posix.c equivalent (windows_io.c): VirtualAlloc + VirtualProtect for
- * execmem, ReadFile / WriteFile / CreateFile for file_io, MoveFileEx
- * for atomic replace, _stat64 for mtime, SetConsoleCtrlHandler in
- * place of sigaction(SIGINT).
- * - posix_dbg.c equivalent (windows_dbg.c): CreateThread / Events
- * (CreateEvent + Set/Wait) instead of pthreads, structured exception
- * handling (__try/__except) instead of sigaction + sigsetjmp,
- * AddVectoredExceptionHandler for the W^X fault path.
- * - jit_tls equivalent (windows_jit_tls.c): TlsAlloc / TlsGetValue /
- * TlsSetValue (note the TLV thunk's first-field contract still holds).
- * - dlsym equivalent: GetProcAddress over a snapshot of loaded modules.
+ * Coverage:
+ * - file_io (CreateFileW/ReadFile/WriteFile + UTF-8 path conversion)
+ * - path helpers (GetFileAttributesExW for exists/mtime, mkdir_p via
+ * CreateDirectoryW, mark_executable is a no-op on NTFS)
+ * - stdin / read_line / edit_temp (GetTempPath + system())
+ * - monotonic time (QueryPerformanceCounter)
+ * - SIGINT shim via SetConsoleCtrlHandler
+ * - dlsym via GetProcAddress over an EnumProcessModules snapshot
+ * - execmem: dual-mapping via CreateFileMappingW + MapViewOfFile; the
+ * write alias is RW, the runtime alias is RX after a protect flip.
+ * Single-mapping fallback uses VirtualAlloc.
+ * - dbg_os: CreateThread for the worker, event objects, vectored
+ * exception handling for SEGV/ILL/BP/etc, __try/__except guarded_copy,
+ * setjmp/longjmp for call_with_catch / thread_abort, SuspendThread +
+ * GetThreadContext for the interrupt path (the interrupt's on_fault
+ * runs on the *caller* thread; natural faults run on the worker
+ * thread inside the VEH, matching POSIX semantics there).
+ * - jit_tls: FlsAlloc/FlsGetValue/FlsSetValue with a per-thread dtor.
+ * The TLV thunk's first-field contract is preserved (`get_block` is
+ * the first field of JitTlsCtx).
*
- * Until that work lands, attempting to build for Windows is a hard error
- * rather than a silently-broken binary. */
+ * The W^X model on Windows mirrors the Linux/FreeBSD memfd path: a single
+ * pagefile-backed file-mapping object is mapped twice -- once RW (write
+ * alias) and once RX (runtime alias). The dbg code-write path translates
+ * a runtime address into the corresponding write alias via the same
+ * exec_dual registry the POSIX side uses (re-implemented locally with
+ * SRWLock since we don't link the POSIX TUs). Single-mapping reservations
+ * (no CFREE_PROT_EXEC) just VirtualAlloc a single RW region.
+ */
-#error "cfree: Windows host port is not yet implemented (driver/env/windows.c)"
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#ifndef _WIN32_WINNT
+#define _WIN32_WINNT 0x0601 /* Windows 7+: SRWLock, AddVectoredExceptionHandler */
+#endif
+#include <windows.h>
+#include <psapi.h>
+
+#include <io.h>
+#include <process.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include <setjmp.h>
+
+#include "env_internal.h"
+
+/* Win32 dbg interrupt code: a synthetic signo handed up to on_fault. The
+ * value just needs to be distinct from real exception codes; we pick a
+ * small positive int so it round-trips through the int field of
+ * CfreeDbgOs.interrupt_signo. */
+#define DBG_WIN_INTERRUPT_SIGNO 100
+
+/* ============================================================
+ * UTF-8 <-> UTF-16 path conversion
+ * ============================================================ */
+
+/* Convert a UTF-8 path to a freshly-malloc'd wide string. Returns NULL on
+ * empty input or allocation failure. Callers free with `free`. */
+static wchar_t* widen(const char* utf8) {
+ int need;
+ wchar_t* w;
+ if (!utf8 || !*utf8) return NULL;
+ need = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
+ if (need <= 0) return NULL;
+ w = (wchar_t*)malloc((size_t)need * sizeof(wchar_t));
+ if (!w) return NULL;
+ if (MultiByteToWideChar(CP_UTF8, 0, utf8, -1, w, need) <= 0) {
+ free(w);
+ return NULL;
+ }
+ return w;
+}
+
+/* ============================================================
+ * exec_dual registry (write/runtime alias bookkeeping)
+ * ============================================================ */
+
+typedef struct ExecDualNode {
+ void* write_base;
+ void* runtime_base;
+ size_t size;
+ struct ExecDualNode* next;
+} ExecDualNode;
+
+static ExecDualNode* g_jit_dual_map;
+static SRWLOCK g_jit_dual_map_lock = SRWLOCK_INIT;
+
+static void exec_dual_register_w(void* write_base, void* runtime_base,
+ size_t size) {
+ ExecDualNode* n;
+ if (write_base == runtime_base) return;
+ n = (ExecDualNode*)malloc(sizeof(*n));
+ if (!n) return;
+ n->write_base = write_base;
+ n->runtime_base = runtime_base;
+ n->size = size;
+ AcquireSRWLockExclusive(&g_jit_dual_map_lock);
+ n->next = g_jit_dual_map;
+ g_jit_dual_map = n;
+ ReleaseSRWLockExclusive(&g_jit_dual_map_lock);
+}
+
+static void exec_dual_unregister_w(void* runtime_base) {
+ ExecDualNode** pp;
+ AcquireSRWLockExclusive(&g_jit_dual_map_lock);
+ for (pp = &g_jit_dual_map; *pp; pp = &(*pp)->next) {
+ if ((*pp)->runtime_base == runtime_base) {
+ ExecDualNode* dead = *pp;
+ *pp = dead->next;
+ free(dead);
+ break;
+ }
+ }
+ ReleaseSRWLockExclusive(&g_jit_dual_map_lock);
+}
+
+static int exec_dual_lookup_w(void* runtime_addr, size_t n, void** write_out) {
+ ExecDualNode* cur;
+ uintptr_t a = (uintptr_t)runtime_addr;
+ AcquireSRWLockShared(&g_jit_dual_map_lock);
+ for (cur = g_jit_dual_map; cur; cur = cur->next) {
+ uintptr_t base = (uintptr_t)cur->runtime_base;
+ if (a >= base && a + n <= base + cur->size) {
+ *write_out = (void*)((uintptr_t)cur->write_base + (a - base));
+ ReleaseSRWLockShared(&g_jit_dual_map_lock);
+ return 0;
+ }
+ }
+ ReleaseSRWLockShared(&g_jit_dual_map_lock);
+ return 1;
+}
+
+/* ============================================================
+ * exec memory: dual-map via CreateFileMappingW, single via VirtualAlloc
+ * ============================================================ */
+
+typedef struct ExecMemTokenWin {
+ HANDLE mapping; /* NULL for single-mapping reservations */
+ void* write_addr;
+ void* runtime_addr;
+ size_t size;
+} ExecMemTokenWin;
+
+static DWORD cfree_to_win_prot(int prot) {
+ int r = (prot & CFREE_PROT_READ) != 0;
+ int w = (prot & CFREE_PROT_WRITE) != 0;
+ int x = (prot & CFREE_PROT_EXEC) != 0;
+ if (x && w) return PAGE_EXECUTE_READWRITE;
+ if (x && r) return PAGE_EXECUTE_READ;
+ if (x) return PAGE_EXECUTE;
+ if (w) return PAGE_READWRITE;
+ if (r) return PAGE_READONLY;
+ return PAGE_NOACCESS;
+}
+
+static size_t driver_host_page_size_win(void) {
+ SYSTEM_INFO si;
+ GetSystemInfo(&si);
+ return si.dwPageSize ? (size_t)si.dwPageSize : (size_t)0x1000;
+}
+
+static CfreeStatus execmem_reserve_single_win(size_t size,
+ CfreeExecMemRegion* out) {
+ void* p = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
+ if (!p) return CFREE_NOMEM;
+ out->write = p;
+ out->runtime = p;
+ out->size = size;
+ out->token = NULL;
+ return CFREE_OK;
+}
+
+static CfreeStatus execmem_reserve_dual_win(size_t size,
+ CfreeExecMemRegion* out) {
+ HANDLE map;
+ void* w;
+ void* r;
+ ExecMemTokenWin* tok;
+ DWORD lo = (DWORD)(size & 0xFFFFFFFFu);
+ DWORD hi = (DWORD)((uint64_t)size >> 32);
+
+ /* PAGE_EXECUTE_READWRITE on the section object is the max protection any
+ * view can request; per-view protections are narrower (RW vs RX). */
+ map = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL,
+ PAGE_EXECUTE_READWRITE, hi, lo, NULL);
+ if (!map) return CFREE_ERR;
+
+ w = MapViewOfFile(map, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, size);
+ if (!w) {
+ CloseHandle(map);
+ return CFREE_NOMEM;
+ }
+ r = MapViewOfFile(map, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, size);
+ if (!r) {
+ UnmapViewOfFile(w);
+ CloseHandle(map);
+ return CFREE_NOMEM;
+ }
+
+ tok = (ExecMemTokenWin*)malloc(sizeof(*tok));
+ if (!tok) {
+ UnmapViewOfFile(r);
+ UnmapViewOfFile(w);
+ CloseHandle(map);
+ return CFREE_NOMEM;
+ }
+ tok->mapping = map;
+ tok->write_addr = w;
+ tok->runtime_addr = r;
+ tok->size = size;
+
+ exec_dual_register_w(w, r, size);
+
+ out->write = w;
+ out->runtime = r;
+ out->size = size;
+ out->token = tok;
+ return CFREE_OK;
+}
+
+static CfreeStatus execmem_reserve_win(void* user, size_t size, int prot,
+ CfreeExecMemRegion* out) {
+ (void)user;
+ if (!out || !size) return CFREE_INVALID;
+ if (prot & CFREE_PROT_EXEC) return execmem_reserve_dual_win(size, out);
+ return execmem_reserve_single_win(size, out);
+}
+
+static CfreeStatus execmem_protect_win(void* user, void* addr, size_t size,
+ int prot) {
+ DWORD old;
+ (void)user;
+ return VirtualProtect(addr, size, cfree_to_win_prot(prot), &old) ? CFREE_OK
+ : CFREE_ERR;
+}
+
+static void execmem_release_win(void* user, CfreeExecMemRegion* region) {
+ (void)user;
+ if (!region || !region->size) return;
+ if (region->token) {
+ ExecMemTokenWin* tok = (ExecMemTokenWin*)region->token;
+ if (tok->runtime_addr && tok->runtime_addr != tok->write_addr) {
+ exec_dual_unregister_w(tok->runtime_addr);
+ UnmapViewOfFile(tok->runtime_addr);
+ }
+ if (tok->write_addr) UnmapViewOfFile(tok->write_addr);
+ if (tok->mapping) CloseHandle(tok->mapping);
+ free(tok);
+ } else if (region->write) {
+ VirtualFree(region->write, 0, MEM_RELEASE);
+ }
+ region->write = NULL;
+ region->runtime = NULL;
+ region->size = 0;
+ region->token = NULL;
+}
+
+static void execmem_flush_icache_win(void* user, void* addr, size_t size) {
+ (void)user;
+ FlushInstructionCache(GetCurrentProcess(), addr, size);
+}
+
+static CfreeExecMem g_execmem_win;
+
+/* ============================================================
+ * Writer vtables: HANDLE-backed and stdio-backed
+ * ============================================================ */
+
+typedef struct DriverHandleWriter {
+ CfreeWriter base;
+ CfreeHeap* heap;
+ HANDLE h;
+ CfreeStatus status;
+ uint64_t pos;
+} DriverHandleWriter;
+
+static CfreeStatus hw_write(CfreeWriter* w, const void* data, size_t n) {
+ DriverHandleWriter* fw = (DriverHandleWriter*)w;
+ const unsigned char* p = (const unsigned char*)data;
+ if (fw->status != CFREE_OK) return fw->status;
+ while (n > 0) {
+ DWORD chunk = n > 0x40000000u ? 0x40000000u : (DWORD)n;
+ DWORD wrote = 0;
+ if (!WriteFile(fw->h, p, chunk, &wrote, NULL) || wrote == 0) {
+ fw->status = CFREE_IO;
+ return CFREE_IO;
+ }
+ p += wrote;
+ n -= wrote;
+ fw->pos += wrote;
+ }
+ return CFREE_OK;
+}
+
+static CfreeStatus hw_seek(CfreeWriter* w, uint64_t off) {
+ DriverHandleWriter* fw = (DriverHandleWriter*)w;
+ LARGE_INTEGER li;
+ if (fw->status != CFREE_OK) return fw->status;
+ li.QuadPart = (LONGLONG)off;
+ if (!SetFilePointerEx(fw->h, li, NULL, FILE_BEGIN)) {
+ fw->status = CFREE_IO;
+ return CFREE_IO;
+ }
+ fw->pos = off;
+ return CFREE_OK;
+}
+
+static uint64_t hw_tell(CfreeWriter* w) {
+ return ((DriverHandleWriter*)w)->pos;
+}
+static CfreeStatus hw_status(CfreeWriter* w) {
+ return ((DriverHandleWriter*)w)->status;
+}
+static void hw_close(CfreeWriter* w) {
+ DriverHandleWriter* fw = (DriverHandleWriter*)w;
+ if (fw->h && fw->h != INVALID_HANDLE_VALUE) CloseHandle(fw->h);
+ fw->heap->free(fw->heap, fw, sizeof(*fw));
+}
+
+static CfreeWriter* driver_writer_handle(CfreeHeap* h, HANDLE fh) {
+ DriverHandleWriter* fw = (DriverHandleWriter*)h->alloc(
+ h, sizeof(*fw), _Alignof(DriverHandleWriter));
+ if (!fw) return NULL;
+ fw->base.write = hw_write;
+ fw->base.seek = hw_seek;
+ fw->base.tell = hw_tell;
+ fw->base.status = hw_status;
+ fw->base.close = hw_close;
+ fw->heap = h;
+ fw->h = fh;
+ fw->status = CFREE_OK;
+ fw->pos = 0;
+ return &fw->base;
+}
+
+typedef struct DriverStdioWriter {
+ CfreeWriter base;
+ CfreeHeap* heap;
+ FILE* fp;
+ CfreeStatus status;
+} DriverStdioWriter;
+
+static CfreeStatus stdio_w_write(CfreeWriter* w, const void* data, size_t n) {
+ DriverStdioWriter* sw = (DriverStdioWriter*)w;
+ if (n) {
+ size_t got = fwrite(data, 1, n, sw->fp);
+ if (got != n) {
+ sw->status = CFREE_IO;
+ return CFREE_IO;
+ }
+ }
+ return CFREE_OK;
+}
+static CfreeStatus stdio_w_seek(CfreeWriter* w, uint64_t off) {
+ DriverStdioWriter* sw = (DriverStdioWriter*)w;
+ return fseek(sw->fp, (long)off, SEEK_SET) == 0 ? CFREE_OK : CFREE_IO;
+}
+static uint64_t stdio_w_tell(CfreeWriter* w) {
+ long t = ftell(((DriverStdioWriter*)w)->fp);
+ return t < 0 ? 0u : (uint64_t)t;
+}
+static CfreeStatus stdio_w_status(CfreeWriter* w) {
+ DriverStdioWriter* sw = (DriverStdioWriter*)w;
+ if (sw->status != CFREE_OK) return sw->status;
+ return ferror(sw->fp) ? CFREE_IO : CFREE_OK;
+}
+static void stdio_w_close(CfreeWriter* w) {
+ DriverStdioWriter* sw = (DriverStdioWriter*)w;
+ fflush(sw->fp);
+ sw->heap->free(sw->heap, sw, sizeof(*sw));
+}
+
+CfreeWriter* driver_stdout_writer(DriverEnv* e) {
+ DriverStdioWriter* sw = (DriverStdioWriter*)e->heap->alloc(
+ e->heap, sizeof(*sw), _Alignof(DriverStdioWriter));
+ if (!sw) return NULL;
+ sw->base.write = stdio_w_write;
+ sw->base.seek = stdio_w_seek;
+ sw->base.tell = stdio_w_tell;
+ sw->base.status = stdio_w_status;
+ sw->base.close = stdio_w_close;
+ sw->heap = e->heap;
+ sw->fp = stdout;
+ sw->status = CFREE_OK;
+ return &sw->base;
+}
+
+/* ============================================================
+ * file_io (CreateFileW + ReadFile/WriteFile)
+ * ============================================================ */
+
+static CfreeStatus win_read_all(void* user, const char* path,
+ CfreeFileData* out) {
+ DriverEnv* env = (DriverEnv*)user;
+ wchar_t* wpath;
+ HANDLE h;
+ LARGE_INTEGER sz;
+ size_t size;
+ size_t got;
+ void* buf;
+
+ wpath = widen(path);
+ if (!wpath) return CFREE_NOT_FOUND;
+ h = CreateFileW(wpath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL, NULL);
+ free(wpath);
+ if (h == INVALID_HANDLE_VALUE) return CFREE_NOT_FOUND;
+ if (!GetFileSizeEx(h, &sz)) {
+ CloseHandle(h);
+ return CFREE_IO;
+ }
+ size = (size_t)sz.QuadPart;
+ buf = size ? env->heap->alloc(env->heap, size, 1) : NULL;
+ if (size && !buf) {
+ CloseHandle(h);
+ return CFREE_NOMEM;
+ }
+ got = 0;
+ while (got < size) {
+ DWORD chunk = (size - got) > 0x40000000u ? 0x40000000u : (DWORD)(size - got);
+ DWORD n = 0;
+ if (!ReadFile(h, (unsigned char*)buf + got, chunk, &n, NULL) || n == 0) {
+ env->heap->free(env->heap, buf, size);
+ CloseHandle(h);
+ return CFREE_IO;
+ }
+ got += n;
+ }
+ CloseHandle(h);
+ out->data = (const uint8_t*)buf;
+ out->size = size;
+ out->token = buf;
+ return CFREE_OK;
+}
+
+static void win_release(void* user, CfreeFileData* d) {
+ DriverEnv* env = (DriverEnv*)user;
+ if (d->token) env->heap->free(env->heap, d->token, d->size);
+ d->data = NULL;
+ d->size = 0;
+ d->token = NULL;
+}
+
+static CfreeStatus win_open_writer(void* user, const char* path,
+ CfreeWriter** out) {
+ DriverEnv* env = (DriverEnv*)user;
+ wchar_t* wpath = widen(path);
+ HANDLE h;
+ CfreeWriter* w;
+ if (!wpath) return CFREE_IO;
+ h = CreateFileW(wpath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL, NULL);
+ free(wpath);
+ if (h == INVALID_HANDLE_VALUE) return CFREE_IO;
+ w = driver_writer_handle(env->heap, h);
+ if (!w) {
+ CloseHandle(h);
+ return CFREE_NOMEM;
+ }
+ *out = w;
+ return CFREE_OK;
+}
+
+/* ============================================================
+ * Path helpers
+ * ============================================================ */
+
+int driver_path_exists(const char* path) {
+ WIN32_FILE_ATTRIBUTE_DATA fad;
+ wchar_t* wpath;
+ BOOL ok;
+ if (!path) return 0;
+ wpath = widen(path);
+ if (!wpath) return 0;
+ ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &fad);
+ free(wpath);
+ return ok ? 1 : 0;
+}
+
+/* Convert a FILETIME (100-ns ticks since 1601-01-01 UTC) to ns since the
+ * Unix epoch (1970-01-01 UTC). 11644473600 sec is the gap. */
+static int64_t filetime_to_unix_ns(FILETIME ft) {
+ uint64_t t = ((uint64_t)ft.dwHighDateTime << 32) | (uint64_t)ft.dwLowDateTime;
+ /* Subtract Win-to-Unix epoch offset in 100-ns ticks. */
+ static const uint64_t EPOCH_DIFF_100NS = 116444736000000000ull;
+ if (t < EPOCH_DIFF_100NS) t = EPOCH_DIFF_100NS;
+ return (int64_t)((t - EPOCH_DIFF_100NS) * 100ull);
+}
+
+int driver_path_mtime_ns(const char* path, int64_t* out) {
+ WIN32_FILE_ATTRIBUTE_DATA fad;
+ wchar_t* wpath;
+ BOOL ok;
+ if (!path || !out) return 1;
+ wpath = widen(path);
+ if (!wpath) return 1;
+ ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &fad);
+ free(wpath);
+ if (!ok) return 1;
+ *out = filetime_to_unix_ns(fad.ftLastWriteTime);
+ return 0;
+}
+
+int driver_mkdir_p(DriverEnv* env, const char* path) {
+ size_t len;
+ char* buf;
+ size_t i;
+ if (!path || !path[0]) return 1;
+ len = cfree_slice_cstr(path).len;
+ buf = (char*)driver_alloc(env, len + 1);
+ if (!buf) return 1;
+ memcpy(buf, path, len + 1);
+
+ /* Walk separators, accepting both '/' and '\\'. Skip the drive prefix
+ * ("C:") and the leading separator(s) of a UNC path so we don't try to
+ * CreateDirectory("\\\\server"). */
+ for (i = 0; i <= len; ++i) {
+ int at_end = (i == len);
+ char ch = buf[i];
+ int is_sep = (ch == '/' || ch == '\\');
+ int do_create = at_end || is_sep;
+ if (!do_create) continue;
+ if (!at_end) buf[i] = '\0';
+ /* Skip pure roots: "", ".", drive-letter-only ("C:"), and UNC roots
+ * ("\\\\server" or "\\\\server\\share"). */
+ if (buf[0] == '\0') {
+ /* nothing yet */
+ } else if (driver_streq(buf, ".")) {
+ /* skip */
+ } else if (i >= 2 && buf[1] == ':' && buf[2] == '\0') {
+ /* "C:" — drive prefix only */
+ } else {
+ wchar_t* wpath = widen(buf);
+ if (!wpath) {
+ driver_free(env, buf, len + 1);
+ return 1;
+ }
+ if (!CreateDirectoryW(wpath, NULL)) {
+ DWORD err = GetLastError();
+ if (err != ERROR_ALREADY_EXISTS) {
+ free(wpath);
+ driver_free(env, buf, len + 1);
+ return 1;
+ }
+ }
+ {
+ WIN32_FILE_ATTRIBUTE_DATA fad;
+ BOOL ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &fad);
+ free(wpath);
+ if (!ok || !(fad.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
+ driver_free(env, buf, len + 1);
+ return 1;
+ }
+ }
+ }
+ if (!at_end) buf[i] = ch;
+ }
+
+ driver_free(env, buf, len + 1);
+ return 0;
+}
+
+int driver_mark_executable_output(const char* path) {
+ /* Windows has no Unix +x bit; file extension governs executability and
+ * NTFS ACLs are inherited from the parent directory. No-op success. */
+ (void)path;
+ return 0;
+}
+
+/* ============================================================
+ * Time
+ * ============================================================ */
+
+uint64_t driver_now_ns(void) {
+ /* QueryPerformanceCounter is monotonic across cores on every supported
+ * Windows version since Vista. */
+ LARGE_INTEGER freq;
+ LARGE_INTEGER ctr;
+ if (!QueryPerformanceFrequency(&freq) || freq.QuadPart <= 0) return 0;
+ if (!QueryPerformanceCounter(&ctr)) return 0;
+ /* Avoid 128-bit multiply: split ticks into integer-seconds and remainder. */
+ {
+ int64_t sec = ctr.QuadPart / freq.QuadPart;
+ int64_t rem = ctr.QuadPart % freq.QuadPart;
+ return (uint64_t)sec * 1000000000ull +
+ (uint64_t)((rem * 1000000000) / freq.QuadPart);
+ }
+}
+
+/* ============================================================
+ * load helpers
+ * ============================================================ */
+
+int driver_load_bytes(const CfreeFileIO* io, const char* tool, const char* path,
+ DriverLoad* out, CfreeSlice* in) {
+ out->loaded = 0;
+ out->fd.data = NULL;
+ out->fd.size = 0;
+ out->fd.token = NULL;
+ if (!io || !io->read_all) {
+ driver_errf(tool, "host file I/O unavailable");
+ return 1;
+ }
+ if (io->read_all(io->user, path, &out->fd) != CFREE_OK) {
+ driver_errf(tool, "failed to read: %.*s",
+ CFREE_SLICE_ARG(cfree_slice_cstr(path)));
+ return 1;
+ }
+ out->loaded = 1;
+ in->data = out->fd.data;
+ in->len = out->fd.size;
+ return 0;
+}
+
+void driver_release_bytes(const CfreeFileIO* io, DriverLoad* lf) {
+ if (!lf || !lf->loaded) return;
+ if (io && io->release) io->release(io->user, &lf->fd);
+ lf->loaded = 0;
+}
+
+/* ============================================================
+ * stdin / edit_temp / read_line
+ * ============================================================ */
+
+int driver_read_stdin(DriverEnv* e, uint8_t** out_data, size_t* out_size) {
+ size_t cap = 4096;
+ size_t len = 0;
+ uint8_t* buf = e->heap->alloc(e->heap, cap, 1);
+ HANDLE h = GetStdHandle(STD_INPUT_HANDLE);
+ if (!buf) return 0;
+ for (;;) {
+ DWORD n;
+ if (len == cap) {
+ size_t newcap = cap * 2;
+ uint8_t* nb = e->heap->realloc(e->heap, buf, cap, newcap, 1);
+ if (!nb) {
+ e->heap->free(e->heap, buf, cap);
+ return 0;
+ }
+ buf = nb;
+ cap = newcap;
+ }
+ if (!ReadFile(h, buf + len, (DWORD)(cap - len), &n, NULL)) {
+ if (GetLastError() == ERROR_BROKEN_PIPE) break; /* EOF on pipe */
+ e->heap->free(e->heap, buf, cap);
+ return 0;
+ }
+ if (n == 0) break;
+ len += n;
+ }
+ if (len < cap) {
+ uint8_t* shrunk = len ? e->heap->realloc(e->heap, buf, cap, len, 1) : NULL;
+ if (len && !shrunk) {
+ *out_data = buf;
+ *out_size = cap;
+ return 1;
+ }
+ if (!len) {
+ e->heap->free(e->heap, buf, cap);
+ buf = NULL;
+ } else {
+ buf = shrunk;
+ }
+ }
+ *out_data = buf;
+ *out_size = len;
+ return 1;
+}
+
+static int driver_write_handle_all(HANDLE h, const uint8_t* data, size_t n) {
+ size_t off = 0;
+ while (off < n) {
+ DWORD chunk = (n - off) > 0x40000000u ? 0x40000000u : (DWORD)(n - off);
+ DWORD wr = 0;
+ if (!WriteFile(h, data + off, chunk, &wr, NULL) || wr == 0) return 0;
+ off += wr;
+ }
+ return 1;
+}
+
+int driver_edit_temp(DriverEnv* e, const char* suffix, const uint8_t* initial,
+ size_t initial_size, uint8_t** out_data,
+ size_t* out_size) {
+ /* Windows temp-file dance:
+ * - GetTempPathW gives us %TMP%/%TEMP%/%USERPROFILE% with trailing '\\'.
+ * - GetTempFileNameW makes a unique "<dir>\\cfXXXX.tmp" path. We ignore
+ * the ".tmp" and append our requested suffix below by renaming.
+ * - We rename to "<base><suffix>" so the editor sees the right extension.
+ * - Editor is launched via system() so shell quoting / PATH lookup is
+ * handled by cmd.exe.
+ */
+ wchar_t tmp_dir[MAX_PATH + 1];
+ wchar_t tmp_path[MAX_PATH + 1];
+ DWORD got;
+ HANDLE h = INVALID_HANDLE_VALUE;
+ wchar_t* wsuffix = NULL;
+ wchar_t final_path[MAX_PATH + 64];
+ size_t final_len;
+ int ok = 0;
+ CfreeFileData fd_data;
+ const char* editor;
+ char* cmd = NULL;
+ int rc;
+ size_t cmd_cap;
+ char utf8_path[MAX_PATH * 4 + 1];
+ int utf8_len;
+
+ if (!out_data || !out_size) return 0;
+ *out_data = NULL;
+ *out_size = 0;
+
+ got = GetTempPathW(MAX_PATH + 1, tmp_dir);
+ if (got == 0 || got > MAX_PATH) return 0;
+ if (GetTempFileNameW(tmp_dir, L"cf", 0, tmp_path) == 0) return 0;
+
+ /* Build the final path = tmp_path with ".tmp" stripped + requested suffix.
+ * GetTempFileNameW already created the file at tmp_path; we MoveFileEx to
+ * rename it to final_path. */
+ {
+ size_t tplen = wcslen(tmp_path);
+ /* Strip the ".tmp" extension GetTempFileNameW appends. */
+ if (tplen >= 4 && tmp_path[tplen - 4] == L'.') tplen -= 4;
+ if (tplen >= sizeof(final_path) / sizeof(wchar_t) - 16) {
+ DeleteFileW(tmp_path);
+ return 0;
+ }
+ memcpy(final_path, tmp_path, tplen * sizeof(wchar_t));
+ final_path[tplen] = L'\0';
+ final_len = tplen;
+ if (suffix && *suffix) {
+ wsuffix = widen(suffix);
+ if (!wsuffix) {
+ DeleteFileW(tmp_path);
+ return 0;
+ }
+ {
+ size_t sl = wcslen(wsuffix);
+ if (final_len + sl + 1 >= sizeof(final_path) / sizeof(wchar_t)) {
+ free(wsuffix);
+ DeleteFileW(tmp_path);
+ return 0;
+ }
+ memcpy(final_path + final_len, wsuffix, sl * sizeof(wchar_t));
+ final_len += sl;
+ final_path[final_len] = L'\0';
+ }
+ free(wsuffix);
+ }
+ if (!MoveFileExW(tmp_path, final_path, MOVEFILE_REPLACE_EXISTING)) {
+ DeleteFileW(tmp_path);
+ return 0;
+ }
+ }
+
+ /* Open the renamed file and write initial contents. */
+ h = CreateFileW(final_path, GENERIC_WRITE, FILE_SHARE_READ, NULL,
+ TRUNCATE_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+ if (h == INVALID_HANDLE_VALUE) goto out;
+ if (initial_size &&
+ !driver_write_handle_all(h, initial ? initial : (const uint8_t*)"",
+ initial_size))
+ goto out;
+ CloseHandle(h);
+ h = INVALID_HANDLE_VALUE;
+
+ /* Convert final_path back to UTF-8 for the editor command. */
+ utf8_len = WideCharToMultiByte(CP_UTF8, 0, final_path, -1, utf8_path,
+ (int)sizeof(utf8_path), NULL, NULL);
+ if (utf8_len <= 0) goto out;
+
+ editor = getenv("VISUAL");
+ if (!editor || !*editor) editor = getenv("EDITOR");
+ if (!editor || !*editor) editor = "notepad";
+ cmd_cap = strlen(editor) + (size_t)utf8_len + 16;
+ cmd = (char*)malloc(cmd_cap);
+ if (!cmd) goto out;
+ /* Wrap the entire command in extra quotes so cmd.exe /S doesn't strip
+ * matching outer quotes when the editor path itself has spaces. */
+ rc = snprintf(cmd, cmd_cap, "\"\"%s\" \"%s\"\"", editor, utf8_path);
+ if (rc < 0 || (size_t)rc >= cmd_cap) goto out;
+ if (system(cmd) != 0) goto out;
+
+ fd_data.data = NULL;
+ fd_data.size = 0;
+ fd_data.token = NULL;
+ if (win_read_all(e, utf8_path, &fd_data) != CFREE_OK) goto out;
+ *out_data = (uint8_t*)fd_data.data;
+ *out_size = fd_data.size;
+ ok = 1;
+
+out:
+ if (h != INVALID_HANDLE_VALUE) CloseHandle(h);
+ if (cmd) free(cmd);
+ DeleteFileW(final_path);
+ return ok;
+}
+
+int driver_read_line(char* buf, size_t cap) {
+ size_t len = 0;
+ if (!buf || cap < 2) return -1;
+ for (;;) {
+ int c = fgetc(stdin);
+ if (c == EOF) {
+ buf[len] = '\0';
+ if (ferror(stdin)) return -1;
+ if (len == 0) return 0;
+ return (int)len;
+ }
+ if (c == '\n') {
+ /* Strip a trailing CR if present (CRLF line ending). */
+ if (len > 0 && buf[len - 1] == '\r') --len;
+ buf[len] = '\0';
+ return (int)len;
+ }
+ if (len + 1 < cap) buf[len++] = (char)c;
+ }
+}
+
+/* ============================================================
+ * dlsym via GetProcAddress over loaded modules
+ * ============================================================ */
+
+/* Snapshot the loaded modules at first call and remember them. We don't
+ * track DLL load/unload events; that's a reasonable trade-off for a JIT
+ * (the relevant DLLs -- ucrt, kernel32, the cfree binary -- are loaded
+ * before the JIT extern resolver runs). */
+static HMODULE g_dlsym_modules[256];
+static DWORD g_dlsym_count;
+static SRWLOCK g_dlsym_lock = SRWLOCK_INIT;
+static int g_dlsym_inited;
+
+static void dlsym_init_once(void) {
+ HANDLE proc;
+ DWORD need = 0;
+ AcquireSRWLockExclusive(&g_dlsym_lock);
+ if (g_dlsym_inited) {
+ ReleaseSRWLockExclusive(&g_dlsym_lock);
+ return;
+ }
+ proc = GetCurrentProcess();
+ if (EnumProcessModules(proc, g_dlsym_modules, sizeof(g_dlsym_modules),
+ &need)) {
+ DWORD have = need / (DWORD)sizeof(HMODULE);
+ g_dlsym_count =
+ have > (DWORD)(sizeof(g_dlsym_modules) / sizeof(HMODULE))
+ ? (DWORD)(sizeof(g_dlsym_modules) / sizeof(HMODULE))
+ : have;
+ } else {
+ /* Fall back to the executable module and the CRT/kernel essentials. */
+ g_dlsym_modules[0] = GetModuleHandleW(NULL);
+ g_dlsym_modules[1] = GetModuleHandleW(L"kernel32.dll");
+ g_dlsym_modules[2] = GetModuleHandleW(L"msvcrt.dll");
+ g_dlsym_modules[3] = GetModuleHandleW(L"ucrtbase.dll");
+ g_dlsym_count = 4;
+ }
+ g_dlsym_inited = 1;
+ ReleaseSRWLockExclusive(&g_dlsym_lock);
+}
+
+static void* win_dlsym(const char* name) {
+ DWORD i;
+ if (!name) return NULL;
+ dlsym_init_once();
+ /* Try the source-level name; if that misses and there's a leading '_'
+ * (Mach-O-mangled), try without it. */
+ AcquireSRWLockShared(&g_dlsym_lock);
+ for (i = 0; i < g_dlsym_count; ++i) {
+ void* p;
+ if (!g_dlsym_modules[i]) continue;
+ p = (void*)GetProcAddress(g_dlsym_modules[i], name);
+ if (p) {
+ ReleaseSRWLockShared(&g_dlsym_lock);
+ return p;
+ }
+ }
+ if (name[0] == '_' && name[1] != '\0') {
+ for (i = 0; i < g_dlsym_count; ++i) {
+ void* p;
+ if (!g_dlsym_modules[i]) continue;
+ p = (void*)GetProcAddress(g_dlsym_modules[i], name + 1);
+ if (p) {
+ ReleaseSRWLockShared(&g_dlsym_lock);
+ return p;
+ }
+ }
+ }
+ ReleaseSRWLockShared(&g_dlsym_lock);
+ return NULL;
+}
+
+void* driver_dlsym_resolver(void* user, CfreeSlice name_s) {
+ (void)user;
+ if (!name_s.s || name_s.len == 0) return NULL;
+ return win_dlsym(name_s.s);
+}
+
+/* ============================================================
+ * SIGINT shim via SetConsoleCtrlHandler
+ * ============================================================ */
+
+static void (*g_ctrlc_cb)(void*);
+static void* g_ctrlc_cb_user;
+
+static BOOL WINAPI ctrlc_trampoline(DWORD type) {
+ if (type == CTRL_C_EVENT || type == CTRL_BREAK_EVENT) {
+ if (g_ctrlc_cb) g_ctrlc_cb(g_ctrlc_cb_user);
+ return TRUE; /* handled */
+ }
+ return FALSE;
+}
+
+int driver_install_sigint(void (*cb)(void*), void* user) {
+ g_ctrlc_cb = cb;
+ g_ctrlc_cb_user = user;
+ return SetConsoleCtrlHandler(ctrlc_trampoline, TRUE) ? 0 : 1;
+}
+
+void driver_restore_sigint(void) {
+ SetConsoleCtrlHandler(ctrlc_trampoline, FALSE);
+ g_ctrlc_cb = NULL;
+ g_ctrlc_cb_user = NULL;
+}
+
+/* ============================================================
+ * Win32 CONTEXT <-> CfreeUnwindFrame marshalling
+ * ============================================================ */
+
+/* Inline per-arch marshalling: the Win32 dbg path is short enough that a
+ * separate uctx_*_windows.c isn't worth the extra TU. Only the host arch
+ * matters; cross-arch debug isn't a Win32 concern (the worker runs in our
+ * own process). */
+
+#if defined(_M_X64) || defined(__x86_64__)
+static void ctx_to_frame(const CONTEXT* c, CfreeUnwindFrame* f) {
+ memset(f, 0, sizeof(*f));
+ f->pc = (uint64_t)c->Rip;
+ f->cfa = (uint64_t)c->Rsp;
+ /* SysV DWARF mapping: rax=0, rdx=1, rcx=2, rbx=3, rsi=4, rdi=5, rbp=6,
+ * rsp=7, r8..r15=8..15. We use the same mapping so cfree's DWARF reader
+ * can interpret these directly. */
+ f->regs[0] = c->Rax;
+ f->regs[1] = c->Rdx;
+ f->regs[2] = c->Rcx;
+ f->regs[3] = c->Rbx;
+ f->regs[4] = c->Rsi;
+ f->regs[5] = c->Rdi;
+ f->regs[6] = c->Rbp;
+ f->regs[7] = c->Rsp;
+ f->regs[8] = c->R8;
+ f->regs[9] = c->R9;
+ f->regs[10] = c->R10;
+ f->regs[11] = c->R11;
+ f->regs[12] = c->R12;
+ f->regs[13] = c->R13;
+ f->regs[14] = c->R14;
+ f->regs[15] = c->R15;
+}
+
+static void frame_to_ctx(const CfreeUnwindFrame* f, CONTEXT* c) {
+ c->Rip = f->pc;
+ c->Rax = f->regs[0];
+ c->Rdx = f->regs[1];
+ c->Rcx = f->regs[2];
+ c->Rbx = f->regs[3];
+ c->Rsi = f->regs[4];
+ c->Rdi = f->regs[5];
+ c->Rbp = f->regs[6];
+ c->Rsp = f->regs[7];
+ c->R8 = f->regs[8];
+ c->R9 = f->regs[9];
+ c->R10 = f->regs[10];
+ c->R11 = f->regs[11];
+ c->R12 = f->regs[12];
+ c->R13 = f->regs[13];
+ c->R14 = f->regs[14];
+ c->R15 = f->regs[15];
+}
+#elif defined(_M_ARM64) || defined(__aarch64__)
+static void ctx_to_frame(const CONTEXT* c, CfreeUnwindFrame* f) {
+ unsigned i;
+ memset(f, 0, sizeof(*f));
+ f->pc = (uint64_t)c->Pc;
+ f->cfa = (uint64_t)c->Sp;
+ for (i = 0; i < 31; ++i) f->regs[i] = c->X[i];
+ f->regs[31] = c->Sp;
+}
+
+static void frame_to_ctx(const CfreeUnwindFrame* f, CONTEXT* c) {
+ unsigned i;
+ c->Pc = f->pc;
+ for (i = 0; i < 31; ++i) c->X[i] = f->regs[i];
+ c->Sp = f->regs[31];
+}
+#else
+static void ctx_to_frame(const CONTEXT* c, CfreeUnwindFrame* f) {
+ (void)c;
+ memset(f, 0, sizeof(*f));
+}
+static void frame_to_ctx(const CfreeUnwindFrame* f, CONTEXT* c) {
+ (void)f;
+ (void)c;
+}
+#endif
+
+/* Map a Win32 exception code to the POSIX-style signo the dbg session
+ * expects. Anything we don't recognize falls through as a generic SEGV. */
+static int exception_code_to_signo(DWORD code) {
+ switch (code) {
+ case EXCEPTION_BREAKPOINT:
+ case EXCEPTION_SINGLE_STEP:
+ return 5; /* SIGTRAP */
+ case EXCEPTION_ILLEGAL_INSTRUCTION:
+ case EXCEPTION_PRIV_INSTRUCTION:
+ return 4; /* SIGILL */
+ case EXCEPTION_INT_DIVIDE_BY_ZERO:
+ case EXCEPTION_INT_OVERFLOW:
+ case EXCEPTION_FLT_DIVIDE_BY_ZERO:
+ case EXCEPTION_FLT_OVERFLOW:
+ case EXCEPTION_FLT_UNDERFLOW:
+ case EXCEPTION_FLT_INVALID_OPERATION:
+ case EXCEPTION_FLT_DENORMAL_OPERAND:
+ case EXCEPTION_FLT_INEXACT_RESULT:
+ case EXCEPTION_FLT_STACK_CHECK:
+ return 8; /* SIGFPE */
+ case EXCEPTION_DATATYPE_MISALIGNMENT:
+ return 7; /* SIGBUS */
+ case EXCEPTION_ACCESS_VIOLATION:
+ case EXCEPTION_IN_PAGE_ERROR:
+ case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
+ case EXCEPTION_STACK_OVERFLOW:
+ default:
+ return 11; /* SIGSEGV */
+ }
+}
+
+/* ============================================================
+ * dbg_os: threads, events, exceptions, guarded_copy
+ * ============================================================ */
+
+static CfreeDbgSignalOps g_dbg_ops;
+static int g_dbg_ops_set;
+static void* g_dbg_session;
+static DWORD g_dbg_worker_tid;
+static int g_dbg_worker_tid_valid;
+static PVOID g_veh_cookie;
+
+/* Guarded-copy landing for the VEH path. Both __try/__except and a VEH
+ * fallback set g_guard_armed; if the VEH fires while it's set the
+ * exception is "translated" into a longjmp via the VEH's ability to
+ * rewrite the resume context. We use __try/__except for the primary
+ * path and treat the VEH armed-check as a backstop. */
+static __declspec(thread) int g_guard_armed;
+static __declspec(thread) jmp_buf g_guard_buf;
+
+static int win_dbg_caller_is_worker(void) {
+ return g_dbg_worker_tid_valid && GetCurrentThreadId() == g_dbg_worker_tid;
+}
+
+/* --- thread shim --- */
+
+typedef struct DbgWinThread {
+ HANDLE handle;
+ DWORD tid;
+ void (*fn)(void*);
+ void* arg;
+} DbgWinThread;
+
+static unsigned __stdcall dbg_thread_trampoline(void* p) {
+ DbgWinThread* t = (DbgWinThread*)p;
+ t->fn(t->arg);
+ return 0;
+}
+
+static CfreeStatus dbg_thread_start_win(void* user, void (*fn)(void*),
+ void* arg, void** thread_out) {
+ DbgWinThread* t;
+ uintptr_t h;
+ (void)user;
+ t = (DbgWinThread*)malloc(sizeof(*t));
+ if (!t) return CFREE_NOMEM;
+ t->fn = fn;
+ t->arg = arg;
+ /* _beginthreadex sets up the CRT TLS for the new thread; pure CreateThread
+ * leaves the CRT in an undefined state for code that calls printf/malloc. */
+ h = _beginthreadex(NULL, 0, dbg_thread_trampoline, t, 0, (unsigned*)&t->tid);
+ if (h == 0) {
+ free(t);
+ return CFREE_ERR;
+ }
+ t->handle = (HANDLE)h;
+ g_dbg_worker_tid = t->tid;
+ g_dbg_worker_tid_valid = 1;
+ *thread_out = t;
+ return CFREE_OK;
+}
+
+static void dbg_thread_join_win(void* user, void* thread) {
+ DbgWinThread* t = (DbgWinThread*)thread;
+ (void)user;
+ if (!t) return;
+ WaitForSingleObject(t->handle, INFINITE);
+ CloseHandle(t->handle);
+ g_dbg_worker_tid_valid = 0;
+ free(t);
+}
+
+/* Interrupt the worker by suspending it, snapshotting its CONTEXT,
+ * calling on_fault on the caller thread (the REPL), and writing back any
+ * register edits. This differs from POSIX (where the signal handler runs
+ * the on_fault on the worker), but the observable contract -- "the worker
+ * is stopped while on_fault runs and resumes with the edited frame" --
+ * matches. Locking quirks of SuspendThread aren't a concern here: the
+ * worker is, by construction, running JIT'd user code that holds no host
+ * locks. */
+static CfreeStatus dbg_thread_interrupt_win(void* user, void* thread) {
+ DbgWinThread* t = (DbgWinThread*)thread;
+ CONTEXT ctx;
+ CfreeUnwindFrame frame;
+ CfreeStatus rc;
+ (void)user;
+ if (!t || !g_dbg_ops_set || !g_dbg_ops.on_fault) return CFREE_INVALID;
+ if (SuspendThread(t->handle) == (DWORD)-1) return CFREE_ERR;
+ memset(&ctx, 0, sizeof(ctx));
+ ctx.ContextFlags = CONTEXT_FULL;
+ if (!GetThreadContext(t->handle, &ctx)) {
+ ResumeThread(t->handle);
+ return CFREE_ERR;
+ }
+ ctx_to_frame(&ctx, &frame);
+ rc = g_dbg_ops.on_fault(g_dbg_session, DBG_WIN_INTERRUPT_SIGNO, &frame);
+ if (rc == CFREE_OK) {
+ frame_to_ctx(&frame, &ctx);
+ SetThreadContext(t->handle, &ctx);
+ }
+ ResumeThread(t->handle);
+ return rc;
+}
+
+/* --- event shim --- */
+
+static CfreeStatus dbg_event_new_win(void* user, void** event_out) {
+ HANDLE h;
+ (void)user;
+ /* Manual-reset so a signaler that races ahead of the waiter doesn't lose
+ * the wake-up; the dbg core explicitly resets via event_reset. */
+ h = CreateEventW(NULL, TRUE, FALSE, NULL);
+ if (!h) return CFREE_ERR;
+ *event_out = (void*)h;
+ return CFREE_OK;
+}
+
+static void dbg_event_free_win(void* user, void* ev) {
+ (void)user;
+ if (ev) CloseHandle((HANDLE)ev);
+}
+
+static CfreeStatus dbg_event_wait_win(void* user, void* ev) {
+ (void)user;
+ if (WaitForSingleObject((HANDLE)ev, INFINITE) != WAIT_OBJECT_0)
+ return CFREE_ERR;
+ /* Auto-reset semantics expected by dbg_event_wait on POSIX (it clears
+ * the flag after consuming). Mirror that here so the contract holds. */
+ ResetEvent((HANDLE)ev);
+ return CFREE_OK;
+}
+
+static CfreeStatus dbg_event_signal_win(void* user, void* ev) {
+ (void)user;
+ return SetEvent((HANDLE)ev) ? CFREE_OK : CFREE_ERR;
+}
+
+static CfreeStatus dbg_event_reset_win(void* user, void* ev) {
+ (void)user;
+ return ResetEvent((HANDLE)ev) ? CFREE_OK : CFREE_ERR;
+}
+
+/* --- vectored exception handler --- */
+
+/* Recovery thunk: when guarded_copy's __try/__except is unavailable (or
+ * for natural faults raised inside a guarded region) we hand control back
+ * to the longjmp target by rewriting the resume context. */
+static LONG WINAPI dbg_veh(EXCEPTION_POINTERS* ep) {
+ DWORD code = ep->ExceptionRecord->ExceptionCode;
+ CfreeUnwindFrame frame;
+ CfreeStatus rc;
+ int signo;
+
+ /* Filter out C++-style exceptions / debug strings we don't care about. */
+ if (code == 0x406D1388u /* MS_VC_EXCEPTION (SetThreadName) */ ||
+ code == 0xE06D7363u /* CXX EH */ || code == DBG_PRINTEXCEPTION_C ||
+ code == DBG_PRINTEXCEPTION_WIDE_C)
+ return EXCEPTION_CONTINUE_SEARCH;
+
+ /* SEGV/BUS during a guarded_copy: bail out without involving the
+ * session. The VEH backstop only fires if SEH/__try wasn't compiled in
+ * for the function that armed the guard. */
+ if (g_guard_armed &&
+ (code == EXCEPTION_ACCESS_VIOLATION || code == EXCEPTION_IN_PAGE_ERROR ||
+ code == EXCEPTION_DATATYPE_MISALIGNMENT)) {
+ g_guard_armed = 0;
+ longjmp(g_guard_buf, 1);
+ /* not reached */
+ return EXCEPTION_CONTINUE_EXECUTION;
+ }
+
+ if (!win_dbg_caller_is_worker() || !g_dbg_ops_set || !g_dbg_ops.on_fault)
+ return EXCEPTION_CONTINUE_SEARCH;
+
+ signo = exception_code_to_signo(code);
+ ctx_to_frame(ep->ContextRecord, &frame);
+ rc = g_dbg_ops.on_fault(g_dbg_session, signo, &frame);
+ if (rc != CFREE_OK) return EXCEPTION_CONTINUE_SEARCH;
+ frame_to_ctx(&frame, ep->ContextRecord);
+ return EXCEPTION_CONTINUE_EXECUTION;
+}
+
+static CfreeStatus dbg_signals_install_win(void* user,
+ const CfreeDbgSignalOps* ops,
+ void* session) {
+ (void)user;
+ if (g_veh_cookie) return CFREE_ERR;
+ g_dbg_ops = *ops;
+ g_dbg_ops_set = 1;
+ g_dbg_session = session;
+ /* First==1 ensures we run before any other VEH and before the system's
+ * default last-chance handler. */
+ g_veh_cookie = AddVectoredExceptionHandler(1, dbg_veh);
+ if (!g_veh_cookie) {
+ memset(&g_dbg_ops, 0, sizeof(g_dbg_ops));
+ g_dbg_ops_set = 0;
+ g_dbg_session = NULL;
+ return CFREE_ERR;
+ }
+ return CFREE_OK;
+}
+
+static void dbg_signals_uninstall_win(void* user) {
+ (void)user;
+ if (g_veh_cookie) {
+ RemoveVectoredExceptionHandler(g_veh_cookie);
+ g_veh_cookie = NULL;
+ }
+ memset(&g_dbg_ops, 0, sizeof(g_dbg_ops));
+ g_dbg_ops_set = 0;
+ g_dbg_session = NULL;
+}
+
+/* --- W^X transitions --- */
+
+static size_t page_floor(size_t v, size_t pg) { return v & ~(pg - 1); }
+static size_t page_ceil(size_t v, size_t pg) {
+ return (v + pg - 1) & ~(pg - 1);
+}
+
+static CfreeStatus dbg_code_write_begin_win(void* user, void* runtime_addr,
+ size_t n, void** write_out) {
+ size_t pg;
+ uintptr_t a;
+ uintptr_t base;
+ size_t span;
+ DWORD old;
+ (void)user;
+ if (!runtime_addr || !n || !write_out) return CFREE_INVALID;
+ /* Dual-mapped reservation: write through the alias, no protect flip. */
+ if (exec_dual_lookup_w(runtime_addr, n, write_out) == 0) return CFREE_OK;
+ /* Single-mapping fallback: transient PAGE_EXECUTE_READWRITE. */
+ pg = driver_host_page_size_win();
+ a = (uintptr_t)runtime_addr;
+ base = page_floor(a, pg);
+ span = page_ceil((a - base) + n, pg);
+ if (!VirtualProtect((void*)base, span, PAGE_EXECUTE_READWRITE, &old))
+ return CFREE_ERR;
+ *write_out = runtime_addr;
+ return CFREE_OK;
+}
+
+static void dbg_code_write_end_win(void* user, void* runtime_addr, size_t n) {
+ void* w;
+ size_t pg;
+ uintptr_t a;
+ uintptr_t base;
+ size_t span;
+ DWORD old;
+ (void)user;
+ if (exec_dual_lookup_w(runtime_addr, n, &w) == 0) return; /* nothing to flip */
+ pg = driver_host_page_size_win();
+ a = (uintptr_t)runtime_addr;
+ base = page_floor(a, pg);
+ span = page_ceil((a - base) + n, pg);
+ VirtualProtect((void*)base, span, PAGE_EXECUTE_READ, &old);
+}
+
+static void dbg_flush_icache_win(void* user, void* runtime_addr, size_t n) {
+ (void)user;
+ FlushInstructionCache(GetCurrentProcess(), runtime_addr, n);
+}
+
+/* --- guarded copy via SEH __try/__except --- */
+
+static CfreeStatus dbg_guarded_copy_win(void* user, void* dst, const void* src,
+ size_t n) {
+ (void)user;
+ /* MinGW-w64 supports __try/__except natively for the SEH model on x86_64
+ * and arm64 (GCC: -fseh-exceptions; clang: built-in). The VEH backstop
+ * above catches it if the toolchain ever falls back to setjmp-based EH. */
+ if (setjmp(g_guard_buf) != 0) {
+ g_guard_armed = 0;
+ return CFREE_ERR;
+ }
+ g_guard_armed = 1;
+ __try {
+ memcpy(dst, src, n);
+ g_guard_armed = 0;
+ return CFREE_OK;
+ } __except (EXCEPTION_EXECUTE_HANDLER) {
+ g_guard_armed = 0;
+ return CFREE_ERR;
+ }
+}
+
+/* --- call_with_catch / thread_abort (longjmp-based) --- */
+
+static __declspec(thread) jmp_buf g_dbg_abort_buf;
+
+static int dbg_call_with_catch_win(void* user, void (*fn)(void*), void* arg) {
+ (void)user;
+ if (setjmp(g_dbg_abort_buf) == 0) {
+ fn(arg);
+ return 0;
+ }
+ return 1;
+}
+
+static void dbg_thread_abort_win(void* user) {
+ (void)user;
+ longjmp(g_dbg_abort_buf, 1);
+}
+
+static CfreeDbgOs g_dbg_os_win = {
+ .thread_start = dbg_thread_start_win,
+ .thread_join = dbg_thread_join_win,
+ .thread_interrupt = dbg_thread_interrupt_win,
+ .event_new = dbg_event_new_win,
+ .event_free = dbg_event_free_win,
+ .event_wait = dbg_event_wait_win,
+ .event_signal = dbg_event_signal_win,
+ .event_reset = dbg_event_reset_win,
+ .signals_install = dbg_signals_install_win,
+ .signals_uninstall = dbg_signals_uninstall_win,
+ .interrupt_signo = DBG_WIN_INTERRUPT_SIGNO,
+ .code_write_begin = dbg_code_write_begin_win,
+ .code_write_end = dbg_code_write_end_win,
+ .flush_icache = dbg_flush_icache_win,
+ .guarded_copy = dbg_guarded_copy_win,
+ .call_with_catch = dbg_call_with_catch_win,
+ .thread_abort = dbg_thread_abort_win,
+ .user = NULL,
+};
+
+/* ============================================================
+ * jit_tls (FlsAlloc with per-thread dtor)
+ * ============================================================ */
+
+/* The TLV thunk's contract (src/jit/tlv_thunk.h): the first 8 bytes of
+ * ctx must be a function pointer the asm calls with arg0 == ctx and
+ * expects back arg0 == TLS block. We satisfy that by making `get_block`
+ * the first field. */
+typedef struct JitTlsCtxWin {
+ void* (*get_block)(void* ctx);
+ DWORD fls_index;
+ size_t image_size;
+ size_t image_filesz;
+ size_t align;
+ void* init_bytes;
+} JitTlsCtxWin;
+
+static void __stdcall jit_tls_thread_dtor(PVOID block) {
+ /* FlsAlloc destructor: runs on the thread that's exiting, OR when the
+ * FLS index is freed via FlsFree (for live threads' blocks). */
+ if (block) free(block);
+}
+
+static void* jit_tls_alloc_block_win(JitTlsCtxWin* ctx) {
+ size_t a = ctx->align ? ctx->align : sizeof(void*);
+ size_t sz;
+ void* block;
+ if (a < sizeof(void*)) a = sizeof(void*);
+ sz = (ctx->image_size + a - 1u) & ~(a - 1u);
+ if (sz == 0) sz = a;
+ /* _aligned_malloc is the MSVCRT/UCRT counterpart to aligned_alloc; the
+ * matching free is _aligned_free, but only the FLS destructor frees
+ * these blocks and it uses plain free. So allocate with malloc-style
+ * alignment instead: overallocate and align manually -- but FLS dtor
+ * needs to know the original pointer. Simpler: require align <=
+ * MEMORY_ALLOCATION_ALIGNMENT (16 on Win64), which the TLS images we
+ * see in practice satisfy. */
+ (void)a;
+ block = malloc(sz);
+ if (!block) return NULL;
+ if (ctx->image_filesz && ctx->init_bytes)
+ memcpy(block, ctx->init_bytes, ctx->image_filesz);
+ if (ctx->image_size > ctx->image_filesz)
+ memset((char*)block + ctx->image_filesz, 0,
+ ctx->image_size - ctx->image_filesz);
+ return block;
+}
+
+static void* jit_tls_get_block_win(void* ctx_v) {
+ JitTlsCtxWin* ctx = (JitTlsCtxWin*)ctx_v;
+ void* block = FlsGetValue(ctx->fls_index);
+ if (block) return block;
+ block = jit_tls_alloc_block_win(ctx);
+ if (!block) {
+ fprintf(stderr,
+ "cfree run: out of memory allocating per-thread TLS block\n");
+ abort();
+ }
+ if (!FlsSetValue(ctx->fls_index, block)) {
+ fprintf(stderr, "cfree run: FlsSetValue failed in TLV thunk\n");
+ abort();
+ }
+ return block;
+}
+
+static void* jit_tls_ctx_new_win(void* user, const void* init_bytes,
+ size_t image_filesz, size_t image_size,
+ size_t align) {
+ JitTlsCtxWin* ctx;
+ (void)user;
+ ctx = (JitTlsCtxWin*)malloc(sizeof(*ctx));
+ if (!ctx) return NULL;
+ ctx->get_block = jit_tls_get_block_win;
+ ctx->image_size = image_size;
+ ctx->image_filesz = image_filesz;
+ ctx->align = align ? align : sizeof(void*);
+ ctx->init_bytes = NULL;
+ if (image_filesz && init_bytes) {
+ ctx->init_bytes = malloc(image_filesz);
+ if (!ctx->init_bytes) {
+ free(ctx);
+ return NULL;
+ }
+ memcpy(ctx->init_bytes, init_bytes, image_filesz);
+ }
+ ctx->fls_index = FlsAlloc(jit_tls_thread_dtor);
+ if (ctx->fls_index == FLS_OUT_OF_INDEXES) {
+ free(ctx->init_bytes);
+ free(ctx);
+ return NULL;
+ }
+ return ctx;
+}
+
+static void jit_tls_ctx_destroy_win(void* user, void* ctx_v) {
+ JitTlsCtxWin* ctx = (JitTlsCtxWin*)ctx_v;
+ (void)user;
+ if (!ctx) return;
+ /* FlsFree runs the destructor for every live thread's block before
+ * releasing the index. The calling thread's block is reaped here too. */
+ FlsFree(ctx->fls_index);
+ free(ctx->init_bytes);
+ free(ctx);
+}
+
+static CfreeJitTls g_jit_tls_win = {
+ .ctx_new = jit_tls_ctx_new_win,
+ .ctx_destroy = jit_tls_ctx_destroy_win,
+ .user = NULL,
+};
+
+/* ============================================================
+ * host target
+ * ============================================================ */
+
+static CfreeArchKind host_arch_self_win(void) {
+#if defined(_M_X64) || defined(__x86_64__)
+ return CFREE_ARCH_X86_64;
+#elif defined(_M_ARM64) || defined(__aarch64__)
+ return CFREE_ARCH_ARM_64;
+#elif defined(_M_IX86) || defined(__i386__)
+ return CFREE_ARCH_X86_32;
+#elif defined(_M_ARM) || defined(__arm__)
+ return CFREE_ARCH_ARM_32;
+#else
+ return CFREE_ARCH_X86_64;
+#endif
+}
+
+CfreeTarget driver_host_target(void) {
+ CfreeTarget t;
+ t.arch = host_arch_self_win();
+ t.os = CFREE_OS_WINDOWS;
+ t.obj = CFREE_OBJ_COFF;
+ t.ptr_size = (uint8_t)sizeof(void*);
+ t.ptr_align = (uint8_t)sizeof(void*);
+ t.big_endian = 0;
+ t.pic = driver_default_pic(t.obj, t.os);
+ t.code_model = CFREE_CM_DEFAULT;
+ return t;
+}
+
+/* ============================================================
+ * env wiring
+ * ============================================================ */
+
+static char g_cache_dir_win[4096];
+
+void driver_env_init(DriverEnv* e) {
+ e->heap = &g_heap_libc;
+ e->diag = &g_diag_stderr;
+ e->file_io.read_all = win_read_all;
+ e->file_io.release = win_release;
+ e->file_io.open_writer = win_open_writer;
+ e->file_io.user = e;
+
+ g_execmem_win.page_size = driver_host_page_size_win();
+ g_execmem_win.reserve = execmem_reserve_win;
+ g_execmem_win.protect = execmem_protect_win;
+ g_execmem_win.release = execmem_release_win;
+ g_execmem_win.flush_icache = execmem_flush_icache_win;
+ g_execmem_win.user = NULL;
+ e->execmem = &g_execmem_win;
+
+ e->dbg_os = &g_dbg_os_win;
+ e->jit_tls = &g_jit_tls_win;
+ e->metrics = NULL;
+
+ {
+ /* XDG_CACHE_HOME wins if set (cross-platform tooling convention),
+ * otherwise fall back to %LOCALAPPDATA%\\cfree, otherwise a
+ * build-tree-local path. */
+ const char* xdg = getenv("XDG_CACHE_HOME");
+ const char* lap = getenv("LOCALAPPDATA");
+ if (xdg && *xdg) {
+ snprintf(g_cache_dir_win, sizeof(g_cache_dir_win), "%s/cfree", xdg);
+ } else if (lap && *lap) {
+ snprintf(g_cache_dir_win, sizeof(g_cache_dir_win), "%s\\cfree", lap);
+ } else {
+ snprintf(g_cache_dir_win, sizeof(g_cache_dir_win), "build\\cfree-cache");
+ }
+ g_cache_dir_win[sizeof(g_cache_dir_win) - 1] = '\0';
+ e->cache_dir = g_cache_dir_win;
+ }
+
+ {
+ const char* sde = getenv("SOURCE_DATE_EPOCH");
+ if (sde && *sde) {
+ char* endp = NULL;
+ long long v = strtoll(sde, &endp, 10);
+ e->now = (endp != sde && v >= 0) ? (int64_t)v : (int64_t)-1;
+ } else {
+ time_t t = time(NULL);
+ e->now = (t == (time_t)-1) ? (int64_t)-1 : (int64_t)t;
+ }
+ }
+}
+
+void driver_env_fini(DriverEnv* e) {
+ (void)e;
+}
+
+CfreeContext driver_env_to_context(const DriverEnv* e) {
+ CfreeContext c;
+ c.heap = e->heap;
+ c.file_io = &e->file_io;
+ c.diag = e->diag;
+ c.metrics = e->metrics;
+ c.now = e->now;
+ return c;
+}
+
+CfreeJitHost driver_env_to_jit_host(const DriverEnv* e) {
+ CfreeJitHost h;
+ h.execmem = e->execmem;
+ h.tls = e->jit_tls;
+ return h;
+}
+
+CfreeDbgHost driver_env_to_dbg_host(const DriverEnv* e) {
+ CfreeDbgHost h;
+ h.os = e->dbg_os;
+ return h;
+}
diff --git a/mk/env.mk b/mk/env.mk
@@ -0,0 +1,176 @@
+# mk/env.mk
+# ===========================================================================
+# Host environment detection and driver/env source selection.
+#
+# This file is the *only* place in the build system that branches on the
+# host OS or arch. It produces the variables below; the rest of the build
+# consumes them directly without re-deriving anything from `uname`.
+#
+# Contract:
+#
+# Detection:
+# HOST_UNAME raw `uname -s` (Darwin / Linux / FreeBSD /
+# MINGW64_NT-* / MSYS_NT-* / CYGWIN_NT-*)
+# HOST_ARCH_RAW raw `uname -m`
+# HOST_OS normalized OS tag: darwin | linux | freebsd
+# | windows. Unknown is a hard error.
+# HOST_ARCH normalized arch tag: x86_64 | aarch64 | rv64.
+# Unknown is a hard error.
+#
+# Host SDK (Darwin only; empty elsewhere so `$(HOST_SYSROOT_CFLAGS)` is
+# still safe to splice into any command line):
+# HOST_SDK_PATH the resolved -isysroot path, or empty
+# HOST_SYSROOT_CFLAGS `-isysroot <path>` or empty
+# HOST_SYSROOT_LDFLAGS `-isysroot <path>` or empty
+#
+# driver/env wiring:
+# DRIVER_ENV_OS_CFLAGS feature-test macros required by the per-OS env
+# TU before any libc header is included
+# (-D_XOPEN_SOURCE, -D_GNU_SOURCE, -D_WIN32_WINNT,
+# ...)
+# DRIVER_ENV_SRCS exact set of driver/env/*.c files to compile
+# into the host binary. One file per OS, one
+# per arch (icache), and on POSIX one per
+# (arch, OS) for ucontext marshalling. Removing
+# OS/arch ifdefs from the impl is the explicit
+# point of this layout.
+#
+# Link surface:
+# HOST_LDLIBS extra libraries the host link needs because of
+# env choices (-lpsapi on Windows for
+# EnumProcessModules). The main Makefile splices
+# this onto the binary's link line.
+#
+# Not produced here: HOST_OPTFLAGS / HOST_MODE_*FLAGS. Those are
+# build-mode (debug vs release) concerns, not env concerns; they live in
+# the main Makefile.
+
+HOST_UNAME := $(shell uname -s)
+HOST_ARCH_RAW := $(shell uname -m)
+
+# ---- HOST_OS ---------------------------------------------------------------
+
+ifeq ($(HOST_UNAME),Darwin)
+HOST_OS := darwin
+else ifeq ($(HOST_UNAME),Linux)
+HOST_OS := linux
+else ifeq ($(HOST_UNAME),FreeBSD)
+HOST_OS := freebsd
+else ifneq ($(filter MINGW% MSYS% CYGWIN%,$(HOST_UNAME)),)
+HOST_OS := windows
+else
+$(error env.mk: unsupported HOST_UNAME=$(HOST_UNAME); see driver/env/windows.c for the Windows port skeleton)
+endif
+
+# ---- HOST_ARCH -------------------------------------------------------------
+
+ifneq ($(filter $(HOST_ARCH_RAW),x86_64 amd64),)
+HOST_ARCH := x86_64
+else ifneq ($(filter $(HOST_ARCH_RAW),aarch64 arm64),)
+HOST_ARCH := aarch64
+else ifneq ($(filter $(HOST_ARCH_RAW),riscv64),)
+HOST_ARCH := rv64
+else
+$(error env.mk: unsupported HOST_ARCH_RAW=$(HOST_ARCH_RAW))
+endif
+
+# ---- host SDK (Darwin only) -----------------------------------------------
+# -isysroot is a Darwin-only flag and xcrun lives there too. On Linux /
+# FreeBSD / Windows we leave the vars empty so the host link/compile lines
+# stay well-formed when they splice $(HOST_SYSROOT_*FLAGS).
+
+HOST_SDK_PATH :=
+HOST_SYSROOT_CFLAGS :=
+HOST_SYSROOT_LDFLAGS :=
+ifeq ($(HOST_OS),darwin)
+HOST_SDK_PATH := $(shell xcrun --show-sdk-path 2>/dev/null)
+ifneq ($(HOST_SDK_PATH),)
+HOST_SYSROOT_CFLAGS := -isysroot $(HOST_SDK_PATH)
+HOST_SYSROOT_LDFLAGS := -isysroot $(HOST_SDK_PATH)
+endif
+endif
+
+# ---- per-OS env wiring -----------------------------------------------------
+
+DRIVER_ENV_OS_CFLAGS :=
+DRIVER_ENV_HINT_SRC :=
+HOST_LDLIBS :=
+
+ifeq ($(HOST_OS),darwin)
+DRIVER_ENV_OS_CFLAGS := -D_XOPEN_SOURCE=600 -D_DARWIN_C_SOURCE=1
+DRIVER_ENV_OS_SRC := driver/env/macos.c
+else ifeq ($(HOST_OS),linux)
+DRIVER_ENV_OS_CFLAGS := -D_GNU_SOURCE=1
+DRIVER_ENV_OS_SRC := driver/env/linux.c
+ifeq ($(HOST_ARCH),x86_64)
+DRIVER_ENV_HINT_SRC := driver/env/linux_exec_hint_x86_64.c
+else
+DRIVER_ENV_HINT_SRC := driver/env/linux_exec_hint_default.c
+endif
+else ifeq ($(HOST_OS),freebsd)
+DRIVER_ENV_OS_SRC := driver/env/freebsd.c
+else ifeq ($(HOST_OS),windows)
+# windows.c subsumes posix.c / posix_dbg.c / jit_tls_posix.c and folds in
+# its own CONTEXT-based register marshalling -- there's no POSIX overlap
+# worth sharing. EnumProcessModules pulls in -lpsapi.
+DRIVER_ENV_OS_CFLAGS := -D_WIN32_WINNT=0x0601
+DRIVER_ENV_OS_SRC := driver/env/windows.c
+HOST_LDLIBS += -lpsapi
+endif
+
+# ---- per-arch icache flush TU ---------------------------------------------
+
+ifeq ($(HOST_ARCH),x86_64)
+DRIVER_ENV_ICACHE_SRC := driver/env/icache_x86.c
+else ifeq ($(HOST_ARCH),aarch64)
+DRIVER_ENV_ICACHE_SRC := driver/env/icache_arm.c
+else ifeq ($(HOST_ARCH),rv64)
+DRIVER_ENV_ICACHE_SRC := driver/env/icache_riscv.c
+endif
+
+# ---- per-(arch, OS) ucontext marshalling (POSIX only) ---------------------
+
+DRIVER_ENV_UCTX_SRC :=
+ifeq ($(HOST_OS),darwin)
+ifeq ($(HOST_ARCH),aarch64)
+DRIVER_ENV_UCTX_SRC := driver/env/uctx_aarch64_macos.c
+endif
+else ifeq ($(HOST_OS),linux)
+ifeq ($(HOST_ARCH),aarch64)
+DRIVER_ENV_UCTX_SRC := driver/env/uctx_aarch64_linux.c
+else ifeq ($(HOST_ARCH),x86_64)
+DRIVER_ENV_UCTX_SRC := driver/env/uctx_x86_64_linux.c
+else ifeq ($(HOST_ARCH),rv64)
+DRIVER_ENV_UCTX_SRC := driver/env/uctx_rv64_linux.c
+endif
+endif
+
+ifneq ($(HOST_OS),windows)
+ifeq ($(DRIVER_ENV_UCTX_SRC),)
+$(error env.mk: no ucontext marshalling for HOST_OS=$(HOST_OS) HOST_ARCH=$(HOST_ARCH))
+endif
+endif
+
+# ---- DRIVER_ENV_SRCS -------------------------------------------------------
+# common.c is the libc-pure floor shared by every host (heap/diag/printf/
+# string helpers; no syscalls). POSIX hosts layer on the shared POSIX
+# scaffolding (file I/O, mkdir, sigint, exec_dual registry, signals,
+# pthread TLS) plus the per-OS file and the per-(arch, OS) uctx TU.
+# Windows has no POSIX overlap; windows.c is the entire host surface.
+
+ifeq ($(HOST_OS),windows)
+DRIVER_ENV_SRCS := \
+ driver/env/common.c \
+ $(DRIVER_ENV_OS_SRC) \
+ $(DRIVER_ENV_ICACHE_SRC)
+else
+DRIVER_ENV_SRCS := \
+ driver/env/common.c \
+ driver/env/posix.c \
+ driver/env/posix_dbg.c \
+ driver/env/jit_tls_posix.c \
+ $(DRIVER_ENV_OS_SRC) \
+ $(DRIVER_ENV_HINT_SRC) \
+ $(DRIVER_ENV_ICACHE_SRC) \
+ $(DRIVER_ENV_UCTX_SRC)
+endif