diff --git a/examples/1631-net-kqueue.sx b/examples/1631-net-kqueue.sx new file mode 100644 index 0000000..274d29c --- /dev/null +++ b/examples/1631-net-kqueue.sx @@ -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; +} diff --git a/examples/expected/1631-net-kqueue.exit b/examples/expected/1631-net-kqueue.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/1631-net-kqueue.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1631-net-kqueue.stderr b/examples/expected/1631-net-kqueue.stderr new file mode 100644 index 0000000..e69de29 diff --git a/examples/expected/1631-net-kqueue.stdout b/examples/expected/1631-net-kqueue.stdout new file mode 100644 index 0000000..97cd932 --- /dev/null +++ b/examples/expected/1631-net-kqueue.stdout @@ -0,0 +1,4 @@ +idle wait: timeout, 0 events +after write: READ ready, 4 pending, udata 7777 +after close: EV_EOF +kqueue ok diff --git a/library/modules/std/net/kqueue.sx b/library/modules/std/net/kqueue.sx new file mode 100644 index 0000000..0f4d33b --- /dev/null +++ b/library/modules/std/net/kqueue.sx @@ -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";