authorgravatar for kenta@lithdew.netlithdew <kenta@lithdew.net> 2021-04-21 16:33:53+09:00
committergravatar for kenta@lithdew.netlithdew <kenta@lithdew.net> 2021-04-25 21:15:02+09:00
logb435d7fa6050a8867b7228eeaa71a399a9d8d7c1
treedfc4d3a13f9bff17b4a82fd7034b02546f2a63ba
parent37b05742ff1544bccf7c8ae9b12c6707a5a54df2

x, x/os/Socket: initial work on new Socket abstraction

Kick-start initial work on new cross-platform abstraction for sockets. Adds a test for read timeouts and a test for creating a non-blocking socket pair on Linux. The new Socket abstraction is barebones and is made to support both blocking and non-blocking abstractions, alongside different socket protocols and domains. Support for platform-dependant socket options that handles unsupported platforms gracefully via. comptime checks is provided for the new Socket abstraction. This also marks the first out of many commits for introducing breaking changes to the standard library in a separate `x` folder, which was pre-approved by @andrewrk. The intent for the new `x` package is to introduce new async, event loop, networking, and operating system abstractions that would require breaking the standard library significantly. By having the `x` package, code in the standard library and compiler may then slowly be refactored to use the `x` package. Once modules in the `x` package are stabilized, they can be moved out of the `x` package, and a global 'grep' can be done to update import paths that resolve to the stabilized module in the `x` package.

4 files changed, 287 insertions(+), 0 deletions(-)

lib/std/std.zig+1
...@@ -88,6 +88,7 @@ pub const time = @import("time.zig");...@@ -88,6 +88,7 @@ pub const time = @import("time.zig");
88pub const unicode = @import("unicode.zig");88pub const unicode = @import("unicode.zig");
89pub const valgrind = @import("valgrind.zig");89pub const valgrind = @import("valgrind.zig");
90pub const wasm = @import("wasm.zig");90pub const wasm = @import("wasm.zig");
91pub const x = @import("x.zig");
91pub const zig = @import("zig.zig");92pub const zig = @import("zig.zig");
92pub const start = @import("start.zig");93pub const start = @import("start.zig");
9394
lib/std/x.zig created+1
...@@ -0,0 +1 @@
1pub const os = @import("x/os/os.zig");
\ No newline at end of file
lib/std/x/os/Socket.zig created+276
...@@ -0,0 +1,276 @@
1const std = @import("../../std.zig");
2
3const os = std.os;
4const mem = std.mem;
5const net = std.net;
6const time = std.time;
7const builtin = std.builtin;
8const testing = std.testing;
9
10const Socket = @This();
11
12/// A socket-address pair.
13pub const Connection = struct {
14 socket: Socket,
15 address: net.Address,
16};
17
18/// The underlying handle of a socket.
19fd: os.socket_t,
20
21/// Open a new socket.
22pub fn init(domain: u32, socket_type: u32, protocol: u32) !Socket {
23 return Socket{ .fd = try os.socket(domain, socket_type, protocol) };
24}
25
26/// Closes the socket.
27pub fn deinit(self: Socket) void {
28 os.closeSocket(self.fd);
29}
30
31/// Shutdown either the read side, or write side, or the entirety of a socket.
32pub fn shutdown(self: Socket, how: os.ShutdownHow) !void {
33 return os.shutdown(self.fd, how);
34}
35
36/// Binds the socket to an address.
37pub fn bind(self: Socket, address: net.Address) !void {
38 return os.bind(self.fd, &address.any, address.getOsSockLen());
39}
40
41/// Start listening for incoming connections on the socket.
42pub fn listen(self: Socket, max_backlog_size: u31) !void {
43 return os.listen(self.fd, max_backlog_size);
44}
45
46/// Have the socket attempt to the connect to an address.
47pub fn connect(self: Socket, address: net.Address) !void {
48 return os.connect(self.fd, &address.any, address.getOsSockLen());
49}
50
51/// Accept a pending incoming connection queued to the kernel backlog
52/// of the socket.
53pub fn accept(self: Socket, flags: u32) !Socket.Connection {
54 var address: os.sockaddr = undefined;
55 var address_len: u32 = @sizeOf(os.sockaddr);
56
57 const fd = try os.accept(self.fd, &address, &address_len, flags);
58
59 return Connection{
60 .socket = Socket{ .fd = fd },
61 .address = net.Address.initPosix(@alignCast(4, &address)),
62 };
63}
64
65/// Read data from the socket into the buffer provided. It returns the
66/// number of bytes read into the buffer provided.
67pub fn read(self: Socket, buf: []u8) !usize {
68 return os.read(self.fd, buf);
69}
70
71/// Read data from the socket into the buffer provided with a set of flags
72/// specified. It returns the number of bytes read into the buffer provided.
73pub fn recv(self: Socket, buf: []u8, flags: u32) !usize {
74 return os.recv(self.fd, buf, flags);
75}
76
77/// Write a buffer of data provided to the socket. It returns the number
78/// of bytes that are written to the socket.
79pub fn write(self: Socket, buf: []const u8) !usize {
80 return os.write(self.fd, buf);
81}
82
83/// Writes multiple I/O vectors to the socket. It returns the number
84/// of bytes that are written to the socket.
85pub fn writev(self: Socket, buffers: []const os.iovec_const) !usize {
86 return os.writev(self.fd, buffers);
87}
88
89/// Write a buffer of data provided to the socket with a set of flags specified.
90/// It returns the number of bytes that are written to the socket.
91pub fn send(self: Socket, buf: []const u8, flags: u32) !usize {
92 return os.send(self.fd, buf, flags);
93}
94
95/// Writes multiple I/O vectors with a prepended message header to the socket
96/// with a set of flags specified. It returns the number of bytes that are
97/// written to the socket.
98pub fn sendmsg(self: Socket, msg: os.msghdr_const, flags: u32) !usize {
99 return os.sendmsg(self.fd, msg, flags);
100}
101
102/// Query the address that the socket is locally bounded to.
103pub fn getLocalAddress(self: Socket) !net.Address {
104 var address: os.sockaddr = undefined;
105 var address_len: u32 = @sizeOf(os.sockaddr);
106 try os.getsockname(self.fd, &address, &address_len);
107 return net.Address.initPosix(@alignCast(4, &address));
108}
109
110/// Query and return the latest cached error on the socket.
111pub fn getError(self: Socket) !void {
112 return os.getsockoptError(self.fd);
113}
114
115/// Query the read buffer size of the socket.
116pub fn getReadBufferSize(self: Socket) !u32 {
117 var value: u32 = undefined;
118 var value_len: u32 = @sizeOf(u32);
119
120 const rc = os.system.getsockopt(self.fd, os.SOL_SOCKET, os.SO_RCVBUF, mem.asBytes(&value), &value_len);
121 return switch (os.errno(rc)) {
122 0 => value,
123 os.EBADF => error.BadFileDescriptor,
124 os.EFAULT => error.InvalidAddressSpace,
125 os.EINVAL => error.InvalidSocketOption,
126 os.ENOPROTOOPT => error.UnknownSocketOption,
127 os.ENOTSOCK => error.NotASocket,
128 else => |err| os.unexpectedErrno(err),
129 };
130}
131
132/// Query the write buffer size of the socket.
133pub fn getWriteBufferSize(self: Socket) !u32 {
134 var value: u32 = undefined;
135 var value_len: u32 = @sizeOf(u32);
136
137 const rc = os.system.getsockopt(self.fd, os.SOL_SOCKET, os.SO_SNDBUF, mem.asBytes(&value), &value_len);
138 return switch (os.errno(rc)) {
139 0 => value,
140 os.EBADF => error.BadFileDescriptor,
141 os.EFAULT => error.InvalidAddressSpace,
142 os.EINVAL => error.InvalidSocketOption,
143 os.ENOPROTOOPT => error.UnknownSocketOption,
144 os.ENOTSOCK => error.NotASocket,
145 else => |err| os.unexpectedErrno(err),
146 };
147}
148
149/// Allow multiple sockets on the same host to listen on the same address. It returns `error.UnsupportedSocketOption` if
150/// the host does not support sockets listening the same address.
151pub fn setReuseAddress(self: Socket, enabled: bool) !void {
152 if (comptime @hasDecl(os, "SO_REUSEADDR")) {
153 return os.setsockopt(self.fd, os.SOL_SOCKET, os.SO_REUSEADDR, mem.asBytes(&@as(usize, @boolToInt(enabled))));
154 }
155 return error.UnsupportedSocketOption;
156}
157
158/// Allow multiple sockets on the same host to listen on the same port. It returns `error.UnsupportedSocketOption` if
159/// the host does not supports sockets listening on the same port.
160pub fn setReusePort(self: Socket, enabled: bool) !void {
161 if (comptime @hasDecl(os, "SO_REUSEPORT")) {
162 return os.setsockopt(self.fd, os.SOL_SOCKET, os.SO_REUSEPORT, mem.asBytes(&@as(usize, @boolToInt(enabled))));
163 }
164 return error.UnsupportedSocketOption;
165}
166
167/// Disable Nagle's algorithm on a TCP socket. It returns `error.UnsupportedSocketOption` if the host does not support
168/// sockets disabling Nagle's algorithm.
169pub fn setNoDelay(self: Socket, enabled: bool) !void {
170 if (comptime @hasDecl(os, "TCP_NODELAY")) {
171 return os.setsockopt(self.fd, os.IPPROTO_TCP, os.TCP_NODELAY, mem.asBytes(&@as(usize, @boolToInt(enabled))));
172 }
173 return error.UnsupportedSocketOption;
174}
175
176/// Enables TCP Fast Open (RFC 7413) on a TCP socket. It returns `error.UnsupportedSocketOption` if the host does not
177/// support TCP Fast Open.
178pub fn setFastOpen(self: Socket, enabled: bool) !void {
179 if (comptime @hasDecl(os, "TCP_FASTOPEN")) {
180 return os.setsockopt(self.fd, os.IPPROTO_TCP, os.TCP_FASTOPEN, mem.asBytes(&@as(usize, @boolToInt(enabled))));
181 }
182 return error.UnsupportedSocketOption;
183}
184
185/// Enables TCP Quick ACK on a TCP socket to immediately send rather than delay ACKs when necessary. It returns
186/// `error.UnsupportedSocketOption` if the host does not support TCP Quick ACK.
187pub fn setQuickACK(self: Socket, enabled: bool) !void {
188 if (comptime @hasDecl(os, "TCP_QUICKACK")) {
189 return os.setsockopt(self.fd, os.IPPROTO_TCP, os.TCP_QUICKACK, mem.asBytes(&@as(usize, @boolToInt(enabled))));
190 }
191 return error.UnsupportedSocketOption;
192}
193
194/// Set the write buffer size of the socket.
195pub fn setWriteBufferSize(self: Socket, size: u32) !void {
196 return os.setsockopt(self.fd, os.SOL_SOCKET, os.SO_SNDBUF, mem.asBytes(&size));
197}
198
199/// Set the read buffer size of the socket.
200pub fn setReadBufferSize(self: Socket, size: u32) !void {
201 return os.setsockopt(self.fd, os.SOL_SOCKET, os.SO_RCVBUF, mem.asBytes(&size));
202}
203
204/// Set a timeout on the socket that is to occur if no messages are successfully written
205/// to its bound destination after a specified number of milliseconds. A subsequent write
206/// to the socket will thereafter return `error.WouldBlock` should the timeout be exceeded.
207pub fn setWriteTimeout(self: Socket, milliseconds: usize) !void {
208 const timeout = os.timeval{
209 .tv_sec = @intCast(isize, milliseconds / time.ms_per_s),
210 .tv_usec = @intCast(isize, (milliseconds % time.ms_per_s) * time.us_per_ms),
211 };
212
213 return os.setsockopt(self.fd, os.SOL_SOCKET, os.SO_SNDTIMEO, mem.asBytes(&timeout));
214}
215
216/// Set a timeout on the socket that is to occur if no messages are successfully read
217/// from its bound destination after a specified number of milliseconds. A subsequent
218/// read from the socket will thereafter return `error.WouldBlock` should the timeout be
219/// exceeded.
220pub fn setReadTimeout(self: Socket, milliseconds: usize) !void {
221 const timeout = os.timeval{
222 .tv_sec = @intCast(isize, milliseconds / time.ms_per_s),
223 .tv_usec = @intCast(isize, (milliseconds % time.ms_per_s) * time.us_per_ms),
224 };
225
226 return os.setsockopt(self.fd, os.SOL_SOCKET, os.SO_RCVTIMEO, mem.asBytes(&timeout));
227}
228
229test {
230 testing.refAllDecls(@This());
231}
232
233test "socket/linux: set read timeout of 1 millisecond on blocking socket" {
234 if (builtin.os.tag != .linux) return error.SkipZigTest;
235
236 const a = try Socket.init(os.AF_INET, os.SOCK_STREAM | os.SOCK_CLOEXEC, os.IPPROTO_TCP);
237 defer a.deinit();
238
239 try a.bind(net.Address.initIp4([_]u8{ 0, 0, 0, 0 }, 0));
240 try a.listen(128);
241
242 const binded_address = try a.getLocalAddress();
243
244 const b = try Socket.init(os.AF_INET, os.SOCK_STREAM | os.SOCK_CLOEXEC, os.IPPROTO_TCP);
245 defer b.deinit();
246
247 try b.connect(binded_address);
248 try b.setReadTimeout(1);
249
250 const ab = try a.accept(os.SOCK_CLOEXEC);
251 defer ab.socket.deinit();
252
253 var buf: [1]u8 = undefined;
254 testing.expectError(error.WouldBlock, b.read(&buf));
255}
256
257test "socket/linux: create non-blocking socket pair" {
258 if (builtin.os.tag != .linux) return error.SkipZigTest;
259
260 const a = try Socket.init(os.AF_INET, os.SOCK_STREAM | os.SOCK_NONBLOCK | os.SOCK_CLOEXEC, os.IPPROTO_TCP);
261 defer a.deinit();
262
263 try a.bind(net.Address.initIp4([_]u8{ 0, 0, 0, 0 }, 0));
264 try a.listen(128);
265
266 const binded_address = try a.getLocalAddress();
267
268 const b = try Socket.init(os.AF_INET, os.SOCK_STREAM | os.SOCK_NONBLOCK | os.SOCK_CLOEXEC, os.IPPROTO_TCP);
269 defer b.deinit();
270
271 testing.expectError(error.WouldBlock, b.connect(binded_address));
272 try b.getError();
273
274 const ab = try a.accept(os.SOCK_NONBLOCK | os.SOCK_CLOEXEC);
275 defer ab.socket.deinit();
276}
\ No newline at end of file
lib/std/x/os/os.zig created+9
...@@ -0,0 +1,9 @@
1const std = @import("../../std.zig");
2
3const testing = std.testing;
4
5pub const Socket = @import("Socket.zig");
6
7test {
8 testing.refAllDecls(@This());
9}
\ No newline at end of file