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    super::events::METHODS
157        .contains(&method)
158        .then(|| super::events::instance_call(recv, method, args.to_vec()))
159}
160
161// ── module: dgram.createSocket ────────────────────────────────────────────────
162
163/// `stdlib::call` entry for `dgram.<method>`.
164pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
165    match method {
166        "createSocket" => Some(Ok(create_socket(args))),
167        _ => None,
168    }
169}
170
171/// `dgram.createSocket(type[, callback])` or `dgram.createSocket(options[, callback])`.
172/// `type` is `'udp4'`/`'udp6'`; an options object reads its `.type`. A `callback`
173/// is registered as a one-time-per-`emit` `'message'` listener (Node semantics).
174pub fn create_socket(args: &[Value]) -> Value {
175    let first = args.first().cloned().unwrap_or(Value::Undef);
176    let sock_type = if is_str(&first) {
177        with_host(|h| h.str_of(&first))
178    } else {
179        // options object: read `.type`.
180        with_host(|h| match h.get(&first) {
181            Some(JsObj::Object(p)) => p.get("type").map(|v| h.str_of(v)),
182            _ => None,
183        })
184        .unwrap_or_else(|| "udp4".to_string())
185    };
186    let sock_type = if sock_type == "udp6" { "udp6" } else { "udp4" };
187
188    let mut extra = IndexMap::new();
189    extra.insert("@@udptype".into(), with_host(|h| h.new_str(sock_type)));
190    let socket = super::net::new_emitter_object(SOCKET_TAG, extra);
191
192    // A trailing callback becomes a `message` listener.
193    if let Some(cb) = args
194        .get(1)
195        .filter(|v| with_host(|h| crate::host::is_callable(h, v)))
196    {
197        let _ = super::events::instance_call(
198            &socket,
199            "on",
200            vec![with_host(|h| h.new_str("message")), cb.clone()],
201        );
202    }
203    socket
204}
205
206// ── instance dispatch ─────────────────────────────────────────────────────────
207
208pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
209    if let Some(r) = emitter_dispatch(recv, method, &args) {
210        return r;
211    }
212    match method {
213        "bind" => socket_bind(recv, &args),
214        "send" => socket_send(recv, &args),
215        "close" => socket_close(recv, &args),
216        "address" => socket_address(recv),
217        // Best-effort socket options: applied to the live `UdpSocket` where std
218        // exposes them, otherwise accepted no-ops (multicast/buffer sizing).
219        "setBroadcast" => {
220            let on = with_host(|h| h.truthy(args.first().unwrap_or(&Value::Undef)));
221            if let Some(sock) = live_socket(recv) {
222                sock.set_broadcast(on).ok();
223            }
224            Ok(recv.clone())
225        }
226        "setTTL" | "setMulticastTTL" => {
227            let ttl = with_host(|h| h.to_number(args.first().unwrap_or(&Value::Undef))) as u32;
228            if let Some(sock) = live_socket(recv) {
229                if method == "setTTL" {
230                    sock.set_ttl(ttl).ok();
231                } else {
232                    sock.set_multicast_ttl_v4(ttl).ok();
233                }
234            }
235            Ok(args.first().cloned().unwrap_or(Value::Undef))
236        }
237        "getRecvBufferSize" | "getSendBufferSize" => Ok(Value::Float(65536.0)),
238        // Accepted no-ops: multicast membership, buffer sizing, connect/disconnect,
239        // ref counting. Documented as best-effort — std::net exposes no portable
240        // API for most, and the datagram path does not need them.
241        "setMulticastLoopback"
242        | "setMulticastInterface"
243        | "addMembership"
244        | "dropMembership"
245        | "addSourceSpecificMembership"
246        | "dropSourceSpecificMembership"
247        | "setRecvBufferSize"
248        | "setSendBufferSize"
249        | "connect"
250        | "disconnect"
251        | "remoteAddress"
252        | "ref"
253        | "unref" => Ok(recv.clone()),
254        _ => Err(crate::host::type_error(&format!(
255            "socket.{method} is not a function"
256        ))),
257    }
258}
259
260/// The live `UdpSocket` for `recv`, if it is currently bound.
261fn live_socket(recv: &Value) -> Option<Arc<UdpSocket>> {
262    let id = u64_prop(recv, "@@dgramid")?;
263    DGRAM.with(|s| s.borrow().sockets.get(&id).map(|r| r.socket.clone()))
264}
265
266// ── bind ──────────────────────────────────────────────────────────────────────
267
268/// `socket.bind([port][, address][, callback])`. Binds on the main thread (so a
269/// bind error surfaces from the call), registers the socket as a live handle, and
270/// spawns the `recv_from` loop. The `listening` event + callback fire
271/// asynchronously via a posted `IoTask`.
272fn socket_bind(recv: &Value, args: &[Value]) -> Result<Value, String> {
273    // Argument shapes: (), (port), (port, cb), (port, addr), (port, addr, cb),
274    // and an options-object first arg `{ port, address }`.
275    let mut port: u16 = 0;
276    let mut host = default_bind_host(recv).to_string();
277    let mut cb: Option<Value> = None;
278
279    if let Some(first) = args.first() {
280        if is_num(first) {
281            port = with_host(|h| h.to_number(first)) as u16;
282        } else if with_host(
283            |h| matches!(h.get(first), Some(JsObj::Object(p)) if !p.contains_key("@@native")),
284        ) {
285            // Options object `{ port, address }`.
286            with_host(|h| {
287                if let Some(JsObj::Object(p)) = h.get(first) {
288                    if let Some(pv) = p.get("port") {
289                        port = h.to_number(pv) as u16;
290                    }
291                    if let Some(av) = p.get("address").map(|v| h.str_of(v)) {
292                        host = av;
293                    }
294                }
295            });
296        }
297    }
298    for a in args.iter().skip(1) {
299        if is_str(a) {
300            host = with_host(|h| h.str_of(a));
301        } else if with_host(|h| crate::host::is_callable(h, a)) {
302            cb = Some(a.clone());
303        }
304    }
305
306    do_bind(recv, &host, port)?;
307
308    // Fire `listening` + callback asynchronously on the main thread.
309    let socket = recv.clone();
310    let _ = with_host(|h| h.io_sender()).send(Box::new(move || {
311        if let Some(cb) = cb {
312            super::events::instance_call(
313                &socket,
314                "once",
315                vec![with_host(|h| h.new_str("listening")), cb],
316            )?;
317        }
318        super::events::instance_call(&socket, "emit", vec![with_host(|h| h.new_str("listening"))])?;
319        Ok(())
320    }));
321    Ok(recv.clone())
322}
323
324/// Bind the socket (idempotent: returns the existing socket if already bound),
325/// register its record, take an event-loop handle, and spawn the recv loop.
326fn do_bind(recv: &Value, host: &str, port: u16) -> Result<Arc<UdpSocket>, String> {
327    if let Some(sock) = live_socket(recv) {
328        return Ok(sock);
329    }
330    let socket =
331        UdpSocket::bind((host, port)).map_err(|e| format!("Error: bind EADDRINUSE: {e}"))?;
332    // A read timeout lets the recv loop re-check its stop flag on a quiet socket.
333    socket.set_read_timeout(Some(POLL)).ok();
334    let socket = Arc::new(socket);
335
336    let id = next_id();
337    set_prop(recv, "@@dgramid", Value::Float(id as f64));
338    let stop = Arc::new(AtomicBool::new(false));
339    DGRAM.with(|s| {
340        s.borrow_mut().sockets.insert(
341            id,
342            UdpRec {
343                emitter: recv.clone(),
344                socket: socket.clone(),
345                stop: stop.clone(),
346            },
347        );
348    });
349    with_host(|h| h.incr_handle());
350
351    // Spawn the recv loop: raw datagrams → posted IoTasks. Never touches the host.
352    let tx = with_host(|h| h.io_sender());
353    let recv_sock = socket.clone();
354    std::thread::spawn(move || recv_loop(recv_sock, id, stop, tx));
355
356    Ok(socket)
357}
358
359/// Background reader: blocking `recv_from` loop posting `message` events. Runs off
360/// the main thread and only moves `Send` data (bytes, addr, port) into the closure.
361fn recv_loop(
362    socket: Arc<UdpSocket>,
363    id: u64,
364    stop: Arc<AtomicBool>,
365    tx: std::sync::mpsc::Sender<crate::host::IoTask>,
366) {
367    let mut buf = [0u8; 65536];
368    loop {
369        if stop.load(Ordering::Acquire) {
370            break;
371        }
372        match socket.recv_from(&mut buf) {
373            Ok((n, src)) => {
374                let bytes = buf[..n].to_vec();
375                let address = src.ip().to_string();
376                let port = src.port();
377                let family = if src.is_ipv6() { "IPv6" } else { "IPv4" };
378                let _ = tx.send(Box::new(move || {
379                    on_message(id, bytes, address, port, family)
380                }));
381            }
382            // A read-timeout (or non-blocking would-block) just re-checks `stop`.
383            Err(ref e)
384                if e.kind() == std::io::ErrorKind::WouldBlock
385                    || e.kind() == std::io::ErrorKind::TimedOut =>
386            {
387                continue;
388            }
389            Err(_) => break,
390        }
391    }
392}
393
394/// Main-thread delivery of one datagram: emit `message` with `(msg, rinfo)` where
395/// `rinfo = { address, family, port, size }` (matching Node).
396fn on_message(
397    id: u64,
398    bytes: Vec<u8>,
399    address: String,
400    port: u16,
401    family: &'static str,
402) -> Result<(), String> {
403    let socket = DGRAM.with(|s| s.borrow().sockets.get(&id).map(|r| r.emitter.clone()));
404    let Some(socket) = socket else { return Ok(()) };
405
406    let size = bytes.len();
407    let msg = super::buffer::from_bytes(&bytes);
408    let rinfo = with_host(|h| {
409        let mut m = IndexMap::new();
410        m.insert("address".into(), h.new_str(address));
411        m.insert("family".into(), h.new_str(family));
412        m.insert("port".into(), Value::Float(port as f64));
413        m.insert("size".into(), Value::Float(size as f64));
414        h.new_object(m)
415    });
416    super::events::instance_call(
417        &socket,
418        "emit",
419        vec![with_host(|h| h.new_str("message")), msg, rinfo],
420    )?;
421    Ok(())
422}
423
424// ── send ──────────────────────────────────────────────────────────────────────
425
426/// `socket.send(msg[, offset, length], port[, address][, callback])`. Auto-binds
427/// to an ephemeral port on the socket's address family if not yet bound (Node
428/// semantics), then `send_to` the bytes. The callback fires with `null` on
429/// success (asynchronously, on the main thread).
430fn socket_send(recv: &Value, args: &[Value]) -> Result<Value, String> {
431    let msg = args.first().cloned().unwrap_or(Value::Undef);
432    let full = value_bytes(&msg);
433
434    // Collect the leading numeric args after `msg`: either `[port]` or
435    // `[offset, length, port]` (Node distinguishes by count).
436    let mut nums: Vec<f64> = Vec::new();
437    let mut i = 1;
438    while i < args.len() && is_num(&args[i]) {
439        nums.push(with_host(|h| h.to_number(&args[i])));
440        i += 1;
441    }
442    let (offset, length, port) = if nums.len() >= 3 {
443        (
444            nums[0].max(0.0) as usize,
445            nums[1].max(0.0) as usize,
446            nums[2] as u16,
447        )
448    } else if let Some(p) = nums.first() {
449        (0usize, full.len(), *p as u16)
450    } else {
451        return Err(crate::host::type_error("Port should be > 0 and < 65536"));
452    };
453
454    // Trailing args: optional address (string) then optional callback.
455    let mut address = default_send_host(recv).to_string();
456    let mut cb: Option<Value> = None;
457    for a in args.iter().skip(i) {
458        if is_str(a) {
459            address = with_host(|h| h.str_of(a));
460        } else if with_host(|h| crate::host::is_callable(h, a)) {
461            cb = Some(a.clone());
462        }
463    }
464
465    // Slice the payload to [offset, offset+length).
466    let end = offset.saturating_add(length).min(full.len());
467    let start = offset.min(full.len());
468    let data = &full[start..end.max(start)];
469
470    // Auto-bind to an ephemeral port on the socket's family if needed.
471    let socket = do_bind(recv, default_bind_host(recv), 0)?;
472    socket
473        .send_to(data, (address.as_str(), port))
474        .map_err(|e| format!("Error: send {e}"))?;
475
476    // The send callback fires asynchronously with `(null)`.
477    if let Some(cb) = cb {
478        let _ = with_host(|h| h.io_sender()).send(Box::new(move || {
479            let nul = with_host(|h| h.null());
480            invoke(&cb, vec![nul], None)?;
481            Ok(())
482        }));
483    }
484    Ok(Value::Undef)
485}
486
487// ── close / address ───────────────────────────────────────────────────────────
488
489/// `socket.close([callback])`: stop the recv loop, drop the handle, emit `close`,
490/// and wake the event loop so a closed last handle lets it exit.
491fn socket_close(recv: &Value, args: &[Value]) -> Result<Value, String> {
492    if let Some(id) = u64_prop(recv, "@@dgramid") {
493        let rec = DGRAM.with(|s| s.borrow_mut().sockets.remove(&id));
494        if let Some(rec) = rec {
495            rec.stop.store(true, Ordering::Release);
496            with_host(|h| h.decr_handle());
497            // Wake the blocking loop so it can re-evaluate `open_handles`.
498            let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
499        }
500    }
501    // A `close` callback registers as a one-shot `close` listener in Node.
502    if let Some(cb) = args
503        .first()
504        .filter(|v| with_host(|h| crate::host::is_callable(h, v)))
505    {
506        invoke(cb, Vec::new(), None)?;
507    }
508    super::events::instance_call(recv, "emit", vec![with_host(|h| h.new_str("close"))])?;
509    Ok(Value::Undef)
510}
511
512/// `socket.address()` → `{ address, family, port }` from `local_addr`. Throws if
513/// the socket is not bound (matching Node's `Not running` error).
514fn socket_address(recv: &Value) -> Result<Value, String> {
515    let socket = live_socket(recv)
516        .ok_or_else(|| "Error: getsockname EBADF: bad file descriptor".to_string())?;
517    let addr = socket
518        .local_addr()
519        .map_err(|e| format!("Error: getsockname {e}"))?;
520    Ok(with_host(|h| {
521        let mut m = IndexMap::new();
522        m.insert("address".into(), h.new_str(addr.ip().to_string()));
523        m.insert(
524            "family".into(),
525            h.new_str(if addr.is_ipv6() { "IPv6" } else { "IPv4" }),
526        );
527        m.insert("port".into(), Value::Float(addr.port() as f64));
528        h.new_object(m)
529    }))
530}