Skip to content

feat(zig): add bindings/zig, example/zig-hello and Linux/macOS CI on Zig 0.17.0 - #321

Merged
gg582 merged 4 commits into
c4punks:devfrom
AyushVUpadhye:feat/zig-bindings
Oct 6, 2026
Merged

gg582 merged 4 commits into
c4punks:devfrom
AyushVUpadhye:feat/zig-bindings

Conversation

@AyushVUpadhye

Copy link
Copy Markdown
Contributor

First step of the Zig bindings agreed in #36: bindings/zig with a build.zig that links through cwist.pc, a hello example, and Linux/macOS CI pinned to Zig 0.17.0.

What it adds

bindings/zig is a Zig package (build.zig.zon, minimum_zig_version = "0.17.0") that exports a cwist module.

  • C API. Zig 0.17 no longer has @cImport, so build.zig translates the installed CWIST headers with b.addTranslateC (src/cwist.h). The raw API is available as cwist.c.
  • Linking. build.zig runs pkg-config --cflags --libs --static cwist, the same way the Rust build.rs does:
    • the -I paths feed translate-c;
    • -L/-l go to the module, so libcwist and its bundled dependencies link statically from the cwist.pc directories;
    • --static keeps Libs.private (curl, nghttp2);
    • -lstdc++ maps to Zig's libc++;
    • on macOS the Homebrew directories of the system dependencies come from their own .pc files;
    • PKG_CONFIG is honoured, and a missing cwist.pc fails with install instructions.
  • Zig API (src/cwist.zig). A small layer on top:
    • App.init/deinit;
    • get/post/put/delete/patch, registered through the cwist_app_*_ex functions from feat(app): per-route user context with cwist_app_*_ex() registration (issue #36) #279, with a typed context (a pointer, or void) and a comptime trampoline;
    • Request (method, path, body, header, param, query) and Response (setStatus, setBody, addHeader) as views for one handler call;
    • in-memory dispatch;
    • listen (reactor server, one worker, no fork, same as App::listen in Rust) and shutdown.
  • Handlers. Handlers return nothing, so no error crosses into C, and Zig does not unwind. The README spells out the lifetime and thread rules.

example/zig-hello serves the same routes as example/rust-hello (/ and /users/:id, with an X-Powered-By: CWIST Zig header) and stops on SIGTERM/SIGINT.

.github/workflows/zig.yml runs on ubuntu-latest and macos-15:

  1. downloads the official Zig 0.17.0 archive and checks it against its published SHA-256, which is hard-coded in the workflow, so the pin cannot drift;
  2. builds and installs CWIST as the Rust job does;
  3. runs zig build test;
  4. builds zig-hello and runs it through the same smoke check as rust-hello: readiness, GET /, GET /users/42, 404, and SIGTERM with exit status 0.

scripts/ci/rust_hello_smoke.sh becomes scripts/ci/hello_smoke.sh, which takes the expected X-Powered-By value as an argument. rust.yml passes "CWIST Rust"; the checks are otherwise unchanged.

One translate-c detail: libttak's stdatomic.h falls back to a portable shim when the compiler does not look like GCC, and translate-c cannot parse that shim. src/cwist.h includes the system <stdatomic.h>/<pthread.h> first, and build.zig defines __TTAK_STDATOMIC_SYSTEM_INCLUDED. This is the same problem bindgen needed -D_Atomic=_Atomic for.

Tests

zig build test runs these against the real libcwist through in-memory dispatch:

  • routes read method, path parameter, query, header and body, and write status, header and body; POST echo; 404 for an unknown path;
  • a pointer context is handed back to its handler on every request;
  • a header with CR/LF is rejected (error.Header) and nothing is injected;
  • a malformed request returns error.Dispatch.

In CI, example/zig-hello serves real requests over TCP and shuts down cleanly on SIGTERM.

Verification

Run locally on Windows with the official Zig 0.17.0. There is no libcwist build for these targets here, so linking stops at the expected undefined cwist_* symbols.

  • zig build test -Dtarget=x86_64-linux-gnu: translate-c of the CWIST headers and full semantic analysis of src/cwist.zig and its tests succeed. The only errors are undefined cwist_* symbols at link, from a stand-in empty libcwist.
  • zig build -Dtarget=x86_64-linux-gnu in example/zig-hello: same result, with the path dependency on bindings/zig resolving.
  • zig build test -Dtarget=aarch64-macos: translate-c of the headers against the Darwin libc headers and semantic analysis succeed. The only errors come from the linker rejecting the Linux-format stand-in libcwist.a.
  • zig fmt --check on all Zig and ZON files: clean
  • bash -n scripts/ci/hello_smoke.sh: OK
  • git diff --check, ASCII check: clean
  • python scripts/ci/check_test_wiring.py, python scripts/ci/check_inline_exports.py: OK

The real link, running the tests and the example over TCP happen in this PR's CI (Zig bindings on Linux and macOS). The Rust job checks the renamed smoke script.

Not in this PR

  • Middleware, deferred (async) responses, TLS and the built-in middleware factories. cwist.c exposes them raw in the meantime, and they can follow one at a time as they did for Rust.
  • Packaging for the Zig package index.
  • Any C change.

…Zig 0.17.0 (issue c4punks#36)

First step of the Zig bindings agreed in issue c4punks#36.

bindings/zig is a Zig package (minimum_zig_version 0.17.0) exporting a
`cwist` module:
- build.zig translates the installed CWIST headers with addTranslateC
  (Zig 0.17 has no @cImport) and links libcwist statically through
  `pkg-config --static cwist`, like the Rust build.rs: -I feeds
  translate-c, -L/-l the module, -lstdc++ maps to Zig's libc++, and the
  Homebrew directories of the system deps come from their own .pc files.
- src/cwist.zig wraps App (init/deinit, get/post/put/delete/patch routes
  through the cwist_app_*_ex functions with a typed context and a comptime
  trampoline, in-memory dispatch, listen on the reactor server with one
  worker) plus Request/Response views and shutdown(), with tests against
  the real library.
- src/cwist.h includes the system <stdatomic.h>/<pthread.h> first and
  build.zig defines __TTAK_STDATOMIC_SYSTEM_INCLUDED, so libttak does not
  fall back to its portable atomics shim, which translate-c cannot parse.

example/zig-hello serves the same routes as example/rust-hello.

.github/workflows/zig.yml (Linux and macOS) installs the official Zig
0.17.0 archive checked against its SHA-256, builds and installs CWIST,
runs `zig build test`, and serves real requests with zig-hello through
scripts/ci/hello_smoke.sh, the former rust_hello_smoke.sh, which now takes
the expected X-Powered-By value as an argument (rust.yml updated).
@AyushVUpadhye

Copy link
Copy Markdown
Contributor Author

The two WASM failures ("WASI 0.2 socket server smoke" and "Component pipeline (WIT + jco guests)") don't come from this PR, which changes no C code. Both stop compiling src/core/mem/gc.c for wasm32-wasip2:

sys/mman.h: error: "WASI lacks a true mmap; to enable minimal mmap emulation, compile with -D_WASI_EMULATED_MMAN and link with -lwasi-emulated-mman"
src/core/mem/gc.c:197:9: error: call to undeclared function 'mmap'
src/core/mem/gc.c:267:5: error: call to undeclared function 'mprotect'

dev fails the same way. The WASM workflow on dev was green at a9706e3 and has been red since 5c3a6dd (the merge of #318), on every push since (latest 6cb74e5). In that range, 88ca5c2 ("fix(wasm): include gc.c in WASIP2_EXTRA_SRCS for cwist_full_gc_enabled symbol") added gc.c to the WASI build, and its mmap/mprotect path has no WASI guard.

So I've left it out of this PR. Happy to send a separate fix if useful, for example compiling that path out under __wasi__, or building with -D_WASI_EMULATED_MMAN and linking -lwasi-emulated-mman as the error suggests.

@AyushVUpadhye

Copy link
Copy Markdown
Contributor Author

The two cwist-sys (Rust) failures don't come from this PR either; dev's Rust workflow has failed the same way since 5c3a6dd, the merge of #318 (last green a9706e3). The only Rust-side change here is the smoke-script rename, and that step never ran: cargo test failed first.

Root cause, as far as I can tell: #318 (c299f60) made cwist_async_claim() spin until the dispatch ack when a completion comes from another thread:

while (!atomic_load_explicit(&a->ack, memory_order_acquire)) {
    if (pthread_equal(pthread_self(), a->dispatch_thread)) return true;
    sched_yield();
}

The ack is published only after the handler returns. The Rust server tests from #296/#299 deliberately complete the AsyncResponse on another thread while the handler waits for that completion, to prove the handler cannot write a deferred response meanwhile. With the new claim that is a deadlock: respond() waits for the handler, the handler waits for respond(), until the test's 5 s timeout:

panicked at cwist/tests/server.rs:391:69: respond: Timeout
a_deferred_response_is_read_only_for_the_handler_and_middleware ... FAILED
  left: ["middleware Err(Deferred)"]
 right: ["responded true", "deferred true", "status 0", ...]

Since #318 guarantees a foreign completion never touches the response before the handler has returned, the race those tests force can no longer happen. The tests (and the AsyncResponse docs) should be updated to the new rule rather than C being changed back. I can send that as a separate Rust PR if that matches your intent for #318.

AyushVUpadhye and others added 3 commits October 6, 2026 09:58
…unks#36)

On Linux the bundled C++ code in libcwist (BoringSSL) is built by g++
against libstdc++ and references its internals; the CI link failed on
`std::__throw_out_of_range_fmt`, which Zig's own libc++ (what -lstdc++
otherwise maps to) does not provide. For a native Linux target build.zig
now links the system libstdc++ found through `c++ -print-file-name`
(CXX is honoured), and keeps Zig's libc++ elsewhere, including macOS,
where the system runtime is libc++ and the job already passes.
…c4punks#36)

With the system libstdc++ linked, the Linux CI link moved on to
`_Unwind_Resume`: the g++-built objects need the libgcc_s unwinder,
which the g++ driver links next to -lstdc++ and Zig does not. build.zig
now resolves libgcc_s.so.1 (libgcc_s.so is a linker script) through
`c++ -print-file-name` alongside libstdc++.so, and falls back to Zig's
libc++ only if either is missing.
@gg582
gg582 merged commit eabcf8d into c4punks:dev Oct 6, 2026
18 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.

2 participants