feat: std.socket nonblocking surface — fcntl, errno, typed _nb wrappers (PLAN-HTTPZ S2)
set_nonblocking (C-variadic fcntl), errno via __error (darwin; C3 selects per-OS), and accept_nb/read_nb/write_nb returning a typed SockErr — WouldBlock / Closed / Fault — so readiness-loop callers never parse -1/errno pairs. EINTR retries internally; accept_nb skips ECONNABORTED. Adds connect, shutdown, socketpair, AF_UNIX, SHUT_*. examples/1630 pins the result algebra on a socketpair and a nonblocking TCP listener (WouldBlock on empty backlog, accept after loopback connect); verified under sx run AND sx build. The .ir snapshot regen is mechanical: new std decls shift @str/@tag.str numbering and grow the type table (179 -> 185).
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// POSIX socket module (macOS only)
|
||||
// sockaddr_in layout and constants are platform-specific.
|
||||
// sockaddr_in layout, errno symbol, and constants are platform-specific;
|
||||
// per-OS selection is PLAN-HTTPZ C3.
|
||||
|
||||
libc :: #library "c";
|
||||
|
||||
@@ -9,15 +10,23 @@ setsockopt :: (fd: i32, level: i32, optname: i32, optval: *i32, optlen: u32) ->
|
||||
bind :: (fd: i32, addr: *SockAddr, addrlen: u32) -> i32 #foreign libc;
|
||||
listen :: (fd: i32, backlog: i32) -> i32 #foreign libc;
|
||||
accept :: (fd: i32, addr: *SockAddr, addrlen: *u32) -> i32 #foreign libc;
|
||||
connect :: (fd: i32, addr: *SockAddr, addrlen: u32) -> i32 #foreign libc;
|
||||
read :: (fd: i32, buf: [*]u8, count: usize) -> isize #foreign libc;
|
||||
write :: (fd: i32, buf: [*]u8, count: usize) -> isize #foreign libc;
|
||||
close :: (fd: i32) -> i32 #foreign libc;
|
||||
shutdown :: (fd: i32, how: i32) -> i32 #foreign libc;
|
||||
socketpair :: (domain: i32, kind: i32, protocol: i32, fds: *i32) -> i32 #foreign libc;
|
||||
fcntl :: (fd: i32, cmd: i32, ..args: []i32) -> i32 #foreign libc;
|
||||
|
||||
// Constants (macOS)
|
||||
AF_UNIX :i32: 1;
|
||||
AF_INET :i32: 2;
|
||||
SOCK_STREAM :i32: 1;
|
||||
SOL_SOCKET :i32: 0xFFFF;
|
||||
SO_REUSEADDR :i32: 0x4;
|
||||
SHUT_RD :i32: 0;
|
||||
SHUT_WR :i32: 1;
|
||||
SHUT_RDWR :i32: 2;
|
||||
|
||||
// macOS sockaddr_in (16 bytes, has sin_len field)
|
||||
SockAddr :: struct {
|
||||
@@ -31,3 +40,98 @@ SockAddr :: struct {
|
||||
htons :: (port: i64) -> u16 {
|
||||
cast(u16) (((port & 0xFF) << 8) | ((port >> 8) & 0xFF))
|
||||
}
|
||||
|
||||
// ── nonblocking I/O (PLAN-HTTPZ S2) ──────────────────────────────────
|
||||
// Typed wrappers over the readiness-loop syscall patterns: callers see
|
||||
// WouldBlock / Closed / Fault, never a raw -1/errno pair. EINTR is
|
||||
// retried internally — a readiness loop has nothing useful to do with
|
||||
// an interrupted syscall except issue it again.
|
||||
|
||||
// errno resolves to a real function under the C macro: `__error` on
|
||||
// darwin, `__errno_location` on linux (C3 selects per-OS).
|
||||
errno_slot :: () -> *i32 #foreign libc "__error";
|
||||
|
||||
// fcntl file-status flags + errno values (macOS).
|
||||
F_GETFL :i32: 3;
|
||||
F_SETFL :i32: 4;
|
||||
O_NONBLOCK :i32: 4;
|
||||
EINTR :i32: 4;
|
||||
EPIPE :i32: 32;
|
||||
EAGAIN :i32: 35; // == EWOULDBLOCK on darwin (and linux's 11 == its EWOULDBLOCK)
|
||||
EINPROGRESS :i32: 36;
|
||||
ECONNABORTED :i32: 53;
|
||||
ECONNRESET :i32: 54;
|
||||
|
||||
errno :: () -> i32 {
|
||||
return errno_slot().*;
|
||||
}
|
||||
|
||||
is_wouldblock :: (e: i32) -> bool {
|
||||
return e == EAGAIN;
|
||||
}
|
||||
|
||||
// Put `fd` in nonblocking mode (reads/writes/accepts return EAGAIN
|
||||
// instead of parking the thread). False when fcntl refuses.
|
||||
set_nonblocking :: (fd: i32) -> bool {
|
||||
flags := fcntl(fd, F_GETFL);
|
||||
if flags < 0 { return false; }
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK) >= 0;
|
||||
}
|
||||
|
||||
// Failure classes for the _nb wrappers.
|
||||
// WouldBlock — no data/space/pending connection right now; wait for
|
||||
// readiness and retry.
|
||||
// Closed — the peer is gone: EOF on read, EPIPE/ECONNRESET on
|
||||
// write, ECONNRESET on read.
|
||||
// Fault — any other errno (caller treats the fd as broken).
|
||||
SockErr :: error {
|
||||
WouldBlock,
|
||||
Closed,
|
||||
Fault,
|
||||
}
|
||||
|
||||
// Accept one pending connection on a nonblocking listener. A connection
|
||||
// that died between queueing and accept (ECONNABORTED) is skipped, not
|
||||
// surfaced — the listener is fine.
|
||||
accept_nb :: (fd: i32) -> (i32, !SockErr) {
|
||||
while true {
|
||||
c := accept(fd, null, null);
|
||||
if c >= 0 { return c; }
|
||||
e := errno();
|
||||
if e == EINTR or e == ECONNABORTED { continue; }
|
||||
if is_wouldblock(e) { raise error.WouldBlock; }
|
||||
raise error.Fault;
|
||||
}
|
||||
raise error.Fault;
|
||||
}
|
||||
|
||||
// Read up to `cap` bytes. Returns the byte count (> 0); an orderly EOF
|
||||
// or a peer reset is Closed.
|
||||
read_nb :: (fd: i32, buf: [*]u8, cap: usize) -> (i64, !SockErr) {
|
||||
while true {
|
||||
n := read(fd, buf, cap);
|
||||
if n > 0 { return xx n; }
|
||||
if n == 0 { raise error.Closed; }
|
||||
e := errno();
|
||||
if e == EINTR { continue; }
|
||||
if is_wouldblock(e) { raise error.WouldBlock; }
|
||||
if e == ECONNRESET { raise error.Closed; }
|
||||
raise error.Fault;
|
||||
}
|
||||
raise error.Fault;
|
||||
}
|
||||
|
||||
// Write up to `len` bytes, returning how many the kernel took (possibly
|
||||
// fewer — the caller continues from there on the next writability).
|
||||
write_nb :: (fd: i32, buf: [*]u8, len: usize) -> (i64, !SockErr) {
|
||||
while true {
|
||||
n := write(fd, buf, len);
|
||||
if n >= 0 { return xx n; }
|
||||
e := errno();
|
||||
if e == EINTR { continue; }
|
||||
if is_wouldblock(e) { raise error.WouldBlock; }
|
||||
if e == EPIPE or e == ECONNRESET { raise error.Closed; }
|
||||
raise error.Fault;
|
||||
}
|
||||
raise error.Fault;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user