code wiki / (root) / net.nx

net.nx source

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