refactor(delib-1c/1d/1e/1f): un-link-libc — full migration#49
Merged
Conversation
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.
…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.
2 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 —
cErrnoToWasireadsplatform.pfdErrno()(threadlocal)platform.pfd_last_errno+pfdErrno()+syncErrnoFromLibC().cResult*wrappers that sync fromstd.c._errno().std.c.*call sites in wasi.zig getswitch (comptime ...)gates with Linux direct syscalls + Mac fallback + explicit errno sync.cErrnoToWasi()now readsplatform.pfdErrno()— no morestd.c._errno().*.1d —
trace.zigstderr viaplatform.pfdWriteOne-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.appCacheDirreadsstd.process.Environ.Mapcli.maincallsplatform.setEnvironMap(init.environ_map)once;envPathreads from the captured pointer. No morestd.c.getenvanywhere on the Linux path. The remainingHostHandle.close/HostHandle.duplicatewere also routed throughplatform.pfd{Close,Dup}.1f —
.link_libc = falseacross all build.zig modulesAll lib / cli / tests / examples / shared-lib / static-lib / FFI / C-API modules drop libc linkage. Mac still auto-links libSystem (so
std.c.*decls inelsearms still resolve). Linux usesstd.os.linux.*direct syscalls; Windows uses kernel32 externs declared inplatform.zig.Expected outcomes
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.Tracks W46 (closes if CI green). W47 (
tgo_strops_cached+24% on Mac aarch64) remains open separately.