code wiki / (root) / nx_io_v1.nx

nx_io_v1.nx source

↩ module page · 145 lines · 5057 B

1// io.nx -- epoll-based non-blocking event loop (Phase G3). 2// 3// Research / reference: 4// Linux epoll (2.5.45, 2002) — the scalable alternative to select 5// and poll. Watches thousands of fds in O(1) per ready event. 6// Nginx / redis / node.js all run on this model. 7// 8// Sovereignty: direct epoll_create1 / epoll_ctl / epoll_wait 9// syscalls. No libev, no libuv, no glibc epoll wrapper. The same 10// API works on NishiOS once its equivalent interrupt-dispatched 11// ready-queue ships. 12// 13// Pattern for single-threaded server on this loop: 14// 15// let ep: i64 = io_epoll_new() 16// io_epoll_add(ep, listen_fd, IO_READ) 17// let evs: *IoEvent = io_events_alloc(64) 18// while serving { 19// let n: i64 = io_wait(ep, evs, 64, -1) 20// var i: i64 = 0 21// while i < n { 22// let e: *IoEvent = io_event_at(evs, i) 23// handle(e.fd, e.events) 24// i = i + 1 25// } 26// } 27// 28// Syscalls (Linux RV64): 29// 20 epoll_create1 30// 21 epoll_ctl 31// 22 epoll_pwait 32// 33// Event masks (subset we support): 34// EPOLLIN = 0x001 — readable 35// EPOLLOUT = 0x004 — writable 36// EPOLLHUP = 0x010 — peer hangup 37// EPOLLET = 0x80000000 — edge-triggered (uint32 bit 31) 38 39// nx_safety_envelope: 40// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 41// sil_target: SIL1 42// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 43// verdict: NOT_YET_EVALUATED 44 45import "nx_syscalls.nx" 46 47const SYS_EPOLL_CREATE1: i64 = 20 48const SYS_EPOLL_CTL: i64 = 21 49const SYS_EPOLL_PWAIT: i64 = 22 50 51const EPOLL_CTL_ADD: i64 = 1 52const EPOLL_CTL_DEL: i64 = 2 53const EPOLL_CTL_MOD: i64 = 3 54 55const IO_READ: i64 = 0x001 // EPOLLIN 56const IO_WRITE: i64 = 0x004 // EPOLLOUT 57const IO_HUP: i64 = 0x010 // EPOLLHUP 58 59// Linux `struct epoll_event` is packed (12 bytes): 60// u32 events 61// u64 data (union: we always use the fd variant as a tagged i64) 62// 63// On x86 it's explicitly `__attribute__((__packed__))`. On RV64 64// alignment matches so 12 bytes natively. We use 16 to give 65// ourselves an extra slot for app-private metadata + stay on 66// standard 8-byte alignment NishiLang structs expect. 67struct IoEvent { 68 events: i64, // low 32 bits = mask 69 fd: i64, // kernel writes data.u64 here; we stuff fd at add 70} 71 72const IO_EVENT_BYTES: i64 = 16 73 74// Create a new epoll instance. Returns fd or -errno. flags = 0 for 75// default; EPOLL_CLOEXEC = 0x80000 to auto-close on exec. 76func io_epoll_new() -> i64 { 77 return __syscall(SYS_EPOLL_CREATE1, 0, 0, 0, 0, 0, 0) 78} 79 80// Register `fd` with the epoll instance for the given event mask. 81// Stuffs `fd` into the event data field so io_wait can return it. 82// Returns 0 or -errno. 83func io_epoll_add(ep: i64, fd: i64, events: i64) -> i64 { 84 let ev_raw: *u8 = sys_mmap(16) 85 let ev: *IoEvent = ev_raw as *IoEvent 86 ev.events = events 87 ev.fd = fd 88 return __syscall(SYS_EPOLL_CTL, ep, EPOLL_CTL_ADD, fd, 89 ev as i64, 0, 0) 90} 91 92// Modify the event mask on an already-registered fd. 93func io_epoll_mod(ep: i64, fd: i64, events: i64) -> i64 { 94 let ev_raw: *u8 = sys_mmap(16) 95 let ev: *IoEvent = ev_raw as *IoEvent 96 ev.events = events 97 ev.fd = fd 98 return __syscall(SYS_EPOLL_CTL, ep, EPOLL_CTL_MOD, fd, 99 ev as i64, 0, 0) 100} 101 102// Unregister. Returns 0 or -errno. Safe to call even if fd was 103// already closed (kernel treats it as no-op with -ENOENT in that case). 104func io_epoll_del(ep: i64, fd: i64) -> i64 { 105 return __syscall(SYS_EPOLL_CTL, ep, EPOLL_CTL_DEL, fd, 0, 0, 0) 106} 107 108// Allocate a zeroed IoEvent array with `n` slots. Caller reuses this 109// across io_wait calls -- don't re-allocate per iteration. 110func io_events_alloc(n: i64) -> *IoEvent { 111 let raw: *u8 = sys_mmap(n * IO_EVENT_BYTES + 16) 112 return raw as *IoEvent 113} 114 115func io_event_at(evs: *IoEvent, i: i64) -> *IoEvent { 116 let base: i64 = evs as i64 117 return (base + i * IO_EVENT_BYTES) as *IoEvent 118} 119 120// Wait for events. `timeout_ms` < 0 means block indefinitely, 0 means 121// poll, >0 means wait up to ms. Returns event count or -errno. 122// `max_events` = caller-promised slots in `evs`. 123func io_wait(ep: i64, evs: *IoEvent, max_events: i64, 124 timeout_ms: i64) -> i64 { 125 return __syscall(SYS_EPOLL_PWAIT, ep, evs as i64, 126 max_events, timeout_ms, 0, 0) 127} 128 129// ---- fcntl for non-blocking mode ----------------------------------- 130// 131// Accepting a connection requires setting O_NONBLOCK on the fd so 132// recv/send don't stall the event loop. fcntl(fd, F_SETFL, O_NONBLOCK). 133 134const SYS_FCNTL: i64 = 25 135const F_GETFL: i64 = 3 136const F_SETFL: i64 = 4 137const O_NONBLOCK: i64 = 0x800 138 139// Make fd non-blocking. Returns 0 or -errno. Reads current flags 140// first, ORs in O_NONBLOCK, writes back. 141func io_set_nonblock(fd: i64) -> i64 { 142 let cur: i64 = __syscall(SYS_FCNTL, fd, F_GETFL, 0, 0, 0, 0) 143 if cur < 0 { return cur } 144 return __syscall(SYS_FCNTL, fd, F_SETFL, cur | O_NONBLOCK, 0, 0, 0) 145}