Skip to content

Stop forcing libc: direct system calls on Linux - #8

Merged
renerocksai merged 1 commit into
mainfrom
feat/no-forced-libc
Sep 25, 2026
Merged

renerocksai merged 1 commit into
mainfrom
feat/no-forced-libc

Conversation

@renerocksai

Copy link
Copy Markdown
Contributor

Why

The library module set .link_libc = true, so every consumer linked a libc even though the framework is pure Zig. Consumers such as omajot want one static Linux binary with no interpreter and no shared libraries.

What

  • src/sys.zig: the 17 POSIX calls the framework makes outside the Windows transport (socket, setsockopt, bind, listen, getsockname, fcntl, close, connect, send, recv, poll, shutdown, pipe, read, write, clock_gettime, nanosleep) with libc's signatures and -1/errno convention. With libc linked (and on every OS but Linux) they are std.c; on Linux without libc they are direct std.os.linux system calls with a thread-local errno. Call sites in server.zig, transport.zig and transport_linux.zig changed from c. to sys.; transport_macos.zig keeps std.c (macOS always links libSystem).
  • failFast keeps _exit semantics: std.c._exit with libc, else std.process.exit, which in Zig 0.16 is exit_group on Linux (RtlExitUserProcess on Windows) without atexit handlers or unwinding. With libc, std.process.exit would call exit(3) and run atexit handlers, hence _exit there.
  • The library module, the demo executable and the embedding example no longer set link_libc. The demo's stall uses sys.nanosleep; the example's fatal path uses failFast.
  • New Linux test runs without libc for transport.zig, server.zig and sys.zig (part of verify).
  • README, USING.md and ARCHITECTURE.md document "No libc required on Linux".

Evidence (exact Zig 0.16.0)

Linux (omarx1, io_uring), under the measurement lock:

  • zig build verify: 24/24 steps, 207/211 tests (4 skipped), incl. no-libc runs (transport 9, server 71 + 2 skipped, sys 1).
  • Integration suites against the now libc-free demo server: integration 34, continuation 24 groups, batch 29, inline 10, gather 11, arena lifecycle 8.
  • Embedding example (an independent consumer), ReleaseSafe, for the native target, x86_64-linux-musl and x86_64-linux-none: statically linked, no PT_INTERP, "There is no dynamic section in this file." The -linux-none binary serves HTTP/1.1 200 OK with a correct Date header (clock_gettime via direct syscall).

macOS (maxross, kqueue, native): zig build verify 18/18 steps, 124/128 tests (4 skipped); integration 30; continuation 24 groups; otool -L shows only libSystem.B.dylib.

Windows: zig build check -Dtarget=x86_64-windows-gnu and a ReleaseSafe cross build pass here; native Windows runs in CI.

🤖 Generated with Claude Code

The library module set link_libc, which made every consumer link a libc.
src/sys.zig now provides the 17 POSIX calls the framework makes outside
the Windows transport, with libc's signatures and errno convention:
std.c when libc is linked (and on every system but Linux), direct
std.os.linux system calls otherwise. failFast keeps _exit semantics:
std.c._exit with libc, std.process.exit without (exit_group on Linux,
no atexit handlers). The demo and the embedding example no longer link
libc; macOS still links libSystem automatically.

A Linux consumer now builds as one static executable with no
interpreter and no dynamic section, for the native target, musl or
none. The transport, server and sys tests also run without libc.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@renerocksai
renerocksai merged commit bb9a5bf into main Sep 25, 2026
6 of 7 checks passed
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