diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a50ed21..b1120c7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,10 +11,7 @@ jobs: strategy: fail-fast: false matrix: - # Windows dropped during Zig 0.16 port — src/sys.zig, sockaddr, and - # the libxev dep have @compileError branches for Windows pending a - # proper Winsock implementation. - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -36,6 +33,8 @@ jobs: # real server over a socket, no browser needed. The browser legs stay # local — see SPEC/webtransport_conformance.md. - name: WebTransport conformance (Zig client) + # A bash script that has not been run on Windows yet. + if: runner.os != 'Windows' run: ./tools/wt_conformance.sh --zig-only # RFC 6455 conformance: the Autobahn fuzzing client against ws-echo-server, diff --git a/.gitignore b/.gitignore index 61d0fb0..f9d9dea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .claude zig-out .zig-cache +zig-pkg .DS_Store .vscode interop/quiche/target diff --git a/CHANGELOG.md b/CHANGELOG.md index b1054c6..4976f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -371,6 +371,9 @@ need a design decision. Thanks @notramo for suggesting it @wallace0409 ([#45](https://github.com/endel/quic-zig/issues/45)). - A peer that acknowledged nothing could run the server out of memory. Thanks @wallace0409 ([#46](https://github.com/endel/quic-zig/issues/46)). +- Windows. The library, its tests and the apps build and run natively: sockets + on Winsock, the event loop on libxev's IOCP backend. ECN is not read or set + on Windows yet. See [SPEC/STATUS.md](SPEC/STATUS.md#platforms). ## 0.5.0 diff --git a/SPEC/STATUS.md b/SPEC/STATUS.md index fd41a50..cd6e793 100644 --- a/SPEC/STATUS.md +++ b/SPEC/STATUS.md @@ -574,6 +574,48 @@ Track at: https://datatracker.ietf.org/doc/draft-ietf-quic-multipath/ --- +## Platforms + +Every call into the OS goes through [`src/sys.zig`](../src/sys.zig); Windows +lives in [`src/sys/windows.zig`](../src/sys/windows.zig). + +| Platform | Event loop | Tested | +|---|---|---| +| Linux | libxev epoll | CI | +| macOS | libxev kqueue | CI | +| Windows | libxev IOCP, readiness through AFD poll | CI | + +### Windows caveats + +- **libxev.** The event loop waits for readiness (`xev.File.poll`), which + upstream libxev's IOCP backend does not have. `build.zig.zon` pins a fork + that adds it by polling through the AFD driver, the mechanism beneath + `WSAPoll`, so the result completes on the loop's port. +- **ECN** is neither read nor set: receive is plain `recvfrom`, with no + `WSARecvMsg` control data, and marking is a no-op. ECN validation sees an + unmarked path. +- **No batching.** One `sendto` per datagram, and no UDP send/receive offload. +- **Timer resolution.** Timed waits round up to the system tick, about 15.6 ms. + The event loop's timers wait on the completion port in milliseconds and share + the tick. `sys.sleepNs` under a millisecond yields in a loop instead, so it + stays accurate and the manual-loop apps (`bench`, `*_manual`) measure the + stack, at the cost of a busy core while they wait. +- **ICMP resets are off.** Winsock fails a UDP socket's next receive after an + ICMP port-unreachable (`WSAECONNRESET`), so one departed client could break a + server's receive loop. `sys.udpSocket` turns that off. +- **`SO_REUSEADDR` is not set**: on Windows it lets a second socket take a port + that is still in use. +- **No Unix sockets or `SO_REUSEPORT`.** `Http1Config.unix_path` fails with + `error.UnixSocketsUnsupported`, and `reuse_port` with an error from `setReusePort`. +- **An HTTP/1.1 connection can't move between loops.** A socket stays on its + first completion port, so `Socket.rebind` onto another loop isn't supported. +- **A nonblocking `send` can take all of a large write.** Backpressure shows on + the next one, so `high_water` is only reached a write later than on POSIX. +- The interop servers find their preferred address with `getifaddrs`, which is + POSIX; on Windows they advertise none. + +--- + ## Overall Progress | RFC | Done | Partial | Missing | Completion | diff --git a/apps/bench.zig b/apps/bench.zig index 20efc68..9287961 100644 --- a/apps/bench.zig +++ b/apps/bench.zig @@ -236,7 +236,7 @@ fn runBench(alloc: std.mem.Allocator, config: BenchConfig) !void { .skip_cert_verify = true, }; - const sockfd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const sockfd = try sys.udpSocket(posix.AF.INET, .{}); defer sys.close(sockfd); const local_addr = try net.Address.parseIp4("127.0.0.1", 0); try sys.bind(sockfd, &local_addr.any, local_addr.getOsSockLen()); @@ -282,7 +282,7 @@ fn runBench(alloc: std.mem.Allocator, config: BenchConfig) !void { var conn_idx: u32 = 0; while (conn_idx < config.num_connections) : (conn_idx += 1) { - const sockfd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const sockfd = try sys.udpSocket(posix.AF.INET, .{}); defer sys.close(sockfd); const local_addr = try net.Address.parseIp4("127.0.0.1", 0); try sys.bind(sockfd, &local_addr.any, local_addr.getOsSockLen()); @@ -367,7 +367,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var config = BenchConfig{}; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/bench_wt.zig b/apps/bench_wt.zig index a0d494c..ace2f45 100644 --- a/apps/bench_wt.zig +++ b/apps/bench_wt.zig @@ -63,7 +63,7 @@ fn percentile(sorted: []const i64, p: f64) i64 { // ════════════════════════════════════════════════════════ fn createSocket() !struct { fd: posix.socket_t, addr: net.Address } { - const fd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const fd = try sys.udpSocket(posix.AF.INET, .{}); const addr = try net.Address.parseIp4("127.0.0.1", 0); try sys.bind(fd, &addr.any, addr.getOsSockLen()); ecn_socket.enableEcnRecv(fd) catch {}; @@ -442,7 +442,7 @@ pub fn main(init: std.process.Init.Minimal) !void { const alloc = arena.allocator(); var config = Config{}; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/bench_wt_server.zig b/apps/bench_wt_server.zig index a0eebab..280030c 100644 --- a/apps/bench_wt_server.zig +++ b/apps/bench_wt_server.zig @@ -36,7 +36,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var port: u16 = 4434; var cert_path: []const u8 = "interop/certs/server.crt"; var key_path: []const u8 = "interop/certs/server.key"; - var args = std.process.Args.Iterator.init(init.args); + var args = quic.sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/client.zig b/apps/client.zig index 5acd087..5c4a945 100644 --- a/apps/client.zig +++ b/apps/client.zig @@ -45,7 +45,7 @@ pub fn main(init: std.process.Init.Minimal) !void { // Parse --port argument var port: u16 = 4434; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); // skip program name while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { @@ -56,7 +56,7 @@ pub fn main(init: std.process.Init.Minimal) !void { } const server_addr = try net.Address.parseIp4("127.0.0.1", port); - const sockfd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const sockfd = try sys.udpSocket(posix.AF.INET, .{}); defer sys.close(sockfd); // Create a local socket with any available port diff --git a/apps/h3_client.zig b/apps/h3_client.zig index ef023ae..36df235 100644 --- a/apps/h3_client.zig +++ b/apps/h3_client.zig @@ -64,7 +64,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var port: u16 = 4434; var path: []const u8 = "/"; var insecure = false; - var args = std.process.Args.Iterator.init(init.args); + var args = quic.sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/interop_client.zig b/apps/interop_client.zig index 5698616..317289b 100644 --- a/apps/interop_client.zig +++ b/apps/interop_client.zig @@ -147,7 +147,7 @@ pub fn main(init: std.process.Init.Minimal) !void { defer if (keylog_file) |f| f.close(); // Parse request URLs from CLI args - var args_iter = std.process.Args.Iterator.init(init.args); + var args_iter = sys.argsIterator(init.args); _ = args_iter.next(); // skip program name var urls: std.ArrayList(ParsedUrl) = .empty; while (args_iter.next()) |arg| { @@ -275,12 +275,8 @@ fn downloadAll( // Always create IPv6 dual-stack socket to support preferred_address migration across families. // If the initial server address is IPv4, sendto converts it to IPv4-mapped IPv6 automatically. - const sockfd = try sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const sockfd = try sys.udpSocket(posix.AF.INET6, .{}); defer sys.close(sockfd); - // Disable IPV6_V6ONLY to allow dual-stack (IPv4 and IPv6 on same socket) - const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27; - const zero: c_int = 0; - posix.setsockopt(sockfd, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&zero)) catch {}; const local_addr = try net.Address.parseIp6("::", 0); try sys.bind(sockfd, &local_addr.any, local_addr.getOsSockLen()); @@ -589,11 +585,8 @@ fn downloadH0( // Switch DCID before migrating (RFC 9000 §9.5) if (conn.initiateClientMigration()) { // Create a new socket bound to a different port - const new_sockfd = sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0) catch null; + const new_sockfd = sys.udpSocket(posix.AF.INET6, .{}) catch null; if (new_sockfd) |nfd| { - const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27; - const zero: c_int = 0; - posix.setsockopt(nfd, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&zero)) catch {}; const new_local = net.Address.parseIp6("::", 0) catch unreachable; sys.bind(nfd, &new_local.any, new_local.getOsSockLen()) catch { sys.close(nfd); diff --git a/apps/interop_client_manual.zig b/apps/interop_client_manual.zig index 602d628..c521b1d 100644 --- a/apps/interop_client_manual.zig +++ b/apps/interop_client_manual.zig @@ -120,7 +120,7 @@ pub fn main(init: std.process.Init.Minimal) !void { defer if (keylog_file) |f| f.close(); // Parse request URLs from CLI args - var args_iter = std.process.Args.Iterator.init(init.args); + var args_iter = sys.argsIterator(init.args); _ = args_iter.next(); // skip program name var urls: std.ArrayList(ParsedUrl) = .empty; while (args_iter.next()) |arg| { @@ -226,12 +226,8 @@ fn downloadAll( // Always create IPv6 dual-stack socket to support preferred_address migration across families. // If the initial server address is IPv4, sendto converts it to IPv4-mapped IPv6 automatically. - const sockfd = try sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const sockfd = try sys.udpSocket(posix.AF.INET6, .{}); defer sys.close(sockfd); - // Disable IPV6_V6ONLY to allow dual-stack (IPv4 and IPv6 on same socket) - const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27; - const zero: c_int = 0; - posix.setsockopt(sockfd, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&zero)) catch {}; const local_addr = try net.Address.parseIp6("::", 0); try sys.bind(sockfd, &local_addr.any, local_addr.getOsSockLen()); diff --git a/apps/interop_server.zig b/apps/interop_server.zig index a175233..15be97e 100644 --- a/apps/interop_server.zig +++ b/apps/interop_server.zig @@ -295,6 +295,10 @@ fn loadFile(alloc: std.mem.Allocator, path: []const u8) ![]u8 { /// Discover the server's non-loopback IPv4 and IPv6 addresses from network interfaces. fn getServerAddresses() struct { ipv4: ?[4]u8, ipv6: ?[16]u8 } { + // getifaddrs is POSIX, and the interop runner only runs this in a Linux + // container. Elsewhere it finds nothing. + if (@import("builtin").os.tag == .windows) return .{ .ipv4 = null, .ipv6 = null }; + const IfAddrs = extern struct { ifa_next: ?*@This(), ifa_name: [*:0]const u8, diff --git a/apps/interop_server_manual.zig b/apps/interop_server_manual.zig index 2e24017..046673f 100644 --- a/apps/interop_server_manual.zig +++ b/apps/interop_server_manual.zig @@ -140,34 +140,18 @@ pub fn main() !void { // Create UDP socket (dual-stack: try IPv6 first, fall back to IPv4) const listen_port: u16 = std.fmt.parseInt(u16, port_str, 10) catch 443; const sockfd, const local_addr = blk: { - // Try IPv6 dual-stack socket first (handles both IPv4 and IPv6) - const addr6 = try net.Address.parseIp6("::", listen_port); - const fd6 = sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0) catch { - // Fall back to IPv4-only - const addr4 = try net.Address.parseIp4("0.0.0.0", listen_port); - const fd4 = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); - sys.bind(fd4, &addr4.any, addr4.getOsSockLen()) catch { - sys.close(fd4); - return error.BindFailed; - }; - break :blk .{ fd4, addr4 }; - }; - // Allow dual-stack (disable IPV6_V6ONLY) - const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27; - const zero: c_int = 0; - posix.setsockopt(fd6, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&zero)) catch {}; - sys.bind(fd6, &addr6.any, addr6.getOsSockLen()) catch { - sys.close(fd6); - // Fall back to IPv4-only - const addr4 = try net.Address.parseIp4("0.0.0.0", listen_port); - const fd4 = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); - sys.bind(fd4, &addr4.any, addr4.getOsSockLen()) catch { - sys.close(fd4); - return error.BindFailed; + for ([_]net.Address{ + try net.Address.parseIp6("::", listen_port), + try net.Address.parseIp4("0.0.0.0", listen_port), + }) |addr| { + const fd = sys.udpSocket(addr.any.family, .{}) catch continue; + sys.bind(fd, &addr.any, addr.getOsSockLen()) catch { + sys.close(fd); + continue; }; - break :blk .{ fd4, addr4 }; - }; - break :blk .{ fd6, addr6 }; + break :blk .{ fd, addr }; + } + return error.BindFailed; }; defer sys.close(sockfd); ecn_socket.enableEcnRecv(sockfd) catch {}; @@ -403,8 +387,12 @@ fn loadFile(alloc: std.mem.Allocator, path: []const u8) ![]u8 { } /// Discover the server's non-loopback IPv4 and IPv6 addresses from network interfaces. -/// Uses C getifaddrs() which works on Linux (Docker containers). +/// Uses C getifaddrs() which works on Linux (Docker containers). Elsewhere it +/// finds nothing: getifaddrs is POSIX, and the interop runner never runs this +/// on Windows. fn getServerAddresses() struct { ipv4: ?[4]u8, ipv6: ?[16]u8 } { + if (@import("builtin").os.tag == .windows) return .{ .ipv4 = null, .ipv6 = null }; + const IfAddrs = extern struct { ifa_next: ?*@This(), ifa_name: [*:0]const u8, diff --git a/apps/interop_wt_client.zig b/apps/interop_wt_client.zig index b944d7d..adc0fdb 100644 --- a/apps/interop_wt_client.zig +++ b/apps/interop_wt_client.zig @@ -200,7 +200,7 @@ pub fn main(init: std.process.Init.Minimal) !void { } // Parse request URLs from CLI args - var args_iter = std.process.Args.Iterator.init(init.args); + var args_iter = sys.argsIterator(init.args); _ = args_iter.next(); // skip program name var urls: std.ArrayList(ParsedUrl) = .empty; while (args_iter.next()) |arg| { @@ -239,11 +239,8 @@ pub fn main(init: std.process.Init.Minimal) !void { }; // Create dual-stack UDP socket - const sockfd = try sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const sockfd = try sys.udpSocket(posix.AF.INET6, .{}); defer sys.close(sockfd); - const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27; - const zero: c_int = 0; - posix.setsockopt(sockfd, posix.IPPROTO.IPV6, IPV6_V6ONLY, mem.asBytes(&zero)) catch {}; const local_addr = try net.Address.parseIp6("::", 0); try sys.bind(sockfd, &local_addr.any, local_addr.getOsSockLen()); ecn_socket.enableEcnRecv(sockfd) catch {}; diff --git a/apps/interop_wt_server.zig b/apps/interop_wt_server.zig index d2fdc3f..95d4064 100644 --- a/apps/interop_wt_server.zig +++ b/apps/interop_wt_server.zig @@ -200,32 +200,20 @@ pub fn main(_: std.process.Init.Minimal) !void { .keylog_file = keylog_file, }; - // Create UDP socket (dual-stack) + // Create UDP socket: IPv6 dual-stack, or IPv4 where IPv6 is unavailable const sockfd, const local_addr = blk: { - const addr6 = try net.Address.parseIp6("::", 443); - const fd6 = sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0) catch { - const addr4 = try net.Address.parseIp4("0.0.0.0", 443); - const fd4 = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); - sys.bind(fd4, &addr4.any, addr4.getOsSockLen()) catch { - sys.close(fd4); - return error.BindFailed; - }; - break :blk .{ fd4, addr4 }; - }; - const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27; - const zero: c_int = 0; - posix.setsockopt(fd6, posix.IPPROTO.IPV6, IPV6_V6ONLY, mem.asBytes(&zero)) catch {}; - sys.bind(fd6, &addr6.any, addr6.getOsSockLen()) catch { - sys.close(fd6); - const addr4 = try net.Address.parseIp4("0.0.0.0", 443); - const fd4 = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); - sys.bind(fd4, &addr4.any, addr4.getOsSockLen()) catch { - sys.close(fd4); - return error.BindFailed; + for ([_]net.Address{ + try net.Address.parseIp6("::", 443), + try net.Address.parseIp4("0.0.0.0", 443), + }) |addr| { + const fd = sys.udpSocket(addr.any.family, .{}) catch continue; + sys.bind(fd, &addr.any, addr.getOsSockLen()) catch { + sys.close(fd); + continue; }; - break :blk .{ fd4, addr4 }; - }; - break :blk .{ fd6, addr6 }; + break :blk .{ fd, addr }; + } + return error.BindFailed; }; defer sys.close(sockfd); ecn_socket.enableEcnRecv(sockfd) catch {}; diff --git a/apps/moq_client.zig b/apps/moq_client.zig index 526bd6f..0aa66f4 100644 --- a/apps/moq_client.zig +++ b/apps/moq_client.zig @@ -349,7 +349,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var track_names = std.ArrayList([]const u8).empty; defer track_names.deinit(alloc); - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--addr")) { diff --git a/apps/moq_lite.zig b/apps/moq_lite.zig index 9d4552f..588e7fc 100644 --- a/apps/moq_lite.zig +++ b/apps/moq_lite.zig @@ -693,7 +693,7 @@ pub fn main(init: std.process.Init.Minimal) !u8 { var cert_path: []const u8 = "interop/certs/server.crt"; var key_path: []const u8 = "interop/certs/server.key"; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "publish")) { diff --git a/apps/moq_lite_relay.zig b/apps/moq_lite_relay.zig index aa3e433..ff93112 100644 --- a/apps/moq_lite_relay.zig +++ b/apps/moq_lite_relay.zig @@ -578,7 +578,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var cert_path: []const u8 = "interop/browser/certs/server.crt"; var key_path: []const u8 = "interop/browser/certs/server.key"; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/moq_relay.zig b/apps/moq_relay.zig index 0c10f09..4c4abef 100644 --- a/apps/moq_relay.zig +++ b/apps/moq_relay.zig @@ -1598,7 +1598,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var cert_path: []const u8 = "interop/browser/certs/server.crt"; var key_path: []const u8 = "interop/browser/certs/server.key"; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/moq_server.zig b/apps/moq_server.zig index 6cf95da..349dda4 100644 --- a/apps/moq_server.zig +++ b/apps/moq_server.zig @@ -206,7 +206,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var cert_path: []const u8 = "interop/certs/server.crt"; var key_path: []const u8 = "interop/certs/server.key"; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/moq_test_client.zig b/apps/moq_test_client.zig index ed7a644..ac51f46 100644 --- a/apps/moq_test_client.zig +++ b/apps/moq_test_client.zig @@ -747,7 +747,7 @@ fn Runner(comptime proto: event_loop.Protocol) type { fn out(comptime fmt: []const u8, args: anytype) void { var buf: [512]u8 = undefined; const s = std.fmt.bufPrint(&buf, fmt, args) catch return; - (sys.File{ .fd = 1 }).writeAll(s) catch {}; + sys.stdout().writeAll(s) catch {}; } fn reportTap( @@ -798,7 +798,7 @@ pub fn main(init: std.process.Init.Minimal) !u8 { var tls_disable_verify = envFlag("TLS_DISABLE_VERIFY"); var draft: moq_version.Draft = moq_version.DEFAULT; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--relay") or std.mem.eql(u8, arg, "-r")) { diff --git a/apps/quic_client.zig b/apps/quic_client.zig index 2b46de6..bf465dc 100644 --- a/apps/quic_client.zig +++ b/apps/quic_client.zig @@ -45,7 +45,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var port: u16 = 4434; var insecure = false; - var args = std.process.Args.Iterator.init(init.args); + var args = quic.sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/quic_lb.zig b/apps/quic_lb.zig index be0eb11..46aa423 100644 --- a/apps/quic_lb.zig +++ b/apps/quic_lb.zig @@ -173,7 +173,7 @@ pub fn main(init: std.process.Init.Minimal) !void { const allocator = gpa.allocator(); // Parse command-line args - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); // skip program name const config_path = args.next() orelse { std.log.err("usage: quic-lb ", .{}); @@ -194,7 +194,7 @@ pub fn main(init: std.process.Init.Minimal) !void { std.log.info("QUIC-LB: {d} backend server(s) configured", .{config.server_count}); // Create UDP socket - const sock = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const sock = try sys.udpSocket(posix.AF.INET, .{}); defer sys.close(sock); // Bind to listen address diff --git a/apps/quic_server.zig b/apps/quic_server.zig index 553a775..91c9e50 100644 --- a/apps/quic_server.zig +++ b/apps/quic_server.zig @@ -28,7 +28,7 @@ pub fn main(init: std.process.Init.Minimal) !void { const alloc = std.heap.smp_allocator; var port: u16 = 4434; - var args = std.process.Args.Iterator.init(init.args); + var args = quic.sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/server.zig b/apps/server.zig index 815865d..5f63ebf 100644 --- a/apps/server.zig +++ b/apps/server.zig @@ -39,7 +39,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var port: u16 = 4434; var server_id_hex: ?[]const u8 = null; var lb_key_hex: ?[]const u8 = null; - var args = std.process.Args.Iterator.init(init.args); + var args = quic.sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/tls_echo_server.zig b/apps/tls_echo_server.zig index a527b17..8d01118 100644 --- a/apps/tls_echo_server.zig +++ b/apps/tls_echo_server.zig @@ -31,7 +31,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var tickets = false; var client_ca: ?[]const u8 = null; var client_mode: tls13.ClientAuth.Mode = .required; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { @@ -76,8 +76,7 @@ pub fn main(init: std.process.Init.Minimal) !void { const addr = try quic.sockaddr.Address.parseIp4("127.0.0.1", port); const listener = try sys.socket(posix.AF.INET, posix.SOCK.STREAM, 0); - const yes: c_int = 1; - posix.setsockopt(listener, posix.SOL.SOCKET, posix.SO.REUSEADDR, std.mem.asBytes(&yes)) catch {}; + sys.setReuseAddr(listener); try sys.bind(listener, &addr.any, addr.getOsSockLen()); try sys.listen(listener, 128); log.info("listening on 127.0.0.1:{d}", .{port}); diff --git a/apps/wpt_client.zig b/apps/wpt_client.zig index 81474b7..c6a52ad 100644 --- a/apps/wpt_client.zig +++ b/apps/wpt_client.zig @@ -769,7 +769,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var only: ?[]const u8 = null; var timeout_ms: i64 = 8000; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/wpt_server.zig b/apps/wpt_server.zig index 4d0292f..159c011 100644 --- a/apps/wpt_server.zig +++ b/apps/wpt_server.zig @@ -765,7 +765,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var cert_path: []const u8 = "interop/browser/certs/server.crt"; var key_path: []const u8 = "interop/browser/certs/server.key"; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/ws_echo_server.zig b/apps/ws_echo_server.zig index 711d621..782c2c6 100644 --- a/apps/ws_echo_server.zig +++ b/apps/ws_echo_server.zig @@ -57,7 +57,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var static_dir: ?[]const u8 = null; var plain = false; - var args = std.process.Args.Iterator.init(init.args); + var args = quic.sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/wt_browser_server.zig b/apps/wt_browser_server.zig index b9b08a3..76d978c 100644 --- a/apps/wt_browser_server.zig +++ b/apps/wt_browser_server.zig @@ -74,7 +74,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var lb_key_hex: ?[]const u8 = null; var disable_pmtud = false; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/wt_browser_server_manual.zig b/apps/wt_browser_server_manual.zig index f325e94..acabaa1 100644 --- a/apps/wt_browser_server_manual.zig +++ b/apps/wt_browser_server_manual.zig @@ -38,7 +38,7 @@ pub fn main(init: std.process.Init.Minimal) !void { var cert_path: []const u8 = "interop/browser/certs/server.crt"; var key_path: []const u8 = "interop/browser/certs/server.key"; - var args = std.process.Args.Iterator.init(init.args); + var args = sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { @@ -104,30 +104,18 @@ pub fn main(init: std.process.Init.Minimal) !void { // Create UDP socket (dual-stack: try IPv6 first, fall back to IPv4) const sockfd, const local_addr = blk: { - const addr6 = try net.Address.parseIp6("::", port); - const fd6 = sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0) catch { - const addr4 = try net.Address.parseIp4("0.0.0.0", port); - const fd4 = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); - sys.bind(fd4, &addr4.any, addr4.getOsSockLen()) catch { - sys.close(fd4); - return error.BindFailed; + for ([_]net.Address{ + try net.Address.parseIp6("::", port), + try net.Address.parseIp4("0.0.0.0", port), + }) |addr| { + const fd = sys.udpSocket(addr.any.family, .{}) catch continue; + sys.bind(fd, &addr.any, addr.getOsSockLen()) catch { + sys.close(fd); + continue; }; - break :blk .{ fd4, addr4 }; - }; - const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27; - const zero: c_int = 0; - posix.setsockopt(fd6, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&zero)) catch {}; - sys.bind(fd6, &addr6.any, addr6.getOsSockLen()) catch { - sys.close(fd6); - const addr4 = try net.Address.parseIp4("0.0.0.0", port); - const fd4 = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); - sys.bind(fd4, &addr4.any, addr4.getOsSockLen()) catch { - sys.close(fd4); - return error.BindFailed; - }; - break :blk .{ fd4, addr4 }; - }; - break :blk .{ fd6, addr6 }; + break :blk .{ fd, addr }; + } + return error.BindFailed; }; defer sys.close(sockfd); ecn_socket.enableEcnRecv(sockfd) catch {}; diff --git a/apps/wt_client.zig b/apps/wt_client.zig index 0939e3a..fe45486 100644 --- a/apps/wt_client.zig +++ b/apps/wt_client.zig @@ -85,7 +85,7 @@ pub fn main(init: std.process.Init.Minimal) !void { // Parse --port argument var port: u16 = 4434; - var args = std.process.Args.Iterator.init(init.args); + var args = quic.sys.argsIterator(init.args); _ = args.next(); while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/apps/wt_echo_server.zig b/apps/wt_echo_server.zig index 00651b5..f8f786f 100644 --- a/apps/wt_echo_server.zig +++ b/apps/wt_echo_server.zig @@ -230,7 +230,7 @@ pub fn main(init: std.process.Init.Minimal) !void { // they give back — an arena would grow for as long as the process runs. const alloc = std.heap.smp_allocator; - var args_iter = std.process.Args.Iterator.init(init.args); + var args_iter = quic.sys.argsIterator(init.args); _ = args_iter.next(); // skip program name var cert_path: []const u8 = "/etc/letsencrypt/live/echo.web-transport.dev/fullchain.pem"; diff --git a/apps/wt_server.zig b/apps/wt_server.zig index 753339d..37674df 100644 --- a/apps/wt_server.zig +++ b/apps/wt_server.zig @@ -53,7 +53,7 @@ pub fn main(init: std.process.Init.Minimal) !void { // Parse --port argument var port: u16 = 4434; - var args = std.process.Args.Iterator.init(init.args); + var args = quic.sys.argsIterator(init.args); _ = args.next(); // skip program name while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--port")) { diff --git a/build.zig b/build.zig index bd7d875..d84ef7e 100644 --- a/build.zig +++ b/build.zig @@ -7,8 +7,8 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - // Link libc on platforms that need it (Linux requires explicit libc for std.c.recvmsg). - const need_libc: ?bool = if (target.result.os.tag == .windows) null else true; + // Linux requires explicit libc for std.c.recvmsg; tests use it everywhere. + const need_libc: ?bool = true; // libxev dependency (used by event-loop-based servers) const xev_dep = b.dependency("libxev", .{ .target = target, .optimize = optimize }); diff --git a/build.zig.zon b/build.zig.zon index 215f2bb..ad0ce1d 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,11 +2,11 @@ .name = .quic, .version = "0.5.0", .dependencies = .{ - // endel/libxev tag quic-zig-2026-10-28: upstream plus the PRs in its + // endel/libxev tag quic-zig-2026-10-30: upstream plus the PRs in its // fork/patches. See "libxev" in CLAUDE.md before changing this. .libxev = .{ - .url = "https://github.com/endel/libxev/archive/59bcd05d277a18586d9071e9c9d2112de8a9721b.tar.gz", - .hash = "libxev-0.0.0-86vtc7v-FADKPIiRQFovAtJyeMMC_WNJvgTHVdYNTFJT", + .url = "https://github.com/endel/libxev/archive/403f43cd68f670f65a4297c434ab339478930cd5.tar.gz", + .hash = "libxev-0.0.0-86vtcwYSFQCUiuN9GSOY7ycm1bhDlMeB_QagIRURAq8_", }, }, .fingerprint = 0xc079525234abfc4c, diff --git a/src/event_loop.zig b/src/event_loop.zig index 3273e10..2508011 100644 --- a/src/event_loop.zig +++ b/src/event_loop.zig @@ -82,6 +82,14 @@ const MAX_RECV_DATAGRAM: usize = (transport_params.TransportParams{}).max_udp_pa /// test, where the OS default receive buffer holds only a few hundred. const max_recv_per_pass: usize = if (builtin.is_test) 64 else 1024; +/// A nonblocking UDP socket bound to `addr`. +fn bindUdp(addr: net.Address, options: sys.UdpSocketOptions) !posix.socket_t { + const fd = try sys.udpSocket(addr.any.family, options); + errdefer sys.close(fd); + try sys.bind(fd, &addr.any, addr.getOsSockLen()); + return fd; +} + pub const Config = struct { address: []const u8 = "127.0.0.1", port: u16 = 4433, @@ -1817,9 +1825,11 @@ pub fn Server(comptime Handler: type) type { r: xev.PollError!xev.PollEvent, ) xev.CallbackAction { forgetPollResult(c); - _ = r catch return .rearm; const self = self_opt orelse return .disarm; + // Before the result: a cancelled watch gets its error on some + // backends, and rearming it would put the watch back. if (self.halted) return halted_poll_action; + _ = r catch return .rearm; const began = sys.nanoTimestamp(); self.in_callback = true; @@ -2678,19 +2688,11 @@ fn openUdpSocket(config: Config, port: u16) !struct { posix.socket_t, net.Addres else try net.Address.parseIp4(config.address, port); const family: u32 = if (config.ipv6) posix.AF.INET6 else posix.AF.INET; - const fd = try sys.socket(family, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const fd = try sys.udpSocket(family, .{ .reuse_addr = true }); errdefer sys.close(fd); - if (config.ipv6) { - // Dual-stack: also accept IPv4 peers. - const IPV6_V6ONLY: u32 = if (builtin.target.os.tag == .linux) 26 else 27; - posix.setsockopt(fd, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&@as(c_int, 0))) catch {}; - } - posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, std.mem.asBytes(&@as(c_int, 1))) catch {}; // Asked for explicitly, so a failure is the caller's to see. - if (config.reuse_port) { - try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, std.mem.asBytes(&@as(c_int, 1))); - } + if (config.reuse_port) try sys.setReusePort(fd); if (config.recv_buffer_size) |n| try setBufferSize(fd, .recv, n); if (config.send_buffer_size) |n| try setBufferSize(fd, .send, n); @@ -2707,7 +2709,7 @@ fn openUdpSocket(config: Config, port: u16) !struct { posix.socket_t, net.Addres fn setBufferSize(fd: posix.socket_t, comptime which: enum { recv, send }, n: u32) !void { const opt = if (which == .recv) posix.SO.RCVBUF else posix.SO.SNDBUF; const v: c_int = @intCast(@min(n, std.math.maxInt(c_int))); - try posix.setsockopt(fd, posix.SOL.SOCKET, opt, std.mem.asBytes(&v)); + try sys.setsockopt(fd, posix.SOL.SOCKET, opt, std.mem.asBytes(&v)); if (builtin.target.os.tag != .linux or bufferSize(fd, opt) >= n) return; const force: u32 = if (which == .recv) 33 else 32; // SO_RCVBUFFORCE, SO_SNDBUFFORCE posix.setsockopt(fd, posix.SOL.SOCKET, force, std.mem.asBytes(&v)) catch {}; @@ -3342,22 +3344,11 @@ pub fn Client(comptime Handler: type) type { }; // Create non-blocking UDP socket, bind to ephemeral port - const sockfd, const local_addr = if (config.ipv6) blk: { - const addr6 = try net.Address.parseIp6("::", 0); - const fd = try sys.socket(posix.AF.INET6, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); - errdefer sys.close(fd); - const IPV6_V6ONLY: u32 = if (@import("builtin").target.os.tag == .linux) 26 else 27; - const zero_val: c_int = 0; - posix.setsockopt(fd, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&zero_val)) catch {}; - try sys.bind(fd, &addr6.any, addr6.getOsSockLen()); - break :blk .{ fd, addr6 }; - } else blk: { - const addr4 = try net.Address.parseIp4("0.0.0.0", 0); - const fd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); - errdefer sys.close(fd); - try sys.bind(fd, &addr4.any, addr4.getOsSockLen()); - break :blk .{ fd, addr4 }; - }; + const local_addr = if (config.ipv6) + try net.Address.parseIp6("::", 0) + else + try net.Address.parseIp4("0.0.0.0", 0); + const sockfd = try bindUdp(local_addr, .{}); errdefer sys.close(sockfd); ecn_socket.enableEcnRecv(sockfd) catch {}; ecn_socket.setDontFragment(sockfd); @@ -3600,9 +3591,11 @@ pub fn Client(comptime Handler: type) type { r: xev.PollError!xev.PollEvent, ) xev.CallbackAction { forgetPollResult(c); - _ = r catch return .rearm; const self = self_opt orelse return .disarm; + // Before the result: a cancelled watch gets its error on some + // backends, and rearming it would put the watch back. if (self.halted) return halted_poll_action; + _ = r catch return .rearm; self.in_callback = true; // Process loop: catch packets arriving during processing @@ -5140,6 +5133,7 @@ test "e2e: an RSA certificate, picked by SNI next to an EC one" { } test "Server: socket options, ALPN and connection cap come from Config" { + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; // no SO_REUSEPORT const alpn = [_][]const u8{ "h3", "hq-interop" }; var handler = TestH3Handler{}; const S = Server(TestH3Handler); @@ -5170,6 +5164,7 @@ test "Server: socket options, ALPN and connection cap come from Config" { } test "Server: serves on a socket it is given" { + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; // no dup for sockets var handler = TestH3Handler{}; const S = Server(TestH3Handler); var a = try S.init(testing.allocator, &handler, .{ .port = 29416 }); @@ -5184,6 +5179,7 @@ test "Server: serves on a socket it is given" { } test "Server: port 0 takes one the kernel picks, which port() names for siblings to join" { + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; // no SO_REUSEPORT var handler = TestH3Handler{}; const S = Server(TestH3Handler); var a = try S.init(testing.allocator, &handler, .{ .port = 0, .reuse_port = true }); @@ -7512,6 +7508,7 @@ test "e2e: a client and server that both offer QUIC v2 finish the handshake in i } test "Server: workers sharing a reset key, given or derived, send no resets unless they steer" { + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; // no SO_REUSEPORT // A packet for one worker's connection can reach another, whose reset // handed the sender that connection's token (RFC 9000 §21.11). var handler = HelloServer{}; @@ -7571,7 +7568,8 @@ test "e2e: a flood of datagrams can't keep a server reading" { const Waiting = struct { fn f(srv: *Server(HelloServer)) bool { var peek: [1]u8 = undefined; - return std.c.recv(srv.sockfd, &peek, 1, std.posix.MSG.PEEK | std.posix.MSG.DONTWAIT) > 0; + const dontwait = if (builtin.target.os.tag == .windows) 0 else std.posix.MSG.DONTWAIT; + return std.c.recv(srv.sockfd, &peek, 1, std.posix.MSG.PEEK | dontwait) > 0; } fn not(srv: *Server(HelloServer)) bool { return !f(srv); @@ -7852,7 +7850,7 @@ const RebindingRelay = struct { client_len: posix.socklen_t = 0, fn udp(port: u16) !posix.socket_t { - const fd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const fd = try sys.udpSocket(posix.AF.INET, .{}); errdefer sys.close(fd); const addr = try net.Address.parseIp4("127.0.0.1", port); try sys.bind(fd, &addr.any, addr.getOsSockLen()); @@ -7887,6 +7885,7 @@ const RebindingRelay = struct { }; test "e2e: a migrated peer landing on the wrong worker is steered to its owner" { + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; // no SO_REUSEPORT const relay_port: u16 = 29430; const ports = [2]u16{ 29431, 29432 }; diff --git a/src/h0/connection.zig b/src/h0/connection.zig index 5be7eba..5891f8c 100644 --- a/src/h0/connection.zig +++ b/src/h0/connection.zig @@ -278,7 +278,7 @@ test "serveFile answers nothing for a path outside the root or too long to build var h0 = H0Connection.init(testing.allocator, conn, true); defer h0.deinit(); - var long: [5000]u8 = @splat('a'); + var long: [std.fs.max_path_bytes + 1]u8 = @splat('a'); long[0] = '/'; for ([_][]const u8{ "/../etc/hosts", "/a/../../etc/hosts", "..", &long }) |path| { h0.serveFile(s.stream_id, "/tmp", path) catch {}; diff --git a/src/handover_test.zig b/src/handover_test.zig index b92db43..9809775 100644 --- a/src/handover_test.zig +++ b/src/handover_test.zig @@ -328,7 +328,7 @@ const Relay = struct { thread: std.Thread = undefined, fn udp(port: u16) !posix.socket_t { - const fd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const fd = try sys.udpSocket(posix.AF.INET, .{}); errdefer sys.close(fd); const addr = try net.Address.parseIp4("127.0.0.1", port); try sys.bind(fd, &addr.any, addr.getOsSockLen()); diff --git a/src/http1/conn.zig b/src/http1/conn.zig index 7dc8c24..59a7ab8 100644 --- a/src/http1/conn.zig +++ b/src/http1/conn.zig @@ -556,9 +556,9 @@ pub const Conn = struct { .peer = undefined, }; var len: posix.socklen_t = @sizeOf(posix.sockaddr.storage); - if (std.c.getpeername(fd, @ptrCast(&self.peer), &len) != 0) { + sys.getpeername(fd, @ptrCast(&self.peer), &len) catch { self.peer = std.mem.zeroes(posix.sockaddr.storage); - } + }; try self.sock.init(self, server.loop, &server.timers, gpa, server.xevTcp(fd)); server.timers.set(&self.deadline, server.config.handshake_timeout_ms); self.sock.startReading(); diff --git a/src/http1/e2e_test.zig b/src/http1/e2e_test.zig index da2f535..e461824 100644 --- a/src/http1/e2e_test.zig +++ b/src/http1/e2e_test.zig @@ -354,7 +354,7 @@ const ServerFrame = struct { }; const TestClient = struct { - fd: posix.socket_t = -1, + fd: posix.socket_t = sys.invalid_socket, tls_config: tls_client.Config = .{}, tls: ?tls_client.Conn = null, /// Plaintext from the server; `pos` bytes of it consumed. @@ -369,7 +369,7 @@ const TestClient = struct { errdefer sys.close(fd); const addr = try net.Address.parseIp4("127.0.0.1", port); if (std.c.connect(fd, &addr.any, addr.getOsSockLen()) != 0) return error.ConnectionRefused; - socket.setNonBlocking(fd); + sys.setNonBlocking(fd); self.fd = fd; if (use_tls) { self.tls_config = .{ .server_name = "localhost", .skip_cert_verify = true, .alpn = &.{"http/1.1"} }; @@ -385,15 +385,15 @@ const TestClient = struct { var addr: posix.sockaddr.un = .{ .path = @splat(0) }; @memcpy(addr.path[0..path.len], path); if (std.c.connect(fd, @ptrCast(&addr), @sizeOf(posix.sockaddr.un)) != 0) return error.ConnectionRefused; - socket.setNonBlocking(fd); + sys.setNonBlocking(fd); self.fd = fd; } fn deinit(self: *TestClient) void { if (self.tls) |*t| t.deinit(); self.in.deinit(testing.allocator); - if (self.fd >= 0) sys.close(self.fd); - self.fd = -1; + if (self.fd != sys.invalid_socket) sys.close(self.fd); + self.fd = sys.invalid_socket; } /// Sends nothing more, reading on: a FIN, or over TLS a close_notify. @@ -402,13 +402,13 @@ const TestClient = struct { t.close(); return self.flushTls(); } - _ = std.c.shutdown(self.fd, posix.SHUT.WR); + sys.shutdown(self.fd, .send); } /// Drops the connection with no Close frame. fn abort(self: *TestClient) void { sys.close(self.fd); - self.fd = -1; + self.fd = sys.invalid_socket; } fn writeAll(self: *TestClient, bytes: []const u8) void { @@ -416,7 +416,7 @@ const TestClient = struct { while (rest.len > 0) { const rc = std.c.send(self.fd, rest.ptr, rest.len, 0); if (rc < 0) { - if (std.posix.errno(rc) == .AGAIN) { + if (sys.wouldBlock()) { sys.sleepNs(100 * std.time.ns_per_us); continue; } @@ -468,12 +468,12 @@ const TestClient = struct { } fn poll(self: *TestClient) void { - if (self.eof or self.fd < 0) return; + if (self.eof or self.fd == sys.invalid_socket) return; var buf: [16 * 1024]u8 = undefined; while (true) { const rc = std.c.recv(self.fd, &buf, buf.len, 0); if (rc < 0) { - if (std.posix.errno(rc) == .AGAIN) return; + if (sys.wouldBlock()) return; self.eof = true; return; } @@ -593,6 +593,7 @@ fn pathExists(path: [*:0]const u8) bool { } test "unix socket: a WebSocket and a request, and stop() removes the file" { + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; const path = "/tmp/quic-zig-e2e-29620.sock"; var h: Harness = undefined; try h.init(29620, .{ .tls = false, .unix_path = path }); @@ -624,6 +625,7 @@ test "unix socket: a WebSocket and a request, and stop() removes the file" { } test "unix socket: replaces a dead server's file, not a live server's or a regular file" { + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; const path = "/tmp/quic-zig-e2e-29622.sock"; const opts: http1.ListenOptions = .{ .quic_port = 0 }; const config: http1.Config = .{ .tls = false, .unix_path = path }; @@ -1083,6 +1085,8 @@ test "ws: a WebTransport client and a WebSocket client share one handler and loo } test "ws: handed over to a server on another loop, with the message behind the move" { + // A socket stays on its first completion port. + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; for ([_]bool{ false, true }) |use_tls| { var p: Pair = undefined; try p.init(if (use_tls) 29472 else 29470, .{ .tls = use_tls }); @@ -1118,6 +1122,7 @@ test "ws: handed over to a server on another loop, with the message behind the m } test "ws: a handover waits out a write in flight, and the new server sends the rest" { + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; // see the handover test // Asked for with a write in flight, or once the flood drained enough for // the next message to be read. `Pair.step` checks nothing was in flight // when it moved. @@ -1155,6 +1160,7 @@ fn expectFloodThenBehind(p: *Pair, c: *TestClient) !void { } test "ws: a peer that drops while handed over is closed by the new server" { + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; // see the handover test var p: Pair = undefined; try p.init(29480, .{ .tls = false }); defer p.deinit(); @@ -1170,6 +1176,7 @@ test "ws: a peer that drops while handed over is closed by the new server" { } test "ws: handOver only from onWsMessage; a discarded handover ends the connection" { + if (comptime builtin.target.os.tag == .windows) return error.SkipZigTest; // see the handover test var h: Harness = undefined; try h.init(29478, .{ .tls = false }); defer h.deinit(); diff --git a/src/http1/server.zig b/src/http1/server.zig index d8a016a..89baad4 100644 --- a/src/http1/server.zig +++ b/src/http1/server.zig @@ -359,12 +359,14 @@ pub const Server = struct { fn closeListenFd(self: *Server) void { if (self.listen_fd == closed_fd) return; // Before the close: past it, the path may be a new server's. - if (self.unix_addr) |*addr| _ = std.c.unlink(@ptrCast(&addr.path)); + if (comptime builtin.target.os.tag != .windows) { + if (self.unix_addr) |*addr| _ = std.c.unlink(@ptrCast(&addr.path)); + } sys.close(self.listen_fd); self.listen_fd = closed_fd; } - const closed_fd: posix.socket_t = -1; + const closed_fd = sys.invalid_socket; /// Stop accepting and end every connection: each open WebSocket gets a /// Close with 1001 (sent if the socket takes it at once) and @@ -431,7 +433,7 @@ pub const Server = struct { if (self.listen_fd == closed_fd or self.unix_addr != null) return 0; var addr: net.Address = undefined; var len: posix.socklen_t = @sizeOf(net.Address); - if (std.c.getsockname(self.listen_fd, &addr.any, &len) != 0) return 0; + sys.getsockname(self.listen_fd, &addr.any, &len) catch return 0; return addr.getPort(); } @@ -507,17 +509,11 @@ fn openListener(opts: ListenOptions, tcp_port: u16) !posix.socket_t { else try net.Address.parseIp4(opts.address, tcp_port); const family: u32 = if (opts.ipv6) posix.AF.INET6 else posix.AF.INET; - const fd = try sys.socket(family, posix.SOCK.STREAM | posix.SOCK.NONBLOCK | posix.SOCK.CLOEXEC, 0); + const fd = try sys.tcpSocket(family); errdefer sys.close(fd); - if (opts.ipv6) { - // Dual-stack: also accept IPv4 peers. - const IPV6_V6ONLY: u32 = if (builtin.target.os.tag == .linux) 26 else 27; - posix.setsockopt(fd, posix.IPPROTO.IPV6, IPV6_V6ONLY, std.mem.asBytes(&@as(c_int, 0))) catch {}; - } - posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, std.mem.asBytes(&@as(c_int, 1))) catch {}; - if (opts.reuse_port) { - try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, std.mem.asBytes(&@as(c_int, 1))); - } + if (opts.ipv6) sys.setDualStack(fd); // also accept IPv4 peers + sys.setReuseAddr(fd); + if (opts.reuse_port) try sys.setReusePort(fd); try sys.bind(fd, &addr.any, addr.getOsSockLen()); try sys.listen(fd, 1024); return fd; @@ -534,6 +530,7 @@ fn unixAddress(path: []const u8) !posix.sockaddr.un { } fn openUnixListener(addr: *const posix.sockaddr.un) !posix.socket_t { + if (comptime builtin.target.os.tag == .windows) return error.UnixSocketsUnsupported; const fd = try sys.socket(posix.AF.UNIX, posix.SOCK.STREAM | posix.SOCK.NONBLOCK | posix.SOCK.CLOEXEC, 0); errdefer sys.close(fd); sys.bind(fd, @ptrCast(addr), @sizeOf(posix.sockaddr.un)) catch |err| { diff --git a/src/http1/socket.zig b/src/http1/socket.zig index 7b3b2cc..8f32519 100644 --- a/src/http1/socket.zig +++ b/src/http1/socket.zig @@ -24,6 +24,7 @@ const std = @import("std"); const builtin = @import("builtin"); const xev = @import("../xev_backend.zig").xev; const timers = @import("timers.zig"); +const sys = @import("../sys.zig"); /// With epoll every socket on a thread reads into one buffer, so an idle /// connection doesn't hold one: epoll reads only just before running that @@ -177,7 +178,7 @@ pub fn Socket(comptime Owner: type) type { // costs an epoll registration round trip (and on epoll a dup of // the fd), which most writes to a healthy socket don't need. if (!self.corked and !self.writing and self.buffered() == 0) { - const rc = std.c.send(self.tcp.fd, data.ptr, data.len, send_flags); + const rc = sys.send(self.tcp.fd, data, send_flags); if (rc > 0) { const n: usize = @intCast(rc); if (n == data.len) return; @@ -198,19 +199,7 @@ pub fn Socket(comptime Owner: type) type { if (self.state != .open) return; var sent: usize = 0; if (!self.corked and !self.writing and self.buffered() == 0) { - var iov: [4]std.posix.iovec_const = undefined; - std.debug.assert(parts.len <= iov.len); - for (parts, 0..) |part, i| iov[i] = .{ .base = part.ptr, .len = part.len }; - const msg: std.c.msghdr_const = .{ - .name = null, - .namelen = 0, - .iov = &iov, - .iovlen = @intCast(parts.len), - .control = null, - .controllen = 0, - .flags = 0, - }; - const rc = std.c.sendmsg(self.tcp.fd, &msg, @intCast(send_flags)); + const rc = sys.sendVec(self.tcp.fd, parts, send_flags); // An error (not EAGAIN) resurfaces on the queued write below. if (rc > 0) sent = @intCast(rc); } @@ -249,7 +238,7 @@ pub fn Socket(comptime Owner: type) type { // which otherwise only a write completion gives them. const was_full = self.buffered() >= low_water; if (!self.writing and !self.held() and self.active_off >= self.active.items.len and self.pending.items.len > 0) { - const rc = std.c.send(self.tcp.fd, self.pending.items.ptr, self.pending.items.len, send_flags); + const rc = sys.send(self.tcp.fd, self.pending.items, send_flags); // An error (not EAGAIN) resurfaces on the queued write below. if (rc > 0) { const n: usize = @intCast(rc); @@ -350,7 +339,7 @@ pub fn Socket(comptime Owner: type) type { fn finishFlush(self: *Self) void { // Half-close so the peer sees everything we sent before any RST // that closing with unread input would trigger. - _ = std.c.shutdown(self.tcp.fd, std.posix.SHUT.WR); + sys.shutdown(self.tcp.fd, .send); self.state = .lingering; self.read_paused = false; self.startReading(); @@ -365,7 +354,7 @@ pub fn Socket(comptime Owner: type) type { self.state = .closing; self.dropOutput(); // Wakes any in-flight read or write so it completes promptly. - _ = std.c.shutdown(self.tcp.fd, std.posix.SHUT.RDWR); + sys.shutdown(self.tcp.fd, .both); self.maybeFinishClose(); } @@ -407,7 +396,7 @@ pub fn Socket(comptime Owner: type) type { pub fn closeFd(self: *Self) void { if (self.fd_closed) return; self.fd_closed = true; - _ = std.c.close(self.tcp.fd); + sys.close(self.tcp.fd); } /// Free the buffers without touching the fd; see `closeFd`. @@ -421,26 +410,24 @@ pub fn Socket(comptime Owner: type) type { /// Accept one queued connection without waiting; null when none is queued. pub fn acceptNow(listen_fd: std.posix.socket_t) ?std.posix.socket_t { + if (comptime builtin.target.os.tag == .windows) { + const fd = sys.accept(listen_fd) catch return null; + sys.setNonBlocking(fd); + return fd; + } if (comptime builtin.target.os.tag == .linux) { const fd = std.c.accept4(listen_fd, null, null, std.posix.SOCK.NONBLOCK | std.posix.SOCK.CLOEXEC); return if (fd < 0) null else fd; } const fd = std.c.accept(listen_fd, null, null); if (fd < 0) return null; - setNonBlocking(fd); + sys.setNonBlocking(fd); _ = std.c.fcntl(fd, std.c.F.SETFD, @as(c_int, std.c.FD_CLOEXEC)); return fd; } -pub fn setNonBlocking(fd: std.posix.socket_t) void { - const nonblock: u32 = @bitCast(std.c.O{ .NONBLOCK = true }); - const flags = std.c.fcntl(fd, std.c.F.GETFL, @as(c_int, 0)); - _ = std.c.fcntl(fd, std.c.F.SETFL, flags | @as(c_int, @bitCast(nonblock))); -} - pub fn setNoDelay(fd: std.posix.socket_t) void { - const one: c_int = 1; - _ = std.c.setsockopt(fd, std.posix.IPPROTO.TCP, std.posix.TCP.NODELAY, std.mem.asBytes(&one), @sizeOf(c_int)); + sys.setsockopt(fd, std.posix.IPPROTO.TCP, std.posix.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1))) catch {}; } /// The kernel's smoothed round trip for a TCP socket, in nanoseconds, at the diff --git a/src/quic/ecn_socket.zig b/src/quic/ecn_socket.zig index bf1efb7..4bdd465 100644 --- a/src/quic/ecn_socket.zig +++ b/src/quic/ecn_socket.zig @@ -448,17 +448,7 @@ pub const SendBatch = struct { fn sendEach(self: *SendBatch, from: usize, to: usize) void { for (from..to) |i| { const data = self.data_buf[self.offsets[i]..][0..self.lengths[i]]; - var iov = [1]posix.iovec_const{.{ .base = data.ptr, .len = data.len }}; - const msg = std.c.msghdr_const{ - .name = @ptrCast(&self.addrs[i]), - .namelen = self.addr_lens[i], - .iov = &iov, - .iovlen = 1, - .control = null, - .controllen = 0, - .flags = 0, - }; - _ = std.c.sendmsg(self.sockfd, &msg, 0); + sendDatagram(self.sockfd, data, &self.addrs[i], self.addr_lens[i]); } } @@ -592,10 +582,34 @@ fn cmsgSpace(len: usize) usize { return std.mem.alignForward(usize, cmsgLen(len), @alignOf(usize)); } -// Tests — ECN ancillary data tests only run on POSIX platforms. +/// One datagram out, best effort. POSIX uses sendmsg (matches quic-go), which +/// delivered more reliably than sendto on macOS loopback. Winsock has no +/// sendmsg, and without control data sendto is all WSASendMsg would add up to. +fn sendDatagram(sockfd: posix.socket_t, data: []const u8, addr: *const posix.sockaddr.storage, addr_len: posix.socklen_t) void { + if (comptime is_windows) { + _ = sys.sendto(sockfd, data, 0, @ptrCast(addr), addr_len) catch {}; + return; + } + var iov = [1]posix.iovec_const{.{ + .base = data.ptr, + .len = data.len, + }}; + const msg = std.c.msghdr_const{ + .name = @ptrCast(addr), + .namelen = addr_len, + .iov = &iov, + .iovlen = 1, + .control = null, + .controllen = 0, + .flags = 0, + }; + _ = std.c.sendmsg(sockfd, &msg, 0); +} + +// On Windows the ECN calls are no-ops and receive falls back to recvfrom, so +// these check the sockets work rather than the ancillary data. test "enableEcnRecv on a real socket" { - if (comptime is_windows) return error.SkipZigTest; - const sockfd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const sockfd = try sys.udpSocket(posix.AF.INET, .{}); defer sys.close(sockfd); const addr = try net.Address.parseIp4("127.0.0.1", 0); @@ -605,8 +619,7 @@ test "enableEcnRecv on a real socket" { } test "setEcnMark on a real socket" { - if (comptime is_windows) return error.SkipZigTest; - const sockfd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const sockfd = try sys.udpSocket(posix.AF.INET, .{}); defer sys.close(sockfd); const addr = try net.Address.parseIp4("127.0.0.1", 0); @@ -619,8 +632,7 @@ test "setEcnMark on a real socket" { } test "recvmsgEcn returns WouldBlock on empty socket" { - if (comptime is_windows) return error.SkipZigTest; - const sockfd = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + const sockfd = try sys.udpSocket(posix.AF.INET, .{}); defer sys.close(sockfd); const addr = try net.Address.parseIp4("127.0.0.1", 0); @@ -633,6 +645,7 @@ test "recvmsgEcn returns WouldBlock on empty socket" { } test "RecvBatch: datagrams from two senders arrive whole, in order, with their sources" { + if (comptime is_windows) return error.SkipZigTest; // no recvmmsg if (comptime is_windows) return error.SkipZigTest; const loopback = try net.Address.parseIp4("127.0.0.1", 0); const rx = try sys.socket(posix.AF.INET, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); @@ -688,7 +701,7 @@ test "RecvBatch: datagrams from two senders arrive whole, in order, with their s } test "SendBatch.segmentEnd: equal packets and one shorter last" { - var b = SendBatch.init(0); + var b = SendBatch.init(sys.invalid_socket); @memset(&b.addr_lens, 0); // one address for all const cases = [_]struct { lens: []const u32, from: usize, want: usize }{ // All the same size: the whole run. diff --git a/src/quic/qlog.zig b/src/quic/qlog.zig index 50d8d7d..9e4f877 100644 --- a/src/quic/qlog.zig +++ b/src/quic/qlog.zig @@ -4,6 +4,7 @@ // Events follow draft-ietf-quic-qlog-quic-events naming conventions. const std = @import("std"); +const builtin = @import("builtin"); const sys = @import("../sys.zig"); const frame_mod = @import("frame.zig"); const Frame = frame_mod.Frame; @@ -366,7 +367,9 @@ test "a trace never opens a file that is already there" { var dir_buf: [64]u8 = undefined; var rnd: [4]u8 = undefined; sys.randomBytes(&rnd); - const dir = try std.fmt.bufPrint(&dir_buf, "/tmp/quic-zig-qlog-{x}", .{rnd}); + // Windows has no /tmp: there it goes in the working directory. + const root = if (builtin.target.os.tag == .windows) "" else "/tmp/"; + const dir = try std.fmt.bufPrint(&dir_buf, "{s}quic-zig-qlog-{x}", .{ root, rnd }); try sys.makeDir(dir); const odcid = [_]u8{ 0xab, 0xcd }; var path_buf: [96]u8 = undefined; diff --git a/src/sys.zig b/src/sys.zig index 9de6263..01e3c0d 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -1,7 +1,12 @@ -//! Thin POSIX syscall wrappers over `std.c`, kept as the library's single -//! syscall seam. Structured for cross-platform with `@compileError` branches -//! for unimplemented platforms — we only claim to work where we actually -//! test. Currently: Linux, macOS, FreeBSD. +//! Thin syscall wrappers, kept as the library's single syscall seam: every +//! call into the OS goes through here, so a port is a matter of this file. +//! We only claim to work where we actually test. Currently: Linux, macOS, +//! FreeBSD and Windows. +//! +//! POSIX targets call `std.c` directly. Windows lives in `sys/windows.zig`: +//! Winsock for sockets, `std.Io` for files, clocks and randomness. Each +//! function here hands Windows off on its first line, so what follows is the +//! POSIX version. //! //! These helpers did not disappear in Zig 0.16, they moved onto the `Io` //! interface: sockets to `std.Io.net`, files to `std.Io.Dir`, randomness to @@ -15,12 +20,22 @@ //! `Socket.sendMany` (`sendmmsg` on Linux) — without touching callers. //! //! Naming mirrors the pre-0.16 `std.posix.X`. Constants (`AF`, `SOCK`, -//! `IPPROTO`, `SOL`, `SO`) still live on `std.posix`. +//! `SOL`, `SO`) still live on `std.posix`. const std = @import("std"); const builtin = @import("builtin"); const posix = std.posix; const c = std.c; +const windows = @import("sys/windows.zig"); + +const is_windows = builtin.target.os.tag == .windows; + +comptime { + switch (builtin.target.os.tag) { + .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly, .windows => {}, + else => @compileError("sys: unsupported OS"), + } +} // BSD/Darwin-only. 0.16 std.c drops the declaration; declare locally. extern "c" fn arc4random_buf(buf: [*]u8, nbytes: usize) void; @@ -32,9 +47,9 @@ pub const sockaddr = posix.sockaddr; pub const socklen_t = posix.socklen_t; pub const timespec = posix.timespec; -/// setsockopt survived in std.posix on POSIX targets; re-export so callers -/// only need one namespace. -pub const setsockopt = posix.setsockopt; +// Not on every target's `std.posix` — Winsock's `IPPROTO` has no IPv6 entry. +const IPPROTO_IPV6: i32 = 41; +const IPV6_V6ONLY: u32 = if (builtin.target.os.tag == .linux) 26 else 27; // --- errors --- @@ -99,6 +114,8 @@ pub const GetSockNameError = error{ Unexpected, }; +pub const SetSockOptError = posix.SetSockOptError; + pub const ClockGetTimeError = error{ UnsupportedClock, Unexpected, @@ -111,11 +128,7 @@ pub const GetRandomError = error{ // --- functions --- pub fn socket(domain: u32, sock_type: u32, protocol: u32) SocketError!socket_t { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.socket: Windows support pending"), - else => @compileError("sys.socket: unsupported OS"), - } + if (is_windows) return windows.socket(domain, sock_type, protocol); // On Darwin, SOCK.NONBLOCK / SOCK.CLOEXEC are not accepted by socket(2) // and must be applied via fcntl after creation. const darwin_family = switch (builtin.target.os.tag) { @@ -151,23 +164,129 @@ pub fn socket(domain: u32, sock_type: u32, protocol: u32) SocketError!socket_t { return fd; } -pub fn close(fd: fd_t) void { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.close: Windows support pending"), - else => @compileError("sys.close: unsupported OS"), +pub const UdpSocketOptions = struct { + /// IPv6 sockets only: carry IPv4 too, as v4-mapped addresses. + dual_stack: bool = true, + /// See `setReuseAddr`. + reuse_addr: bool = false, +}; + +/// A nonblocking UDP socket for `family`, ready to bind. +/// +/// On Windows it also stops ICMP errors from failing the socket's next +/// receive. By default Winsock reports a port-unreachable as WSAECONNRESET, +/// so on a server socket one departed client would break the receive loop +/// for every other connection. +pub fn udpSocket(family: u32, options: UdpSocketOptions) SocketError!socket_t { + const fd = if (is_windows) + try windows.udpSocket(family) + else + try socket(family, posix.SOCK.DGRAM | posix.SOCK.NONBLOCK, 0); + errdefer close(fd); + if (family == posix.AF.INET6) { + const v6only: c_int = @intFromBool(!options.dual_stack); + setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, std.mem.asBytes(&v6only)) catch {}; } + if (options.reuse_addr) setReuseAddr(fd); + return fd; +} + +/// A nonblocking TCP socket for `family`, ready to bind or connect. +pub fn tcpSocket(family: u32) SocketError!socket_t { + if (is_windows) return windows.tcpSocket(family); + return socket(family, posix.SOCK.STREAM | posix.SOCK.NONBLOCK | posix.SOCK.CLOEXEC, 0); +} + +/// What a closed socket's variable holds: sockets are handles on Windows. +pub const invalid_socket: socket_t = if (is_windows) windows.INVALID_SOCKET else -1; + +pub fn setsockopt(sock: socket_t, level: i32, optname: u32, opt: []const u8) SetSockOptError!void { + if (is_windows) return windows.setsockopt(sock, level, optname, opt); + return posix.setsockopt(sock, level, optname, opt); +} + +/// Let a restarted server bind its port straight away. A no-op on Windows, +/// where SO_REUSEADDR means something else: it lets a second socket bind a +/// port that is still in use. +pub fn setReuseAddr(sock: socket_t) void { + if (is_windows) return; + setsockopt(sock, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1))) catch {}; +} + +/// Share a port with sibling sockets, the kernel spreading traffic across +/// them. Windows has no such thing, so asking for it there is an error. +pub fn setReusePort(sock: socket_t) SetSockOptError!void { + if (is_windows) return error.InvalidProtocolOption; + try setsockopt(sock, posix.SOL.SOCKET, posix.SO.REUSEPORT, &std.mem.toBytes(@as(c_int, 1))); +} + +/// Let an IPv6 socket carry IPv4 too, as v4-mapped addresses. +pub fn setDualStack(sock: socket_t) void { + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &std.mem.toBytes(@as(c_int, 0))) catch {}; +} + +pub fn setNonBlocking(sock: socket_t) void { + if (is_windows) return windows.setNonBlocking(sock); + const nonblock: u32 = @bitCast(c.O{ .NONBLOCK = true }); + const flags = c.fcntl(sock, c.F.GETFL, @as(c_int, 0)); + _ = c.fcntl(sock, c.F.SETFL, flags | @as(c_int, @bitCast(nonblock))); +} + +/// `send`, for a nonblocking TCP socket. Negative on failure, would-block +/// included, as callers fall back to a queued write either way. +pub fn send(sock: socket_t, bytes: []const u8, flags: c_int) isize { + if (is_windows) return windows.send(sock, bytes); + return c.send(sock, bytes.ptr, bytes.len, @intCast(flags)); +} + +/// `send` for up to four pieces in one call. See `send`. +pub fn sendVec(sock: socket_t, parts: []const []const u8, flags: c_int) isize { + if (is_windows) return windows.sendVec(sock, parts); + var iov: [4]posix.iovec_const = undefined; + std.debug.assert(parts.len <= iov.len); + for (parts, 0..) |part, i| iov[i] = .{ .base = part.ptr, .len = part.len }; + const msg: c.msghdr_const = .{ + .name = null, + .namelen = 0, + .iov = &iov, + .iovlen = @intCast(parts.len), + .control = null, + .controllen = 0, + .flags = 0, + }; + return c.sendmsg(sock, &msg, @intCast(flags)); +} + +/// After `send` came back negative, or a `recv`: whether the socket was only +/// full or empty. +pub fn wouldBlock() bool { + if (is_windows) return windows.wouldBlock(); + return posix.errno(@as(isize, -1)) == .AGAIN; +} + +pub const ShutdownHow = enum { send, both }; + +/// Shut a TCP socket down. `.both` also wakes reads and writes in flight on +/// the socket's event loop. +pub fn shutdown(sock: socket_t, how: ShutdownHow) void { + if (is_windows) return windows.shutdown(sock, how); + _ = c.shutdown(sock, if (how == .send) posix.SHUT.WR else posix.SHUT.RDWR); +} + +pub fn getpeername(sock: socket_t, addr: *sockaddr, addrlen: *socklen_t) GetSockNameError!void { + if (is_windows) return windows.getpeername(sock, addr, addrlen); + if (c.getpeername(sock, addr, addrlen) != 0) return error.Unexpected; +} + +pub fn close(fd: fd_t) void { + if (is_windows) return windows.close(fd); // close(2) failures are unrecoverable from userspace (EINTR/EIO on some systems), // and retrying EINTR close is itself an anti-pattern on Linux — follow libc practice. _ = c.close(fd); } pub fn bind(sock: socket_t, addr: *const sockaddr, len: socklen_t) BindError!void { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.bind: Windows support pending"), - else => @compileError("sys.bind: unsupported OS"), - } + if (is_windows) return windows.bind(sock, addr, len); const rc = c.bind(sock, addr, len); switch (posix.errno(rc)) { .SUCCESS => return, @@ -196,11 +315,7 @@ pub fn sendto( dest_addr: ?*const sockaddr, addrlen: socklen_t, ) SendToError!usize { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.sendto: Windows support pending"), - else => @compileError("sys.sendto: unsupported OS"), - } + if (is_windows) return windows.sendto(sock, buf, flags, dest_addr, addrlen); const rc = c.sendto(sock, buf.ptr, buf.len, flags, dest_addr, addrlen); switch (posix.errno(rc)) { .SUCCESS => return @intCast(rc), @@ -240,11 +355,7 @@ pub fn recvfrom( src_addr: ?*sockaddr, addrlen: ?*socklen_t, ) RecvFromError!usize { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.recvfrom: Windows support pending"), - else => @compileError("sys.recvfrom: unsupported OS"), - } + if (is_windows) return windows.recvfrom(sock, buf, flags, src_addr, addrlen); const rc = c.recvfrom(sock, buf.ptr, buf.len, flags, src_addr, addrlen); switch (posix.errno(rc)) { .SUCCESS => return @intCast(rc), @@ -265,6 +376,7 @@ pub fn recvfrom( /// Raw write(2) — replaces `posix.write` for TCP streams etc. pub fn write(fd: fd_t, bytes: []const u8) WriteError!usize { + if (is_windows) return windows.write(fd, bytes); while (true) { const rc = c.write(fd, bytes.ptr, bytes.len); if (rc < 0) switch (posix.errno(rc)) { @@ -284,6 +396,7 @@ pub fn write(fd: fd_t, bytes: []const u8) WriteError!usize { /// Raw read(2) — replaces `posix.read` for TCP streams etc. pub fn read(fd: fd_t, dest: []u8) ReadError!usize { + if (is_windows) return windows.read(fd, dest); while (true) { const rc = c.read(fd, dest.ptr, dest.len); if (rc < 0) switch (posix.errno(rc)) { @@ -323,11 +436,7 @@ pub const ListenError = error{ }; pub fn listen(sock: socket_t, backlog: u31) ListenError!void { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.listen: Windows support pending"), - else => @compileError("sys.listen: unsupported OS"), - } + if (is_windows) return windows.listen(sock, backlog); const rc = c.listen(sock, @intCast(backlog)); switch (posix.errno(rc)) { .SUCCESS => return, @@ -345,16 +454,20 @@ pub const ResolveError = error{ Unexpected, }; +/// getaddrinfo, however the target spells it. +const netdb = if (is_windows) windows else struct { + const addrinfo = c.addrinfo; + const freeaddrinfo = c.freeaddrinfo; + fn getaddrinfo(node: ?[*:0]const u8, service: ?[*:0]const u8, hints: ?*const addrinfo, result: *?*addrinfo) i32 { + return @backingInt(c.getaddrinfo(node, service, hints, result)); + } +}; + /// Resolve `host` (numeric IP or hostname) to an IPv4/IPv6 sockaddr via -/// libc getaddrinfo. Replaces 0.15 `std.net.getAddressList` which is -/// gone in 0.16. The returned address carries `port` in network byte -/// order ready for `sys.bind`/`sys.sendto`. +/// getaddrinfo. Replaces 0.15 `std.net.getAddressList` which is gone in +/// 0.16. The returned address carries `port` in network byte order ready +/// for `sys.bind`/`sys.sendto`. pub fn resolveHost(host: []const u8, port: u16) ResolveError!posix.sockaddr.storage { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.resolveHost: Windows support pending"), - else => @compileError("sys.resolveHost: unsupported OS"), - } // Null-terminate host on the stack. var host_buf: [256]u8 = undefined; if (host.len >= host_buf.len) return error.PathTooLong; @@ -368,14 +481,13 @@ pub fn resolveHost(host: []const u8, port: u16) ResolveError!posix.sockaddr.stor port_buf[port_str.len] = 0; const service_z: [*:0]const u8 = @ptrCast(&port_buf); - var hints: std.c.addrinfo = std.mem.zeroes(std.c.addrinfo); + var hints: netdb.addrinfo = std.mem.zeroes(netdb.addrinfo); hints.family = posix.AF.UNSPEC; hints.socktype = posix.SOCK.DGRAM; - var res: ?*std.c.addrinfo = null; - const eai = std.c.getaddrinfo(host_z, service_z, &hints, &res); - if (eai != @as(std.c.EAI, @fromBackingInt(@intCast(0)))) return error.UnknownHostName; - defer if (res) |r| std.c.freeaddrinfo(r); + var res: ?*netdb.addrinfo = null; + if (netdb.getaddrinfo(host_z, service_z, &hints, &res) != 0) return error.UnknownHostName; + defer if (res) |r| netdb.freeaddrinfo(r); const first = res orelse return error.UnknownHostName; const addr_ptr = first.addr orelse return error.UnknownHostName; @@ -400,11 +512,7 @@ pub const AcceptError = error{ }; pub fn accept(sock: socket_t) AcceptError!socket_t { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.accept: Windows support pending"), - else => @compileError("sys.accept: unsupported OS"), - } + if (is_windows) return windows.accept(sock); const rc = c.accept(sock, null, null); switch (posix.errno(rc)) { .SUCCESS => return @intCast(rc), @@ -425,11 +533,7 @@ pub fn accept(sock: socket_t) AcceptError!socket_t { } pub fn getsockname(sock: socket_t, addr: *sockaddr, addrlen: *socklen_t) GetSockNameError!void { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.getsockname: Windows support pending"), - else => @compileError("sys.getsockname: unsupported OS"), - } + if (is_windows) return windows.getsockname(sock, addr, addrlen); const rc = c.getsockname(sock, addr, addrlen); switch (posix.errno(rc)) { .SUCCESS => return, @@ -446,11 +550,7 @@ pub fn getsockname(sock: socket_t, addr: *sockaddr, addrlen: *socklen_t) GetSock /// Sleep for the given number of nanoseconds. Replaces `std.Thread.sleep` /// which was removed in Zig 0.16. pub fn sleepNs(ns: u64) void { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.sleepNs: Windows support pending (use SleepEx)"), - else => @compileError("sys.sleepNs: unsupported OS"), - } + if (is_windows) return windows.sleepNs(ns); var req: timespec = .{ .sec = @intCast(ns / std.time.ns_per_s), .nsec = @intCast(ns % std.time.ns_per_s), @@ -473,11 +573,7 @@ pub fn sleepNs(ns: u64) void { /// written into something another host will read back. `nanoTimestamp` is /// monotonic, so its zero is the last boot. pub fn realtimeSeconds() i64 { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.realtimeSeconds: Windows support pending"), - else => @compileError("sys.realtimeSeconds: unsupported OS"), - } + if (is_windows) return windows.realtimeSeconds(); var ts: timespec = undefined; if (c.clock_gettime(posix.CLOCK.REALTIME, &ts) != 0) return 0; return @intCast(ts.sec); @@ -492,6 +588,7 @@ pub var test_clock: if (builtin.is_test) ?i64 else void = if (builtin.is_test) n /// stamped on the same clock, such as SO_TIMESTAMPNS: it can step, so it is /// never the clock for a deadline. pub fn realtimeNs() i64 { + if (is_windows) return windows.realtimeNs(); var ts: timespec = undefined; if (c.clock_gettime(posix.CLOCK.REALTIME, &ts) != 0) return 0; return @as(i64, ts.sec) * std.time.ns_per_s + @as(i64, ts.nsec); @@ -499,11 +596,7 @@ pub fn realtimeNs() i64 { pub fn nanoTimestamp() i64 { if (builtin.is_test) if (test_clock) |t| return t; - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.nanoTimestamp: Windows support pending (use QueryPerformanceCounter)"), - else => @compileError("sys.nanoTimestamp: unsupported OS"), - } + if (is_windows) return windows.nanoTimestamp(); var ts: timespec = undefined; const rc = c.clock_gettime(posix.CLOCK.MONOTONIC, &ts); if (rc != 0) return 0; // monotonic clock should never fail; fall back to 0. @@ -558,7 +651,8 @@ pub fn randomInt(comptime T: type) T { } /// Read cryptographic randomness into buf (replaces `std.crypto.random.bytes`). -/// Uses arc4random_buf on macOS/BSD and getrandom on Linux. +/// Uses arc4random_buf on macOS/BSD, getrandom on Linux and the CNG device on +/// Windows. pub fn randomBytes(buf: []u8) void { switch (builtin.target.os.tag) { .linux => { @@ -576,7 +670,7 @@ pub fn randomBytes(buf: []u8) void { // arc4random_buf is always available and never fails on BSD/Darwin. c_arc4random_buf(buf.ptr, buf.len); }, - .windows => @compileError("sys.randomBytes: Windows support pending (use BCryptGenRandom)"), + .windows => windows.randomBytes(buf), else => @compileError("sys.randomBytes: unsupported OS"), } } @@ -585,15 +679,21 @@ pub fn randomBytes(buf: []u8) void { /// On POSIX this is a direct libc getenv() call; the returned slice /// points into libc-managed static storage and must not be freed. pub fn getenv(name: [*:0]const u8) ?[:0]const u8 { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.getenv: Windows support pending (use GetEnvironmentVariableW)"), - else => @compileError("sys.getenv: unsupported OS"), - } + if (is_windows) return windows.getenv(name); const raw = c.getenv(name) orelse return null; return std.mem.span(raw); } +/// The command line, one argument at a time. Windows has to decode it into an +/// allocation first; like the arguments themselves, that lives as long as the +/// process. +pub fn argsIterator(args: std.process.Args) std.process.Args.Iterator { + if (is_windows) { + return args.iterateAllocator(std.heap.page_allocator) catch @panic("sys.argsIterator: out of memory"); + } + return args.iterate(); +} + fn unexpected(err: posix.E) error{Unexpected} { if (builtin.mode == .debug) { std.log.warn("sys: unexpected errno: {t}", .{err}); @@ -644,6 +744,7 @@ pub const File = struct { } pub fn writeAll(self: File, bytes: []const u8) WriteError!void { + if (is_windows) return windows.writeAll(self.fd, bytes); var written: usize = 0; while (written < bytes.len) { const rc = c.write(self.fd, bytes[written..].ptr, bytes.len - written); @@ -665,6 +766,7 @@ pub const File = struct { } pub fn read(self: File, dest: []u8) ReadError!usize { + if (is_windows) return windows.readFile(self.fd, dest); while (true) { const rc = c.read(self.fd, dest.ptr, dest.len); if (rc < 0) { @@ -687,6 +789,7 @@ pub const File = struct { /// needing the platform-specific fstat struct layout). Preserves file /// position so sequential read()s still work. pub fn stat(self: File) !Stat { + if (is_windows) return .{ .size = try windows.fileSize(self.fd) }; const cur = c.lseek(self.fd, 0, std.c.SEEK.CUR); if (cur < 0) switch (posix.errno(cur)) { else => |err| return unexpected(err), @@ -706,6 +809,7 @@ pub const File = struct { /// anything else. statx on Linux, where 0.16's `std.c.fstat` is void; /// fstat elsewhere. pub fn kind(self: File) !Kind { + if (is_windows) return windows.fileKind(self.fd); const mode: u32 = if (builtin.target.os.tag == .linux) blk: { const linux = std.os.linux; var stx: linux.Statx = undefined; @@ -726,12 +830,13 @@ pub const File = struct { } }; +/// The process's standard output. +pub fn stdout() File { + return .{ .fd = if (is_windows) windows.stdout() else posix.STDOUT_FILENO }; +} + fn closeFd(fd: fd_t) void { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.closeFd: Windows support pending"), - else => @compileError("sys.closeFd: unsupported OS"), - } + if (is_windows) return windows.closeFile(fd); _ = c.close(fd); } @@ -746,11 +851,7 @@ fn pathZ(path: []const u8, buf: *[std.fs.max_path_bytes]u8) OpenError![:0]const /// Open an existing file for reading. Replaces `std.fs.cwd().openFile`. pub fn openFileRead(path: []const u8) OpenError!File { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.openFileRead: Windows support pending"), - else => @compileError("sys.openFileRead: unsupported OS"), - } + if (is_windows) return .{ .fd = try windows.openFileRead(path) }; var path_buf: [std.fs.max_path_bytes]u8 = undefined; const z = try pathZ(path, &path_buf); const flags: posix.O = .{ .ACCMODE = .RDONLY }; @@ -771,21 +872,18 @@ pub fn openFileRead(path: []const u8) OpenError!File { /// Create/truncate a file for writing (mode 0644 on POSIX). Replaces /// the `std.fs.cwd().createFile(path, .{})` pattern. pub fn createFile(path: []const u8) OpenError!File { + if (is_windows) return .{ .fd = try windows.createFile(path) }; return openForWrite(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }); } /// Create a file that must not exist yet: `error.PathAlreadyExists` if /// anything is there, a symlink included (O_EXCL does not follow one). pub fn createFileNew(path: []const u8) OpenError!File { + if (is_windows) return .{ .fd = try windows.createFileNew(path) }; return openForWrite(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .EXCL = true }); } fn openForWrite(path: []const u8, flags: posix.O) OpenError!File { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.createFile: Windows support pending"), - else => @compileError("sys.createFile: unsupported OS"), - } var path_buf: [std.fs.max_path_bytes]u8 = undefined; const z = try pathZ(path, &path_buf); const rc = c.open(z.ptr, flags, @as(posix.mode_t, 0o644)); @@ -817,11 +915,7 @@ pub const MakeDirError = error{ /// Create a directory (mode 0755). Returns `PathAlreadyExists` if it /// already exists — callers can catch that and treat as success. pub fn makeDir(path: []const u8) MakeDirError!void { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.makeDir: Windows support pending"), - else => @compileError("sys.makeDir: unsupported OS"), - } + if (is_windows) return windows.makeDir(path); var path_buf: [std.fs.max_path_bytes]u8 = undefined; const z = try pathZBad(path, &path_buf); const rc = c.mkdir(z.ptr, @as(posix.mode_t, 0o755)); @@ -851,11 +945,7 @@ pub fn readFileAlloc( path: []const u8, max_bytes: usize, ) ![]u8 { - switch (builtin.target.os.tag) { - .linux, .macos, .ios, .watchos, .tvos, .visionos, .freebsd, .netbsd, .openbsd, .dragonfly => {}, - .windows => @compileError("sys.readFileAlloc: Windows support pending"), - else => @compileError("sys.readFileAlloc: unsupported OS"), - } + if (is_windows) return windows.readFileAlloc(gpa, path, max_bytes); var path_buf: [std.fs.max_path_bytes]u8 = undefined; const z = try pathZ(path, &path_buf); const flags: posix.O = .{ .ACCMODE = .RDONLY }; @@ -942,3 +1032,36 @@ test "cpuHasAes sees the AES this CPU has" { } return error.TestUnexpectedResult; } + +test "udpSocket round-trips a datagram over loopback" { + const net = @import("sockaddr.zig"); + const rx = try udpSocket(posix.AF.INET, .{}); + defer close(rx); + const any = try net.Address.parseIp4("127.0.0.1", 0); + try bind(rx, &any.any, any.getOsSockLen()); + + var bound: posix.sockaddr.storage = undefined; + var bound_len: socklen_t = @sizeOf(posix.sockaddr.storage); + try getsockname(rx, @ptrCast(&bound), &bound_len); + + var buf: [16]u8 = undefined; + try std.testing.expectError(error.WouldBlock, recvfrom(rx, &buf, 0, null, null)); + + const tx = try udpSocket(posix.AF.INET, .{}); + defer close(tx); + _ = try sendto(tx, "ping", 0, @ptrCast(&bound), bound_len); + + // Loopback delivery is not synchronous everywhere; give it a moment. + var n: usize = 0; + for (0..100) |_| { + n = recvfrom(rx, &buf, 0, null, null) catch |err| switch (err) { + error.WouldBlock => { + sleepNs(std.time.ns_per_ms); + continue; + }, + else => return err, + }; + break; + } + try std.testing.expectEqualStrings("ping", buf[0..n]); +} diff --git a/src/sys/windows.zig b/src/sys/windows.zig new file mode 100644 index 0000000..513b89d --- /dev/null +++ b/src/sys/windows.zig @@ -0,0 +1,497 @@ +//! Windows backend for `../sys.zig`. +//! +//! Sockets go through Winsock. Zig 0.16 dropped the ws2_32 function bindings +//! from std — its own `Io` talks to the AFD driver instead — so the few this +//! library calls are declared below. Files, clocks, sleep and randomness go +//! through `std.Io`'s global single-threaded instance, which is how std itself +//! implements them on Windows. + +const std = @import("std"); +const builtin = @import("builtin"); +const posix = std.posix; +const Io = std.Io; +const sys = @import("../sys.zig"); + +const socket_t = posix.socket_t; +const fd_t = posix.fd_t; +const sockaddr = posix.sockaddr; +const socklen_t = posix.socklen_t; + +pub const INVALID_SOCKET: socket_t = @ptrFromInt(~@as(usize, 0)); +const SOCKET_ERROR: i32 = -1; + +/// `_IOW('f', 126, u_long)`: set or clear nonblocking mode. +const FIONBIO: i32 = @bitCast(@as(u32, 0x8004667E)); +/// Whether an ICMP port-unreachable resets a UDP socket. +const SIO_UDP_CONNRESET: u32 = 0x9800000C; +/// Whether an ICMP time-exceeded resets a UDP socket. +const SIO_UDP_NETRESET: u32 = 0x9800000F; + +const WSADATA = extern struct { + wVersion: u16, + wHighVersion: u16, + iMaxSockets: u16, + iMaxUdpDg: u16, + lpVendorInfo: ?[*]u8, + szDescription: [257]u8, + szSystemStatus: [129]u8, +}; + +/// `ADDRINFOA`. Field names match `std.c.addrinfo` so `sys.resolveHost` reads +/// either; the order does not, Windows puts `canonname` before `addr`. +pub const addrinfo = extern struct { + flags: i32, + family: i32, + socktype: i32, + protocol: i32, + addrlen: usize, + canonname: ?[*:0]u8, + addr: ?*sockaddr, + next: ?*addrinfo, +}; + +/// The Winsock errors this file tells apart. Anything else is `Unexpected`. +const WSAError = enum(i32) { + WSAEINTR = 10004, + WSAEACCES = 10013, + WSAEINVAL = 10022, + WSAEMFILE = 10024, + WSAEWOULDBLOCK = 10035, + WSAENOTSOCK = 10038, + WSAEMSGSIZE = 10040, + WSAENOPROTOOPT = 10042, + WSAEPROTONOSUPPORT = 10043, + WSAEOPNOTSUPP = 10045, + WSAEAFNOSUPPORT = 10047, + WSAEADDRINUSE = 10048, + WSAEADDRNOTAVAIL = 10049, + WSAENETDOWN = 10050, + WSAENETUNREACH = 10051, + WSAENETRESET = 10052, + WSAECONNABORTED = 10053, + WSAECONNRESET = 10054, + WSAENOBUFS = 10055, + WSAENOTCONN = 10057, + WSAESHUTDOWN = 10058, + WSAECONNREFUSED = 10061, + WSAEHOSTUNREACH = 10065, + _, +}; + +const WSABUF = extern struct { + len: u32, + buf: [*]const u8, +}; + +extern "bcrypt" fn BCryptGenRandom(algorithm: ?*anyopaque, buf: [*]u8, len: u32, flags: u32) callconv(.winapi) i32; +extern "kernel32" fn SwitchToThread() callconv(.winapi) i32; +extern "kernel32" fn CancelIoEx(handle: fd_t, overlapped: ?*anyopaque) callconv(.winapi) i32; + +const ws2 = struct { + extern "ws2_32" fn WSAStartup(version: u16, data: *WSADATA) callconv(.winapi) i32; + extern "ws2_32" fn WSAGetLastError() callconv(.winapi) WSAError; + extern "ws2_32" fn socket(af: i32, @"type": i32, protocol: i32) callconv(.winapi) socket_t; + extern "ws2_32" fn closesocket(s: socket_t) callconv(.winapi) i32; + extern "ws2_32" fn bind(s: socket_t, name: *const sockaddr, namelen: i32) callconv(.winapi) i32; + extern "ws2_32" fn listen(s: socket_t, backlog: i32) callconv(.winapi) i32; + extern "ws2_32" fn accept(s: socket_t, addr: ?*sockaddr, addrlen: ?*i32) callconv(.winapi) socket_t; + extern "ws2_32" fn send(s: socket_t, buf: [*]const u8, len: i32, flags: i32) callconv(.winapi) i32; + extern "ws2_32" fn recv(s: socket_t, buf: [*]u8, len: i32, flags: i32) callconv(.winapi) i32; + extern "ws2_32" fn sendto(s: socket_t, buf: [*]const u8, len: i32, flags: i32, to: ?*const sockaddr, tolen: i32) callconv(.winapi) i32; + extern "ws2_32" fn recvfrom(s: socket_t, buf: [*]u8, len: i32, flags: i32, from: ?*sockaddr, fromlen: ?*i32) callconv(.winapi) i32; + extern "ws2_32" fn getsockname(s: socket_t, name: *sockaddr, namelen: *i32) callconv(.winapi) i32; + extern "ws2_32" fn setsockopt(s: socket_t, level: i32, optname: i32, optval: [*]const u8, optlen: i32) callconv(.winapi) i32; + extern "ws2_32" fn ioctlsocket(s: socket_t, cmd: i32, argp: *u32) callconv(.winapi) i32; + extern "ws2_32" fn WSAIoctl( + s: socket_t, + code: u32, + in_buf: ?*const anyopaque, + in_len: u32, + out_buf: ?*anyopaque, + out_len: u32, + bytes_returned: *u32, + overlapped: ?*anyopaque, + completion_routine: ?*anyopaque, + ) callconv(.winapi) i32; + extern "ws2_32" fn shutdown(s: socket_t, how: i32) callconv(.winapi) i32; + extern "ws2_32" fn getpeername(s: socket_t, name: *sockaddr, namelen: *i32) callconv(.winapi) i32; + extern "ws2_32" fn WSASend( + s: socket_t, + buffers: [*]const WSABUF, + count: u32, + sent: *u32, + flags: u32, + overlapped: ?*anyopaque, + completion_routine: ?*anyopaque, + ) callconv(.winapi) i32; + extern "ws2_32" fn getaddrinfo(node: ?[*:0]const u8, service: ?[*:0]const u8, hints: ?*const addrinfo, result: *?*addrinfo) callconv(.winapi) i32; + extern "ws2_32" fn freeaddrinfo(info: *addrinfo) callconv(.winapi) void; +}; + +var started: std.atomic.Value(bool) = .init(false); + +/// Winsock has to be started before its first call. WSAStartup counts its +/// callers, so two threads racing here cost a count, not correctness. +fn startup() void { + if (started.load(.acquire)) return; + var data: WSADATA = undefined; + if (ws2.WSAStartup(0x0202, &data) == 0) started.store(true, .release); +} + +fn unexpected(err: WSAError) error{Unexpected} { + if (builtin.mode == .debug) { + std.log.warn("sys: unexpected Winsock error: {d}", .{@backingInt(err)}); + } + return error.Unexpected; +} + +fn len32(n: usize) i32 { + return @intCast(@min(n, std.math.maxInt(i32))); +} + +// --- sockets --- + +pub fn socket(domain: u32, sock_type: u32, protocol: u32) sys.SocketError!socket_t { + startup(); + const s = ws2.socket(@intCast(domain), @intCast(sock_type), @intCast(protocol)); + if (s != INVALID_SOCKET) return s; + return switch (ws2.WSAGetLastError()) { + .WSAEACCES => error.PermissionDenied, + .WSAEAFNOSUPPORT => error.AddressFamilyNotSupported, + .WSAEPROTONOSUPPORT => error.ProtocolNotSupported, + .WSAEMFILE => error.ProcessFdQuotaExceeded, + .WSAENOBUFS => error.SystemResources, + else => |err| unexpected(err), + }; +} + +/// See `sys.udpSocket`. +pub fn udpSocket(family: u32) sys.SocketError!socket_t { + const s = try socket(family, posix.SOCK.DGRAM, 0); + errdefer close(s); + var nonblocking: u32 = 1; + if (ws2.ioctlsocket(s, FIONBIO, &nonblocking) != 0) return unexpected(ws2.WSAGetLastError()); + // Best effort: a socket that keeps the default still works, it just sees + // the spurious resets. + for ([_]u32{ SIO_UDP_CONNRESET, SIO_UDP_NETRESET }) |code| { + var enabled: u32 = 0; + var returned: u32 = 0; + _ = ws2.WSAIoctl(s, code, &enabled, @sizeOf(u32), null, 0, &returned, null, null); + } + return s; +} + +/// See `sys.tcpSocket`. +pub fn tcpSocket(family: u32) sys.SocketError!socket_t { + const s = try socket(family, posix.SOCK.STREAM, 0); + errdefer close(s); + var nonblocking: u32 = 1; + if (ws2.ioctlsocket(s, FIONBIO, &nonblocking) != 0) return unexpected(ws2.WSAGetLastError()); + return s; +} + +pub fn setNonBlocking(s: socket_t) void { + var nonblocking: u32 = 1; + _ = ws2.ioctlsocket(s, FIONBIO, &nonblocking); +} + +pub fn wouldBlock() bool { + return ws2.WSAGetLastError() == .WSAEWOULDBLOCK; +} + +pub fn send(s: socket_t, bytes: []const u8) isize { + return ws2.send(s, bytes.ptr, len32(bytes.len), 0); +} + +pub fn sendVec(s: socket_t, parts: []const []const u8) isize { + var bufs: [4]WSABUF = undefined; + std.debug.assert(parts.len <= bufs.len); + for (parts, 0..) |part, i| bufs[i] = .{ .len = @intCast(part.len), .buf = part.ptr }; + var sent: u32 = 0; + if (ws2.WSASend(s, &bufs, @intCast(parts.len), &sent, 0, null, null) != 0) return -1; + return sent; +} + +/// Unlike POSIX, shutdown leaves an overlapped read pending: it would hold +/// the socket's close until the peer sent something. +pub fn shutdown(s: socket_t, how: sys.ShutdownHow) void { + _ = ws2.shutdown(s, if (how == .send) 1 else 2); + if (how == .both) _ = CancelIoEx(s, null); +} + +pub fn getpeername(s: socket_t, addr: *sockaddr, addrlen: *socklen_t) sys.GetSockNameError!void { + if (ws2.getpeername(s, addr, @ptrCast(addrlen)) != 0) return unexpected(ws2.WSAGetLastError()); +} + +pub fn close(s: socket_t) void { + _ = ws2.closesocket(s); +} + +pub fn bind(s: socket_t, addr: *const sockaddr, len: socklen_t) sys.BindError!void { + if (ws2.bind(s, addr, @intCast(len)) == 0) return; + return switch (ws2.WSAGetLastError()) { + .WSAEACCES => error.AccessDenied, + .WSAEADDRINUSE => error.AddressInUse, + .WSAEADDRNOTAVAIL => error.AddressNotAvailable, + .WSAEAFNOSUPPORT => error.AddressFamilyNotSupported, + .WSAEINVAL => error.AlreadyBound, + .WSAENOTSOCK => error.FileDescriptorNotASocket, + .WSAENOBUFS => error.SystemResources, + .WSAENETDOWN => error.NetworkSubsystemFailed, + else => |err| unexpected(err), + }; +} + +pub fn listen(s: socket_t, backlog: u31) sys.ListenError!void { + if (ws2.listen(s, backlog) == 0) return; + return switch (ws2.WSAGetLastError()) { + .WSAEADDRINUSE => error.AddressInUse, + .WSAENOTSOCK => error.FileDescriptorNotASocket, + .WSAEOPNOTSUPP => error.OperationNotSupported, + .WSAENOBUFS => error.SystemResources, + else => |err| unexpected(err), + }; +} + +pub fn accept(s: socket_t) sys.AcceptError!socket_t { + const conn = ws2.accept(s, null, null); + if (conn != INVALID_SOCKET) return conn; + return switch (ws2.WSAGetLastError()) { + .WSAEWOULDBLOCK, .WSAEINTR => error.WouldBlock, + // Also what closing the listener from another thread looks like, + // which is how Http1Server stops its accept loop. + .WSAECONNRESET, .WSAENOTSOCK, .WSAEINVAL => error.ConnectionAborted, + .WSAEMFILE => error.ProcessFdQuotaExceeded, + .WSAENOBUFS => error.SystemResources, + else => |err| unexpected(err), + }; +} + +pub fn sendto( + s: socket_t, + buf: []const u8, + flags: u32, + dest_addr: ?*const sockaddr, + addrlen: socklen_t, +) sys.SendToError!usize { + const rc = ws2.sendto(s, buf.ptr, len32(buf.len), @intCast(flags), dest_addr, @intCast(addrlen)); + if (rc != SOCKET_ERROR) return @intCast(rc); + return switch (ws2.WSAGetLastError()) { + .WSAEWOULDBLOCK => error.WouldBlock, + .WSAEACCES => error.AccessDenied, + .WSAECONNRESET, .WSAENETRESET => error.ConnectionResetByPeer, + .WSAEMSGSIZE => error.MessageTooBig, + .WSAENOBUFS => error.SystemResources, + .WSAENETUNREACH => error.NetworkUnreachable, + .WSAEHOSTUNREACH => error.HostUnreachable, + .WSAENETDOWN => error.NetworkSubsystemFailed, + .WSAENOTSOCK => error.FileDescriptorNotASocket, + .WSAEAFNOSUPPORT => error.AddressFamilyNotSupported, + else => |err| unexpected(err), + }; +} + +pub fn recvfrom( + s: socket_t, + buf: []u8, + flags: u32, + src_addr: ?*sockaddr, + addrlen: ?*socklen_t, +) sys.RecvFromError!usize { + const rc = ws2.recvfrom(s, buf.ptr, len32(buf.len), @intCast(flags), src_addr, @ptrCast(addrlen)); + if (rc != SOCKET_ERROR) return @intCast(rc); + return switch (ws2.WSAGetLastError()) { + .WSAEWOULDBLOCK => error.WouldBlock, + // The datagram was longer than `buf`. Winsock fills the buffer, drops + // the rest and calls it a failure; POSIX returns the full buffer, so + // this does too. + .WSAEMSGSIZE => buf.len, + .WSAECONNRESET, .WSAENETRESET => error.ConnectionResetByPeer, + .WSAECONNREFUSED => error.ConnectionRefused, + .WSAENOTCONN => error.SocketNotConnected, + .WSAENETDOWN => error.NetworkSubsystemFailed, + .WSAENOBUFS => error.SystemResources, + else => |err| unexpected(err), + }; +} + +/// A TCP read. Like the POSIX version, 0 means either EOF or would-block. +pub fn read(s: socket_t, dest: []u8) sys.ReadError!usize { + const rc = ws2.recv(s, dest.ptr, len32(dest.len), 0); + if (rc != SOCKET_ERROR) return @intCast(rc); + return switch (ws2.WSAGetLastError()) { + .WSAEWOULDBLOCK => 0, + .WSAEACCES => error.AccessDenied, + else => |err| unexpected(err), + }; +} + +/// A TCP write. Like the POSIX version, 0 means would-block. +pub fn write(s: socket_t, bytes: []const u8) sys.WriteError!usize { + const rc = ws2.send(s, bytes.ptr, len32(bytes.len), 0); + if (rc != SOCKET_ERROR) return @intCast(rc); + return switch (ws2.WSAGetLastError()) { + .WSAEWOULDBLOCK => 0, + .WSAECONNRESET, .WSAENETRESET => error.ConnectionResetByPeer, + .WSAESHUTDOWN, .WSAECONNABORTED => error.BrokenPipe, + .WSAEACCES => error.AccessDenied, + else => |err| unexpected(err), + }; +} + +pub fn getsockname(s: socket_t, addr: *sockaddr, addrlen: *socklen_t) sys.GetSockNameError!void { + if (ws2.getsockname(s, addr, @ptrCast(addrlen)) == 0) return; + return switch (ws2.WSAGetLastError()) { + .WSAEINVAL => error.SocketNotBound, + .WSAENOTSOCK => error.NotSocket, + .WSAENOBUFS => error.SystemResources, + .WSAENETDOWN => error.NetworkSubsystemFailed, + else => |err| unexpected(err), + }; +} + +pub fn setsockopt(s: socket_t, level: i32, optname: u32, opt: []const u8) posix.SetSockOptError!void { + if (ws2.setsockopt(s, level, @intCast(optname), opt.ptr, len32(opt.len)) == 0) return; + return switch (ws2.WSAGetLastError()) { + .WSAENOPROTOOPT, .WSAEINVAL => error.InvalidProtocolOption, + .WSAENOTSOCK => error.FileDescriptorNotASocket, + .WSAENETDOWN => error.NetworkDown, + .WSAENOBUFS => error.SystemResources, + else => |err| unexpected(err), + }; +} + +pub fn getaddrinfo(node: ?[*:0]const u8, service: ?[*:0]const u8, hints: ?*const addrinfo, result: *?*addrinfo) i32 { + startup(); + return ws2.getaddrinfo(node, service, hints, result); +} + +pub const freeaddrinfo = ws2.freeaddrinfo; + +// --- everything else, through std.Io --- + +fn io() Io { + return Io.Threaded.global_single_threaded.io(); +} + +pub fn sleepNs(ns: u64) void { + // Waits round up to the ~15.6 ms system tick, 300x a 50 µs nap. + if (ns < std.time.ns_per_ms) { + const until = nanoTimestamp() + @as(i64, @intCast(ns)); + while (nanoTimestamp() < until) _ = SwitchToThread(); + return; + } + io().sleep(.fromNanoseconds(ns), .awake) catch {}; +} + +pub fn realtimeSeconds() i64 { + return Io.Clock.real.now(io()).toSeconds(); +} + +pub fn realtimeNs() i64 { + return @intCast(Io.Clock.real.now(io()).toNanoseconds()); +} + +pub fn nanoTimestamp() i64 { + return @intCast(Io.Clock.awake.now(io()).toNanoseconds()); +} + +pub fn randomBytes(buf: []u8) void { + const system_preferred_rng: u32 = 2; + if (BCryptGenRandom(null, buf.ptr, @intCast(buf.len), system_preferred_rng) != 0) { + @panic("sys.randomBytes: no system randomness"); + } +} + +/// The environment is UTF-16 on Windows. The value is converted once and kept +/// for the life of the process, which suits the callers: apps reading their +/// configuration at startup. +pub fn getenv(name: [*:0]const u8) ?[:0]const u8 { + const gpa = std.heap.page_allocator; + const key = std.unicode.wtf8ToWtf16LeAllocZ(gpa, std.mem.span(name)) catch return null; + defer gpa.free(key); + const env: std.process.Environ = .{ .block = .global }; + const value = env.getWindows(key) orelse return null; + return std.unicode.wtf16LeToWtf8AllocZ(gpa, value) catch null; +} + +fn ioFile(handle: fd_t) Io.File { + return .{ .handle = handle, .flags = .{ .nonblocking = false } }; +} + +pub fn stdout() fd_t { + return Io.File.stdout().handle; +} + +pub fn closeFile(handle: fd_t) void { + ioFile(handle).close(io()); +} + +pub fn writeAll(handle: fd_t, bytes: []const u8) sys.WriteError!void { + ioFile(handle).writeStreamingAll(io(), bytes) catch return error.InputOutput; +} + +pub fn readFile(handle: fd_t, dest: []u8) sys.ReadError!usize { + return ioFile(handle).readStreaming(io(), &.{dest}) catch |err| switch (err) { + error.EndOfStream => 0, + else => error.InputOutput, + }; +} + +pub fn fileSize(handle: fd_t) !u64 { + return ioFile(handle).length(io()); +} + +pub fn openFileRead(path: []const u8) sys.OpenError!fd_t { + const file = Io.Dir.cwd().openFile(io(), path, .{}) catch |err| return openError(err); + return file.handle; +} + +pub fn createFile(path: []const u8) sys.OpenError!fd_t { + const file = Io.Dir.cwd().createFile(io(), path, .{}) catch |err| return openError(err); + return file.handle; +} + +pub fn createFileNew(path: []const u8) sys.OpenError!fd_t { + const file = Io.Dir.cwd().createFile(io(), path, .{ .exclusive = true }) catch |err| return openError(err); + return file.handle; +} + +pub fn fileKind(handle: fd_t) !sys.File.Kind { + return switch ((try ioFile(handle).stat(io())).kind) { + .file => .file, + .directory => .directory, + else => .other, + }; +} + +fn openError(err: Io.File.OpenError) sys.OpenError { + return switch (err) { + error.FileNotFound => error.FileNotFound, + error.AccessDenied => error.AccessDenied, + error.IsDir => error.IsDir, + error.NameTooLong => error.NameTooLong, + error.SystemResources => error.SystemResources, + error.NoSpaceLeft => error.NoSpaceLeft, + error.PathAlreadyExists => error.PathAlreadyExists, + error.ProcessFdQuotaExceeded => error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded => error.SystemFdQuotaExceeded, + else => error.Unexpected, + }; +} + +pub fn makeDir(path: []const u8) sys.MakeDirError!void { + Io.Dir.cwd().createDir(io(), path, .default_dir) catch |err| return switch (err) { + error.PathAlreadyExists => error.PathAlreadyExists, + error.AccessDenied => error.AccessDenied, + error.FileNotFound => error.FileNotFound, + error.NoSpaceLeft => error.NoSpaceLeft, + error.NotDir => error.NotDir, + error.NameTooLong => error.PathTooLong, + else => error.Unexpected, + }; +} + +pub fn readFileAlloc(gpa: std.mem.Allocator, path: []const u8, max_bytes: usize) ![]u8 { + return Io.Dir.cwd().readFileAlloc(io(), path, gpa, .limited(max_bytes)); +} diff --git a/src/xev_backend.zig b/src/xev_backend.zig index 23176c9..23c6901 100644 --- a/src/xev_backend.zig +++ b/src/xev_backend.zig @@ -45,6 +45,71 @@ pub fn pollFd(loop: *const xev.Loop) std.posix.fd_t { }; } +/// `pollFd` for a backend with no fd: a completion port can't be polled, so +/// a thread blocks on it instead. What it finds it puts back for the loop to +/// collect, calls `wake`, and waits for `rearm`: one call per tick, however +/// many packets the tick takes. Electron embeds libuv's port the same way. +pub const PortWatch = struct { + const windows = std.os.windows; + const infinite: u32 = 0xFFFFFFFF; + + extern "kernel32" fn GetQueuedCompletionStatus(port: windows.HANDLE, bytes: *u32, key: *usize, overlapped: *?*anyopaque, ms: u32) callconv(.winapi) i32; + extern "kernel32" fn PostQueuedCompletionStatus(port: windows.HANDLE, bytes: u32, key: usize, overlapped: ?*anyopaque) callconv(.winapi) i32; + extern "kernel32" fn CreateEventW(attrs: ?*anyopaque, manual: i32, initial: i32, name: ?[*:0]const u16) callconv(.winapi) ?windows.HANDLE; + extern "kernel32" fn SetEvent(event: windows.HANDLE) callconv(.winapi) i32; + extern "kernel32" fn WaitForSingleObject(handle: windows.HANDLE, ms: u32) callconv(.winapi) u32; + extern "kernel32" fn CloseHandle(handle: windows.HANDLE) callconv(.winapi) i32; + + port: windows.HANDLE, + wake: *const fn (?*anyopaque) void, + ctx: ?*anyopaque, + rearmed: windows.HANDLE, + stopping: std.atomic.Value(bool) = .init(false), + thread: std.Thread, + + /// `self` must stay put until `stop`. + pub fn start(self: *PortWatch, loop: *const xev.Loop, wake: *const fn (?*anyopaque) void, ctx: ?*anyopaque) !void { + self.* = .{ + .port = loop.iocp_handle, + .wake = wake, + .ctx = ctx, + .rearmed = CreateEventW(null, 0, 0, null) orelse return error.SystemResources, + .thread = undefined, + }; + errdefer _ = CloseHandle(self.rearmed); + self.thread = try std.Thread.spawn(.{}, run, .{self}); + } + + pub fn rearm(self: *PortWatch) void { + _ = SetEvent(self.rearmed); + } + + /// Returns with the thread gone, so no `wake` follows. + pub fn stop(self: *PortWatch) void { + self.stopping.store(true, .release); + // Key 0, no overlapped: if the loop takes it, it reads as a wakeup. + _ = PostQueuedCompletionStatus(self.port, 0, 0, null); + self.rearm(); + self.thread.join(); + _ = CloseHandle(self.rearmed); + } + + fn run(self: *PortWatch) void { + while (true) { + var bytes: u32 = 0; + var key: usize = 0; + var overlapped: ?*anyopaque = null; + const ok = GetQueuedCompletionStatus(self.port, &bytes, &key, &overlapped, infinite); + // A failed I/O comes back as false with its overlapped set. + if (ok != 0 or overlapped != null) _ = PostQueuedCompletionStatus(self.port, bytes, key, overlapped); + if (self.stopping.load(.acquire)) return; + self.wake(self.ctx); + _ = WaitForSingleObject(self.rearmed, infinite); + if (self.stopping.load(.acquire)) return; + } + } +}; + /// Whether the loop holds work for its next tick that `pollFd` won't /// announce: completions added but not yet given to the kernel, and on /// kqueue, results waiting for their callbacks. @@ -59,7 +124,11 @@ pub fn nextTimerMs(loop: *xev.Loop) ?u64 { const t = loop.timers.peek() orelse return null; loop.update_now(); const now = loop.cached_now; - const ns = (@as(i128, t.next.sec) - now.sec) * std.time.ns_per_s + (t.next.nsec - now.nsec); + // IOCP keeps plain nanoseconds, the others a timespec. + const ns: i128 = if (xev.backend == .iocp) + @as(i128, t.next) - now + else + (@as(i128, t.next.sec) - now.sec) * std.time.ns_per_s + (t.next.nsec - now.nsec); if (ns <= 0) return 0; return @intCast(@divFloor(ns - 1, std.time.ns_per_ms) + 1); } @@ -101,6 +170,7 @@ test "a timer is queued until the loop ticks, then nextTimerMs counts down to it } test "pollFd is readable while the loop has an event to collect" { + if (xev.backend == .iocp) return error.SkipZigTest; // `PortWatch` instead var loop = try xev.Loop.init(.{}); defer loop.deinit(); var wake = try xev.Async.init();