diff --git a/README.md b/README.md index 61c9785..f98b408 100644 --- a/README.md +++ b/README.md @@ -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`. @@ -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. diff --git a/build.zig b/build.zig index f45613e..40ae5fe 100644 --- a/build.zig +++ b/build.zig @@ -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, @@ -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 }}, }), }); @@ -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, @@ -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); + } + } } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3be1996..d2ae92f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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. | diff --git a/docs/USING.md b/docs/USING.md index 39915a8..04af22e 100644 --- a/docs/USING.md +++ b/docs/USING.md @@ -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. diff --git a/examples/embedding/build.zig b/examples/embedding/build.zig index 01f6a21..97d7d2f 100644 --- a/examples/embedding/build.zig +++ b/examples/embedding/build.zig @@ -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(.{ diff --git a/examples/embedding/src/main.zig b/examples/embedding/src/main.zig index 7609770..82f5c01 100644 --- a/examples/embedding/src/main.zig +++ b/examples/embedding/src/main.zig @@ -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); diff --git a/src/main.zig b/src/main.zig index 11b8fb5..f4e7e06 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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; } } diff --git a/src/server.zig b/src/server.zig index ecd8841..9d04b2b 100644 --- a/src/server.zig +++ b/src/server.zig @@ -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; @@ -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); @@ -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); @@ -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)); } } } @@ -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; @@ -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 { @@ -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)); } @@ -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); } } } diff --git a/src/sys.zig b/src/sys.zig new file mode 100644 index 0000000..c9c6d7f --- /dev/null +++ b/src/sys.zig @@ -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)); +} diff --git a/src/transport.zig b/src/transport.zig index 2962a19..dca2d3b 100644 --- a/src/transport.zig +++ b/src/transport.zig @@ -10,6 +10,7 @@ //! completion, canceled or not. const std = @import("std"); const builtin = @import("builtin"); +const sys = @import("sys.zig"); const c = std.c; /// POSIX descriptor, or a Windows backend-owned logical socket index. @@ -45,7 +46,7 @@ pub fn backendHeapBytes(max_connections: u16) !usize { pub fn setSendBuffer(backend: *Backend, socket: Socket, bytes: u32) !void { if (builtin.os.tag == .windows) return backend.setSendBuffer(socket, bytes); const value: c_int = @intCast(bytes); - if (c.setsockopt(socket, c.SOL.SOCKET, c.SO.SNDBUF, &value, @sizeOf(c_int)) != 0) return error.SocketOptionFailed; + if (sys.setsockopt(socket, c.SOL.SOCKET, c.SO.SNDBUF, &value, @sizeOf(c_int)) != 0) return error.SocketOptionFailed; } // Shared startup-only socket setup. The kernel listen backlog is separate from @@ -60,50 +61,50 @@ pub fn listen(port_number: u16, max_connections: u16, nonblocking: bool, reuse_p pub fn listenBound(bind_address: [4]u8, port_number: u16, max_connections: u16, nonblocking: bool, reuse_port: bool) !Listener { if (max_connections == 0 or max_connections > 16383) return error.InvalidConnectionLimit; - const fd = c.socket(c.AF.INET, c.SOCK.STREAM, 0); + const fd = sys.socket(c.AF.INET, c.SOCK.STREAM, 0); if (fd < 0) return error.SocketFailed; errdefer closeFd(fd); try setFlags(fd, nonblocking); const one: c_int = 1; - if (c.setsockopt(fd, c.SOL.SOCKET, c.SO.REUSEADDR, &one, @sizeOf(c_int)) != 0) return error.SocketOptionFailed; + if (sys.setsockopt(fd, c.SOL.SOCKET, c.SO.REUSEADDR, &one, @sizeOf(c_int)) != 0) return error.SocketOptionFailed; if (reuse_port) { - if (c.setsockopt(fd, c.SOL.SOCKET, c.SO.REUSEPORT, &one, @sizeOf(c_int)) != 0) return error.SocketOptionFailed; + if (sys.setsockopt(fd, c.SOL.SOCKET, c.SO.REUSEPORT, &one, @sizeOf(c_int)) != 0) return error.SocketOptionFailed; } var address: c.sockaddr.in = .{ .port = std.mem.nativeToBig(u16, port_number), .addr = @bitCast(bind_address), }; - if (c.bind(fd, @ptrCast(&address), @sizeOf(@TypeOf(address))) != 0) return error.BindFailed; - if (c.listen(fd, @intCast(max_connections)) != 0) return error.ListenFailed; + if (sys.bind(fd, @ptrCast(&address), @sizeOf(@TypeOf(address))) != 0) return error.BindFailed; + if (sys.listen(fd, @intCast(max_connections)) != 0) return error.ListenFailed; var length: c.socklen_t = @sizeOf(@TypeOf(address)); - if (c.getsockname(fd, @ptrCast(&address), &length) != 0) return error.SocketNameFailed; + if (sys.getsockname(fd, @ptrCast(&address), &length) != 0) return error.SocketNameFailed; return .{ .socket = fd, .port = std.mem.bigToNative(u16, address.port) }; } pub fn setFlags(fd: Socket, nonblocking: bool) !void { - if (c.fcntl(fd, c.F.SETFD, @as(c_int, c.FD_CLOEXEC)) < 0) return error.SocketFlagsFailed; + if (sys.fcntl(fd, c.F.SETFD, c.FD_CLOEXEC) < 0) return error.SocketFlagsFailed; if (nonblocking) { - const flags = c.fcntl(fd, c.F.GETFL); + const flags = sys.fcntl(fd, c.F.GETFL, 0); if (flags < 0) return error.SocketFlagsFailed; const nonblock: u32 = @bitCast(c.O{ .NONBLOCK = true }); - if (c.fcntl(fd, c.F.SETFL, flags | @as(c_int, @intCast(nonblock))) < 0) return error.SocketFlagsFailed; + if (sys.fcntl(fd, c.F.SETFL, @intCast(flags | @as(c_int, @intCast(nonblock)))) < 0) return error.SocketFlagsFailed; } } pub fn configureAccepted(fd: Socket, nonblocking: bool) !void { try setFlags(fd, nonblocking); const one: c_int = 1; - if (c.setsockopt(fd, c.IPPROTO.TCP, c.TCP.NODELAY, &one, @sizeOf(c_int)) != 0) return error.SocketOptionFailed; + if (sys.setsockopt(fd, c.IPPROTO.TCP, c.TCP.NODELAY, &one, @sizeOf(c_int)) != 0) return error.SocketOptionFailed; if (builtin.os.tag == .macos) { - if (c.setsockopt(fd, c.SOL.SOCKET, c.SO.NOSIGPIPE, &one, @sizeOf(c_int)) != 0) return error.SocketOptionFailed; + if (sys.setsockopt(fd, c.SOL.SOCKET, c.SO.NOSIGPIPE, &one, @sizeOf(c_int)) != 0) return error.SocketOptionFailed; } } pub fn closeFd(fd: Socket) void { std.debug.assert(fd >= 0); - const result = c.close(fd); + const result = sys.close(fd); // Do not retry close after EINTR: descriptor reuse makes that unsafe. - std.debug.assert(result == 0 or c.errno(result) == .INTR); + std.debug.assert(result == 0 or sys.errno(result) == .INTR); } pub fn vector(bytes: []const u8) c.iovec_const { @@ -160,14 +161,14 @@ test "transport borrows receive and send buffers and accounts for EOF" { var backend = try Backend.init(std.testing.allocator, 2, 0, false); defer backend.deinit(); try backend.enableGather(); - const client = c.socket(c.AF.INET, c.SOCK.STREAM, 0); + const client = sys.socket(c.AF.INET, c.SOCK.STREAM, 0); if (client < 0) return error.SocketFailed; defer closeFd(client); var address: c.sockaddr.in = .{ .port = std.mem.nativeToBig(u16, backend.port()), .addr = std.mem.nativeToBig(u32, 0x7f000001), }; - if (c.connect(client, @ptrCast(&address), @sizeOf(@TypeOf(address))) != 0) return error.ConnectFailed; + if (sys.connect(client, @ptrCast(&address), @sizeOf(@TypeOf(address))) != 0) return error.ConnectFailed; try backend.accept(test_accept_cell, 21); const accepted = try waitCompletion(&backend); try std.testing.expectEqual(@as(u64, 21), accepted.token); @@ -177,7 +178,7 @@ test "transport borrows receive and send buffers and accounts for EOF" { var buffer: [32]u8 = undefined; try backend.recv(0, 22, peer, &buffer); const input = "borrowed input"; - try std.testing.expectEqual(@as(isize, input.len), c.send(client, input.ptr, input.len, 0)); + try std.testing.expectEqual(@as(isize, input.len), sys.send(client, input.ptr, input.len, 0)); const received = try waitCompletion(&backend); try std.testing.expectEqual(@as(u64, 22), received.token); try std.testing.expectEqual(@as(i32, input.len), received.result); @@ -189,8 +190,8 @@ test "transport borrows receive and send buffers and accounts for EOF" { var response: [32]u8 = undefined; // Poll first so a fixture failure cannot block forever in recv. var readable = [_]c.pollfd{.{ .fd = client, .events = c.POLL.IN, .revents = 0 }}; - try std.testing.expectEqual(@as(c_int, 1), c.poll(&readable, 1, 1000)); - try std.testing.expectEqual(@as(isize, input.len), c.recv(client, &response, response.len, 0)); + try std.testing.expectEqual(@as(c_int, 1), sys.poll(&readable, 1, 1000)); + try std.testing.expectEqual(@as(isize, input.len), sys.recv(client, &response, response.len, 0)); try std.testing.expectEqualStrings(input, response[0..input.len]); const vectors = [_]c.iovec_const{ vector("header:"), vector("body"), vector(":end") }; try backend.sendv(0, 27, peer, &vectors); @@ -198,8 +199,8 @@ test "transport borrows receive and send buffers and accounts for EOF" { try std.testing.expectEqual(@as(u64, 27), gathered.token); const gather_expected = "header:body:end"; try std.testing.expectEqual(@as(i32, gather_expected.len), gathered.result); - try std.testing.expectEqual(@as(c_int, 1), c.poll(&readable, 1, 1000)); - try std.testing.expectEqual(@as(isize, gather_expected.len), c.recv(client, &response, response.len, 0)); + try std.testing.expectEqual(@as(c_int, 1), sys.poll(&readable, 1, 1000)); + try std.testing.expectEqual(@as(isize, gather_expected.len), sys.recv(client, &response, response.len, 0)); try std.testing.expectEqualStrings(gather_expected, response[0..gather_expected.len]); try backend.recv(0, 25, peer, &buffer); try backend.cancel(4, 26, 0); @@ -222,7 +223,7 @@ test "transport borrows receive and send buffers and accounts for EOF" { } } try std.testing.expect(canceled_receive and cancel_acknowledged); - _ = c.shutdown(client, c.SHUT.WR); + _ = sys.shutdown(client, c.SHUT.WR); try backend.recv(0, 24, peer, &buffer); const eof = try waitCompletion(&backend); try std.testing.expectEqual(@as(u64, 24), eof.token); @@ -263,7 +264,7 @@ test "a startup worker can wake a waiting I/O owner" { const Worker = struct { fn run(target: *Backend) void { const delay: c.timespec = .{ .sec = 0, .nsec = 20_000_000 }; - _ = c.nanosleep(&delay, null); + _ = sys.nanosleep(&delay, null); target.wake(); } }; @@ -272,9 +273,9 @@ test "a startup worker can wake a waiting I/O owner" { var completions: [1]Completion = undefined; var before: c.timespec = undefined; var after: c.timespec = undefined; - try std.testing.expectEqual(@as(c_int, 0), c.clock_gettime(c.CLOCK.MONOTONIC, &before)); + try std.testing.expectEqual(@as(c_int, 0), sys.clock_gettime(c.CLOCK.MONOTONIC, &before)); try std.testing.expectEqual(@as(usize, 0), try backend.poll(&completions, 1000)); - try std.testing.expectEqual(@as(c_int, 0), c.clock_gettime(c.CLOCK.MONOTONIC, &after)); + try std.testing.expectEqual(@as(c_int, 0), sys.clock_gettime(c.CLOCK.MONOTONIC, &after)); const elapsed_ns = (@as(i128, after.sec) - before.sec) * std.time.ns_per_s + after.nsec - before.nsec; // Generous fixture watchdog; this is not a scheduler latency guarantee. try std.testing.expect(elapsed_ns < 900 * std.time.ns_per_ms); @@ -299,7 +300,7 @@ test "listener binds the configured IPv4 address and preserves loopback default" const configured = try listenBound(expected, 0, 1, true, false); defer closeFd(configured.socket); var length: c.socklen_t = @sizeOf(@TypeOf(address)); - try std.testing.expectEqual(@as(c_int, 0), c.getsockname(configured.socket, @ptrCast(&address), &length)); + try std.testing.expectEqual(@as(c_int, 0), sys.getsockname(configured.socket, @ptrCast(&address), &length)); try std.testing.expectEqual(expected, @as([4]u8, @bitCast(address.addr))); try std.testing.expect(configured.port != 0); } @@ -307,6 +308,6 @@ test "listener binds the configured IPv4 address and preserves loopback default" const default = try listen(0, 1, true, false); defer closeFd(default.socket); var length: c.socklen_t = @sizeOf(@TypeOf(address)); - try std.testing.expectEqual(@as(c_int, 0), c.getsockname(default.socket, @ptrCast(&address), &length)); + try std.testing.expectEqual(@as(c_int, 0), sys.getsockname(default.socket, @ptrCast(&address), &length)); try std.testing.expectEqual(std.mem.nativeToBig(u32, 0x7f000001), address.addr); } diff --git a/src/transport_linux.zig b/src/transport_linux.zig index 46a77a4..d719707 100644 --- a/src/transport_linux.zig +++ b/src/transport_linux.zig @@ -5,6 +5,7 @@ const std = @import("std"); const c = std.c; const linux = std.os.linux; const common = @import("transport.zig"); +const sys = @import("sys.zig"); const Socket = common.Socket; const Completion = common.Completion; const assert = std.debug.assert; @@ -176,8 +177,8 @@ pub const Backend = struct { .{ .fd = self.ring.fd, .events = c.POLL.IN, .revents = 0 }, .{ .fd = self.wake_fd, .events = c.POLL.IN, .revents = 0 }, }; - const result = c.poll(&fds, fds.len, @intCast(timeout_ms)); - if (result < 0 and c.errno(result) != .INTR) return error.PollFailed; + const result = sys.poll(&fds, fds.len, @intCast(timeout_ms)); + if (result < 0 and sys.errno(result) != .INTR) return error.PollFailed; if (fds[1].revents & c.POLL.IN != 0) { var value: u64 = undefined; const read_result = linux.read(self.wake_fd, @ptrCast(&value), @sizeOf(u64)); @@ -243,7 +244,7 @@ pub const Backend = struct { } pub fn shutdown(_: *Backend, socket: Socket) void { - _ = c.shutdown(socket, c.SHUT.RDWR); + _ = sys.shutdown(socket, c.SHUT.RDWR); } pub fn port(self: *const Backend) u16 {