Skip to main content

nodejs/stdlib/
dgram.rs

1//! Node `dgram` module: real UDP sockets over `std::net::UdpSocket`.
2//!
3//! Threading model mirrors `net` (see `host::run_event_loop`): each bound socket
4//! runs a `recv_from` loop on its own thread. That thread NEVER touches the JS
5//! heap — it only moves raw datagram bytes and posts `IoTask` closures onto the
6//! host channel. Every JS-visible effect (emitting `message`/`listening`/`close`,
7//! running callbacks) happens on the main thread when the event loop runs the
8//! posted closure. The `UdpSocket` is shared with the recv thread through an
9//! `Arc` (both `recv_from` and `send_to` take `&self`). All main-thread records
10//! live in a `thread_local`, so they need no locking of their own.
11
12use crate::host::{invoke, with_host, JsObj};
13use fusevm::Value;
14use indexmap::IndexMap;
15use std::collections::HashMap;
16use std::net::UdpSocket;
17use std::sync::atomic::{AtomicBool, Ordering};
18use std::sync::Arc;
19use std::time::Duration;
20
21/// `dgram` module functions routed through `stdlib::call`.
22pub const MODULE_METHODS: &[&str] = &["createSocket"];
23
24/// The `dgram.Socket` `@@native` tag.
25pub const SOCKET_TAG: &str = "UdpSocket";
26
27/// Instance methods on a `dgram.Socket` (for `instance_has_method` / dispatch).
28pub const SOCKET_METHODS: &[&str] = &[
29    "bind",
30    "send",
31    "close",
32    "address",
33    "setBroadcast",
34    "setTTL",
35    "setMulticastTTL",
36    "setMulticastLoopback",
37    "setMulticastInterface",
38    "addMembership",
39    "dropMembership",
40    "addSourceSpecificMembership",
41    "dropSourceSpecificMembership",
42    "setRecvBufferSize",
43    "setSendBufferSize",
44    "getRecvBufferSize",
45    "getSendBufferSize",
46    "connect",
47    "disconnect",
48    "remoteAddress",
49    "ref",
50    "unref",
51];
52
53/// How long a `recv_from` blocks before the loop re-checks its stop flag.
54const POLL: Duration = Duration::from_millis(200);
55
56/// Main-thread record for a bound socket.
57struct UdpRec {
58    /// The JS socket object (a native emitter).
59    emitter: Value,
60    /// The live UDP socket, shared with the recv thread.
61    socket: Arc<UdpSocket>,
62    /// Set by `close` to stop the `recv_from` loop.
63    stop: Arc<AtomicBool>,
64}
65
66#[derive(Default)]
67struct DgramState {
68    next_id: u64,
69    sockets: HashMap<u64, UdpRec>,
70}
71
72thread_local! {
73    static DGRAM: std::cell::RefCell<DgramState> = std::cell::RefCell::new(DgramState::default());
74}
75
76fn next_id() -> u64 {
77    DGRAM.with(|s| {
78        let mut s = s.borrow_mut();
79        s.next_id += 1;
80        s.next_id
81    })
82}
83
84// ── object helpers ────────────────────────────────────────────────────────────
85
86fn get_prop(recv: &Value, key: &str) -> Option<Value> {
87    with_host(|h| match h.get(recv) {
88        Some(JsObj::Object(p)) => p.get(key).cloned(),
89        _ => None,
90    })
91}
92
93fn set_prop(recv: &Value, key: &str, val: Value) {
94    with_host(|h| {
95        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
96            p.insert(key.to_string(), val);
97        }
98    });
99}
100
101fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
102    get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
103}
104
105fn is_udp6(recv: &Value) -> bool {
106    get_prop(recv, "@@udptype")
107        .map(|v| with_host(|h| h.str_of(&v)))
108        .as_deref()
109        == Some("udp6")
110}
111
112/// The default bind/target host for this socket's address family.
113fn default_bind_host(recv: &Value) -> &'static str {
114    if is_udp6(recv) {
115        "::"
116    } else {
117        "0.0.0.0"
118    }
119}
120
121fn default_send_host(recv: &Value) -> &'static str {
122    if is_udp6(recv) {
123        "::1"
124    } else {
125        "127.0.0.1"
126    }
127}
128
129fn is_num(v: &Value) -> bool {
130    matches!(v, Value::Float(_) | Value::Int(_))
131}
132
133fn is_str(v: &Value) -> bool {
134    matches!(v, Value::Str(_)) || with_host(|h| matches!(h.get(v), Some(JsObj::Str(_))))
135}
136
137/// Raw bytes of a `send` message argument: a Buffer's `@@bytes`, else a string's
138/// UTF-8 (mirrors `net::value_bytes`).
139fn value_bytes(v: &Value) -> Vec<u8> {
140    let is_buffer =
141        with_host(|h| matches!(h.get(v), Some(JsObj::Object(p)) if p.contains_key("@@bytes")));
142    if is_buffer {
143        return with_host(|h| match h.get(v) {
144            Some(JsObj::Object(p)) => match p.get("@@bytes").and_then(|b| h.get(b)) {
145                Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
146                _ => Vec::new(),
147            },
148            _ => Vec::new(),
149        });
150    }
151    with_host(|h| h.str_of(v)).into_bytes()
152}
153
154/// Delegate the EventEmitter methods to `events`; `None` for a non-emitter method.
155fn emitter_dispatch(recv: &Value, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
156    match method {
157        "on"
158        | "addListener"
159        | "prependListener"
160        | "once"
161        | "prependOnceListener"
162        | "emit"
163        | "removeListener"
164        | "off"
165        | "removeAllListeners"
166        | "listeners"
167        | "listenerCount"
168        | "eventNames"
169        | "setMaxListeners"
170        | "getMaxListeners" => Some(super::events::instance_call(recv, method, args.to_vec())),
171        _ => None,
172    }
173}
174
175// ── module: dgram.createSocket ────────────────────────────────────────────────
176
177/// `stdlib::call` entry for `dgram.<method>`.
178pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
179    match method {
180        "createSocket" => Some(Ok(create_socket(args))),
181        _ => None,
182    }
183}
184
185/// `dgram.createSocket(type[, callback])` or `dgram.createSocket(options[, callback])`.
186/// `type` is `'udp4'`/`'udp6'`; an options object reads its `.type`. A `callback`
187/// is registered as a one-time-per-`emit` `'message'` listener (Node semantics).
188pub fn create_socket(args: &[Value]) -> Value {
189    let first = args.first().cloned().unwrap_or(Value::Undef);
190    let sock_type = if is_str(&first) {
191        with_host(|h| h.str_of(&first))
192    } else {
193        // options object: read `.type`.
194        with_host(|h| match h.get(&first) {
195            Some(JsObj::Object(p)) => p.get("type").map(|v| h.str_of(v)),
196            _ => None,
197        })
198        .unwrap_or_else(|| "udp4".to_string())
199    };
200    let sock_type = if sock_type == "udp6" { "udp6" } else { "udp4" };
201
202    let mut extra = IndexMap::new();
203    extra.insert("@@udptype".into(), with_host(|h| h.new_str(sock_type)));
204    let socket = super::net::new_emitter_object(SOCKET_TAG, extra);
205
206    // A trailing callback becomes a `message` listener.
207    if let Some(cb) = args
208        .get(1)
209        .filter(|v| with_host(|h| crate::host::is_callable(h, v)))
210    {
211        let _ = super::events::instance_call(
212            &socket,
213            "on",
214            vec![with_host(|h| h.new_str("message")), cb.clone()],
215        );
216    }
217    socket
218}
219
220// ── instance dispatch ─────────────────────────────────────────────────────────
221
222pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
223    if let Some(r) = emitter_dispatch(recv, method, &args) {
224        return r;
225    }
226    match method {
227        "bind" => socket_bind(recv, &args),
228        "send" => socket_send(recv, &args),
229        "close" => socket_close(recv, &args),
230        "address" => socket_address(recv),
231        // Best-effort socket options: applied to the live `UdpSocket` where std
232        // exposes them, otherwise accepted no-ops (multicast/buffer sizing).
233        "setBroadcast" => {
234            let on = with_host(|h| h.truthy(args.first().unwrap_or(&Value::Undef)));
235            if let Some(sock) = live_socket(recv) {
236                sock.set_broadcast(on).ok();
237            }
238            Ok(recv.clone())
239        }
240        "setTTL" | "setMulticastTTL" => {
241            let ttl = with_host(|h| h.to_number(args.first().unwrap_or(&Value::Undef))) as u32;
242            if let Some(sock) = live_socket(recv) {
243                if method == "setTTL" {
244                    sock.set_ttl(ttl).ok();
245                } else {
246                    sock.set_multicast_ttl_v4(ttl).ok();
247                }
248            }
249            Ok(args.first().cloned().unwrap_or(Value::Undef))
250        }
251        "getRecvBufferSize" | "getSendBufferSize" => Ok(Value::Float(65536.0)),
252        // Accepted no-ops: multicast membership, buffer sizing, connect/disconnect,
253        // ref counting. Documented as best-effort — std::net exposes no portable
254        // API for most, and the datagram path does not need them.
255        "setMulticastLoopback"
256        | "setMulticastInterface"
257        | "addMembership"
258        | "dropMembership"
259        | "addSourceSpecificMembership"
260        | "dropSourceSpecificMembership"
261        | "setRecvBufferSize"
262        | "setSendBufferSize"
263        | "connect"
264        | "disconnect"
265        | "remoteAddress"
266        | "ref"
267        | "unref" => Ok(recv.clone()),
268        _ => Err(crate::host::type_error(&format!(
269            "socket.{method} is not a function"
270        ))),
271    }
272}
273
274/// The live `UdpSocket` for `recv`, if it is currently bound.
275fn live_socket(recv: &Value) -> Option<Arc<UdpSocket>> {
276    let id = u64_prop(recv, "@@dgramid")?;
277    DGRAM.with(|s| s.borrow().sockets.get(&id).map(|r| r.socket.clone()))
278}
279
280// ── bind ──────────────────────────────────────────────────────────────────────
281
282/// `socket.bind([port][, address][, callback])`. Binds on the main thread (so a
283/// bind error surfaces from the call), registers the socket as a live handle, and
284/// spawns the `recv_from` loop. The `listening` event + callback fire
285/// asynchronously via a posted `IoTask`.
286fn socket_bind(recv: &Value, args: &[Value]) -> Result<Value, String> {
287    // Argument shapes: (), (port), (port, cb), (port, addr), (port, addr, cb),
288    // and an options-object first arg `{ port, address }`.
289    let mut port: u16 = 0;
290    let mut host = default_bind_host(recv).to_string();
291    let mut cb: Option<Value> = None;
292
293    if let Some(first) = args.first() {
294        if is_num(first) {
295            port = with_host(|h| h.to_number(first)) as u16;
296        } else if with_host(
297            |h| matches!(h.get(first), Some(JsObj::Object(p)) if !p.contains_key("@@native")),
298        ) {
299            // Options object `{ port, address }`.
300            with_host(|h| {
301                if let Some(JsObj::Object(p)) = h.get(first) {
302                    if let Some(pv) = p.get("port") {
303                        port = h.to_number(pv) as u16;
304                    }
305                    if let Some(av) = p.get("address").map(|v| h.str_of(v)) {
306                        host = av;
307                    }
308                }
309            });
310        }
311    }
312    for a in args.iter().skip(1) {
313        if is_str(a) {
314            host = with_host(|h| h.str_of(a));
315        } else if with_host(|h| crate::host::is_callable(h, a)) {
316            cb = Some(a.clone());
317        }
318    }
319
320    do_bind(recv, &host, port)?;
321
322    // Fire `listening` + callback asynchronously on the main thread.
323    let socket = recv.clone();
324    let _ = with_host(|h| h.io_sender()).send(Box::new(move || {
325        if let Some(cb) = cb {
326            super::events::instance_call(
327                &socket,
328                "once",
329                vec![with_host(|h| h.new_str("listening")), cb],
330            )?;
331        }
332        super::events::instance_call(&socket, "emit", vec![with_host(|h| h.new_str("listening"))])?;
333        Ok(())
334    }));
335    Ok(recv.clone())
336}
337
338/// Bind the socket (idempotent: returns the existing socket if already bound),
339/// register its record, take an event-loop handle, and spawn the recv loop.
340fn do_bind(recv: &Value, host: &str, port: u16) -> Result<Arc<UdpSocket>, String> {
341    if let Some(sock) = live_socket(recv) {
342        return Ok(sock);
343    }
344    let socket =
345        UdpSocket::bind((host, port)).map_err(|e| format!("Error: bind EADDRINUSE: {e}"))?;
346    // A read timeout lets the recv loop re-check its stop flag on a quiet socket.
347    socket.set_read_timeout(Some(POLL)).ok();
348    let socket = Arc::new(socket);
349
350    let id = next_id();
351    set_prop(recv, "@@dgramid", Value::Float(id as f64));
352    let stop = Arc::new(AtomicBool::new(false));
353    DGRAM.with(|s| {
354        s.borrow_mut().sockets.insert(
355            id,
356            UdpRec {
357                emitter: recv.clone(),
358                socket: socket.clone(),
359                stop: stop.clone(),
360            },
361        );
362    });
363    with_host(|h| h.incr_handle());
364
365    // Spawn the recv loop: raw datagrams → posted IoTasks. Never touches the host.
366    let tx = with_host(|h| h.io_sender());
367    let recv_sock = socket.clone();
368    std::thread::spawn(move || recv_loop(recv_sock, id, stop, tx));
369
370    Ok(socket)
371}
372
373/// Background reader: blocking `recv_from` loop posting `message` events. Runs off
374/// the main thread and only moves `Send` data (bytes, addr, port) into the closure.
375fn recv_loop(
376    socket: Arc<UdpSocket>,
377    id: u64,
378    stop: Arc<AtomicBool>,
379    tx: std::sync::mpsc::Sender<crate::host::IoTask>,
380) {
381    let mut buf = [0u8; 65536];
382    loop {
383        if stop.load(Ordering::Acquire) {
384            break;
385        }
386        match socket.recv_from(&mut buf) {
387            Ok((n, src)) => {
388                let bytes = buf[..n].to_vec();
389                let address = src.ip().to_string();
390                let port = src.port();
391                let family = if src.is_ipv6() { "IPv6" } else { "IPv4" };
392                let _ = tx.send(Box::new(move || {
393                    on_message(id, bytes, address, port, family)
394                }));
395            }
396            // A read-timeout (or non-blocking would-block) just re-checks `stop`.
397            Err(ref e)
398                if e.kind() == std::io::ErrorKind::WouldBlock
399                    || e.kind() == std::io::ErrorKind::TimedOut =>
400            {
401                continue;
402            }
403            Err(_) => break,
404        }
405    }
406}
407
408/// Main-thread delivery of one datagram: emit `message` with `(msg, rinfo)` where
409/// `rinfo = { address, family, port, size }` (matching Node).
410fn on_message(
411    id: u64,
412    bytes: Vec<u8>,
413    address: String,
414    port: u16,
415    family: &'static str,
416) -> Result<(), String> {
417    let socket = DGRAM.with(|s| s.borrow().sockets.get(&id).map(|r| r.emitter.clone()));
418    let Some(socket) = socket else { return Ok(()) };
419
420    let size = bytes.len();
421    let msg = super::buffer::from_bytes(&bytes);
422    let rinfo = with_host(|h| {
423        let mut m = IndexMap::new();
424        m.insert("address".into(), h.new_str(address));
425        m.insert("family".into(), h.new_str(family));
426        m.insert("port".into(), Value::Float(port as f64));
427        m.insert("size".into(), Value::Float(size as f64));
428        h.new_object(m)
429    });
430    super::events::instance_call(
431        &socket,
432        "emit",
433        vec![with_host(|h| h.new_str("message")), msg, rinfo],
434    )?;
435    Ok(())
436}
437
438// ── send ──────────────────────────────────────────────────────────────────────
439
440/// `socket.send(msg[, offset, length], port[, address][, callback])`. Auto-binds
441/// to an ephemeral port on the socket's address family if not yet bound (Node
442/// semantics), then `send_to` the bytes. The callback fires with `null` on
443/// success (asynchronously, on the main thread).
444fn socket_send(recv: &Value, args: &[Value]) -> Result<Value, String> {
445    let msg = args.first().cloned().unwrap_or(Value::Undef);
446    let full = value_bytes(&msg);
447
448    // Collect the leading numeric args after `msg`: either `[port]` or
449    // `[offset, length, port]` (Node distinguishes by count).
450    let mut nums: Vec<f64> = Vec::new();
451    let mut i = 1;
452    while i < args.len() && is_num(&args[i]) {
453        nums.push(with_host(|h| h.to_number(&args[i])));
454        i += 1;
455    }
456    let (offset, length, port) = if nums.len() >= 3 {
457        (
458            nums[0].max(0.0) as usize,
459            nums[1].max(0.0) as usize,
460            nums[2] as u16,
461        )
462    } else if let Some(p) = nums.first() {
463        (0usize, full.len(), *p as u16)
464    } else {
465        return Err(crate::host::type_error("Port should be > 0 and < 65536"));
466    };
467
468    // Trailing args: optional address (string) then optional callback.
469    let mut address = default_send_host(recv).to_string();
470    let mut cb: Option<Value> = None;
471    for a in args.iter().skip(i) {
472        if is_str(a) {
473            address = with_host(|h| h.str_of(a));
474        } else if with_host(|h| crate::host::is_callable(h, a)) {
475            cb = Some(a.clone());
476        }
477    }
478
479    // Slice the payload to [offset, offset+length).
480    let end = offset.saturating_add(length).min(full.len());
481    let start = offset.min(full.len());
482    let data = &full[start..end.max(start)];
483
484    // Auto-bind to an ephemeral port on the socket's family if needed.
485    let socket = do_bind(recv, default_bind_host(recv), 0)?;
486    socket
487        .send_to(data, (address.as_str(), port))
488        .map_err(|e| format!("Error: send {e}"))?;
489
490    // The send callback fires asynchronously with `(null)`.
491    if let Some(cb) = cb {
492        let _ = with_host(|h| h.io_sender()).send(Box::new(move || {
493            let nul = with_host(|h| h.null());
494            invoke(&cb, vec![nul], None)?;
495            Ok(())
496        }));
497    }
498    Ok(Value::Undef)
499}
500
501// ── close / address ───────────────────────────────────────────────────────────
502
503/// `socket.close([callback])`: stop the recv loop, drop the handle, emit `close`,
504/// and wake the event loop so a closed last handle lets it exit.
505fn socket_close(recv: &Value, args: &[Value]) -> Result<Value, String> {
506    if let Some(id) = u64_prop(recv, "@@dgramid") {
507        let rec = DGRAM.with(|s| s.borrow_mut().sockets.remove(&id));
508        if let Some(rec) = rec {
509            rec.stop.store(true, Ordering::Release);
510            with_host(|h| h.decr_handle());
511            // Wake the blocking loop so it can re-evaluate `open_handles`.
512            let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
513        }
514    }
515    // A `close` callback registers as a one-shot `close` listener in Node.
516    if let Some(cb) = args
517        .first()
518        .filter(|v| with_host(|h| crate::host::is_callable(h, v)))
519    {
520        invoke(cb, Vec::new(), None)?;
521    }
522    super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))])?;
523    Ok(Value::Undef)
524}
525
526/// `socket.address()` → `{ address, family, port }` from `local_addr`. Throws if
527/// the socket is not bound (matching Node's `Not running` error).
528fn socket_address(recv: &Value) -> Result<Value, String> {
529    let socket = live_socket(recv)
530        .ok_or_else(|| "Error: getsockname EBADF: bad file descriptor".to_string())?;
531    let addr = socket
532        .local_addr()
533        .map_err(|e| format!("Error: getsockname {e}"))?;
534    Ok(with_host(|h| {
535        let mut m = IndexMap::new();
536        m.insert("address".into(), h.new_str(addr.ip().to_string()));
537        m.insert(
538            "family".into(),
539            h.new_str(if addr.is_ipv6() { "IPv6" } else { "IPv4" }),
540        );
541        m.insert("port".into(), Value::Float(addr.port() as f64));
542        h.new_object(m)
543    }))
544}