Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
export LIBCLANG_PATH="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib"
fi
cargo build --manifest-path example/rust-hello/Cargo.toml
bash scripts/ci/rust_hello_smoke.sh example/rust-hello/target/debug/rust-hello
bash scripts/ci/hello_smoke.sh example/rust-hello/target/debug/rust-hello "CWIST Rust"

- name: FFI overhead micro-benchmark
shell: bash
Expand Down
98 changes: 98 additions & 0 deletions .github/workflows/zig.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: "Zig bindings"

# bindings/zig: build and install CWIST, then translate its installed headers
# and link libcwist statically through cwist.pc, run the binding tests, and
# serve real requests with example/zig-hello. Linux and macOS, on one pinned
# Zig release (issue #36: Zig is pre-1.0, so a new release must never change
# what CI builds with until a PR moves the pin).

on:
push:
branches: [main, master, dev]
pull_request:
branches: [main, master, dev]
workflow_dispatch:

permissions:
contents: read

env:
ZIG_VERSION: 0.17.0

jobs:
zig:
name: zig (${{ matrix.os }})
if: github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]')
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
zig_platform: x86_64-linux
zig_sha256: 1cbe9df9f27e6b78d14ccbca43b6703a404ef79ef1c463de901d7f088d4e2026
- os: macos-15
zig_platform: aarch64-macos
zig_sha256: b607e9b9234790a008116ae5bdb71c6243b84b9fb42a53a9e70fde41c06c536a
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install build dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y cmake pkg-config \
libcurl4-openssl-dev libnghttp2-dev libbrotli-dev libzstd-dev

- name: Install build dependencies (macOS)
if: runner.os == 'macOS'
run: brew install cmake pkg-config curl nghttp2 brotli zstd

# The official release archive, checked against its published SHA-256,
# so the pinned compiler cannot change underneath CI.
- name: Install Zig ${{ env.ZIG_VERSION }}
shell: bash
run: |
name="zig-${{ matrix.zig_platform }}-$ZIG_VERSION"
curl -sSfL -o "$RUNNER_TEMP/zig.tar.xz" "https://ziglang.org/download/$ZIG_VERSION/$name.tar.xz"
echo "${{ matrix.zig_sha256 }} $RUNNER_TEMP/zig.tar.xz" | shasum -a 256 -c -
tar -xJf "$RUNNER_TEMP/zig.tar.xz" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/$name" >> "$GITHUB_PATH"

- name: Build and install CWIST
shell: bash
env:
# libttak's default stack adds -flto; AppleClang cannot emit fat LTO
# objects, and plain objects are what the Zig linker consumes.
PERF_STACK_FLAGS: ${{ runner.os == 'macOS' && '-O2 -g -pipe' || '' }}
PKG_CONFIG_PATH: ${{ runner.os == 'macOS' && '/opt/homebrew/opt/curl/lib/pkgconfig' || '' }}
run: |
if [ -z "$PERF_STACK_FLAGS" ]; then unset PERF_STACK_FLAGS; fi
jobs=$(getconf _NPROCESSORS_ONLN)
make -j"$jobs"
make install PREFIX="$RUNNER_TEMP/cwist"

- name: zig build test
shell: bash
run: |
export PKG_CONFIG_PATH="$RUNNER_TEMP/cwist/lib/pkgconfig"
if [ "$RUNNER_OS" = "macOS" ]; then
export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/opt/homebrew/opt/curl/lib/pkgconfig"
fi
zig version
pkg-config --cflags --libs --static cwist
cd bindings/zig
zig build test --summary all

- name: example/zig-hello serves requests
shell: bash
run: |
export PKG_CONFIG_PATH="$RUNNER_TEMP/cwist/lib/pkgconfig"
if [ "$RUNNER_OS" = "macOS" ]; then
export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/opt/homebrew/opt/curl/lib/pkgconfig"
fi
(cd example/zig-hello && zig build)
bash scripts/ci/hello_smoke.sh example/zig-hello/zig-out/bin/zig-hello "CWIST Zig"
2 changes: 2 additions & 0 deletions bindings/zig/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.zig-cache/
zig-out/
63 changes: 63 additions & 0 deletions bindings/zig/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# CWIST Zig bindings

Zig API for CWIST (issue #36), next to the Rust bindings in `bindings/rust/`.

* The C API is translated from the installed CWIST headers at build time
(`b.addTranslateC`, see `src/cwist.h`) and available as `cwist.c`.
* `src/cwist.zig` adds a small Zig layer on top: `App` with `get`/`post`/
`put`/`delete`/`patch` routes, `Request`, `Response`, in-memory `dispatch`,
`listen` and `shutdown`.

## Zig version

Pinned to **Zig 0.17.0** (`minimum_zig_version` in `build.zig.zon`, and the
exact release in CI). Zig is pre-1.0 and changes its build and language APIs
between releases, so the pin moves only in a dedicated PR.

## Build

CWIST must be built and installed first; libcwist is found through
`pkg-config --static cwist`:

```bash
make && make install PREFIX=$HOME/.local
export PKG_CONFIG_PATH=$HOME/.local/lib/pkgconfig
cd bindings/zig && zig build test
```

`build.zig` links libcwist and its bundled dependencies statically from the
`cwist.pc` link directories; `-lstdc++` maps to Zig's own libc++. On macOS,
add Homebrew's curl to `PKG_CONFIG_PATH`
(`/opt/homebrew/opt/curl/lib/pkgconfig`), as for the Rust bindings.

## Use from another package

```zig
// build.zig.zon
.dependencies = .{ .cwist = .{ .path = "path/to/CWIST/bindings/zig" } },

// build.zig
const cwist = b.dependency("cwist", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("cwist", cwist.module("cwist"));
```

See `example/zig-hello/` for a complete server.

## Handlers, lifetimes and threads

* A handler is `fn (Context, cwist.Request, cwist.Response) void`. `Context`
is a pointer, or `void` with `{}` at registration; CWIST hands it back on
every request. It is borrowed: it must outlive the app.
* `Request` and `Response` are views of CWIST's objects for one handler call.
Slices from `Request` point into CWIST's memory and must not be kept after
the handler returns.
* CWIST runs handlers on several worker threads at once, so a context must
be safe to use from any thread.
* A handler returns nothing, so no error crosses into C, and a panic aborts
the process (Zig does not unwind).
* `listen` serves in the calling process on the reactor server, with no
forked workers, and returns after a graceful shutdown (`cwist.shutdown()`,
SIGTERM or SIGINT) with every handler thread joined.

Not wrapped yet: middleware, deferred (async) responses, TLS. The raw API in
`cwist.c` covers them in the meantime.
141 changes: 141 additions & 0 deletions bindings/zig/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//! Zig bindings for CWIST: `zig build test` against an installed libcwist.
//!
//! libcwist is found through `pkg-config --static cwist` (the `cwist.pc` that
//! `make install` writes); set PKG_CONFIG_PATH when it is not in a default
//! location. Other packages use the `cwist` module this file exports.

const std = @import("std");

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const flags = pkgConfigFlags(b);

// The C API, translated from the installed headers (src/cwist.h).
const translate = b.addTranslateC(.{
.root_source_file = b.path("src/cwist.h"),
.target = target,
.optimize = optimize,
});
// src/cwist.h includes the system <stdatomic.h> itself; this keeps
// libttak's portable fallback, which translate-c cannot parse, out.
translate.defineCMacro("__TTAK_STDATOMIC_SYSTEM_INCLUDED", "1");
for (flags.include_dirs.items) |dir| translate.addIncludePath(b.graph.cwdRelativePath(dir));

const mod = b.addModule("cwist", .{
.root_source_file = b.path("src/cwist.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{.{ .name = "c", .module = translate.createModule() }},
});
for (flags.lib_dirs.items) |dir| mod.addLibraryPath(b.graph.cwdRelativePath(dir));
// -lc, -lm, -lpthread and -ldl map to Zig's own libc handling; everything
// else is searched in the -L directories first, so libcwist and its
// bundled dependencies link statically.
for (flags.libs.items) |lib| {
if (std.mem.eql(u8, lib, "stdc++")) {
linkCxxRuntime(b, mod, target);
} else {
mod.linkSystemLibrary(lib, .{ .use_pkg_config = .no });
}
}

const tests = b.addTest(.{ .root_module = mod });
const test_step = b.step("test", "Run the binding tests against libcwist");
test_step.dependOn(&b.addRunArtifact(tests).step);
}

/// Links the C++ runtime libcwist's bundled C++ code (BoringSSL) was built
/// against. On a native Linux build that is the system libstdc++ plus the
/// libgcc_s unwinder, as the g++ driver links them: the objects reference
/// libstdc++ internals (std::__throw_out_of_range_fmt) and _Unwind_Resume,
/// which Zig's own libc++ does not provide. Elsewhere, including macOS where
/// the system runtime is libc++, Zig's libc++ is used.
fn linkCxxRuntime(b: *std.Build, mod: *std.Build.Module, target: std.Build.ResolvedTarget) void {
if (target.query.isNative() and target.result.os.tag == .linux) {
const cxx = b.graph.environ_map.get("CXX") orelse "c++";
// libgcc_s.so is usually a linker script; the .so.1 is the library.
const stdcxx = compilerFile(b, cxx, "libstdc++.so");
const gcc_s = compilerFile(b, cxx, "libgcc_s.so.1");
if (stdcxx != null and gcc_s != null) {
mod.addObjectFile(b.graph.cwdRelativePath(stdcxx.?));
mod.addObjectFile(b.graph.cwdRelativePath(gcc_s.?));
mod.link_libc = true;
return;
}
}
mod.linkSystemLibrary("stdc++", .{ .use_pkg_config = .no });
}

/// The absolute path the C++ compiler resolves `name` to, or null.
fn compilerFile(b: *std.Build, cxx: []const u8, name: []const u8) ?[]const u8 {
const arg = b.fmt("-print-file-name={s}", .{name});
switch (b.runFallible(&.{ cxx, arg }, .{ .stderr_behavior = .ignore })) {
.success => |stdout| {
const path = std.mem.trim(u8, stdout, " \t\r\n");
// A bare name back means the compiler did not find it.
return if (std.fs.path.isAbsolute(path)) path else null;
},
else => return null,
}
}

const Flags = struct {
include_dirs: std.ArrayList([]const u8) = .empty,
lib_dirs: std.ArrayList([]const u8) = .empty,
libs: std.ArrayList([]const u8) = .empty,
};

/// System libraries `cwist.pc` names as plain `-l` flags. Their own
/// pkg-config files supply the directory when it is not a default one
/// (Homebrew on macOS), the same way the CWIST Makefile finds them.
const system_deps = [_][]const u8{
"libzstd", "libbrotlienc", "libbrotlicommon", "libbrotlidec", "libcurl", "libnghttp2",
};

fn pkgConfigFlags(b: *std.Build) Flags {
const arena = b.graph.arena;
const pkg_config = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config";
var flags: Flags = .{};

// --static: Libs.private (curl, nghttp2) is needed for a static libcwist.
const out = switch (b.runFallible(&.{ pkg_config, "--cflags", "--libs", "--static", "cwist" }, .{
.stderr_behavior = .inherit,
})) {
.success => |stdout| stdout,
else => std.debug.panic(
"libcwist was not found through {s}. Build and install CWIST first, for example\n" ++
" make && make install PREFIX=$HOME/.local\n" ++
"then set PKG_CONFIG_PATH=$HOME/.local/lib/pkgconfig",
.{pkg_config},
),
};
addFlags(arena, &flags, out);

for (system_deps) |dep| {
switch (b.runFallible(&.{ pkg_config, "--libs-only-L", dep }, .{ .stderr_behavior = .ignore })) {
.success => |stdout| addFlags(arena, &flags, stdout),
// Optional: without a .pc file the library must be on a default path.
else => {},
}
}
return flags;
}

fn addFlags(arena: std.mem.Allocator, flags: *Flags, output: []const u8) void {
var it = std.mem.tokenizeAny(u8, output, " \t\r\n");
while (it.next()) |arg| {
const list, const value = if (std.mem.startsWith(u8, arg, "-I"))
.{ &flags.include_dirs, arg[2..] }
else if (std.mem.startsWith(u8, arg, "-L"))
.{ &flags.lib_dirs, arg[2..] }
else if (std.mem.startsWith(u8, arg, "-l"))
.{ &flags.libs, arg[2..] }
else
continue;
for (list.items) |seen| {
if (std.mem.eql(u8, seen, value)) break;
} else list.append(arena, value) catch @panic("OOM");
}
}
16 changes: 16 additions & 0 deletions bindings/zig/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.{
.name = .cwist,
.version = "0.1.0",
.fingerprint = 0xfa240d1a22ae273b, // Changing this has security and trust implications.
// Pinned on purpose (issue #36): Zig is pre-1.0 and changes its build
// and language APIs between releases. Move to a newer one in a
// dedicated PR.
.minimum_zig_version = "0.17.0",
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"README.md",
},
}
17 changes: 17 additions & 0 deletions bindings/zig/src/cwist.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* The C API the Zig bindings use, translated by build.zig (addTranslateC)
* from the installed CWIST headers. Only the canonical headers: the legacy
* umbrella headers carry stale copies of some structs (tests/abi_layout.h).
*
* The system <stdatomic.h> and <pthread.h> come first so that libttak's
* headers use them instead of their portable fallback (build.zig defines
* __TTAK_STDATOMIC_SYSTEM_INCLUDED to match). */
#include <stdatomic.h>
#include <pthread.h>

#include <cwist/core/mem/alloc.h>
#include <cwist/core/sstring/sstring.h>
#include <cwist/net/http/http.h>
#include <cwist/net/http/query.h>
#include <cwist/sys/app/app.h>
#include <cwist/sys/app/shutdown.h>
#include <cwist/sys/err/cwist_err.h>
Loading
Loading