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}