Add UDP socket support to runtime (udp.bind, udp.send-to, udp.receive-from, udp.close) #433

Closed
opened 2026-04-26 19:46:06 +00:00 by navicore · 1 comment
navicore commented 2026-04-26 19:46:06 +00:00 (Migrated from github.com)

Motivation

Seq has TCP socket primitives but no UDP. UDP unblocks a long list of common protocols — DNS resolvers, NTP clients, multicast discovery, syslog, OSC (audio/lighting control), QUIC's underlying transport, and most game-network patterns. A near-term motivating use case is a Seq → OSC → Csound live-coding music POC, but the API design should serve all the above, not just OSC.

The design context lives at docs/design/LIVE_CODING_CSOUND_POC.md — it explicitly treats UDP as a prerequisite that's worth landing even if the music POC fails.

Proposed API

Mirror the shape of the existing tcp.* words. Sockets are integer handles managed by a registry, just like tcp.*. All operations return a success Bool on top so callers can branch with [ ... ] [ ... ] if.

udp.bind          ( port -- socket Bool )
   Bind a UDP socket to a local port. port=0 lets the OS pick.

udp.send-to       ( bytes host port socket -- Bool )
   Send a datagram to host:port from socket.
   bytes is a String (datagram payload — OSC encoders will produce this).

udp.receive-from  ( socket -- bytes host port Bool )
   Receive one datagram. Yields the strand (does not block the OS thread).
   On failure pushes ("" "" 0 false).

udp.close         ( socket -- )
   Release the socket.

Stack-effect conventions match tcp.*: failure pushes zeroed/empty values plus false. Success Bool is always on top.

Implementation Notes (non-prescriptive)

  • Use may::net::UdpSocket for coroutine-aware non-blocking I/O — strands must yield while waiting on recv_from, same as tcp.read does today.
  • Add crates/runtime/src/udp.rs mirroring the structure of tcp.rs. The SocketRegistry<T> pattern (with free-list reuse) used by tcp.rs should be lifted into something shared if it isn't already, or duplicated for UDP and refactored later.
  • Same safety caps: MAX_SOCKETS = 10_000 and MAX_READ_SIZE = 1 MB (per datagram) feel right.
  • C-ABI exports in lib.rs, type signatures in crates/compiler/src/builtins.rs, AST validation entry, codegen runtime symbol.

Tests

Loopback test that proves round-trip: bind a UDP socket on 127.0.0.1 port 0, get the assigned port back, send a payload to that port from another socket, receive it, assert byte-exact match including the source host/port. Mirror the tcp/tests.rs structure so it lives under crates/runtime/src/udp/tests.rs.

Negative tests: invalid port (negative, > 65535) returns (0, false); closed-socket send returns false; closed-socket recv returns the failure tuple.

What does NOT change

  • TCP API stays unchanged.
  • No multicast, no broadcast, no IPv6-specific API for the first cut. (IPv6 should work transparently if the host string is an IPv6 literal, but no first-class API for join-multicast-group etc.)

Downstream

Once UDP lands, the live-coding POC adds an OSC encoder (in Seq, in the example dir) and a Csound example without further runtime changes. UDP is the only required runtime addition for the whole POC.

## Motivation Seq has TCP socket primitives but no UDP. UDP unblocks a long list of common protocols — DNS resolvers, NTP clients, multicast discovery, syslog, OSC (audio/lighting control), QUIC's underlying transport, and most game-network patterns. A near-term motivating use case is a Seq → OSC → Csound live-coding music POC, but the API design should serve all the above, not just OSC. The design context lives at [`docs/design/LIVE_CODING_CSOUND_POC.md`](https://github.com/navicore/patch-seq/blob/main/docs/design/LIVE_CODING_CSOUND_POC.md) — it explicitly treats UDP as a prerequisite that's worth landing even if the music POC fails. ## Proposed API Mirror the shape of the existing `tcp.*` words. Sockets are integer handles managed by a registry, just like `tcp.*`. All operations return a success Bool on top so callers can branch with `[ ... ] [ ... ] if`. ``` udp.bind ( port -- socket Bool ) Bind a UDP socket to a local port. port=0 lets the OS pick. udp.send-to ( bytes host port socket -- Bool ) Send a datagram to host:port from socket. bytes is a String (datagram payload — OSC encoders will produce this). udp.receive-from ( socket -- bytes host port Bool ) Receive one datagram. Yields the strand (does not block the OS thread). On failure pushes ("" "" 0 false). udp.close ( socket -- ) Release the socket. ``` Stack-effect conventions match `tcp.*`: failure pushes zeroed/empty values plus `false`. Success Bool is always on top. ## Implementation Notes (non-prescriptive) - Use `may::net::UdpSocket` for coroutine-aware non-blocking I/O — strands must yield while waiting on `recv_from`, same as `tcp.read` does today. - Add `crates/runtime/src/udp.rs` mirroring the structure of `tcp.rs`. The `SocketRegistry<T>` pattern (with free-list reuse) used by `tcp.rs` should be lifted into something shared if it isn't already, or duplicated for UDP and refactored later. - Same safety caps: `MAX_SOCKETS = 10_000` and `MAX_READ_SIZE = 1 MB` (per datagram) feel right. - C-ABI exports in `lib.rs`, type signatures in `crates/compiler/src/builtins.rs`, AST validation entry, codegen runtime symbol. ## Tests Loopback test that proves round-trip: bind a UDP socket on `127.0.0.1` port 0, get the assigned port back, send a payload to that port from another socket, receive it, assert byte-exact match including the source host/port. Mirror the `tcp/tests.rs` structure so it lives under `crates/runtime/src/udp/tests.rs`. Negative tests: invalid port (negative, > 65535) returns `(0, false)`; closed-socket send returns `false`; closed-socket recv returns the failure tuple. ## What does NOT change - TCP API stays unchanged. - No multicast, no broadcast, no IPv6-specific API for the first cut. (IPv6 should work transparently if the host string is an IPv6 literal, but no first-class API for join-multicast-group etc.) ## Downstream Once UDP lands, the live-coding POC adds an OSC encoder (in Seq, in the example dir) and a Csound example without further runtime changes. UDP is the only required runtime addition for the whole POC.
navicore commented 2026-04-26 21:37:22 +00:00 (Migrated from github.com)
https://github.com/navicore/patch-seq/pull/434
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
navicore/patch-seq#433
No description provided.