commit 128504789846f077dc83cd5a54b2d05dbf9083a0
parent f20d82f2139cc962ceffd8060d2e54b39e8c329f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 9 Jun 2026 08:13:24 -0700
windows: cross-compile kit + run cc/JIT natively on aarch64-windows
Bring up kit as a self-hosted Windows toolchain: cross-compile the kit
binary on the dev host (build/kit cc -target aarch64-windows) into a
PE/COFF kit.exe, then run `kit cc` and `kit run` (JIT) on a Windows VM.
Verified on an aarch64 Windows 11 VM: kit.exe --help; `kit cc hello.c`
-> a working hello.exe (correct stdout + exit code); and `kit run`
(JIT) of compute / data / extern-call / puts programs (libc I/O via
dlsym).
Fixes, each a real gap surfaced by the bring-up:
- c frontend: honor GCC asm-label symbol renames on declarations
(`extern T f(...) __asm__("alt")`). Parsed before but ignored except
for register locals; the emitted/referenced linkage symbol now uses
the label (Decl.asm_name) while the C name stays for diagnostics.
mingw's <time.h> redirects e.g. time->_time64 this way; without it
the link had an undefined `time`.
- coff: honor the Microsoft short-import NameType field (was
`(void)name_type`). NOPREFIX/UNDECORATE strip decoration; EXPORTAS
carries the real DLL export name separately. The PE hint/name table
now uses that import name, not the local symbol -- UCRT aliases e.g.
local `__msvcrt_assert` onto export `_assert`; emitting the alias
name made kit.exe fail to load (STATUS_ENTRYPOINT_NOT_FOUND).
- driver/lib/hosted.c: add libwinpthread to the Windows hosted link.
<time.h>'s pthread_time.h inline wrappers reference nanosleep64 /
clock_*64, which live there (llvm-mingw ships it as a core lib);
static archive, so it adds no runtime DLL dependency.
- rt/include/setjmp.h: on Windows, asm-rename setjmp/longjmp onto
mingw's non-SEH __mingw_setjmp/__mingw_longjmp (libmingwex). kit's
freestanding code wants pure register save/restore, and mingw exposes
no bare `setjmp` symbol (it is a macro over _setjmpex/__mingw_setjmp).
- driver/env/windows.c:
- gate the SEH __try guarded-copy under !defined(__kit__) (kit's C
frontend implements no SEH); rely on the VEH backstop already there.
- JIT dlsym fallback for the libucrt-static __local_stdio_{printf,
scanf}_options helpers (referenced by mingw's printf/scanf inlines,
not DLL-exported); hand JIT'd code kit.exe's own copies.
- map the JIT execmem dual-map's runtime/exec view FILE_MAP_WRITE so
writable runtime segments (an import GOT, .data) can be
VirtualProtect'd PAGE_READWRITE. A FILE_MAP_EXECUTE-only view
rejects RW (err 87), which broke `kit run` of any program with an
external call or writable global.
- scripts/windows_cross.sh: reproducible cross-build harness (toolchain
wrappers + the HOST_OS=windows make invocation).
Known gap: printf-family via `kit run` still needs libucrt's static
`printf` (not a DLL export) and the JIT links no static archives; AOT
`kit cc` printf works fully, as do JIT puts()/external calls.
Host regression: test-coff/link/elf/cg-api/parse/toy green. test-macho
36_tls_basic/J is a pre-existing JIT-TLS failure (confirmed failing at
HEAD via a clean worktree build).
Diffstat:
14 files changed, 275 insertions(+), 14 deletions(-)
diff --git a/driver/env/windows.c b/driver/env/windows.c
@@ -218,7 +218,15 @@ static KitStatus execmem_reserve_dual_win(size_t size, KitExecMemRegion* out) {
CloseHandle(map);
return KIT_NOMEM;
}
- r = MapViewOfFile(map, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, size);
+ /* Include FILE_MAP_WRITE in the runtime/exec view's mapping access. A view's
+ * VirtualProtect ceiling is bounded by its map-access flags, not just the
+ * section's max protection: a view mapped READ|EXECUTE can be reprotected to
+ * R/RX/RO but NOT to PAGE_READWRITE (err 87). JIT images carry writable
+ * runtime segments (an import GOT, .data/.bss) whose final perms are RW, so
+ * the exec view must permit write to be VirtualProtect'd RW. Per-segment page
+ * protection still enforces W^X (code stays RX, never writable). */
+ r = MapViewOfFile(map, FILE_MAP_READ | FILE_MAP_WRITE | FILE_MAP_EXECUTE, 0, 0,
+ size);
if (!r) {
UnmapViewOfFile(w);
CloseHandle(map);
@@ -1199,10 +1207,29 @@ static void* win_dlsym(const char* name) {
return NULL;
}
+/* mingw's <stdio.h> printf/scanf inline wrappers call these per-module option
+ * helpers, whose out-of-line definitions live ONLY in libucrt.a — no DLL
+ * exports them. AOT links them in; the JIT resolves externs via dlsym over
+ * loaded DLLs (win_dlsym), which therefore can't find them, leaving a
+ * printf-using JIT program with an undefined __local_stdio_printf_options.
+ * kit.exe statically links libucrt.a, and referencing these here pulls their
+ * definitions into our own image, so we can hand JIT'd code our copies. The
+ * options storage only ever holds the default (0) flags, so sharing it between
+ * the host and the JIT'd program is benign. */
+static int win_name_eq(KitSlice s, const char* lit, size_t litlen) {
+ return s.len == litlen && memcmp(s.s, lit, litlen) == 0;
+}
+
void* driver_dlsym_resolver(void* user, KitSlice name_s) {
(void)user;
if (!name_s.s || name_s.len == 0) return NULL;
- return win_dlsym(name_s.s);
+ void* p = win_dlsym(name_s.s);
+ if (p) return p;
+ if (win_name_eq(name_s, "__local_stdio_printf_options", 28))
+ return (void*)(uintptr_t)&__local_stdio_printf_options;
+ if (win_name_eq(name_s, "__local_stdio_scanf_options", 27))
+ return (void*)(uintptr_t)&__local_stdio_scanf_options;
+ return NULL;
}
/* ============================================================
@@ -1634,6 +1661,15 @@ static KitStatus dbg_guarded_copy_win(void* user, void* dst, const void* src,
return KIT_ERR;
}
g_guard_armed = 1;
+#if defined(__kit__)
+ /* kit's C frontend implements no SEH (__try/__except). Rely on the VEH
+ * backstop (dbg_veh) installed by dbg_signals_install_win: it sees the
+ * thread-local g_guard_armed and longjmps back through g_guard_buf on an
+ * access fault, which is the exact fallback the comment above anticipates. */
+ memcpy(dst, src, n);
+ g_guard_armed = 0;
+ return KIT_OK;
+#else
__try {
memcpy(dst, src, n);
g_guard_armed = 0;
@@ -1642,6 +1678,7 @@ static KitStatus dbg_guarded_copy_win(void* user, void* dst, const void* src,
g_guard_armed = 0;
return KIT_ERR;
}
+#endif
}
/* --- call_with_catch / thread_abort (longjmp-based) --- */
diff --git a/driver/lib/hosted.c b/driver/lib/hosted.c
@@ -517,6 +517,15 @@ static int hosted_resolve_windows_mingw(const DriverHostedRequest* req,
hosted_add_required_search(
plan->after, &plan->nafter, DRIVER_HOSTED_MAX_AFTER, req, dirs,
"libmingwex.a", DRIVER_HOSTED_INPUT_ARCHIVE) != 0 ||
+ /* winpthreads provides mingw's POSIX time/clock/threading entry points
+ * (nanosleep64, clock_gettime64, ...). <time.h> pulls in pthread_time.h's
+ * inline wrappers that call these, so it belongs in the default mingw
+ * runtime set (llvm-mingw ships it as a core lib). Static archive -> no
+ * libwinpthread-1.dll dependency; archive semantics keep it out of links
+ * that reference none of its members. */
+ hosted_add_required_search(
+ plan->after, &plan->nafter, DRIVER_HOSTED_MAX_AFTER, req, dirs,
+ "libwinpthread.a", DRIVER_HOSTED_INPUT_ARCHIVE) != 0 ||
hosted_add_required_search(
plan->after, &plan->nafter, DRIVER_HOSTED_MAX_AFTER, req, dirs,
"libucrt.a", DRIVER_HOSTED_INPUT_ARCHIVE) != 0 ||
@@ -543,6 +552,9 @@ static int hosted_resolve_windows_mingw(const DriverHostedRequest* req,
"libmingwex.a", DRIVER_HOSTED_INPUT_ARCHIVE) != 0 ||
hosted_add_required_search(
plan->after, &plan->nafter, DRIVER_HOSTED_MAX_AFTER, req, dirs,
+ "libwinpthread.a", DRIVER_HOSTED_INPUT_ARCHIVE) != 0 ||
+ hosted_add_required_search(
+ plan->after, &plan->nafter, DRIVER_HOSTED_MAX_AFTER, req, dirs,
"libucrt.a", DRIVER_HOSTED_INPUT_ARCHIVE) != 0 ||
hosted_add_required_search(
plan->after, &plan->nafter, DRIVER_HOSTED_MAX_AFTER, req, dirs,
diff --git a/lang/c/decl/decl.c b/lang/c/decl/decl.c
@@ -93,7 +93,8 @@ static ObjSymId decl_emit_cg_sym(DeclTable* t, const Decl* slot) {
decl.kind = (slot->type && slot->type->kind == TY_FUNC) ? KIT_CG_DECL_FUNC
: KIT_CG_DECL_OBJECT;
decl.display_name = slot->name;
- decl.linkage_name = kit_cg_c_linkage_name(t->c, slot->name);
+ decl.linkage_name = kit_cg_c_linkage_name(
+ t->c, slot->asm_name ? slot->asm_name : slot->name);
decl.type = type_cg_id_in_pool(t->c, t->pool, slot->type);
decl.sym = decl_sym_attrs(slot);
if (decl.kind == KIT_CG_DECL_FUNC) {
diff --git a/lang/c/decl/decl.h b/lang/c/decl/decl.h
@@ -84,6 +84,11 @@ typedef struct Decl {
/* Phase 2 attribute carriers — populated by attr_list_to_decl. */
u32 align; /* explicit alignment from _Alignas or aligned(N); 0=natural */
Sym alias_target; /* target name for __attribute__((alias("..."))); 0=none */
+ /* GCC asm-label rename (`extern T f(...) __asm__("altname")`): when nonzero,
+ * the emitted/referenced linkage symbol uses this name (run through the
+ * target's linkage-name rule) instead of `name`. `name` stays the C source
+ * identifier for diagnostics/debug. mingw redirects e.g. time -> _time64. */
+ Sym asm_name;
/* Wasm import overrides from __attribute__((import_module("M"),
* import_name("F"))). Honored only by the wasm backend; ignored by other
* targets. Both 0 = backend default (module="env", name=<sym name>). */
diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c
@@ -689,8 +689,9 @@ Sym mint_static_local_sym(Parser* p, Sym orig) {
/* Parse a single init-declarator after the decl-specs have been consumed. */
static SymEntry* declare_function(Parser* p, Sym fname, const Type* fn_ty,
const DeclSpecs* specs, SrcLoc fname_loc,
- const Attr* dattrs, ObjSecId* out_section_id,
- u32* out_decl_flags, Sym* out_alias_target);
+ const Attr* dattrs, Sym asm_label,
+ ObjSecId* out_section_id, u32* out_decl_flags,
+ Sym* out_alias_target);
static void parse_init_declarator(Parser* p, const DeclSpecs* specs) {
SrcLoc loc;
@@ -742,8 +743,8 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) {
if (is_punct(&p->cur, '=')) {
perr(p, "function declarator cannot have initializer");
}
- (void)declare_function(p, name, var_ty, specs, loc, NULL, §ion_id,
- &decl_flags, &alias_target);
+ (void)declare_function(p, name, var_ty, specs, loc, NULL, dinfo.asm_label,
+ §ion_id, &decl_flags, &alias_target);
(void)section_id;
(void)decl_flags;
(void)alias_target;
@@ -769,6 +770,7 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) {
}
memset(&decl_in, 0, sizeof decl_in);
decl_in.name = lname;
+ decl_in.asm_name = dinfo.asm_label;
decl_in.type = var_ty;
decl_in.loc = loc;
decl_in.storage = DS_STATIC;
@@ -816,6 +818,7 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) {
}
memset(&decl_in, 0, sizeof decl_in);
decl_in.name = name;
+ decl_in.asm_name = dinfo.asm_label;
decl_in.type = var_ty;
decl_in.loc = loc;
decl_in.storage = DS_EXTERN;
@@ -1023,8 +1026,9 @@ void parse_param_list(Parser* p, ParamInfo** infos_out, u16* nparams_out,
static SymEntry* declare_function(Parser* p, Sym fname, const Type* fn_ty,
const DeclSpecs* specs, SrcLoc fname_loc,
- const Attr* dattrs, ObjSecId* out_section_id,
- u32* out_decl_flags, Sym* out_alias_target) {
+ const Attr* dattrs, Sym asm_label,
+ ObjSecId* out_section_id, u32* out_decl_flags,
+ Sym* out_alias_target) {
SymEntry* visible;
if (out_section_id) *out_section_id = OBJ_SEC_NONE;
if (out_decl_flags) *out_decl_flags = 0;
@@ -1099,6 +1103,7 @@ static SymEntry* declare_function(Parser* p, Sym fname, const Type* fn_ty,
SymEntry* e;
memset(&decl_in, 0, sizeof decl_in);
decl_in.name = fname;
+ decl_in.asm_name = asm_label;
decl_in.type = fn_ty;
decl_in.loc = fname_loc;
decl_in.storage =
@@ -1284,8 +1289,8 @@ static void parse_external_decl(Parser* p) {
ObjSecId fn_section_id;
u32 fn_decl_flags;
Sym fn_alias_target;
- fent = declare_function(p, name, fn_ty, &specs, loc, dattrs, &fn_section_id,
- &fn_decl_flags, &fn_alias_target);
+ fent = declare_function(p, name, fn_ty, &specs, loc, dattrs, dinfo.asm_label,
+ &fn_section_id, &fn_decl_flags, &fn_alias_target);
attr_list_append(&fent->attrs, dattrs);
if (is_punct(&p->cur, '{')) {
@@ -1374,6 +1379,7 @@ static void parse_external_decl(Parser* p) {
DeclId did;
memset(&decl_in, 0, sizeof decl_in);
decl_in.name = name;
+ decl_in.asm_name = dinfo.asm_label;
decl_in.type = base_ty;
decl_in.loc = loc;
if (specs.storage == DS_STATIC) {
diff --git a/rt/include/setjmp.h b/rt/include/setjmp.h
@@ -24,7 +24,21 @@ typedef struct {
_Alignas(16) unsigned char __kit_storage[256];
} jmp_buf[1];
+/* On Windows (mingw) there is no bare `setjmp` symbol to link against: libc's
+ * <setjmp.h> defines setjmp as a macro over _setjmpex / __mingw_setjmp. kit's
+ * freestanding code wants POSIX-style pure register save/restore with no SEH
+ * frame unwinding (kit is arena-allocated; longjmp targets have no cleanup to
+ * run), which is exactly mingw's non-SEH __mingw_setjmp / __mingw_longjmp pair
+ * (libmingwex, self-contained, no .pdata dependency). Bind to them by asm-label
+ * so freestanding libkit links against the mingw runtime; kit's 256-byte
+ * jmp_buf covers mingw's aarch64/x86_64 _JBLEN. Non-Windows targets resolve
+ * bare setjmp from the host libc (POSIX) or libkit_rt's per-arch coro asm. */
+#if defined(_WIN32)
+int setjmp(jmp_buf env) __asm__("__mingw_setjmp");
+_Noreturn void longjmp(jmp_buf env, int val) __asm__("__mingw_longjmp");
+#else
int setjmp(jmp_buf env);
_Noreturn void longjmp(jmp_buf env, int val);
+#endif
#endif
diff --git a/scripts/windows_cross.sh b/scripts/windows_cross.sh
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+# Cross-compile the kit binary itself into a Windows PE/COFF kit.exe, using the
+# host build/kit as the cross-compiler against the llvm-mingw UCRT sysroot.
+#
+# Unlike the Linux/FreeBSD bootstraps (native 3-stage builds inside a VM/
+# container), the Windows kit binary is produced by CROSS-compiling on the dev
+# host: there is no C compiler in the Windows VM to seed a native build, but the
+# already-built build/kit can compile + link every libkit/driver TU for the
+# Windows target. The build system selects driver/env/windows.c by overriding
+# HOST_OS=windows (mk/env.mk normally derives it from `uname`).
+#
+# usage: scripts/windows_cross.sh [arch]
+# arch aarch64 (default) | x64
+#
+# env:
+# KIT_WIN_OPT codegen opt level (default -O0; -O1 exercises the optimizer)
+#
+# Output: build/win-cross/<arch>/kit.exe (a PE32+ console executable).
+#
+# Prerequisites:
+# make bin # the host build/kit cross-compiler
+# scripts/llvm_mingw_sysroot.sh prepare <arch> # mingw UCRT headers + libs
+# make rt-aarch64-windows | rt-x86_64-pc-windows # target runtime (if linking rt)
+
+set -eu
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$ROOT"
+
+ARCH="${1:-aarch64}"
+case "$ARCH" in
+ aarch64|arm64|aa64) ARCH=aarch64; TRIPLE=aarch64-windows; HOST_ARCH=aarch64 ;;
+ x64|x86_64|amd64) ARCH=x64; TRIPLE=x86_64-windows; HOST_ARCH=x86_64 ;;
+ *) echo "windows_cross: unsupported arch '$ARCH' (want aarch64|x64)" >&2; exit 2 ;;
+esac
+
+KIT="$ROOT/build/kit"
+[ -x "$KIT" ] || { echo "windows_cross: $KIT missing; run 'make bin' first" >&2; exit 1; }
+
+SR="$("$ROOT/scripts/llvm_mingw_sysroot.sh" path "$ARCH" 2>/dev/null || true)"
+[ -n "$SR" ] && [ -r "$SR/include/windows.h" ] || {
+ echo "windows_cross: mingw sysroot for $ARCH missing; run" >&2
+ echo " scripts/llvm_mingw_sysroot.sh prepare $ARCH" >&2
+ exit 1
+}
+
+BUILD_DIR="build/win-cross/$ARCH"
+TC="$ROOT/$BUILD_DIR/toolchain"
+mkdir -p "$TC"
+
+# A single-word cc wrapper that injects -target/--sysroot: make splits a
+# multi-word `CC=...` command-line value on whitespace, so the flags can't ride
+# on CC directly. The ar/ranlib/as/ld busybox links dispatch by argv[0] basename
+# (same pattern as mk/bootstrap.mk's stage symlinks).
+cat > "$TC/cc" <<EOF
+#!/bin/sh
+exec "$KIT" cc -target $TRIPLE --sysroot "$SR" "\$@"
+EOF
+chmod +x "$TC/cc"
+for t in ar ranlib as ld; do ln -sf "$KIT" "$TC/$t"; done
+
+# kit cc emits no -MMD dependency files, so make cannot see header edits or a
+# rebuilt compiler. Wipe the object tree (keeping the toolchain) so every run is
+# a correct full rebuild by the current build/kit.
+find "$BUILD_DIR" -mindepth 1 -maxdepth 1 ! -name toolchain -exec rm -rf {} +
+
+# RELEASE=1 supplies -ffunction-sections -fdata-sections internally (no
+# multi-word CLI value, which make would mis-split); --gc-sections then drops
+# the dead per-function/data sections the mingw headers' unused static inlines
+# pull in. HOST_OPTFLAGS keeps the simpler -O0 codegen and HOST_MODE_CPPFLAGS=
+# keeps asserts enabled; HOST_SYSROOT_*FLAGS are cleared because the sysroot
+# rides on the cc wrapper's --sysroot, not env.mk's Darwin -isysroot.
+JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"
+make bin -j"$JOBS" \
+ BUILD_DIR="$BUILD_DIR" \
+ HOST_OS=windows HOST_ARCH="$HOST_ARCH" \
+ RELEASE=1 HOST_OPTFLAGS="${KIT_WIN_OPT:--O0}" HOST_MODE_CPPFLAGS= \
+ HOST_MODE_LDFLAGS=-Wl,--gc-sections \
+ HOST_SYSROOT_CFLAGS= HOST_SYSROOT_LDFLAGS= \
+ CC="$TC/cc" AR="$TC/ar"
+
+cp "$BUILD_DIR/kit" "$BUILD_DIR/kit.exe"
+printf 'windows_cross: built %s\n' "$BUILD_DIR/kit.exe"
+file "$BUILD_DIR/kit.exe" 2>/dev/null || true
diff --git a/src/link/link.h b/src/link/link.h
@@ -59,6 +59,12 @@ typedef struct LinkInput {
* the runtime loader looks up the dependency by SONAME, not by the
* filesystem path passed at link time. */
Sym soname;
+ /* COFF short-import only: the name the loader must resolve in the DLL when
+ * it differs from the symbol's link name (Microsoft short-import NameType
+ * NOPREFIX/UNDECORATE/EXPORTAS — e.g. local __msvcrt_assert -> export
+ * _assert). 0 when the import name equals the symbol name. Consumed by the
+ * COFF import-table synthesis for the PE hint/name-table entry. */
+ Sym coff_import_name;
} LinkInput;
typedef struct LinkSymbol {
diff --git a/src/link/link_resolve.c b/src/link/link_resolve.c
@@ -884,6 +884,13 @@ static void include_archive_member(Linker* l, const LinkArchive* ar,
if (mem->obj && obj_get_coff_import_dll(mem->obj, &coff_dll) && coff_dll) {
in->kind = LINK_INPUT_DSO_BYTES;
in->soname = coff_dll;
+ /* Short-import NameType may make the DLL export name differ from the
+ * local symbol name (EXPORTAS etc.); carry it for import-table synthesis. */
+ {
+ Sym coff_imp_name = 0;
+ if (obj_get_coff_import_name(mem->obj, &coff_imp_name))
+ in->coff_import_name = coff_imp_name;
+ }
} else {
in->kind = LINK_INPUT_OBJ_BYTES;
}
diff --git a/src/obj/coff/coff.h b/src/obj/coff/coff.h
@@ -488,6 +488,11 @@ typedef struct ImportObjectHeader {
#define IMPORT_OBJECT_NAME 1u
#define IMPORT_OBJECT_NAME_NOPREFIX 2u
#define IMPORT_OBJECT_NAME_UNDECORATE 3u
+/* NameType 4 (EXPORTAS): the actual DLL export name is stored as a third
+ * NUL-terminated string after the symbol and DLL names. Used by the UCRT
+ * import libs to alias a mingw-local symbol (e.g. __msvcrt_assert) onto the
+ * real UCRT export (_assert). */
+#define IMPORT_OBJECT_NAME_EXPORTAS 4u
/* ---- debug directory (IMAGE_DEBUG_DIRECTORY) ----
* Pointed at by IMAGE_DIRECTORY_ENTRY_DEBUG. kit emits a single
diff --git a/src/obj/coff/link.c b/src/obj/coff/link.c
@@ -398,6 +398,7 @@ static void coff_define_tls_used(LinkImage* img,
typedef struct CoffImport {
LinkSymId sym; /* canonical LinkSymId from img->syms */
+ Sym import_name; /* DLL export name override (short-import NameType); 0=use sym */
u32 dll_idx; /* index into CoffImportTable.dlls */
u32 stub_off; /* offset in .text bucket (functions only) */
u32 iat_off; /* offset in .idata IAT block */
@@ -474,6 +475,19 @@ static const char* coff_import_lookup_name(Compiler* c, const LinkSymbol* s,
return nm;
}
+/* The name placed in the PE hint/name table for an import. Honors the
+ * short-import NameType override (CoffImport.import_name, e.g. EXPORTAS's real
+ * DLL export name) when present, else derives it from the symbol name. */
+static const char* coff_import_emit_name(Compiler* c, const CoffImport* imp,
+ const LinkSymbol* s, size_t* nlen_out) {
+ if (imp->import_name) {
+ Slice nm_s = pool_slice(c->global, imp->import_name);
+ if (nlen_out) *nlen_out = nm_s.len;
+ return nm_s.s;
+ }
+ return coff_import_lookup_name(c, s, nlen_out);
+}
+
/* True iff the import classifies as function-like. Mirrors the ELF
* `sym_is_func_import` heuristic: if the canonical kind is known
* we trust it, otherwise we default to function (which matches the
@@ -560,6 +574,7 @@ static int coff_collect_imports(LinkImage* img, CoffImportTable* it) {
compiler_panic(c, SRCLOC_NONE, "link_emit_coff: oom on imports");
memset(&it->imports[it->nimports], 0, sizeof(it->imports[it->nimports]));
it->imports[it->nimports].sym = s->id;
+ it->imports[it->nimports].import_name = in->coff_import_name;
it->imports[it->nimports].dll_idx = dll_idx;
it->imports[it->nimports].is_func = (u8)coff_import_is_func(c, s);
if (it->imports[it->nimports].is_func) ++it->nfunc_imports;
@@ -639,7 +654,7 @@ static void coff_plan_idata_layout(LinkImage* img, CoffImportTable* it) {
for (u32 i = 0; i < it->nimports; ++i) {
LinkSymbol* s = LinkSyms_at(&img->syms, it->imports[i].sym - 1);
size_t nlen = 0;
- const char* nm = coff_import_lookup_name(c, s, &nlen);
+ const char* nm = coff_import_emit_name(c, &it->imports[i], s, &nlen);
if (!nm || nlen == 0)
compiler_panic(c, SRCLOC_NONE,
"link_emit_coff: imported symbol has empty name");
@@ -789,7 +804,7 @@ static void coff_emit_idata(LinkImage* img, const CoffImportTable* it,
for (u32 i = 0; i < it->nimports; ++i) {
LinkSymbol* s = LinkSyms_at(&img->syms, it->imports[i].sym - 1);
size_t nlen = 0;
- const char* nm = coff_import_lookup_name(c, s, &nlen);
+ const char* nm = coff_import_emit_name(c, &it->imports[i], s, &nlen);
u8* p = buf + it->imports[i].hint_off;
wr_u16_le(p, PE_IMPORT_HINT_NONE);
memcpy(p + 2, nm, nlen);
diff --git a/src/obj/coff/read.c b/src/obj/coff/read.c
@@ -286,7 +286,46 @@ static ObjBuilder* read_coff_short_import(Compiler* c, const char* name,
c->global, (Slice){.s = (const char*)dll_p, .len = dll_name_len});
obj_set_coff_import_dll(ob, dll_sn);
- (void)name_type;
+ /* NameType decides what the loader resolves IN THE DLL, which can differ
+ * from the local symbol name. The local symbol keeps its own name (so kit's
+ * references resolve); the PE hint/name-table entry must use the real
+ * export name. Record an override whenever they differ. */
+ Slice imp_name = {.s = (const char*)body, .len = sym_name_len};
+ if (name_type == IMPORT_OBJECT_NAME_NOPREFIX ||
+ name_type == IMPORT_OBJECT_NAME_UNDECORATE) {
+ /* Strip one leading decoration char (?, @, or _). UNDECORATE also
+ * truncates at the first '@' (MS @argbytes stdcall/fastcall suffix). */
+ if (imp_name.len > 0 && (imp_name.s[0] == '?' || imp_name.s[0] == '@' ||
+ imp_name.s[0] == '_')) {
+ ++imp_name.s;
+ --imp_name.len;
+ }
+ if (name_type == IMPORT_OBJECT_NAME_UNDECORATE) {
+ u32 at = 0;
+ while (at < imp_name.len && imp_name.s[at] != '@') ++at;
+ imp_name.len = at;
+ }
+ } else if (name_type == IMPORT_OBJECT_NAME_EXPORTAS) {
+ /* The real export name is a third NUL-terminated string after the DLL. */
+ u32 exp_off = dll_name_off + dll_name_len + 1u;
+ if (exp_off >= size_of_data)
+ compiler_panic(c, SRCLOC_NONE,
+ "read_coff: short-import EXPORTAS missing export name");
+ const u8* exp_p = body + exp_off;
+ u32 exp_max = size_of_data - exp_off;
+ u32 exp_len = 0;
+ while (exp_len < exp_max && exp_p[exp_len] != '\0') ++exp_len;
+ if (exp_len == exp_max)
+ compiler_panic(c, SRCLOC_NONE,
+ "read_coff: short-import EXPORTAS name not NUL-terminated");
+ imp_name.s = (const char*)exp_p;
+ imp_name.len = exp_len;
+ }
+ if (imp_name.len != sym_name_len ||
+ memcmp(imp_name.s, body, sym_name_len) != 0) {
+ obj_set_coff_import_name(ob, pool_intern_slice(c->global, imp_name));
+ }
+
obj_finalize(ob);
return ob;
}
diff --git a/src/obj/obj.c b/src/obj/obj.c
@@ -58,6 +58,15 @@ struct KitObjBuilder {
* import record; zero / unset otherwise. See obj_set_coff_import_dll. */
Sym coff_import_dll;
u8 coff_import_dll_set;
+ /* COFF short-import IMPORT NAME override. The Microsoft short-import
+ * NameType field can make the name the loader resolves in the DLL differ
+ * from the local symbol name (NOPREFIX/UNDECORATE strip decoration;
+ * EXPORTAS stores an explicit export name). Carries that resolved import
+ * name when it differs from the symbol name; zero / unset otherwise. The
+ * local symbol keeps its own name so references still resolve; only the PE
+ * hint/name-table entry uses this. See obj_set_coff_import_name. */
+ Sym coff_import_name;
+ u8 coff_import_name_set;
/* Cached undef extern `__tlv_bootstrap` (Mach-O on-disk name) used by
* obj_define_tls when emitting `_Thread_local` storage on Mach-O.
* Lazily materialized on the first TLV emission; OBJ_SYM_NONE otherwise. */
@@ -170,6 +179,18 @@ int obj_get_coff_import_dll(const ObjBuilder* ob, Sym* out) {
return 1;
}
+void obj_set_coff_import_name(ObjBuilder* ob, Sym import_name) {
+ if (!ob) return;
+ ob->coff_import_name = import_name;
+ ob->coff_import_name_set = 1;
+}
+
+int obj_get_coff_import_name(const ObjBuilder* ob, Sym* out) {
+ if (!ob || !ob->coff_import_name_set) return 0;
+ if (out) *out = ob->coff_import_name;
+ return 1;
+}
+
/* ---- linked-image view ---- */
struct ObjImage {
diff --git a/src/obj/obj.h b/src/obj/obj.h
@@ -551,6 +551,15 @@ int obj_get_elf_e_flags(const ObjBuilder*, u32* out);
* the same way obj_set_elf_e_flags does. */
void obj_set_coff_import_dll(ObjBuilder*, Sym dll_name);
int obj_get_coff_import_dll(const ObjBuilder*, Sym* out);
+/* COFF short-import IMPORT NAME override: the name the loader resolves in the
+ * DLL when the short-import NameType makes it differ from the local symbol
+ * name (NOPREFIX/UNDECORATE strip decoration; EXPORTAS carries an explicit
+ * export name). Set by read_coff_short_import; consumed by the COFF
+ * import-table synthesis for the PE hint/name-table entry. The local symbol
+ * keeps its own name so kit's references still resolve. Unset on inputs whose
+ * import name equals the symbol name. */
+void obj_set_coff_import_name(ObjBuilder*, Sym import_name);
+int obj_get_coff_import_name(const ObjBuilder*, Sym* out);
/* Per-symbol format-specific flag bits. ObjSym.flags is otherwise
* unused; readers stash format-specific attribute bits there so the