kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
Mdriver/env/windows.c | 41+++++++++++++++++++++++++++++++++++++++--
Mdriver/lib/hosted.c | 12++++++++++++
Mlang/c/decl/decl.c | 3++-
Mlang/c/decl/decl.h | 5+++++
Mlang/c/parse/parse.c | 22++++++++++++++--------
Mrt/include/setjmp.h | 14++++++++++++++
Ascripts/windows_cross.sh | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/link/link.h | 6++++++
Msrc/link/link_resolve.c | 7+++++++
Msrc/obj/coff/coff.h | 5+++++
Msrc/obj/coff/link.c | 19+++++++++++++++++--
Msrc/obj/coff/read.c | 41++++++++++++++++++++++++++++++++++++++++-
Msrc/obj/obj.c | 21+++++++++++++++++++++
Msrc/obj/obj.h | 9+++++++++
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, &section_id, - &decl_flags, &alias_target); + (void)declare_function(p, name, var_ty, specs, loc, NULL, dinfo.asm_label, + &section_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