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}