feat: std/net/kqueue — raw kqueue/kevent bindings, darwin (PLAN-HTTPZ S3)
32-byte darwin struct kevent, EVFILT_READ/WRITE/TIMER, EV_* flags, and three thin helpers: kev_change (one registration entry), kq_apply (immediate change, no drain), kq_wait (bounded drain, EINTR reissued, negative timeout = forever). Off the std.sx barrel by design — the OS-neutral facade over this and the epoll twin is std.event (S5). examples/1631 pins zero-cost idle timeout, READ readiness with pending byte count + udata round-trip, and EV_EOF on peer close; verified under sx run AND sx build.
This commit is contained in:
55
examples/1631-net-kqueue.sx
Normal file
55
examples/1631-net-kqueue.sx
Normal file
@@ -0,0 +1,55 @@
|
||||
// std/net/kqueue (PLAN-HTTPZ S3): readiness without blocking — an idle
|
||||
// registration times out at zero cost, a write makes the peer readable
|
||||
// (with the pending byte count in `data` and the registration's udata
|
||||
// handed back), and a peer close reports EV_EOF.
|
||||
#import "modules/std.sx";
|
||||
kq :: #import "modules/std/net/kqueue.sx";
|
||||
|
||||
main :: () -> i32 {
|
||||
pair : [2]i32 = ---;
|
||||
if socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0, @pair[0]) != 0 {
|
||||
print("socketpair failed\n");
|
||||
return 1;
|
||||
}
|
||||
a := pair[0];
|
||||
b := pair[1];
|
||||
|
||||
q := kq.kqueue();
|
||||
if q < 0 { print("kqueue failed\n"); return 1; }
|
||||
if !kq.kq_apply(q, kq.kev_change(a, kq.EVFILT_READ, kq.EV_ADD, 7777)) {
|
||||
print("EV_ADD failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
evs : [4]kq.Kevent = ---;
|
||||
|
||||
// Nothing pending: a bounded wait returns 0 events.
|
||||
n := kq.kq_wait(q, @evs[0], 4, 50);
|
||||
if n != 0 { print("idle wait: expected 0 events, got {}\n", n); return 1; }
|
||||
print("idle wait: timeout, 0 events\n");
|
||||
|
||||
// Bytes pending: READ readiness with the byte count and our udata.
|
||||
msg := "ping";
|
||||
socket.write(b, msg.ptr, 4);
|
||||
n = kq.kq_wait(q, @evs[0], 4, 1000);
|
||||
if n != 1 { print("after write: expected 1 event, got {}\n", n); return 1; }
|
||||
if evs[0].ident != xx a { print("event names the wrong fd\n"); return 1; }
|
||||
if evs[0].filter != kq.EVFILT_READ { print("event names the wrong filter\n"); return 1; }
|
||||
if evs[0].udata != 7777 { print("udata did not round-trip\n"); return 1; }
|
||||
if evs[0].data != 4 { print("pending byte count: expected 4, got {}\n", evs[0].data); return 1; }
|
||||
print("after write: READ ready, 4 pending, udata 7777\n");
|
||||
|
||||
// Drain, then close the peer: readiness again, now flagged EV_EOF.
|
||||
buf : [16]u8 = ---;
|
||||
socket.read(a, @buf[0], 16);
|
||||
socket.close(b);
|
||||
n = kq.kq_wait(q, @evs[0], 4, 1000);
|
||||
if n != 1 { print("after close: expected 1 event, got {}\n", n); return 1; }
|
||||
if (evs[0].flags & kq.EV_EOF) == 0 { print("after close: EV_EOF not set\n"); return 1; }
|
||||
print("after close: EV_EOF\n");
|
||||
|
||||
socket.close(a);
|
||||
socket.close(q);
|
||||
print("kqueue ok\n");
|
||||
return 0;
|
||||
}
|
||||
1
examples/expected/1631-net-kqueue.exit
Normal file
1
examples/expected/1631-net-kqueue.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
0
examples/expected/1631-net-kqueue.stderr
Normal file
0
examples/expected/1631-net-kqueue.stderr
Normal file
4
examples/expected/1631-net-kqueue.stdout
Normal file
4
examples/expected/1631-net-kqueue.stdout
Normal file
@@ -0,0 +1,4 @@
|
||||
idle wait: timeout, 0 events
|
||||
after write: READ ready, 4 pending, udata 7777
|
||||
after close: EV_EOF
|
||||
kqueue ok
|
||||
81
library/modules/std/net/kqueue.sx
Normal file
81
library/modules/std/net/kqueue.sx
Normal file
@@ -0,0 +1,81 @@
|
||||
// std/net/kqueue — raw kqueue/kevent bindings (PLAN-HTTPZ S3).
|
||||
// darwin-only by definition; the linux twin is std/net/epoll (S4) and
|
||||
// the OS-neutral Loop facade over both is std.event (S5). Import this
|
||||
// module explicitly — it deliberately does not ride the std.sx barrel.
|
||||
//
|
||||
// One kernel queue multiplexes readiness for any number of fds: a
|
||||
// registered (ident, filter) pair reports through `kevent` when ready,
|
||||
// and an idle registration costs nothing — the head-of-line-free
|
||||
// substrate httpz workers stand on.
|
||||
|
||||
libc :: #library "c";
|
||||
|
||||
// darwin 64-bit struct kevent — 32 bytes:
|
||||
// uintptr_t ident; int16_t filter; uint16_t flags; uint32_t fflags;
|
||||
// intptr_t data; void *udata;
|
||||
// `ident` is the fd for READ/WRITE filters; `udata` is an opaque
|
||||
// caller word handed back verbatim with each event (connection
|
||||
// pointer/index in a loop).
|
||||
Kevent :: struct {
|
||||
ident: usize = 0;
|
||||
filter: i16 = 0;
|
||||
flags: u16 = 0;
|
||||
fflags: u32 = 0;
|
||||
data: i64 = 0;
|
||||
udata: usize = 0;
|
||||
}
|
||||
|
||||
// kevent's timeout — same layout as std.time's Timespec, declared
|
||||
// locally so the module stands alone.
|
||||
KqTimespec :: struct {
|
||||
sec: i64 = 0;
|
||||
nsec: i64 = 0;
|
||||
}
|
||||
|
||||
kqueue :: () -> i32 #foreign libc;
|
||||
kevent :: (kq: i32, changelist: *Kevent, nchanges: i32, eventlist: *Kevent, nevents: i32, timeout: *KqTimespec) -> i32 #foreign libc;
|
||||
|
||||
// Filters (darwin)
|
||||
EVFILT_READ :i16: -1;
|
||||
EVFILT_WRITE :i16: -2;
|
||||
EVFILT_TIMER :i16: -7;
|
||||
|
||||
// Action/state flags (darwin)
|
||||
EV_ADD :u16: 0x0001;
|
||||
EV_DELETE :u16: 0x0002;
|
||||
EV_ENABLE :u16: 0x0004;
|
||||
EV_DISABLE :u16: 0x0008;
|
||||
EV_ONESHOT :u16: 0x0010;
|
||||
EV_CLEAR :u16: 0x0020;
|
||||
EV_ERROR :u16: 0x4000;
|
||||
EV_EOF :u16: 0x8000;
|
||||
|
||||
// A change entry for one (fd, filter) registration.
|
||||
kev_change :: (fd: i32, filter: i16, flags: u16, udata: usize) -> Kevent {
|
||||
return Kevent.{ ident = xx fd, filter = filter, flags = flags, udata = udata };
|
||||
}
|
||||
|
||||
// Apply one registration change immediately (no event drain).
|
||||
// True on success.
|
||||
kq_apply :: (kq: i32, change: Kevent) -> bool {
|
||||
ch := change;
|
||||
return kevent(kq, @ch, 1, null, 0, null) >= 0;
|
||||
}
|
||||
|
||||
// Drain ready events into `events` (capacity `cap`), waiting at most
|
||||
// `timeout_ms` (negative = wait forever). Returns the event count
|
||||
// (0 = timeout); -1 only for a real kevent failure — EINTR is retried.
|
||||
kq_wait :: (kq: i32, events: *Kevent, cap: i32, timeout_ms: i64) -> i32 {
|
||||
ts : KqTimespec = .{ sec = timeout_ms / 1000, nsec = (timeout_ms % 1000) * 1000000 };
|
||||
while true {
|
||||
n := if timeout_ms < 0
|
||||
then kevent(kq, null, 0, events, cap, null)
|
||||
else kevent(kq, null, 0, events, cap, @ts);
|
||||
if n >= 0 { return n; }
|
||||
if errno_slot_kq().* != 4 { return -1; } // 4 = EINTR: reissue
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// errno, bound locally (the std.socket accessor is module-scoped there).
|
||||
errno_slot_kq :: () -> *i32 #foreign libc "__error";
|
||||
Reference in New Issue
Block a user