code wiki / (root) / io.nx

io.nx source

↩ module page · 139 lines · 4942 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 39import "syscalls.nx" 40 41const SYS_EPOLL_CREATE1: i64 = 20 42const SYS_EPOLL_CTL: i64 = 21 43const SYS_EPOLL_PWAIT: i64 = 22 44 45const EPOLL_CTL_ADD: i64 = 1 46const EPOLL_CTL_DEL: i64 = 2 47const EPOLL_CTL_MOD: i64 = 3 48 49const IO_READ: i64 = 0x001 // EPOLLIN 50const IO_WRITE: i64 = 0x004 // EPOLLOUT 51const IO_HUP: i64 = 0x010 // EPOLLHUP 52 53// Linux `struct epoll_event` is packed (12 bytes): 54// u32 events 55// u64 data (union: we always use the fd variant as a tagged i64) 56// 57// On x86 it's explicitly `__attribute__((__packed__))`. On RV64 58// alignment matches so 12 bytes natively. We use 16 to give 59// ourselves an extra slot for app-private metadata + stay on 60// standard 8-byte alignment NishiLang structs expect. 61struct IoEvent { 62 events: i64, // low 32 bits = mask 63 fd: i64, // kernel writes data.u64 here; we stuff fd at add 64} 65 66const IO_EVENT_BYTES: i64 = 16 67 68// Create a new epoll instance. Returns fd or -errno. flags = 0 for 69// default; EPOLL_CLOEXEC = 0x80000 to auto-close on exec. 70func io_epoll_new() -> i64 { 71 return __syscall(SYS_EPOLL_CREATE1, 0, 0, 0, 0, 0, 0) 72} 73 74// Register `fd` with the epoll instance for the given event mask. 75// Stuffs `fd` into the event data field so io_wait can return it. 76// Returns 0 or -errno. 77func io_epoll_add(ep: i64, fd: i64, events: i64) -> i64 { 78 let ev_raw: *u8 = sys_mmap(16) 79 let ev: *IoEvent = ev_raw as *IoEvent 80 ev.events = events 81 ev.fd = fd 82 return __syscall(SYS_EPOLL_CTL, ep, EPOLL_CTL_ADD, fd, 83 ev as i64, 0, 0) 84} 85 86// Modify the event mask on an already-registered fd. 87func io_epoll_mod(ep: i64, fd: i64, events: i64) -> i64 { 88 let ev_raw: *u8 = sys_mmap(16) 89 let ev: *IoEvent = ev_raw as *IoEvent 90 ev.events = events 91 ev.fd = fd 92 return __syscall(SYS_EPOLL_CTL, ep, EPOLL_CTL_MOD, fd, 93 ev as i64, 0, 0) 94} 95 96// Unregister. Returns 0 or -errno. Safe to call even if fd was 97// already closed (kernel treats it as no-op with -ENOENT in that case). 98func io_epoll_del(ep: i64, fd: i64) -> i64 { 99 return __syscall(SYS_EPOLL_CTL, ep, EPOLL_CTL_DEL, fd, 0, 0, 0) 100} 101 102// Allocate a zeroed IoEvent array with `n` slots. Caller reuses this 103// across io_wait calls -- don't re-allocate per iteration. 104func io_events_alloc(n: i64) -> *IoEvent { 105 let raw: *u8 = sys_mmap(n * IO_EVENT_BYTES + 16) 106 return raw as *IoEvent 107} 108 109func io_event_at(evs: *IoEvent, i: i64) -> *IoEvent { 110 let base: i64 = evs as i64 111 return (base + i * IO_EVENT_BYTES) as *IoEvent 112} 113 114// Wait for events. `timeout_ms` < 0 means block indefinitely, 0 means 115// poll, >0 means wait up to ms. Returns event count or -errno. 116// `max_events` = caller-promised slots in `evs`. 117func io_wait(ep: i64, evs: *IoEvent, max_events: i64, 118 timeout_ms: i64) -> i64 { 119 return __syscall(SYS_EPOLL_PWAIT, ep, evs as i64, 120 max_events, timeout_ms, 0, 0) 121} 122 123// ---- fcntl for non-blocking mode ----------------------------------- 124// 125// Accepting a connection requires setting O_NONBLOCK on the fd so 126// recv/send don't stall the event loop. fcntl(fd, F_SETFL, O_NONBLOCK). 127 128const SYS_FCNTL: i64 = 25 129const F_GETFL: i64 = 3 130const F_SETFL: i64 = 4 131const O_NONBLOCK: i64 = 0x800 132 133// Make fd non-blocking. Returns 0 or -errno. Reads current flags 134// first, ORs in O_NONBLOCK, writes back. 135func io_set_nonblock(fd: i64) -> i64 { 136 let cur: i64 = __syscall(SYS_FCNTL, fd, F_GETFL, 0, 0, 0, 0) 137 if cur < 0 { return cur } 138 return __syscall(SYS_FCNTL, fd, F_SETFL, cur | O_NONBLOCK, 0, 0, 0) 139}