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}