Skip to main content

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;