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;