code wiki / (root) / nx_net.nx

nx_net.nx source

↩ module page · 268 lines · 9395 B

1// net.nx -- BSD-style socket primitives (Phase G2). 2// 3// Typed wrappers around Linux/NishiOS socket syscalls. Fallible 4// public calls return `*Result<i64, NetError>` -- the Ok payload is 5// an fd (for socket / accept) or a byte count (for send / recv) or 6// 0 (for bind / listen / connect). Raw `_raw` variants keep the 7// bare syscall return for callers that want the i64 directly (e.g. 8// tight send loops). 9// 10// Syscall numbers (Linux RV64, NishiOS matches): 11// 198 socket 12// 200 bind 13// 201 listen 14// 202 accept 15// 203 connect 16// 206 sendto (we use as send) 17// 207 recvfrom (we use as recv) 18// 208 setsockopt 19// 209 getsockopt 20// 21// Address families (SOL): 22// AF_INET = 2 (IPv4) 23// AF_INET6 = 10 (IPv6) 24// 25// Socket types: 26// SOCK_STREAM = 1 (TCP) 27// SOCK_DGRAM = 2 (UDP) 28// 29// The typed variant struct SockAddrIn matches Linux's `struct 30// sockaddr_in` exactly (16 bytes): sin_family + sin_port (big- 31// endian) + sin_addr + 8 bytes of padding. 32 33// nx_safety_envelope: 34// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 35// sil_target: SIL1 36// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 37// verdict: NOT_YET_EVALUATED 38 39import "nx_syscalls.nx" 40import "nx_stdlib.nx" 41 42const SYS_SOCKET: i64 = 198 43const SYS_BIND: i64 = 200 44const SYS_LISTEN: i64 = 201 45const SYS_ACCEPT: i64 = 202 46const SYS_CONNECT: i64 = 203 47const SYS_SENDTO: i64 = 206 48const SYS_RECVFROM: i64 = 207 49const SYS_SETSOCKOPT: i64 = 208 50 51const AF_INET: i64 = 2 52const SOCK_STREAM: i64 = 1 53const SOCK_DGRAM: i64 = 2 54 55// IPv4 socket address. sockaddr_in on Linux is 16 bytes with the 56// port in big-endian byte order. We expose it as an opaque byte 57// buffer via net_fill_sockaddr_in so callers don't reach into the 58// struct directly (Liskov-style: module boundary hides layout). 59struct SockAddrIn { 60 // family (2 bytes) + port (2 bytes) + addr (4 bytes) + pad (8) 61 // packed into two i64 slots; accessors below set/get the fields. 62 w0: i64, 63 w1: i64, 64} 65 66// ---- NetError ------------------------------------------------------ 67// 68// Aligned with the subset of Linux errnos that sockets actually 69// surface. Keeps the enum small so match-arms stay exhaustive. 70enum NetError { 71 AddrInUse, // -EADDRINUSE = -98 72 AddrNotAvail, // -EADDRNOTAVAIL = -99 73 ConnRefused, // -ECONNREFUSED = -111 74 ConnReset, // -ECONNRESET = -104 75 TimedOut, // -ETIMEDOUT = -110 76 NetDown, // -ENETDOWN = -100 77 PermDenied, // -EACCES = -13 78 InvalidArg, // -EINVAL = -22 79 Unknown, 80} 81 82// Translate negative-errno to NetError discriminant. 83func net_errno_to(e: i64) -> i64 { 84 let err: i64 = 0 - e 85 if err == 98 { return NetError::AddrInUse } 86 if err == 99 { return NetError::AddrNotAvail } 87 if err == 111 { return NetError::ConnRefused } 88 if err == 104 { return NetError::ConnReset } 89 if err == 110 { return NetError::TimedOut } 90 if err == 100 { return NetError::NetDown } 91 if err == 13 { return NetError::PermDenied } 92 if err == 22 { return NetError::InvalidArg } 93 return NetError::Unknown 94} 95 96// ---- socket lifecycle ---------------------------------------------- 97 98// Raw variants return the bare syscall rc (fd or -errno). Kept 99// public so tight I/O loops can avoid allocating a Result per call. 100func net_tcp_raw() -> i64 { 101 return __syscall(SYS_SOCKET, AF_INET, SOCK_STREAM, 0, 0, 0, 0) 102} 103 104func net_udp_raw() -> i64 { 105 return __syscall(SYS_SOCKET, AF_INET, SOCK_DGRAM, 0, 0, 0, 0) 106} 107 108// Typed socket constructors. Result payload is the fd. 109func net_tcp() -> *Result<i64, NetError> { 110 let rc: i64 = net_tcp_raw() 111 if rc < 0 { return Result::Err(net_errno_to(rc)) } 112 return Result::Ok(rc) 113} 114 115func net_udp() -> *Result<i64, NetError> { 116 let rc: i64 = net_udp_raw() 117 if rc < 0 { return Result::Err(net_errno_to(rc)) } 118 return Result::Ok(rc) 119} 120 121// Close a socket fd. Never fails observably in practice; returns 122// Result for symmetry with fs_close. 123func net_close(fd: i64) -> *Result<i64, NetError> { 124 let rc: i64 = sys_close(fd) 125 if rc < 0 { return Result::Err(net_errno_to(rc)) } 126 return Result::Ok(0) 127} 128 129// ---- address helpers ----------------------------------------------- 130 131// Fill a sockaddr_in buffer with AF_INET + port + INADDR_ANY (0.0.0.0). 132// Port is passed in host byte order; we byte-swap to network order. 133func net_addr_any(sa: *SockAddrIn, port: i64) -> i64 { 134 // sin_family (2 bytes) = AF_INET = 2 LE, so low byte = 2, next = 0. 135 // sin_port (2 bytes, BE) — swap. 136 let port_be: i64 = ((port & 0xFF) << 8) | ((port >> 8) & 0xFF) 137 // Lay out as LE bytes: [family_lo, family_hi, port_hi, port_lo, ...] 138 // byte0 = 0x02 139 // byte1 = 0x00 140 // byte2 = port_be_hi (= port_lo in host order) 141 // byte3 = port_be_lo (= port_hi in host order) 142 // byte4..7 = 0 (INADDR_ANY) 143 sa.w0 = 0x02 | (port_be << 16) 144 sa.w1 = 0 145 return 0 146} 147 148// Fill sockaddr_in with an explicit IPv4 address. `ip` is four 149// bytes packed LSB first (matches inet_aton output on LE hosts). 150func net_addr(sa: *SockAddrIn, port: i64, ip: i64) -> i64 { 151 let port_be: i64 = ((port & 0xFF) << 8) | ((port >> 8) & 0xFF) 152 // bytes 0-1 family, 2-3 port_be, 4-7 ip 153 sa.w0 = 0x02 | (port_be << 16) | (ip << 32) 154 sa.w1 = 0 155 return 0 156} 157 158// ---- server side --------------------------------------------------- 159 160// Bind an already-opened socket to an address. `sa` points at a 161// SockAddrIn filled via net_addr_any / net_addr. addrlen = 16 for 162// IPv4. Ok payload is 0. 163func net_bind_raw(fd: i64, sa: *SockAddrIn) -> i64 { 164 return __syscall(SYS_BIND, fd, sa as i64, 16, 0, 0, 0) 165} 166 167func net_bind(fd: i64, sa: *SockAddrIn) -> *Result<i64, NetError> { 168 let rc: i64 = net_bind_raw(fd, sa) 169 if rc < 0 { return Result::Err(net_errno_to(rc)) } 170 return Result::Ok(0) 171} 172 173// Mark the socket as passive, ready to accept. backlog capped at 174// the kernel's somaxconn (typically 128-4096). 175func net_listen_raw(fd: i64, backlog: i64) -> i64 { 176 return __syscall(SYS_LISTEN, fd, backlog, 0, 0, 0, 0) 177} 178 179func net_listen(fd: i64, backlog: i64) -> *Result<i64, NetError> { 180 let rc: i64 = net_listen_raw(fd, backlog) 181 if rc < 0 { return Result::Err(net_errno_to(rc)) } 182 return Result::Ok(0) 183} 184 185// Accept a pending connection. Peer address written to `peer` (may 186// be null if caller doesn't care). Ok payload is the new fd. 187// addrlen_slot: caller allocates an i64 initialised to 16; kernel 188// writes back the actual size. 189func net_accept_raw(fd: i64, peer: *SockAddrIn, 190 addrlen_slot: *i64) -> i64 { 191 return __syscall(SYS_ACCEPT, fd, peer as i64, 192 addrlen_slot as i64, 0, 0, 0) 193} 194 195func net_accept(fd: i64, peer: *SockAddrIn, 196 addrlen_slot: *i64) -> *Result<i64, NetError> { 197 let rc: i64 = net_accept_raw(fd, peer, addrlen_slot) 198 if rc < 0 { return Result::Err(net_errno_to(rc)) } 199 return Result::Ok(rc) 200} 201 202// ---- client side --------------------------------------------------- 203 204// Initiate a connection to `sa`. Ok payload is 0. 205func net_connect_raw(fd: i64, sa: *SockAddrIn) -> i64 { 206 return __syscall(SYS_CONNECT, fd, sa as i64, 16, 0, 0, 0) 207} 208 209func net_connect(fd: i64, sa: *SockAddrIn) -> *Result<i64, NetError> { 210 let rc: i64 = net_connect_raw(fd, sa) 211 if rc < 0 { return Result::Err(net_errno_to(rc)) } 212 return Result::Ok(0) 213} 214 215// ---- I/O ----------------------------------------------------------- 216 217// ---- hot-path I/O ------------------------------------------------ 218// 219// send / recv run inside receive loops where a per-call allocation 220// is a real cost. They stay i64-returning; callers inside tight 221// loops check `< 0`, callers outside loops can wrap at the boundary. 222 223// Send bytes on a connected socket. Returns bytes sent or -errno. 224// Does NOT loop on partial send -- caller does. 225func net_send(fd: i64, buf: *u8, n: i64) -> i64 { 226 return __syscall(SYS_SENDTO, fd, buf, n, 0, 0, 0) 227} 228 229// Receive bytes. Returns bytes read (0 = peer closed cleanly) or 230// -errno. 231func net_recv(fd: i64, buf: *u8, n: i64) -> i64 { 232 return __syscall(SYS_RECVFROM, fd, buf, n, 0, 0, 0) 233} 234 235// Loop-until-complete send. Ok payload is total bytes sent. 236func net_send_all_raw(fd: i64, buf: *u8, n: i64) -> i64 { 237 var sent: i64 = 0 238 while sent < n { 239 let tail_addr: i64 = (buf as i64) + sent 240 let tail: *u8 = tail_addr as *u8 241 let got: i64 = net_send(fd, tail, n - sent) 242 if got <= 0 { return got } 243 sent = sent + got 244 } 245 return 0 246} 247 248func net_send_all(fd: i64, buf: *u8, n: i64) -> *Result<i64, NetError> { 249 let rc: i64 = net_send_all_raw(fd, buf, n) 250 if rc < 0 { return Result::Err(net_errno_to(rc)) } 251 return Result::Ok(0) 252} 253 254// ---- socket options ------------------------------------------------ 255 256const SOL_SOCKET: i64 = 1 257const SO_REUSEADDR: i64 = 2 258const SO_REUSEPORT: i64 = 15 259 260// Enable SO_REUSEADDR so restarts don't hit "Address already in use" 261// on the bind call. Common pattern for servers. 262func net_set_reuseaddr(fd: i64) -> i64 { 263 let val_raw: *u8 = sys_mmap(16) 264 let val: *i64 = val_raw as *i64 265 *val = 1 266 return __syscall(SYS_SETSOCKOPT, fd, SOL_SOCKET, SO_REUSEADDR, 267 val as i64, 4, 0) 268}