From 89d6317b93bbe5aac00c6af5833b3e74211ab37f Mon Sep 17 00:00:00 2001 From: LemonBoy Date: Mon, 16 Nov 2020 10:19:00 +0100 Subject: [PATCH 1/4] std: Decouple network streams from fs.File The overlap between files and sockets is minimal and lumping them together means supporting only a small subset of the functionalities provided by the OS. Moreover the socket and file handles are not always interchangeable: on Windows one should use Winsock's close() call rather than the one used for common files. --- lib/std/net.zig | 64 +++++++++++++++++++++++++++++++++++++++----- lib/std/net/test.zig | 4 +-- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/lib/std/net.zig b/lib/std/net.zig index 037df76907..41da4a2c46 100644 --- a/lib/std/net.zig +++ b/lib/std/net.zig @@ -10,6 +10,7 @@ const net = @This(); const mem = std.mem; const os = std.os; const fs = std.fs; +const io = std.io; pub const has_unix_sockets = @hasDecl(os, "sockaddr_un"); @@ -596,7 +597,7 @@ pub const Ip6Address = extern struct { } }; -pub fn connectUnixSocket(path: []const u8) !fs.File { +pub fn connectUnixSocket(path: []const u8) !Stream { const opt_non_block = if (std.io.is_async) os.SOCK_NONBLOCK else 0; const sockfd = try os.socket( os.AF_UNIX, @@ -614,7 +615,7 @@ pub fn connectUnixSocket(path: []const u8) !fs.File { try os.connect(sockfd, &addr.any, addr.getOsSockLen()); } - return fs.File{ + return Stream{ .handle = sockfd, }; } @@ -648,7 +649,7 @@ pub const AddressList = struct { }; /// All memory allocated with `allocator` will be freed before this function returns. -pub fn tcpConnectToHost(allocator: *mem.Allocator, name: []const u8, port: u16) !fs.File { +pub fn tcpConnectToHost(allocator: *mem.Allocator, name: []const u8, port: u16) !Stream { const list = try getAddressList(allocator, name, port); defer list.deinit(); @@ -665,7 +666,7 @@ pub fn tcpConnectToHost(allocator: *mem.Allocator, name: []const u8, port: u16) return std.os.ConnectError.ConnectionRefused; } -pub fn tcpConnectToAddress(address: Address) !fs.File { +pub fn tcpConnectToAddress(address: Address) !Stream { const nonblock = if (std.io.is_async) os.SOCK_NONBLOCK else 0; const sock_flags = os.SOCK_STREAM | nonblock | (if (builtin.os.tag == .windows) 0 else os.SOCK_CLOEXEC); @@ -679,7 +680,7 @@ pub fn tcpConnectToAddress(address: Address) !fs.File { try os.connect(sockfd, &address.any, address.getOsSockLen()); } - return fs.File{ .handle = sockfd }; + return Stream{ .handle = sockfd }; } /// Call `AddressList.deinit` on the result. @@ -1580,6 +1581,55 @@ fn dnsParseCallback(ctx: dpc_ctx, rr: u8, data: []const u8, packet: []const u8) } } +pub const Stream = struct { + // Underlying socket descriptor. + // Note that on some platforms this may not be interchangeable with a + // regular files descriptor. + handle: os.socket_t, + + pub fn close(self: Stream) void { + os.closeSocket(self.handle); + } + + pub const ReadError = os.ReadError; + pub const WriteError = os.WriteError; + + pub const Reader = io.Reader(Stream, ReadError, read); + pub const Writer = io.Writer(Stream, WriteError, write); + + pub fn reader(self: Stream) Reader { + return .{ .context = self }; + } + + pub fn writer(self: Stream) Writer { + return .{ .context = self }; + } + + pub fn read(self: Stream, buffer: []u8) ReadError!usize { + if (std.Target.current.os.tag == .windows) { + return os.windows.ReadFile(self.handle, buffer, null, io.default_mode); + } + + if (std.io.is_async) { + return std.event.Loop.instance.?.read(self.handle, buffer, false); + } else { + return os.read(self.handle, buffer); + } + } + + pub fn write(self: Stream, buffer: []const u8) WriteError!usize { + if (std.Target.current.os.tag == .windows) { + return os.windows.WriteFile(self.handle, buffer, null, io.default_mode); + } + + if (std.io.is_async) { + return std.event.Loop.instance.?.write(self.handle, buffer, false); + } else { + return os.write(self.handle, buffer); + } + } +}; + pub const StreamServer = struct { /// Copied from `Options` on `init`. kernel_backlog: u31, @@ -1686,7 +1736,7 @@ pub const StreamServer = struct { } || os.UnexpectedError; pub const Connection = struct { - file: fs.File, + stream: Stream, address: Address, }; @@ -1705,7 +1755,7 @@ pub const StreamServer = struct { if (accept_result) |fd| { return Connection{ - .file = fs.File{ .handle = fd }, + .stream = Stream{ .handle = fd }, .address = accepted_addr, }; } else |err| switch (err) { diff --git a/lib/std/net/test.zig b/lib/std/net/test.zig index 74ae1ddf4f..1f156bb1b8 100644 --- a/lib/std/net/test.zig +++ b/lib/std/net/test.zig @@ -166,7 +166,7 @@ test "listen on a port, send bytes, receive bytes" { var client = try server.accept(); var buf: [16]u8 = undefined; - const n = try client.file.reader().read(&buf); + const n = try client.stream.reader().read(&buf); testing.expectEqual(@as(usize, 12), n); testing.expectEqualSlices(u8, "Hello world!", buf[0..n]); @@ -249,6 +249,6 @@ fn testServer(server: *net.StreamServer) anyerror!void { var client = try server.accept(); - const stream = client.file.writer(); + const stream = client.stream.writer(); try stream.print("hello from server\n", .{}); } From 676d7fc63c057d24a3cbaa30b786e63de6f40d4a Mon Sep 17 00:00:00 2001 From: LemonBoy Date: Sat, 21 Nov 2020 21:06:55 +0100 Subject: [PATCH 2/4] std: Add a small test for i/o on unix sockets --- lib/std/net/test.zig | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/lib/std/net/test.zig b/lib/std/net/test.zig index 1f156bb1b8..a49b8f7551 100644 --- a/lib/std/net/test.zig +++ b/lib/std/net/test.zig @@ -165,6 +165,7 @@ test "listen on a port, send bytes, receive bytes" { defer t.wait(); var client = try server.accept(); + defer client.stream.close(); var buf: [16]u8 = undefined; const n = try client.stream.reader().read(&buf); @@ -252,3 +253,37 @@ fn testServer(server: *net.StreamServer) anyerror!void { const stream = client.stream.writer(); try stream.print("hello from server\n", .{}); } + +test "listen on a unix socket, send bytes, receive bytes" { + if (builtin.single_threaded) return error.SkipZigTest; + if (!net.has_unix_sockets) return error.SkipZigTest; + + var server = net.StreamServer.init(.{}); + defer server.deinit(); + + const socket_path = "socket.unix"; + + var socket_addr = try net.Address.initUnix(socket_path); + defer std.fs.cwd().deleteFile(socket_path) catch {}; + try server.listen(socket_addr); + + const S = struct { + fn clientFn(_: void) !void { + const socket = try net.connectUnixSocket(socket_path); + defer socket.close(); + + _ = try socket.writer().writeAll("Hello world!"); + } + }; + + const t = try std.Thread.spawn({}, S.clientFn); + defer t.wait(); + + var client = try server.accept(); + defer client.stream.close(); + var buf: [16]u8 = undefined; + const n = try client.stream.reader().read(&buf); + + testing.expectEqual(@as(usize, 12), n); + testing.expectEqualSlices(u8, "Hello world!", buf[0..n]); +} From d0beb4badb9064893133c996b05c0748317adf24 Mon Sep 17 00:00:00 2001 From: LemonBoy Date: Sun, 22 Nov 2020 10:17:51 +0100 Subject: [PATCH 3/4] Let the kernel pick a random port Avoid errors if the socket enters the TIME_WAIT state and we need to re-execute this test before the OS releases it. This problem was not really a problem before since the accept()-ed socket was never closed on the server-side. --- lib/std/net/test.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/std/net/test.zig b/lib/std/net/test.zig index a49b8f7551..12eb7c6cb2 100644 --- a/lib/std/net/test.zig +++ b/lib/std/net/test.zig @@ -145,7 +145,7 @@ test "listen on a port, send bytes, receive bytes" { // Try only the IPv4 variant as some CI builders have no IPv6 localhost // configured. - const localhost = try net.Address.parseIp("127.0.0.1", 8080); + const localhost = try net.Address.parseIp("127.0.0.1", 0); var server = net.StreamServer.init(.{}); defer server.deinit(); From 34720da3d0a4a98383639cbedcbe1aa885217ed6 Mon Sep 17 00:00:00 2001 From: LemonBoy Date: Sun, 22 Nov 2020 10:29:45 +0100 Subject: [PATCH 4/4] Apparently unix sockets are supported on Windows Starting from Windows 10 build 17063. --- lib/std/net.zig | 6 +++++- lib/std/net/test.zig | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/std/net.zig b/lib/std/net.zig index 41da4a2c46..6fe2d1cd08 100644 --- a/lib/std/net.zig +++ b/lib/std/net.zig @@ -12,7 +12,11 @@ const os = std.os; const fs = std.fs; const io = std.io; -pub const has_unix_sockets = @hasDecl(os, "sockaddr_un"); +// Windows 10 added support for unix sockets in build 17063, redstone 4 is the +// first release to support them. +pub const has_unix_sockets = @hasDecl(os, "sockaddr_un") and + (builtin.os.tag != .windows or + std.Target.current.os.version_range.windows.isAtLeast(.win10_rs4) orelse false); pub const Address = extern union { any: os.sockaddr, diff --git a/lib/std/net/test.zig b/lib/std/net/test.zig index 12eb7c6cb2..10a9c4e18b 100644 --- a/lib/std/net/test.zig +++ b/lib/std/net/test.zig @@ -258,6 +258,15 @@ test "listen on a unix socket, send bytes, receive bytes" { if (builtin.single_threaded) return error.SkipZigTest; if (!net.has_unix_sockets) return error.SkipZigTest; + if (std.builtin.os.tag == .windows) { + _ = try std.os.windows.WSAStartup(2, 2); + } + defer { + if (std.builtin.os.tag == .windows) { + std.os.windows.WSACleanup() catch unreachable; + } + } + var server = net.StreamServer.init(.{}); defer server.deinit();