Skip to content

refactor(delib-1c/1d/1e/1f): un-link-libc — full migration#49

Merged
chaploud merged 6 commits intomainfrom
develop/delib-phase1c
Apr 24, 2026
Merged

refactor(delib-1c/1d/1e/1f): un-link-libc — full migration#49
chaploud merged 6 commits intomainfrom
develop/delib-phase1c

Conversation

@chaploud
Copy link
Copy Markdown
Contributor

@chaploud chaploud commented Apr 24, 2026

Summary

W46 Phase 1c–1f as a single PR. Each phase is its own commit so it bisects cleanly. By the end of this PR, Linux no longer links libc and the binary should return to ~1.38 MB stripped.

1c — cErrnoToWasi reads platform.pfdErrno() (threadlocal)

  • Add platform.pfd_last_errno + pfdErrno() + syncErrnoFromLibC().
  • Linux arms of every pfd helper set it directly from the syscall return.
  • Mac/BSD arms go through cResult* wrappers that sync from std.c._errno().
  • Non-pfd std.c.* call sites in wasi.zig get switch (comptime ...) gates with Linux direct syscalls + Mac fallback + explicit errno sync.
  • cErrnoToWasi() now reads platform.pfdErrno() — no more std.c._errno().*.

1d — trace.zig stderr via platform.pfdWrite

One-line swap. std.c.write(HANDLE, …) was a latent Windows bug (HANDLE passed where int fd expected, same class as the WASI regression fixed in #45); now routes through the correct per-OS path.

1e — platform.appCacheDir reads std.process.Environ.Map

cli.main calls platform.setEnvironMap(init.environ_map) once; envPath reads from the captured pointer. No more std.c.getenv anywhere on the Linux path. The remaining HostHandle.close / HostHandle.duplicate were also routed through platform.pfd{Close,Dup}.

1f — .link_libc = false across all build.zig modules

All lib / cli / tests / examples / shared-lib / static-lib / FFI / C-API modules drop libc linkage. Mac still auto-links libSystem (so std.c.* decls in else arms still resolve). Linux uses std.os.linux.* direct syscalls; Windows uses kernel32 externs declared in platform.zig.

Expected outcomes

  • Linux ELF stripped binary: ~1.65 MB → ~1.38 MB (matching Mac)
  • No change to Mac or Windows binary size
  • No behavior change for any WASI operation
  • 46/46 Windows real-world compat preserved

Size-guard docs (CLAUDE.md / CONTRIBUTING / book / release SKILL) still say 1.80 MB — they will be dialed back to 1.50 MB in a follow-up once CI confirms the new Linux number.

Test plan

  • zig build test (Mac aarch64) — all pass.
  • zig build -Dtarget=x86_64-linux-gnu — clean.
  • zig build -Dtarget=x86_64-windows-gnu — clean.
  • bash test/realworld/run_compat.sh (Mac ReleaseSafe) — 50 / 0 / 0.
  • Full CI: Mac + Ubuntu + Windows green, 46/46 Windows real-world compat preserved, Linux stripped binary well under 1.80 MB.

Tracks W46 (closes if CI green). W47 (tgo_strops_cached +24% on Mac aarch64) remains open separately.

Third step of the W46 un-link-libc migration. Cut the last direct
dependency on `std.c._errno()` from the WASI error-reporting path.

- Add `platform.pfd_last_errno` (threadlocal `std.posix.E`) + public
  readers `pfdErrno()` and `syncErrnoFromLibC()`.
- Linux arms of `pfd{Write,Read,Pread,Pwrite,Seek,Close,Fsync,Dup,
  MkdirAt,UnlinkAt,RenameAt,ReadlinkAt}` now set `pfd_last_errno`
  directly from the syscall return value (no more `std.c._errno().*`
  writes). Windows arms set it to `.IO` on failure.
- Mac/BSD arms route std.c.* results through `cResultAsIsize`/
  `cResultAsI32`/`cResultAsI64` helpers that call
  `syncErrnoFromLibC()` on failure.
- `cErrnoToWasi()` now reads `platform.pfdErrno()` instead of
  `std.c._errno().*`. The WASI mapping table is unchanged.
- Non-pfd `std.c.*` call sites in wasi.zig (`fdatasync`, `fsync`,
  `fcntl`/SETFL, `ftruncate`, `symlinkat`, `linkat`, `futimens`)
  now have per-platform `switch (comptime builtin.os.tag)` blocks:
  Linux uses `std.os.linux.*` direct syscalls (sets `pfd_last_errno`
  inline), Mac/BSD keeps `std.c.*` (calls `syncErrnoFromLibC()` on
  failure).

After this commit the only remaining obstacles to flipping
`link_libc = false` are the `std.c.getenv` in `platform.zig` (Phase
1e) and the `std.c.write` in `trace.zig` (Phase 1d). Several
`std.c.Stat`/`std.c.fstatat` / `std.c.utimensat` sites remain but
they are already inside `if (builtin.os.tag == .linux) {…} else`
branches so Mac-only analysis is already gated.

Verified:
- `zig build test` (Mac aarch64) — all pass.
- `zig build -Dtarget=x86_64-linux-gnu` / `-Dtarget=x86_64-windows-gnu`
  both clean.
- `bash test/realworld/run_compat.sh` (Mac ReleaseSafe) — PASS: 50,
  FAIL: 0.
Fourth step of the W46 un-link-libc migration. `stderrPrint` was still
calling `std.c.write(stderr_fd, msg.ptr, msg.len)` directly — route it
through `platform.pfdWrite` so Linux uses a direct syscall, macOS/BSD
uses the std.c.* path, and Windows goes through kernel32 WriteFile
(which is correct since `stderr_fd` on Windows is already
`windows.peb().ProcessParameters.hStdError` — a real HANDLE).

Before: `std.c.write(HANDLE, ...)` on Windows would have mis-routed to
an arbitrary MSVCRT fd (same class of bug as the WASI fd_write
regression fixed in 1af72d1). That path was never exercised at
runtime because trace output is disabled by default, but the
refactor closes the latent bug too.
Last std.c.* dependency outside of Mac-only code paths:
`platform.zig` was calling `std.c.getenv("HOME")` / `std.c.getenv("APPDATA")`
etc. directly, which requires `link_libc = true` on Linux.

Route env lookups through a process-wide `std.process.Environ.Map`
captured at program start. `cli.main` calls `platform.setEnvironMap
(init.environ_map)` once; `platform.envPath` reads from the captured
pointer (returning null when unset — safe for tests that never
exercise the cache/temp paths).

Also convert the two remaining `std.c.{close,dup}` sites in
`wasi.zig` (`HostHandle.close` and `HostHandle.duplicate`) to the
existing `platform.pfd{Close,Dup}` helpers, which dispatch to
Win32 / Linux syscalls / std.c as appropriate.

Verified:
- `zig build test` (Mac aarch64) — all pass.
- `zig build -Dtarget=x86_64-linux-gnu` — clean.
- `zig build -Dtarget=x86_64-windows-gnu` — clean.
- `bash test/realworld/run_compat.sh` (Mac ReleaseSafe) — 50 / 0 / 0.

The `link_libc = true` flip happens in a follow-up commit (Phase 1f)
so that this change stays mechanically minimal — if something breaks,
it's easier to bisect.
…dules

W46 Phase 1f — the payoff of the earlier phases.

All `std.c.*` references in zwasm are now either:
- routed through `platform.pfd*` helpers (which dispatch per OS), or
- inside `else` branches of `switch (comptime builtin.os.tag)` blocks
  (analyzed only on Mac/BSD where libSystem auto-links), or
- guarded by `if (builtin.os.tag == .linux) { … } else { … }` pairs
  that Zig prunes at comptime.

So `link_libc` can safely drop to false for every module in build.zig
(lib / cli / tests / examples / shared lib / static lib / FFI / C API).
Mac still auto-links libSystem for the `extern "c"` decls on its own;
Linux now builds strictly against `std.os.linux.*` syscalls; Windows
uses kernel32 externs declared in `platform.zig`.

Binary size impact (Linux ELF stripped, ReleaseSafe, expected at CI):
- pre-flip: ~1.65 MB (above the 1.50 MB legacy guard, 1.80 MB current)
- post-flip: expected ~1.38 MB (matching Mac)

CI size guard and related docs will be dialed back from 1.80 MB to
1.50 MB in a follow-up commit once Linux numbers are confirmed green.

Verified locally:
- `zig build test` (Mac aarch64) — all pass.
- `zig build -Dtarget=x86_64-linux-gnu` — clean.
- `zig build -Dtarget=x86_64-windows-gnu` — clean.
- `bash test/realworld/run_compat.sh` (Mac ReleaseSafe) — 50 / 0 / 0.
@chaploud chaploud changed the title refactor(delib-1c/1d): cErrnoToWasi via platform.pfdErrno + trace via pfdWrite refactor(delib-1c/1d/1e/1f): un-link-libc — full migration Apr 24, 2026
…r Linux

PR #49 Phase 1f flipped link_libc=false but three WASI unit tests and
the VM cancellation test still reached for std.c.* POSIX helpers that
require libc on Linux. Add pfdDup2 / pfdPipe / pfdSleepNs to platform.zig
and route the tests through them so Ubuntu CI compiles without libc.

Mac uses std.c.* via libSystem auto-link (unchanged); Linux uses
std.os.linux direct syscalls; Windows returns -1 / uses kernel32.Sleep
(tests already skip Windows early).
…tic-lib, c-test)

PR #49 Phase 1f flipped link_libc=false on every module, but the C API
library modules (src/c_api.zig) use std.heap.c_allocator as their
default backing allocator, which requires libc on every target:
- malloc/realloc/free are extern "c" (Linux glibc/musl, Windows msvcrt)
- Mac libSystem auto-link masked the issue locally

Restoring link_libc=true on lib_shared_mod, lib_static_mod, and the
c_tests ct_mod. The CLI, tests, examples, e2e, bench, and fuzz modules
all remain link_libc=false — that is where the W46 size win comes from.
C API consumers link libc anyway (any real C program does), so this
costs them nothing.
@chaploud chaploud merged commit 30a3680 into main Apr 24, 2026
5 checks passed
notxorand pushed a commit to notxorand/zwasm that referenced this pull request Apr 24, 2026
W46 "un-link libc" Phase 1 landed via PR clojurewasm#49 (delib 1c/1d/1e/1f) on top
of the earlier delib 1a/1b phases. link_libc=false is now in effect for
lib / cli / tests / examples / e2e / bench / fuzz modules; C-API targets
(shared-lib, static-lib, c-test) retain link_libc=true because
std.heap.c_allocator is the intended default allocator for C callers.

- checklist.md: move W46 into Resolved (summary) with measured size
  numbers (Mac 1.38 MB, Linux 1.65 MB stripped vs 1.80 MB ceiling) and
  note that the 1.50 MB target waits for the std.Io migration.
- memo.md: replace the stale "v1.10.0 awaiting PR" Current Task with
  a W46 Phase 1 completion summary; record the cross-compile sanity
  trick and the "c_api.zig needs libc" gotcha as hard-won nuggets.
@chaploud chaploud deleted the develop/delib-phase1c branch April 30, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant