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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ While one connection waits for network progress, its owner can service other rea
The framework reserves memory and threads before serving requests, with explicit connection, request, and response limits.
The build target selects the native backend automatically.

**No libc required on Linux.** The framework does not force libc on its consumers.
On Linux it makes direct system calls ([src/sys.zig](src/sys.zig)) unless the
application links libc itself, so a Linux build can be one static executable with
no interpreter and no shared libraries, for any ABI (`-Dtarget=x86_64-linux-musl`,
`-linux-none`, or the native default). macOS always links libSystem; Windows uses Win32.

The repository and executable use `bounded-http`.
The Zig package and module use `bounded_http`.
The project was previously named `zig-http`.
Expand Down Expand Up @@ -126,7 +132,7 @@ To use the framework, import the `bounded_http` module exported by
[build.zig](build.zig), provide an [api.Handler](src/api.zig), and follow the
startup/run/stop lifecycle in [src/main.zig](src/main.zig). Its actual demo
callback is the maintained example. [src/server.zig](src/server.zig) exposes
`Config`, `Cluster`, `Server`, `api` and `Budget`; builds link libc. Treat this API as
`Config`, `Cluster`, `Server`, `api` and `Budget`; Linux builds need no libc. Treat this API as
experimental and read the [ownership contract](docs/OWNERSHIP.md) before
retaining slices or adding asynchronous application work.

Expand Down
24 changes: 20 additions & 4 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/server.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
// No forced libc: Linux uses direct system calls (src/sys.zig) unless the
// application links libc; macOS always links libSystem.
});
// Arch's GCC 16 CRT contains .sframe R_X86_64_PC64 relocations which
// Zig 0.16's native ELF linker rejects. Use the bundled LLVM/LLD path
// for Linux Debug only. ReleaseSafe uses its default toolchain selection.
// for Linux Debug only; it matters only when an application links glibc.
const exe = b.addExecutable(.{
.use_llvm = if (target.result.os.tag == .linux and optimize == .Debug) true else null,
.use_lld = if (target.result.os.tag == .linux and optimize == .Debug) true else null,
Expand All @@ -25,7 +26,6 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{.{ .name = "bounded_http", .module = module }},
}),
});
Expand All @@ -50,7 +50,7 @@ pub fn build(b: *std.Build) void {
b.step("example-check", "Compile and exercise the independent embedding project").dependOn(&embedding_check.step);
verify.dependOn(&embedding_check.step);
const test_step = b.step("test", "Run unit and transport tests");
for ([_][]const u8{ "src/http.zig", "src/api.zig", "src/budget.zig", "src/transport.zig", "src/server.zig" }) |path| {
for ([_][]const u8{ "src/http.zig", "src/api.zig", "src/budget.zig", "src/transport.zig", "src/server.zig", "src/sys.zig" }) |path| {
const tests = b.addTest(.{ .use_llvm = if (target.result.os.tag == .linux and optimize == .Debug) true else null, .use_lld = if (target.result.os.tag == .linux and optimize == .Debug) true else null, .root_module = b.createModule(.{
.root_source_file = b.path(path),
.target = target,
Expand All @@ -62,4 +62,20 @@ pub fn build(b: *std.Build) void {
verify.dependOn(&run_tests.step);
test_step.dependOn(&run_tests.step);
}
// Linux without libc: the same transport and server tests over direct
// system calls (src/sys.zig), as a libc-free application links them.
if (target.result.os.tag == .linux) {
for ([_][]const u8{ "src/transport.zig", "src/server.zig", "src/sys.zig" }) |path| {
const tests = b.addTest(.{ .name = "no-libc", .root_module = b.createModule(.{
.root_source_file = b.path(path),
.target = target,
.optimize = optimize,
.link_libc = false,
}) });
compile_only.dependOn(&tests.step);
const run_tests = b.addRunArtifact(tests);
verify.dependOn(&run_tests.step);
test_step.dependOn(&run_tests.step);
}
}
}
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ The framework cannot safely kill one arbitrary callback and reuse its borrowed m
| [handoff.zig](../src/handoff.zig) | Fixed queues with exclusive socket ownership and acquire/release publication. |
| [budget.zig](../src/budget.zig) | Framework allocator accounting and sealing. |
| [transport.zig](../src/transport.zig) | Platform selection, common operation types, and listener setup. |
| [sys.zig](../src/sys.zig) | The POSIX calls outside the Windows transport, with libc signatures: `std.c` when libc is linked, direct Linux system calls when it is not. |
| [transport_linux.zig](../src/transport_linux.zig) | Linux completion adapter. |
| [transport_macos.zig](../src/transport_macos.zig) | macOS readiness adapter with explicit completion reports. |
| [transport_windows.zig](../src/transport_windows.zig) | Windows overlapped socket adapter with bounded IOCP records and cancellation drain. |
Expand Down
5 changes: 4 additions & 1 deletion docs/USING.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ The consumer obtains that module with `dependency.module("bounded_http")`.
The consumer exposes it to its source as the `bounded_http` import.

The example passes its selected target and optimization mode to the dependency.
Its executable links libc.
Its executable links no libc on Linux: the framework uses direct system calls there
(see `sys.zig`), so the example builds as one static executable without an
interpreter or shared libraries. An application that links libc for its own reasons
gets the same framework calls through libc. macOS always links libSystem.
The exported module is the framework entry point; the demo executable is not an embedding dependency.

The example uses one shard and an immutable fixed response.
Expand Down
1 change: 0 additions & 1 deletion examples/embedding/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{.{ .name = "bounded_http", .module = dependency.module("bounded_http") }},
});
const executable = b.addExecutable(.{
Expand Down
2 changes: 1 addition & 1 deletion examples/embedding/src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub fn main(init: std.process.Init) !void {
cluster.run() catch |err| {
// An uncertain run error can leave callbacks or kernel operations owning memory.
std.debug.print("FATAL {s}; retained owners require process exit\n", .{@errorName(err)});
std.c._exit(70);
framework.failFast(70);
};
var stats = cluster.stats();
stats.allocation_calls_after_start = budget.late_calls.load(.acquire);
Expand Down
2 changes: 1 addition & 1 deletion src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ fn handle(context: *api.Context) !api.Action {
win32.Sleep(demo.stall_ms);
} else {
var remaining: std.c.timespec = .{ .sec = demo.stall_ms / 1000, .nsec = @as(isize, demo.stall_ms % 1000) * 1_000_000 };
while (std.c.nanosleep(&remaining, &remaining) != 0) {
while (framework.sys.nanosleep(&remaining, &remaining) != 0) {
if (context.cancelled.load(.acquire)) return .close;
}
}
Expand Down
25 changes: 14 additions & 11 deletions src/server.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const std = @import("std");
const c = std.c;
const builtin = @import("builtin");
/// POSIX calls with or without libc (see sys.zig).
pub const sys = @import("sys.zig");
const windows = std.os.windows;
const win32 = struct {
extern "kernel32" fn CreateEventW(?*anyopaque, i32, i32, ?[*:0]const u16) callconv(.winapi) ?windows.HANDLE;
Expand Down Expand Up @@ -367,7 +369,7 @@ const Worker = struct {
return .{ .server = server, .index = index, .event = event };
}
var fds: [2]c.fd_t = undefined;
if (c.pipe(&fds) != 0) return error.WorkerPipeFailed;
if (sys.pipe(&fds) != 0) return error.WorkerPipeFailed;
errdefer transport.closeFd(fds[0]);
errdefer transport.closeFd(fds[1]);
try transport.setFlags(fds[0], false);
Expand All @@ -389,8 +391,8 @@ const Worker = struct {
return;
}
const byte = [_]u8{1};
const result = c.write(self.write_fd, &byte, 1);
if (result < 0) assert(c.errno(result) == .AGAIN or c.errno(result) == .INTR);
const result = sys.write(self.write_fd, &byte, 1);
if (result < 0) assert(sys.errno(result) == .AGAIN or sys.errno(result) == .INTR);
}
fn run(self: *Worker) void {
_ = self.server.ready_workers.fetchAdd(1, .release);
Expand All @@ -416,15 +418,15 @@ const Worker = struct {
assert(result == 0 or result == 258); // signaled or timeout
} else {
var ready = [_]c.pollfd{.{ .fd = self.read_fd, .events = c.POLL.IN, .revents = 0 }};
const polled = c.poll(&ready, 1, 10);
const polled = sys.poll(&ready, 1, 10);
if (polled < 0) {
assert(c.errno(polled) == .INTR);
assert(sys.errno(polled) == .INTR);
return;
}
if (polled > 0) {
var bytes: [64]u8 = undefined;
const read = c.read(self.read_fd, &bytes, bytes.len);
assert(read > 0 or (read < 0 and c.errno(read) == .INTR));
const read = sys.read(self.read_fd, &bytes, bytes.len);
assert(read > 0 or (read < 0 and sys.errno(read) == .INTR));
}
}
}
Expand Down Expand Up @@ -2428,7 +2430,7 @@ pub const Server = struct {
break :seconds @intCast(@divFloor(@max(unix_100ns, 0), 10_000_000));
} else seconds: {
var ts: c.timespec = undefined;
assert(c.clock_gettime(.REALTIME, &ts) == 0);
assert(sys.clock_gettime(.REALTIME, &ts) == 0);
break :seconds @intCast(@max(ts.sec, 0));
};
if (seconds == self.date_second) return;
Expand Down Expand Up @@ -2852,7 +2854,8 @@ pub fn failFast(code: u8) noreturn {
if (win32.TerminateProcess(win32.GetCurrentProcess(), code) == 0) @trap();
unreachable;
}
std.c._exit(code);
// _exit semantics with or without libc (sys.exitNow).
sys.exitNow(code);
}

pub fn nowNs() u64 {
Expand All @@ -2865,7 +2868,7 @@ pub fn nowNs() u64 {
return @intCast(@as(u128, @intCast(counter)) * std.time.ns_per_s / @as(u64, @intCast(frequency)));
}
var time: c.timespec = undefined;
assert(c.clock_gettime(.MONOTONIC, &time) == 0);
assert(sys.clock_gettime(.MONOTONIC, &time) == 0);
return @as(u64, @intCast(time.sec)) * 1_000_000_000 + @as(u64, @intCast(time.nsec));
}

Expand Down Expand Up @@ -3110,7 +3113,7 @@ const ClusterTestWatchdog = struct {
win32.Sleep(1);
} else {
const delay: c.timespec = .{ .sec = 0, .nsec = 1_000_000 };
_ = c.nanosleep(&delay, null);
_ = sys.nanosleep(&delay, null);
}
}
}
Expand Down
156 changes: 156 additions & 0 deletions src/sys.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//! The POSIX calls bounded/http makes outside the Windows transport, with
//! libc's signatures and return conventions (-1 and `errno`).
//!
//! When libc is linked, and on every system other than Linux, these are the
//! `std.c` functions. On Linux without libc they are direct system calls via
//! `std.os.linux`, so an application can link no libc at all (for example
//! `-Dtarget=x86_64-linux-musl` with a static binary, or `x86_64-linux-none`).
//! `errno` reads the calling thread's last failure in both cases.
const std = @import("std");
const builtin = @import("builtin");
const c = std.c;
const linux = std.os.linux;

/// True when calls go straight to the kernel instead of through libc.
pub const direct = builtin.os.tag == .linux and !builtin.link_libc;

pub const E = c.E;

/// The last failure of a direct call on this thread (libc keeps its own).
threadlocal var last_errno: linux.E = .SUCCESS;

fn int(rc: usize) c_int {
const err = linux.errno(rc);
if (err != .SUCCESS) {
last_errno = err;
return -1;
}
return @intCast(rc);
}

fn size(rc: usize) isize {
const err = linux.errno(rc);
if (err != .SUCCESS) {
last_errno = err;
return -1;
}
return @intCast(rc);
}

/// The error of a failed call (`rc == -1`), else `.SUCCESS`.
pub fn errno(rc: anytype) E {
if (!direct) return c.errno(rc);
return if (rc == -1) @enumFromInt(@intFromEnum(last_errno)) else .SUCCESS;
}

pub fn socket(domain: c_uint, socket_type: c_uint, protocol: c_uint) c_int {
if (!direct) return c.socket(domain, socket_type, protocol);
return int(linux.socket(domain, socket_type, protocol));
}

pub fn setsockopt(fd: c.fd_t, level: i32, name: u32, value: *const anyopaque, len: c.socklen_t) c_int {
if (!direct) return c.setsockopt(fd, level, name, value, len);
return int(linux.setsockopt(fd, level, name, @ptrCast(value), len));
}

pub fn bind(fd: c.fd_t, address: *const c.sockaddr, len: c.socklen_t) c_int {
if (!direct) return c.bind(fd, address, len);
return int(linux.bind(fd, address, len));
}

pub fn listen(fd: c.fd_t, backlog: c_uint) c_int {
if (!direct) return c.listen(fd, backlog);
return int(linux.listen(fd, backlog));
}

pub fn getsockname(fd: c.fd_t, noalias address: *c.sockaddr, noalias len: *c.socklen_t) c_int {
if (!direct) return c.getsockname(fd, address, len);
return int(linux.getsockname(fd, address, len));
}

/// `fcntl` with one explicit argument (0 when the command takes none).
pub fn fcntl(fd: c.fd_t, cmd: c_int, arg: usize) c_int {
if (!direct) return c.fcntl(fd, cmd, arg);
return int(linux.fcntl(fd, cmd, arg));
}

pub fn close(fd: c.fd_t) c_int {
if (!direct) return c.close(fd);
return int(linux.close(fd));
}

pub fn connect(fd: c.fd_t, address: *const c.sockaddr, len: c.socklen_t) c_int {
if (!direct) return c.connect(fd, address, len);
return int(linux.connect(fd, address, len));
}

pub fn send(fd: c.fd_t, bytes: *const anyopaque, len: usize, flags: u32) isize {
if (!direct) return c.send(fd, bytes, len, flags);
return size(linux.sendto(fd, @ptrCast(bytes), len, flags, null, 0));
}

pub fn recv(fd: c.fd_t, bytes: *anyopaque, len: usize, flags: u32) isize {
if (!direct) return c.recv(fd, bytes, len, @intCast(flags));
return size(linux.recvfrom(fd, @ptrCast(bytes), len, flags, null, null));
}

pub fn poll(fds: [*]c.pollfd, count: c.nfds_t, timeout_ms: c_int) c_int {
if (!direct) return c.poll(fds, count, timeout_ms);
return int(linux.poll(fds, count, timeout_ms));
}

pub fn shutdown(fd: c.fd_t, how: c_int) c_int {
if (!direct) return c.shutdown(fd, how);
return int(linux.shutdown(fd, how));
}

pub fn pipe(fds: *[2]c.fd_t) c_int {
if (!direct) return c.pipe(fds);
return int(linux.pipe(fds));
}

pub fn read(fd: c.fd_t, bytes: [*]u8, len: usize) isize {
if (!direct) return c.read(fd, bytes, len);
return size(linux.read(fd, bytes, len));
}

pub fn write(fd: c.fd_t, bytes: [*]const u8, len: usize) isize {
if (!direct) return c.write(fd, bytes, len);
return size(linux.write(fd, bytes, len));
}

pub fn clock_gettime(clock: c.clockid_t, time: *c.timespec) c_int {
if (!direct) return c.clock_gettime(clock, time);
return int(linux.clock_gettime(clock, time));
}

pub fn nanosleep(request: *const c.timespec, remaining: ?*c.timespec) c_int {
if (!direct) return c.nanosleep(request, remaining);
return int(linux.nanosleep(request, remaining));
}

/// Exit at once: no atexit handlers, no unwinding (see server.failFast).
pub fn exitNow(code: u8) noreturn {
// std.process.exit without libc is already _exit: exit_group on Linux.
// With libc it would call exit(3) and run atexit handlers, so use _exit.
if (builtin.link_libc) c._exit(code);
std.process.exit(code);
}

test "direct calls report failures through errno like libc" {
if (comptime builtin.os.tag == .windows) return error.SkipZigTest;
const bad: c.fd_t = -1;
const result = close(bad);
try std.testing.expectEqual(@as(c_int, -1), result);
try std.testing.expectEqual(E.BADF, errno(result));
var fds: [2]c.fd_t = undefined;
try std.testing.expectEqual(@as(c_int, 0), pipe(&fds));
defer _ = close(fds[0]);
defer _ = close(fds[1]);
try std.testing.expectEqual(@as(isize, 1), write(fds[1], "x", 1));
var byte: [1]u8 = undefined;
try std.testing.expectEqual(@as(isize, 1), read(fds[0], &byte, 1));
try std.testing.expectEqual(@as(u8, 'x'), byte[0]);
var now: c.timespec = undefined;
try std.testing.expectEqual(@as(c_int, 0), clock_gettime(.MONOTONIC, &now));
}
Loading
Loading