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.
31enum 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. Private — the only
71// cross-module operation is `upgrade_tcp_in_place`, which hides the
72// registry behind a callback.
73struct SocketRegistry<T> {
74    sockets: Vec<Option<T>>,
75    free_ids: Vec<usize>,
76}
77
78impl<T> SocketRegistry<T> {
79    const fn new() -> Self {
80        Self {
81            sockets: Vec::new(),
82            free_ids: Vec::new(),
83        }
84    }
85
86    fn allocate(&mut self, socket: T) -> Result<i64, &'static str> {
87        // Try to reuse a free ID first
88        if let Some(id) = self.free_ids.pop() {
89            self.sockets[id] = Some(socket);
90            return Ok(id as i64);
91        }
92
93        // Check max connections limit
94        if self.sockets.len() >= MAX_SOCKETS {
95            return Err("Maximum socket limit reached");
96        }
97
98        // Allocate new ID
99        let id = self.sockets.len();
100        self.sockets.push(Some(socket));
101        Ok(id as i64)
102    }
103
104    fn get_mut(&mut self, id: usize) -> Option<&mut Option<T>> {
105        self.sockets.get_mut(id)
106    }
107
108    fn free(&mut self, id: usize) {
109        if let Some(slot) = self.sockets.get_mut(id)
110            && slot.is_some()
111        {
112            *slot = None;
113            self.free_ids.push(id);
114        }
115    }
116
117    /// Release an id that was reserved by `take_tcp` but whose
118    /// caller never reinstalled a value into the slot.
119    ///
120    /// Symmetric pair to `take_tcp` for the failure path: after a
121    /// successful take the slot's inner Option is `None`, which
122    /// makes plain `free()` a silent no-op (its `slot.is_some()`
123    /// guard treats reserved-None the same as already-freed). This
124    /// method bypasses that guard to push the id back onto the
125    /// free list so subsequent allocations can reuse it.
126    ///
127    /// The `contains` check protects against double-release should
128    /// a buggy caller invoke this twice or against an already-freed
129    /// id. The free list is normally short, so the linear scan is
130    /// cheap.
131    ///
132    /// Only legitimate caller: `upgrade_tcp_in_place`'s Err branch.
133    fn release_reserved(&mut self, id: usize) {
134        let is_reserved = self
135            .sockets
136            .get(id)
137            .map(|slot| slot.is_none())
138            .unwrap_or(false);
139        if is_reserved && !self.free_ids.contains(&id) {
140            self.free_ids.push(id);
141        }
142    }
143}
144
145// Global registry for TCP listeners and streams. STREAMS holds a
146// StreamKind so that plain-TCP and TLS-wrapped sockets share one id
147// space (and one set of read/write/close builtins).
148//
149// The "free + allocate is dangerous across a strand yield" invariant
150// lives entirely inside this module — `tls::upgrade_tcp_to_tls` (the
151// only cross-module reason to touch STREAMS) goes through
152// `upgrade_tcp_in_place`, which holds the slot reserved (Some(None))
153// across the caller's yield-able TLS handshake so no concurrent
154// strand can grab the id.
155static LISTENERS: Mutex<SocketRegistry<TcpListener>> = Mutex::new(SocketRegistry::new());
156static STREAMS: Mutex<SocketRegistry<StreamKind>> = Mutex::new(SocketRegistry::new());
157
158/// Take the underlying `TcpStream` out of `STREAMS[id]`, only if the
159/// slot holds a `Tcp` variant. A `Tls` variant or empty slot
160/// short-circuits to `None`; a wrong-kind variant is restored so a
161/// double-upgrade caller doesn't accidentally destroy the connection.
162///
163/// On `Some` return, the slot at `id` is left holding `None` —
164/// reserved for the caller. The caller MUST either reinstall a value
165/// into that slot or release the id via `free_stream`. The recommended
166/// way to do this safely across a strand-yielding operation (e.g. a
167/// TLS handshake) is `upgrade_tcp_in_place`, which handles the
168/// reinstall/free for you.
169fn take_tcp(id: usize) -> Option<may::net::TcpStream> {
170    let mut streams = STREAMS.lock().unwrap();
171    let slot = streams.get_mut(id)?;
172    match slot.take() {
173        Some(StreamKind::Tcp(t)) => Some(t),
174        Some(other) => {
175            *slot = Some(other);
176            None
177        }
178        None => None,
179    }
180}
181
182/// Release an id that `take_tcp` reserved but no caller reinstalled.
183/// Bypasses the `slot.is_some()` guard in plain `free()` (which
184/// would silently no-op against a reserved-None slot, leaking the
185/// id for the lifetime of the process).
186fn release_reserved_stream(id: usize) {
187    STREAMS.lock().unwrap().release_reserved(id);
188}
189
190/// In-place upgrade of a TCP socket to its TLS-wrapped form.
191///
192/// The flow:
193/// 1. Take the underlying `TcpStream` out of `STREAMS[id]` via
194///    `take_tcp`. The slot is now reserved (Some(None)) — concurrent
195///    strands can't allocate this id while the caller runs `f`.
196/// 2. Hand the stream to `f` (which is allowed to yield the strand —
197///    typically the TLS handshake).
198/// 3. On success, wrap the returned StreamOwned in `StreamKind::Tls`
199///    and reinstall into the same slot — the Socket id is preserved.
200/// 4. On failure, drop the (already-consumed) `TcpStream` and release
201///    the id back to the free list.
202///
203/// Returns `true` iff the slot was found, the upgrade succeeded, and
204/// the reinstall completed. The Socket id is unchanged on success.
205///
206/// Crate-internal entry point for `tls::patch_seq_tls_client`. Keeps
207/// the "reserve across yield" invariant inside this module.
208pub(crate) fn upgrade_tcp_in_place<F>(id: usize, f: F) -> bool
209where
210    F: FnOnce(
211        may::net::TcpStream,
212    )
213        -> Result<rustls::StreamOwned<rustls::ClientConnection, may::net::TcpStream>, ()>,
214{
215    let tcp = match take_tcp(id) {
216        Some(t) => t,
217        None => return false,
218    };
219    let stream = match f(tcp) {
220        Ok(s) => s,
221        Err(()) => {
222            // `f` consumed (and dropped) the TcpStream — socket
223            // closed. Release the reserved id back to the free list.
224            // Plain `free()` here would silently no-op because
225            // `take_tcp` already nulled the slot's inner Option;
226            // `release_reserved` bypasses that guard.
227            release_reserved_stream(id);
228            return false;
229        }
230    };
231    let mut streams = STREAMS.lock().unwrap();
232    match streams.get_mut(id) {
233        Some(slot) => {
234            *slot = Some(StreamKind::Tls(Box::new(stream)));
235            true
236        }
237        // Currently impossible with the append-only registry; future
238        // eviction would surface here as a clean false rather than a
239        // panic.
240        None => false,
241    }
242}
243
244/// Per-connect timeout in milliseconds. Default 10 000ms.
245///
246/// Bounds the kernel SYN timeout against silent peers. Read once via
247/// `LazyLock`; override per-process with `SEQ_TCP_CONNECT_TIMEOUT_MS`.
248/// Zero or a missing value falls back to the default — disabling
249/// the timeout would re-introduce the original 60–130s hazard.
250const DEFAULT_TCP_CONNECT_TIMEOUT_MS: u64 = 10_000;
251
252static TCP_CONNECT_TIMEOUT: std::sync::LazyLock<std::time::Duration> =
253    std::sync::LazyLock::new(|| {
254        let ms = std::env::var("SEQ_TCP_CONNECT_TIMEOUT_MS")
255            .ok()
256            .and_then(|v| v.parse::<u64>().ok())
257            .filter(|n| *n > 0)
258            .unwrap_or(DEFAULT_TCP_CONNECT_TIMEOUT_MS);
259        std::time::Duration::from_millis(ms)
260    });
261
262/// Test-only override for `TCP_CONNECT_TIMEOUT`. When `Some`, takes
263/// precedence over the LazyLock-cached value (which can't be reset
264/// once initialised). Mirrors the TLS-handshake and HTTP-request
265/// override hooks so the connect-timeout integration test can drive a
266/// deterministic short deadline without depending on env-var read order.
267#[cfg(test)]
268static TCP_CONNECT_TIMEOUT_OVERRIDE: Mutex<Option<std::time::Duration>> = Mutex::new(None);
269
270#[cfg(test)]
271pub(crate) fn set_test_tcp_connect_timeout(dur: Option<std::time::Duration>) {
272    *TCP_CONNECT_TIMEOUT_OVERRIDE.lock().unwrap() = dur;
273}
274
275fn tcp_connect_timeout() -> std::time::Duration {
276    #[cfg(test)]
277    if let Some(dur) = *TCP_CONNECT_TIMEOUT_OVERRIDE.lock().unwrap() {
278        return dur;
279    }
280    *TCP_CONNECT_TIMEOUT
281}
282
283/// Connect to the first reachable address in `addrs` at `port`.
284///
285/// Walks the list in order, returning the first successful
286/// `may::net::TcpStream::connect_timeout`. Yields the strand on each
287/// SYN/SYN-ACK round-trip. Returns `None` if every address fails or
288/// every connect attempt exceeds the configured timeout.
289///
290/// Building `SocketAddr` directly from the `IpAddr` avoids the
291/// IPv6 string-formatting trap (`"::1:80"` is not a parseable
292/// SocketAddr — brackets are required in that form).
293///
294/// Each individual connect attempt is bounded by `TCP_CONNECT_TIMEOUT`
295/// (default 10s, overridable via `SEQ_TCP_CONNECT_TIMEOUT_MS`). A
296/// peer that silently drops SYNs surfaces as `None` in seconds, not
297/// minutes.
298///
299/// Exposed to the crate so the HTTP client (which pre-resolves +
300/// SSRF-validates before connecting) can dial without re-resolving.
301pub(crate) fn connect_to_addrs(addrs: &[IpAddr], port: u16) -> Option<TcpStream> {
302    let timeout = tcp_connect_timeout();
303    addrs
304        .iter()
305        .find_map(|ip| TcpStream::connect_timeout(&SocketAddr::new(*ip, port), timeout).ok())
306}
307
308/// TCP listen on a port
309///
310/// Stack effect: ( port -- listener_id Bool )
311///
312/// Binds to 0.0.0.0:port and returns a listener ID with success flag.
313/// Returns (0, false) on failure (invalid port, bind error, socket limit).
314///
315/// # Safety
316/// Stack must have an Int (port number) on top
317#[unsafe(no_mangle)]
318pub unsafe extern "C" fn patch_seq_tcp_listen(stack: Stack) -> Stack {
319    unsafe {
320        let (stack, port_val) = pop(stack);
321        let port = match port_val {
322            Value::Int(p) => p,
323            _ => {
324                // Type error - return failure
325                let stack = push(stack, Value::Int(0));
326                return push(stack, Value::Bool(false));
327            }
328        };
329
330        // Validate port range (1-65535, or 0 for OS-assigned)
331        if !(0..=65535).contains(&port) {
332            let stack = push(stack, Value::Int(0));
333            return push(stack, Value::Bool(false));
334        }
335
336        // Bind to the port (non-blocking via May)
337        let addr = format!("0.0.0.0:{}", port);
338        let listener = match TcpListener::bind(&addr) {
339            Ok(l) => l,
340            Err(_) => {
341                let stack = push(stack, Value::Int(0));
342                return push(stack, Value::Bool(false));
343            }
344        };
345
346        // Store listener and get ID
347        let mut listeners = LISTENERS.lock().unwrap();
348        match listeners.allocate(listener) {
349            Ok(listener_id) => {
350                let stack = push(stack, Value::Int(listener_id));
351                push(stack, Value::Bool(true))
352            }
353            Err(_) => {
354                let stack = push(stack, Value::Int(0));
355                push(stack, Value::Bool(false))
356            }
357        }
358    }
359}
360
361/// TCP connect to a remote endpoint.
362///
363/// Stack effect: ( host:String port:Int -- Socket Bool )
364///
365/// Resolves `host` through the may-aware DNS layer (cache + worker
366/// pool — no `getaddrinfo` ever runs on a may carrier), then tries
367/// each resolved address in order until one connects via
368/// `may::net::TcpStream::connect`. Yields the strand on every step.
369///
370/// Returns `(0, false)` on resolution failure, every-address-failed,
371/// invalid port, or socket-registry exhaustion.
372///
373/// # Safety
374/// Stack must have a String (host) and Int (port) on top — port topmost.
375#[unsafe(no_mangle)]
376pub unsafe extern "C" fn patch_seq_tcp_connect(stack: Stack) -> Stack {
377    unsafe {
378        let (stack, port_val) = pop(stack);
379        let port = match port_val {
380            Value::Int(p) => p,
381            _ => {
382                let stack = push(stack, Value::Int(0));
383                return push(stack, Value::Bool(false));
384            }
385        };
386        if !(1..=65535).contains(&port) {
387            let stack = push(stack, Value::Int(0));
388            return push(stack, Value::Bool(false));
389        }
390
391        let (stack, host_val) = pop(stack);
392        let host = match host_val {
393            Value::String(s) => s,
394            _ => {
395                let stack = push(stack, Value::Int(0));
396                return push(stack, Value::Bool(false));
397            }
398        };
399        let hostname = host.as_str_or_empty();
400        if hostname.is_empty() {
401            let stack = push(stack, Value::Int(0));
402            return push(stack, Value::Bool(false));
403        }
404
405        let addrs = crate::dns::resolve_to_ips(hostname);
406        if addrs.is_empty() {
407            let stack = push(stack, Value::Int(0));
408            return push(stack, Value::Bool(false));
409        }
410        let stream = match connect_to_addrs(&addrs, port as u16) {
411            Some(s) => s,
412            None => {
413                let stack = push(stack, Value::Int(0));
414                return push(stack, Value::Bool(false));
415            }
416        };
417
418        let mut streams = STREAMS.lock().unwrap();
419        match streams.allocate(StreamKind::Tcp(stream)) {
420            Ok(id) => {
421                let stack = push(stack, Value::Int(id));
422                push(stack, Value::Bool(true))
423            }
424            Err(_) => {
425                let stack = push(stack, Value::Int(0));
426                push(stack, Value::Bool(false))
427            }
428        }
429    }
430}
431
432/// TCP accept a connection
433///
434/// Stack effect: ( listener_id -- client_id Bool )
435///
436/// Accepts a connection (yields the strand until one arrives).
437/// Returns (0, false) on failure (invalid listener, accept error, socket limit).
438///
439/// # Safety
440/// Stack must have an Int (listener_id) on top
441#[unsafe(no_mangle)]
442pub unsafe extern "C" fn patch_seq_tcp_accept(stack: Stack) -> Stack {
443    unsafe {
444        let (stack, listener_id_val) = pop(stack);
445        let listener_id = match listener_id_val {
446            Value::Int(id) => id as usize,
447            _ => {
448                let stack = push(stack, Value::Int(0));
449                return push(stack, Value::Bool(false));
450            }
451        };
452
453        // Take the listener out temporarily (so we don't hold lock during accept)
454        let listener = {
455            let mut listeners = LISTENERS.lock().unwrap();
456            match listeners.get_mut(listener_id).and_then(|opt| opt.take()) {
457                Some(l) => l,
458                None => {
459                    let stack = push(stack, Value::Int(0));
460                    return push(stack, Value::Bool(false));
461                }
462            }
463        };
464        // Lock released
465
466        // Accept connection (this yields the strand, doesn't block OS thread)
467        let (stream, _addr) = match listener.accept() {
468            Ok(result) => result,
469            Err(_) => {
470                // Put listener back before returning
471                let mut listeners = LISTENERS.lock().unwrap();
472                if let Some(slot) = listeners.get_mut(listener_id) {
473                    *slot = Some(listener);
474                }
475                let stack = push(stack, Value::Int(0));
476                return push(stack, Value::Bool(false));
477            }
478        };
479
480        // Put the listener back
481        {
482            let mut listeners = LISTENERS.lock().unwrap();
483            if let Some(slot) = listeners.get_mut(listener_id) {
484                *slot = Some(listener);
485            }
486        }
487
488        // Store stream and get ID
489        let mut streams = STREAMS.lock().unwrap();
490        match streams.allocate(StreamKind::Tcp(stream)) {
491            Ok(client_id) => {
492                let stack = push(stack, Value::Int(client_id));
493                push(stack, Value::Bool(true))
494            }
495            Err(_) => {
496                let stack = push(stack, Value::Int(0));
497                push(stack, Value::Bool(false))
498            }
499        }
500    }
501}
502
503/// TCP read from a socket
504///
505/// Stack effect: ( socket_id -- string Bool )
506///
507/// Reads all available data from the socket.
508/// Returns ("", false) on failure (invalid socket, read error, size limit, invalid UTF-8).
509///
510/// # Safety
511/// Stack must have an Int (socket_id) on top
512#[unsafe(no_mangle)]
513pub unsafe extern "C" fn patch_seq_tcp_read(stack: Stack) -> Stack {
514    unsafe {
515        let (stack, socket_id_val) = pop(stack);
516        let socket_id = match socket_id_val {
517            Value::Int(id) => id as usize,
518            _ => {
519                let stack = push(stack, Value::String("".into()));
520                return push(stack, Value::Bool(false));
521            }
522        };
523
524        // Take the stream out of the registry (so we don't hold the lock during I/O)
525        let mut stream = {
526            let mut streams = STREAMS.lock().unwrap();
527            match streams.get_mut(socket_id).and_then(|opt| opt.take()) {
528                Some(s) => s,
529                None => {
530                    let stack = push(stack, Value::String("".into()));
531                    return push(stack, Value::Bool(false));
532                }
533            }
534        };
535        // Registry lock is now released
536
537        // One read per call. may::net::TcpStream::read suspends the
538        // strand until at least one byte is available (or the peer
539        // closes), then returns whatever the kernel had ready in one
540        // batch. Returning here lets a caller wait for client data
541        // without our own read holding the socket past the first
542        // payload — request/response framing happens in user code.
543        //
544        // The chunk size caps a single batch at 4 KB, well under
545        // MAX_READ_SIZE, so a size check is unnecessary at the
546        // per-read level.
547        let mut buffer = Vec::new();
548        let mut chunk = [0u8; 4096];
549        let mut read_error = false;
550        match stream.read(&mut chunk) {
551            Ok(0) => {} // EOF — return empty payload, success=true
552            Ok(n) => buffer.extend_from_slice(&chunk[..n]),
553            Err(_) => read_error = true,
554        }
555
556        // Put the stream back
557        {
558            let mut streams = STREAMS.lock().unwrap();
559            if let Some(slot) = streams.get_mut(socket_id) {
560                *slot = Some(stream);
561            }
562        }
563
564        if read_error {
565            let stack = push(stack, Value::String("".into()));
566            return push(stack, Value::Bool(false));
567        }
568
569        // The bytes go into a byte-clean SeqString unchanged — TCP can
570        // now serve binary protocols (HTTP/2 frames, gRPC, raw TLS,
571        // protocol-buffer streams, anything that isn't text). UTF-8 is
572        // a property of the application protocol, not of the transport.
573        let stack = push(stack, Value::String(crate::seqstring::global_bytes(buffer)));
574        push(stack, Value::Bool(true))
575    }
576}
577
578/// TCP write to a socket
579///
580/// Stack effect: ( string socket_id -- Bool )
581///
582/// Writes string to the socket.
583/// Returns false on failure (invalid socket, write error).
584///
585/// # Safety
586/// Stack must have Int (socket_id) and String on top
587#[unsafe(no_mangle)]
588pub unsafe extern "C" fn patch_seq_tcp_write(stack: Stack) -> Stack {
589    unsafe {
590        let (stack, socket_id_val) = pop(stack);
591        let socket_id = match socket_id_val {
592            Value::Int(id) => id as usize,
593            _ => {
594                return push(stack, Value::Bool(false));
595            }
596        };
597
598        let (stack, data_val) = pop(stack);
599        let data = match data_val {
600            Value::String(s) => s,
601            _ => {
602                return push(stack, Value::Bool(false));
603            }
604        };
605
606        // Take the stream out of the registry (so we don't hold the lock during I/O)
607        let mut stream = {
608            let mut streams = STREAMS.lock().unwrap();
609            match streams.get_mut(socket_id).and_then(|opt| opt.take()) {
610                Some(s) => s,
611                None => {
612                    return push(stack, Value::Bool(false));
613                }
614            }
615        };
616        // Registry lock is now released
617
618        // Write data (non-blocking via May, yields strand as needed)
619        let write_result = stream.write_all(data.as_bytes());
620        let flush_result = if write_result.is_ok() {
621            stream.flush()
622        } else {
623            write_result
624        };
625
626        // Put the stream back
627        {
628            let mut streams = STREAMS.lock().unwrap();
629            if let Some(slot) = streams.get_mut(socket_id) {
630                *slot = Some(stream);
631            }
632        }
633
634        push(stack, Value::Bool(flush_result.is_ok()))
635    }
636}
637
638/// TCP close a socket
639///
640/// Stack effect: ( socket_id -- Bool )
641///
642/// Closes the socket connection and frees the socket ID for reuse.
643/// Returns true on success, false if socket_id was invalid.
644///
645/// # Safety
646/// Stack must have an Int (socket_id) on top
647#[unsafe(no_mangle)]
648pub unsafe extern "C" fn patch_seq_tcp_close(stack: Stack) -> Stack {
649    unsafe {
650        let (stack, socket_id_val) = pop(stack);
651        let socket_id = match socket_id_val {
652            Value::Int(id) => id as usize,
653            _ => {
654                return push(stack, Value::Bool(false));
655            }
656        };
657
658        // A user-visible `Socket` unifies listeners and connected
659        // streams, so close has to look in both registries. Streams
660        // are checked first because they're far more common at
661        // shutdown time. Ids are not globally unique across the two
662        // registries (each starts at 0); for finite servers that
663        // close exactly one of each that's fine, but multi-socket
664        // shutdowns with id-aliasing across registries remain an
665        // open design wart.
666        {
667            let mut streams = STREAMS.lock().unwrap();
668            if streams
669                .get_mut(socket_id)
670                .is_some_and(|slot| slot.is_some())
671            {
672                streams.free(socket_id);
673                return push(stack, Value::Bool(true));
674            }
675        }
676        {
677            let mut listeners = LISTENERS.lock().unwrap();
678            if listeners
679                .get_mut(socket_id)
680                .is_some_and(|slot| slot.is_some())
681            {
682                listeners.free(socket_id);
683                return push(stack, Value::Bool(true));
684            }
685        }
686        push(stack, Value::Bool(false))
687    }
688}
689
690// Public re-exports with short names for internal use
691pub use patch_seq_tcp_accept as tcp_accept;
692pub use patch_seq_tcp_close as tcp_close;
693pub use patch_seq_tcp_connect as tcp_connect;
694pub use patch_seq_tcp_listen as tcp_listen;
695pub use patch_seq_tcp_local_port as tcp_local_port;
696pub use patch_seq_tcp_read as tcp_read;
697pub use patch_seq_tcp_write as tcp_write;
698
699/// Get the local port a Socket is bound to.
700///
701/// Stack effect: `( Socket -- Int Bool )`
702///
703/// Works on both listeners (returns the port from `net.tcp.listen`,
704/// useful when `0` was passed to let the OS pick) and connected
705/// streams (returns the ephemeral local port the kernel chose for
706/// the connection). For TLS-wrapped sockets, returns the underlying
707/// TCP local port.
708///
709/// Returns `(0, false)` when the socket id is invalid or the OS
710/// can't report the local address.
711///
712/// # Safety
713/// Stack must have an Int (socket_id) on top.
714#[unsafe(no_mangle)]
715pub unsafe extern "C" fn patch_seq_tcp_local_port(stack: Stack) -> Stack {
716    unsafe {
717        let (stack, socket_id_val) = pop(stack);
718        let socket_id = match socket_id_val {
719            Value::Int(id) => id as usize,
720            _ => {
721                let stack = push(stack, Value::Int(0));
722                return push(stack, Value::Bool(false));
723            }
724        };
725
726        // Streams first, then listeners — same dispatch order as close.
727        let port: Option<u16> = {
728            let mut streams = STREAMS.lock().unwrap();
729            streams
730                .get_mut(socket_id)
731                .and_then(|slot| slot.as_ref())
732                .and_then(|sk| match sk {
733                    StreamKind::Tcp(s) => s.local_addr().ok().map(|a| a.port()),
734                    StreamKind::Tls(s) => s.sock.local_addr().ok().map(|a| a.port()),
735                })
736        };
737        if let Some(port) = port {
738            let stack = push(stack, Value::Int(port as i64));
739            return push(stack, Value::Bool(true));
740        }
741        let port: Option<u16> = {
742            let mut listeners = LISTENERS.lock().unwrap();
743            listeners
744                .get_mut(socket_id)
745                .and_then(|slot| slot.as_ref())
746                .and_then(|l| l.local_addr().ok())
747                .map(|a| a.port())
748        };
749        if let Some(port) = port {
750            let stack = push(stack, Value::Int(port as i64));
751            return push(stack, Value::Bool(true));
752        }
753
754        let stack = push(stack, Value::Int(0));
755        push(stack, Value::Bool(false))
756    }
757}
758
759/// Cast between Socket and Int (both directions): identity at runtime.
760///
761/// Socket is a compile-time-only nominal wrapper over the same i64 file
762/// descriptor; the type checker enforces the distinction. This shim exists
763/// so codegen can emit a callable symbol for `fd->socket` / `socket->fd`
764/// without inventing a new value tag.
765///
766/// # Safety
767/// Stack must have an Int (or Socket-shaped Int) value on top.
768#[unsafe(no_mangle)]
769pub unsafe extern "C" fn patch_seq_socket_cast(stack: Stack) -> Stack {
770    assert!(!stack.is_null(), "fd<->socket cast: stack is empty");
771    let (rest, val) = unsafe { pop(stack) };
772    match val {
773        Value::Int(fd) => unsafe { push(rest, Value::Int(fd)) },
774        _ => panic!("fd<->socket cast: expected Int on stack"),
775    }
776}
777
778#[cfg(test)]
779mod tests;