seq_runtime/tcp.rs
1//! TCP Socket Operations for Seq
2//!
3//! Provides non-blocking TCP socket operations using May's coroutine-aware I/O.
4//! All operations yield the strand instead of blocking the OS thread.
5//!
6//! These functions are exported with C ABI for LLVM codegen.
7
8use crate::stack::{Stack, pop, push};
9use crate::value::Value;
10use may::net::{TcpListener, TcpStream};
11use rustls::{ClientConnection, StreamOwned};
12use std::io::{Read, Write};
13use std::net::{IpAddr, SocketAddr};
14use std::sync::Mutex;
15
16/// What a Socket id actually points at in the STREAMS registry.
17///
18/// `Tcp` is a connected plain stream (the only kind PR1/PR2 produced).
19/// `Tls` is a connected stream that has been upgraded via
20/// `net.tls.client` — every read/write goes through rustls over the
21/// same may-aware TcpStream. Both arms implement `Read + Write`, so
22/// `net.tcp.read` / `net.tcp.write` / `net.tcp.close` dispatch over
23/// either variant without the caller knowing the difference.
24///
25/// The TLS arm is boxed not to shrink the *enum* (size_of TcpStream is
26/// non-trivial and tends to dominate the discriminant size anyway) but
27/// to keep the `StreamOwned<ClientConnection, _>` payload — which
28/// embeds rustls's per-connection record buffers — off the registry
29/// allocation. Without the box, every plain-TCP allocation would have
30/// to find a contiguous chunk large enough for the TLS variant.
31pub(crate) enum StreamKind {
32 Tcp(TcpStream),
33 Tls(Box<StreamOwned<ClientConnection, TcpStream>>),
34}
35
36impl Read for StreamKind {
37 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
38 match self {
39 StreamKind::Tcp(s) => s.read(buf),
40 StreamKind::Tls(s) => s.read(buf),
41 }
42 }
43}
44
45impl Write for StreamKind {
46 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
47 match self {
48 StreamKind::Tcp(s) => s.write(buf),
49 StreamKind::Tls(s) => s.write(buf),
50 }
51 }
52 fn flush(&mut self) -> std::io::Result<()> {
53 match self {
54 StreamKind::Tcp(s) => s.flush(),
55 StreamKind::Tls(s) => s.flush(),
56 }
57 }
58}
59
60// Maximum number of concurrent connections to prevent unbounded growth
61const MAX_SOCKETS: usize = 10_000;
62
63// Architectural cap for any future read-N or full-body socket reader
64// (e.g. an HTTP-client migration off ureq). The current `tcp.read` does
65// one 4 KB read per call and so doesn't need to consult this; it stays
66// here as the ceiling that bounded-buffer variants must respect.
67#[allow(dead_code)]
68const MAX_READ_SIZE: usize = 1_048_576; // 1 MB
69
70// Socket registry with ID reuse via free list
71pub(crate) struct SocketRegistry<T> {
72 sockets: Vec<Option<T>>,
73 free_ids: Vec<usize>,
74}
75
76impl<T> SocketRegistry<T> {
77 pub(crate) const fn new() -> Self {
78 Self {
79 sockets: Vec::new(),
80 free_ids: Vec::new(),
81 }
82 }
83
84 pub(crate) fn allocate(&mut self, socket: T) -> Result<i64, &'static str> {
85 // Try to reuse a free ID first
86 if let Some(id) = self.free_ids.pop() {
87 self.sockets[id] = Some(socket);
88 return Ok(id as i64);
89 }
90
91 // Check max connections limit
92 if self.sockets.len() >= MAX_SOCKETS {
93 return Err("Maximum socket limit reached");
94 }
95
96 // Allocate new ID
97 let id = self.sockets.len();
98 self.sockets.push(Some(socket));
99 Ok(id as i64)
100 }
101
102 pub(crate) fn get_mut(&mut self, id: usize) -> Option<&mut Option<T>> {
103 self.sockets.get_mut(id)
104 }
105
106 pub(crate) fn free(&mut self, id: usize) {
107 if let Some(slot) = self.sockets.get_mut(id)
108 && slot.is_some()
109 {
110 *slot = None;
111 self.free_ids.push(id);
112 }
113 }
114}
115
116// Global registry for TCP listeners and streams. STREAMS holds a
117// StreamKind so that plain-TCP and TLS-wrapped sockets share one id
118// space (and one set of read/write/close builtins).
119static LISTENERS: Mutex<SocketRegistry<TcpListener>> = Mutex::new(SocketRegistry::new());
120pub(crate) static STREAMS: Mutex<SocketRegistry<StreamKind>> = Mutex::new(SocketRegistry::new());
121
122/// Connect to the first reachable address in `addrs` at `port`.
123///
124/// Walks the list in order, returning the first successful
125/// `may::net::TcpStream::connect`. Yields the strand on each
126/// SYN/SYN-ACK round-trip. Returns `None` if every address fails.
127///
128/// Building `SocketAddr` directly from the `IpAddr` avoids the
129/// IPv6 string-formatting trap (`"::1:80"` is not a parseable
130/// SocketAddr — brackets are required in that form).
131///
132/// Exposed to the crate so the HTTP client (which pre-resolves +
133/// SSRF-validates before connecting) can dial without re-resolving.
134pub(crate) fn connect_to_addrs(addrs: &[IpAddr], port: u16) -> Option<TcpStream> {
135 addrs
136 .iter()
137 .find_map(|ip| TcpStream::connect(SocketAddr::new(*ip, port)).ok())
138}
139
140/// TCP listen on a port
141///
142/// Stack effect: ( port -- listener_id Bool )
143///
144/// Binds to 0.0.0.0:port and returns a listener ID with success flag.
145/// Returns (0, false) on failure (invalid port, bind error, socket limit).
146///
147/// # Safety
148/// Stack must have an Int (port number) on top
149#[unsafe(no_mangle)]
150pub unsafe extern "C" fn patch_seq_tcp_listen(stack: Stack) -> Stack {
151 unsafe {
152 let (stack, port_val) = pop(stack);
153 let port = match port_val {
154 Value::Int(p) => p,
155 _ => {
156 // Type error - return failure
157 let stack = push(stack, Value::Int(0));
158 return push(stack, Value::Bool(false));
159 }
160 };
161
162 // Validate port range (1-65535, or 0 for OS-assigned)
163 if !(0..=65535).contains(&port) {
164 let stack = push(stack, Value::Int(0));
165 return push(stack, Value::Bool(false));
166 }
167
168 // Bind to the port (non-blocking via May)
169 let addr = format!("0.0.0.0:{}", port);
170 let listener = match TcpListener::bind(&addr) {
171 Ok(l) => l,
172 Err(_) => {
173 let stack = push(stack, Value::Int(0));
174 return push(stack, Value::Bool(false));
175 }
176 };
177
178 // Store listener and get ID
179 let mut listeners = LISTENERS.lock().unwrap();
180 match listeners.allocate(listener) {
181 Ok(listener_id) => {
182 let stack = push(stack, Value::Int(listener_id));
183 push(stack, Value::Bool(true))
184 }
185 Err(_) => {
186 let stack = push(stack, Value::Int(0));
187 push(stack, Value::Bool(false))
188 }
189 }
190 }
191}
192
193/// TCP connect to a remote endpoint.
194///
195/// Stack effect: ( host:String port:Int -- Socket Bool )
196///
197/// Resolves `host` through the may-aware DNS layer (cache + worker
198/// pool — no `getaddrinfo` ever runs on a may carrier), then tries
199/// each resolved address in order until one connects via
200/// `may::net::TcpStream::connect`. Yields the strand on every step.
201///
202/// Returns `(0, false)` on resolution failure, every-address-failed,
203/// invalid port, or socket-registry exhaustion.
204///
205/// # Safety
206/// Stack must have a String (host) and Int (port) on top — port topmost.
207#[unsafe(no_mangle)]
208pub unsafe extern "C" fn patch_seq_tcp_connect(stack: Stack) -> Stack {
209 unsafe {
210 let (stack, port_val) = pop(stack);
211 let port = match port_val {
212 Value::Int(p) => p,
213 _ => {
214 let stack = push(stack, Value::Int(0));
215 return push(stack, Value::Bool(false));
216 }
217 };
218 if !(1..=65535).contains(&port) {
219 let stack = push(stack, Value::Int(0));
220 return push(stack, Value::Bool(false));
221 }
222
223 let (stack, host_val) = pop(stack);
224 let host = match host_val {
225 Value::String(s) => s,
226 _ => {
227 let stack = push(stack, Value::Int(0));
228 return push(stack, Value::Bool(false));
229 }
230 };
231 let hostname = host.as_str_or_empty();
232 if hostname.is_empty() {
233 let stack = push(stack, Value::Int(0));
234 return push(stack, Value::Bool(false));
235 }
236
237 let addr_strings = crate::dns::resolve(hostname);
238 if addr_strings.is_empty() {
239 let stack = push(stack, Value::Int(0));
240 return push(stack, Value::Bool(false));
241 }
242
243 // Resolver only emits IP-string forms produced by
244 // `SocketAddr::ip().to_string()`, so a parse failure here would
245 // mean a runtime invariant violation — skip the address rather
246 // than panicking the carrier.
247 let addrs: Vec<IpAddr> = addr_strings
248 .iter()
249 .filter_map(|s| s.parse::<IpAddr>().ok())
250 .collect();
251 let stream = match connect_to_addrs(&addrs, port as u16) {
252 Some(s) => s,
253 None => {
254 let stack = push(stack, Value::Int(0));
255 return push(stack, Value::Bool(false));
256 }
257 };
258
259 let mut streams = STREAMS.lock().unwrap();
260 match streams.allocate(StreamKind::Tcp(stream)) {
261 Ok(id) => {
262 let stack = push(stack, Value::Int(id));
263 push(stack, Value::Bool(true))
264 }
265 Err(_) => {
266 let stack = push(stack, Value::Int(0));
267 push(stack, Value::Bool(false))
268 }
269 }
270 }
271}
272
273/// TCP accept a connection
274///
275/// Stack effect: ( listener_id -- client_id Bool )
276///
277/// Accepts a connection (yields the strand until one arrives).
278/// Returns (0, false) on failure (invalid listener, accept error, socket limit).
279///
280/// # Safety
281/// Stack must have an Int (listener_id) on top
282#[unsafe(no_mangle)]
283pub unsafe extern "C" fn patch_seq_tcp_accept(stack: Stack) -> Stack {
284 unsafe {
285 let (stack, listener_id_val) = pop(stack);
286 let listener_id = match listener_id_val {
287 Value::Int(id) => id as usize,
288 _ => {
289 let stack = push(stack, Value::Int(0));
290 return push(stack, Value::Bool(false));
291 }
292 };
293
294 // Take the listener out temporarily (so we don't hold lock during accept)
295 let listener = {
296 let mut listeners = LISTENERS.lock().unwrap();
297 match listeners.get_mut(listener_id).and_then(|opt| opt.take()) {
298 Some(l) => l,
299 None => {
300 let stack = push(stack, Value::Int(0));
301 return push(stack, Value::Bool(false));
302 }
303 }
304 };
305 // Lock released
306
307 // Accept connection (this yields the strand, doesn't block OS thread)
308 let (stream, _addr) = match listener.accept() {
309 Ok(result) => result,
310 Err(_) => {
311 // Put listener back before returning
312 let mut listeners = LISTENERS.lock().unwrap();
313 if let Some(slot) = listeners.get_mut(listener_id) {
314 *slot = Some(listener);
315 }
316 let stack = push(stack, Value::Int(0));
317 return push(stack, Value::Bool(false));
318 }
319 };
320
321 // Put the listener back
322 {
323 let mut listeners = LISTENERS.lock().unwrap();
324 if let Some(slot) = listeners.get_mut(listener_id) {
325 *slot = Some(listener);
326 }
327 }
328
329 // Store stream and get ID
330 let mut streams = STREAMS.lock().unwrap();
331 match streams.allocate(StreamKind::Tcp(stream)) {
332 Ok(client_id) => {
333 let stack = push(stack, Value::Int(client_id));
334 push(stack, Value::Bool(true))
335 }
336 Err(_) => {
337 let stack = push(stack, Value::Int(0));
338 push(stack, Value::Bool(false))
339 }
340 }
341 }
342}
343
344/// TCP read from a socket
345///
346/// Stack effect: ( socket_id -- string Bool )
347///
348/// Reads all available data from the socket.
349/// Returns ("", false) on failure (invalid socket, read error, size limit, invalid UTF-8).
350///
351/// # Safety
352/// Stack must have an Int (socket_id) on top
353#[unsafe(no_mangle)]
354pub unsafe extern "C" fn patch_seq_tcp_read(stack: Stack) -> Stack {
355 unsafe {
356 let (stack, socket_id_val) = pop(stack);
357 let socket_id = match socket_id_val {
358 Value::Int(id) => id as usize,
359 _ => {
360 let stack = push(stack, Value::String("".into()));
361 return push(stack, Value::Bool(false));
362 }
363 };
364
365 // Take the stream out of the registry (so we don't hold the lock during I/O)
366 let mut stream = {
367 let mut streams = STREAMS.lock().unwrap();
368 match streams.get_mut(socket_id).and_then(|opt| opt.take()) {
369 Some(s) => s,
370 None => {
371 let stack = push(stack, Value::String("".into()));
372 return push(stack, Value::Bool(false));
373 }
374 }
375 };
376 // Registry lock is now released
377
378 // One read per call. may::net::TcpStream::read suspends the
379 // strand until at least one byte is available (or the peer
380 // closes), then returns whatever the kernel had ready in one
381 // batch. Returning here lets a caller wait for client data
382 // without our own read holding the socket past the first
383 // payload — request/response framing happens in user code.
384 //
385 // The chunk size caps a single batch at 4 KB, well under
386 // MAX_READ_SIZE, so a size check is unnecessary at the
387 // per-read level.
388 let mut buffer = Vec::new();
389 let mut chunk = [0u8; 4096];
390 let mut read_error = false;
391 match stream.read(&mut chunk) {
392 Ok(0) => {} // EOF — return empty payload, success=true
393 Ok(n) => buffer.extend_from_slice(&chunk[..n]),
394 Err(_) => read_error = true,
395 }
396
397 // Put the stream back
398 {
399 let mut streams = STREAMS.lock().unwrap();
400 if let Some(slot) = streams.get_mut(socket_id) {
401 *slot = Some(stream);
402 }
403 }
404
405 if read_error {
406 let stack = push(stack, Value::String("".into()));
407 return push(stack, Value::Bool(false));
408 }
409
410 // The bytes go into a byte-clean SeqString unchanged — TCP can
411 // now serve binary protocols (HTTP/2 frames, gRPC, raw TLS,
412 // protocol-buffer streams, anything that isn't text). UTF-8 is
413 // a property of the application protocol, not of the transport.
414 let stack = push(stack, Value::String(crate::seqstring::global_bytes(buffer)));
415 push(stack, Value::Bool(true))
416 }
417}
418
419/// TCP write to a socket
420///
421/// Stack effect: ( string socket_id -- Bool )
422///
423/// Writes string to the socket.
424/// Returns false on failure (invalid socket, write error).
425///
426/// # Safety
427/// Stack must have Int (socket_id) and String on top
428#[unsafe(no_mangle)]
429pub unsafe extern "C" fn patch_seq_tcp_write(stack: Stack) -> Stack {
430 unsafe {
431 let (stack, socket_id_val) = pop(stack);
432 let socket_id = match socket_id_val {
433 Value::Int(id) => id as usize,
434 _ => {
435 return push(stack, Value::Bool(false));
436 }
437 };
438
439 let (stack, data_val) = pop(stack);
440 let data = match data_val {
441 Value::String(s) => s,
442 _ => {
443 return push(stack, Value::Bool(false));
444 }
445 };
446
447 // Take the stream out of the registry (so we don't hold the lock during I/O)
448 let mut stream = {
449 let mut streams = STREAMS.lock().unwrap();
450 match streams.get_mut(socket_id).and_then(|opt| opt.take()) {
451 Some(s) => s,
452 None => {
453 return push(stack, Value::Bool(false));
454 }
455 }
456 };
457 // Registry lock is now released
458
459 // Write data (non-blocking via May, yields strand as needed)
460 let write_result = stream.write_all(data.as_bytes());
461 let flush_result = if write_result.is_ok() {
462 stream.flush()
463 } else {
464 write_result
465 };
466
467 // Put the stream back
468 {
469 let mut streams = STREAMS.lock().unwrap();
470 if let Some(slot) = streams.get_mut(socket_id) {
471 *slot = Some(stream);
472 }
473 }
474
475 push(stack, Value::Bool(flush_result.is_ok()))
476 }
477}
478
479/// TCP close a socket
480///
481/// Stack effect: ( socket_id -- Bool )
482///
483/// Closes the socket connection and frees the socket ID for reuse.
484/// Returns true on success, false if socket_id was invalid.
485///
486/// # Safety
487/// Stack must have an Int (socket_id) on top
488#[unsafe(no_mangle)]
489pub unsafe extern "C" fn patch_seq_tcp_close(stack: Stack) -> Stack {
490 unsafe {
491 let (stack, socket_id_val) = pop(stack);
492 let socket_id = match socket_id_val {
493 Value::Int(id) => id as usize,
494 _ => {
495 return push(stack, Value::Bool(false));
496 }
497 };
498
499 // A user-visible `Socket` unifies listeners and connected
500 // streams, so close has to look in both registries. Streams
501 // are checked first because they're far more common at
502 // shutdown time. Ids are not globally unique across the two
503 // registries (each starts at 0); for finite servers that
504 // close exactly one of each that's fine, but multi-socket
505 // shutdowns with id-aliasing across registries remain an
506 // open design wart.
507 {
508 let mut streams = STREAMS.lock().unwrap();
509 if streams
510 .get_mut(socket_id)
511 .is_some_and(|slot| slot.is_some())
512 {
513 streams.free(socket_id);
514 return push(stack, Value::Bool(true));
515 }
516 }
517 {
518 let mut listeners = LISTENERS.lock().unwrap();
519 if listeners
520 .get_mut(socket_id)
521 .is_some_and(|slot| slot.is_some())
522 {
523 listeners.free(socket_id);
524 return push(stack, Value::Bool(true));
525 }
526 }
527 push(stack, Value::Bool(false))
528 }
529}
530
531// Public re-exports with short names for internal use
532pub use patch_seq_tcp_accept as tcp_accept;
533pub use patch_seq_tcp_close as tcp_close;
534pub use patch_seq_tcp_connect as tcp_connect;
535pub use patch_seq_tcp_listen as tcp_listen;
536pub use patch_seq_tcp_read as tcp_read;
537pub use patch_seq_tcp_write as tcp_write;
538
539/// Cast between Socket and Int (both directions): identity at runtime.
540///
541/// Socket is a compile-time-only nominal wrapper over the same i64 file
542/// descriptor; the type checker enforces the distinction. This shim exists
543/// so codegen can emit a callable symbol for `fd->socket` / `socket->fd`
544/// without inventing a new value tag.
545///
546/// # Safety
547/// Stack must have an Int (or Socket-shaped Int) value on top.
548#[unsafe(no_mangle)]
549pub unsafe extern "C" fn patch_seq_socket_cast(stack: Stack) -> Stack {
550 assert!(!stack.is_null(), "fd<->socket cast: stack is empty");
551 let (rest, val) = unsafe { pop(stack) };
552 match val {
553 Value::Int(fd) => unsafe { push(rest, Value::Int(fd)) },
554 _ => panic!("fd<->socket cast: expected Int on stack"),
555 }
556}
557
558#[cfg(test)]
559mod tests;