Introduction
The udp_client plugin is the counterpart of udp_server: it opens a UDP-sending endpoint on loopback and works **bidirectionally**. With SendText/SendBlob calls it sends datagrams to a given host:port address, and a concurrent background thread forwards any incoming replies to a script-level callback. So a single plugin serves both request sending and reply receiving — a classic UDP request-response pattern.
Relationship to udp_server and async_meta
The two plugins (client and server) share lifecycle and state model.
State returns async_meta.RouteStateOpen (=1) or RouteStateClosed (=0);
StateText is the “open”/“closed” text counterpart. The callback form is
identical on both sides: the borrowed blob is the raw datagram, and the
peer's host and port arrive as optional extra parameters
(ref blob $X, string $PeerHost, i32 $PeerPort).
Lifecycle in a nutshell
(1) Open(callbackName, timeoutMs, localPort?) opens a UDP socket on
loopback and starts a background thread for incoming replies. (2)
SendText or SendBlob sends a datagram. (3) If a reply arrives, the
background thread forwards it to the callback: the blob is the raw
reply payload, with the peer's host and port as extra parameters. (4) Close shuts down the socket and waits
for the thread to stop. Meanwhile, the state and diagnostic functions
(LocalPort, State, Delivered, Dropped, Info, OpenCount) can be queried
any time.
Loading the plugin
plugin "builtin:print"; plugin "../plugins/udp_client/UdpClientPlugin";
A typical flow — UDP request-response
callback i32 OnReply(ref blob $X) { // ... process the reply ... return(1); } i32 main() { i64 $cli; $cli = udp_client.Open("OnReply", -1, 0); udp_client.SendText($cli, "127.0.0.1", 9999, "PING"); // ... wait (e.g. timer.SleepMs), meanwhile OnReply may fire ... udp_client.Close($cli); return(0); }
What to know about every function (the basics)
Open always binds to LOOPBACK (127.0.0.1) — a security default, as on the server side.
localPort = 0 lets the OS pick a free port; the actual port is returned by LocalPort.
timeout = -1 means infinite wait for callback invocation. With a positive timeoutMs, the background thread DROPS an incoming packet if the callback does not fit in the time limit.
SendBlob and SendText return the ACTUALLY sent byte count — for UDP, this is normally exactly the payload size.
SendText internally calls SendBlob: it wraps the string as a read-only external blob and sends it.
EnableAnsiColors returns 1 on Linux (already supported); on Windows it tries to enable virtual-terminal mode (ENABLE_VIRTUAL_TERMINAL_PROCESSING) — handy for colored logging/diagnostic output.
An error (stopping the script) is caused by a wrong argument count or type, an out-of-range port (0..65535) or timeout, an invalid clientId, a host resolution failure, touching an already-closed client, and a socket/thread startup failure.
How to read the signatures
callbackName is the script-level reply callback name. timeoutMs is the maximum wait for callback invocation in milliseconds; -1 = infinite. localPort is the local (client-side) UDP port (0 = OS picks). clientId is the positive id given by Open. host is an IPv4 string or a name-resolvable name (typically “127.0.0.1” in the sandbox). The type after the -> arrow is the return type.
Lifecycle
Opening the client endpoint on a loopback port and closing it. Open starts a background thread for incoming replies; Close waits for its shutdown.
udp_client.Open
udp_client.Open(callbackName, timeoutMs, localPort?) ->
intOpens a UDP client endpoint on loopback IP and forwards incoming replies to the callback via a background thread.
| Parameter | Type | Description |
|---|---|---|
| callbackName | string | The name of the script-level reply callback. Signature: callback i32 NAME(ref blob $X). |
| timeoutMs | int | The maximum wait for callback invocation in milliseconds. -1 = infinite; otherwise 0..2147483647. |
| localPort | int | Optional local UDP port to bind. 0..65535; 0 lets the OS pick. Default: 0. |
The clientId (positive i64). A bad argument or a bind failure raises a runtime error.
callback i32 OnReply(ref blob $X) { return(1); } i64 $cli; $cli = udp_client.Open("OnReply", -1, 0); printf("%d\n", ($cli > 0));
1
For opening communication with a UDP service. The 0-localPort is handy in CI/test environments since it does not clash with bound ports. The returned clientId is the input of SendText/SendBlob and every further operation.
udp_client.Close
udp_client.Close(clientId) -> intCloses the client: stops the background thread, shuts down the socket, and releases resources.
| Parameter | Type | Description |
|---|---|---|
| clientId | int | The positive client id from Open. |
1 if the client was open and closed successfully; 0 if it was already closed. An unknown or non-positive clientId raises a runtime error.
i64 $cli; $cli = udp_client.Open("OnReply", -1, 0); printf("%d\n", udp_client.Close($cli));
1
For a clean client shutdown. Close waits for the background thread to actually stop, so the reply callback will no longer fire afterward.
Sending
Sending datagrams to a host:port address. Both verbs return the actually sent byte count.
udp_client.SendText
udp_client.SendText(clientId, host, port, text) -> intSends a text datagram to the given address (host:port). Internally calls SendBlob with the text as a read-only blob.
| Parameter | Type | Description |
|---|---|---|
| clientId | int | The client id. |
| host | string | The destination IPv4 or name-resolvable host (e.g. “127.0.0.1”). |
| port | int | The destination UDP port (1..65535). |
| text | string | The text to send (UDP payload bytes). |
The actually sent byte count. A bad argument, an unknown clientId, or
a sendto failure raises a runtime error.
i64 $cli; $cli = udp_client.Open("OnReply", -1, 0); printf("%d\n", udp_client.SendText($cli, "127.0.0.1", 9999, "hello"));
5
For text protocols (e.g. ASCII commands, JSON payloads). The return is usually the text length; if smaller, it's truncation or a network error — worth checking.
udp_client.SendBlob
udp_client.SendBlob(clientId, host, port, blob) -> int
Sends a binary data packet to the given address. Passes the blob's
full content to sendto.
| Parameter | Type | Description |
|---|---|---|
| clientId | int | The client id. |
| host | string | The destination IPv4 or name-resolvable host. |
| port | int | The destination UDP port (1..65535). |
| blob | blob | The binary data to send. The blob's full (Size field) length is sent. |
The actually sent byte count. A bad argument, an unknown clientId, or
a sendto failure raises a runtime error.
i64 $cli; blob $b[4]; $b[0] = 65; $b[1] = 66; $b[2] = 67; $b[3] = 68; $cli = udp_client.Open("OnReply", -1, 0); printf("%d\n", udp_client.SendBlob($cli, "127.0.0.1", 9999, $b));
4
For binary protocols (struct.assign-ed packets, length-prefixed messages, frame packets). Struct-shaped blobs can be sent directly if you cared about correct endianness when building the content.
State queries
Inspecting the client's current state: the local (bound) port, and the “open/closed” state in both numeric and textual form.
udp_client.LocalPort
udp_client.LocalPort(clientId) -> intReturns the client's local (bound) UDP port.
| Parameter | Type | Description |
|---|---|---|
| clientId | int | The client id. |
The local port (1..65535).
i64 $cli; $cli = udp_client.Open("OnReply", -1, 0); printf("%d\n", (udp_client.LocalPort($cli) > 0));
1
Particularly useful when you passed localPort=0 to Open (asking for an OS-picked port). You query the returned port number with this so you know which port replies arrive on.
udp_client.State
udp_client.State(clientId) -> intReturns the client's state using the async_meta route-state constants.
| Parameter | Type | Description |
|---|---|---|
| clientId | int | The client id. |
async_meta.RouteStateOpen (=1) if open; async_meta.RouteStateClosed (=0) if closed.
i64 $cli; $cli = udp_client.Open("OnReply", -1, 0); printf("%d\n", udp_client.State($cli));
1
For programmatic state checking, comparing against async_meta.RouteStateOpen. Shared numeric code in the callback_* family — uniform processing.
udp_client.StateText
udp_client.StateText(clientId) -> string
Returns the state in human-readable text.
| Parameter | Type | Description |
|---|---|---|
| clientId | int | The client id. |
“open” if open; “closed” if closed.
i64 $cli; $cli = udp_client.Open("OnReply", -1, 0); print.Line(udp_client.StateText($cli));
open
For logging or UI display. The textual counterpart of State's numeric value.
Diagnostics
Counters for received replies, an aggregated key=value state, a plugin-level OpenCount, and a diag helper for ANSI colors.
udp_client.Delivered
udp_client.Delivered(clientId) -> intReturns the count of reply packets successfully processed (forwarded to the reply callback) so far.
| Parameter | Type | Description |
|---|---|---|
| clientId | int | The client id. |
The number of processed replies.
i64 $cli; $cli = udp_client.Open("OnReply", -1, 0); printf("%d\n", udp_client.Delivered($cli));
0
For monitoring reply throughput. The difference between start and end values of a period gives the replies handled in it.
udp_client.Dropped
udp_client.Dropped(clientId) -> intReturns the count of dropped reply packets (due to callback timeout or buffer allocation failure).
| Parameter | Type | Description |
|---|---|---|
| clientId | int | The client id. |
The number of dropped replies.
i64 $cli; $cli = udp_client.Open("OnReply", -1, 0); printf("%d\n", udp_client.Dropped($cli));
0
An early signal if the reply callback does not fit in timeoutMs, or the buffer pool is exhausted. A bigger timeoutMs or a faster callback fixes it.
udp_client.Info
udp_client.Info(clientId) -> string
Returns the client's full key=value diagnostic state as a string.
| Parameter | Type | Description |
|---|---|---|
| clientId | int | The client id. |
A space-separated key=value list (id, state, stateCode, callback, timeout, localPort, delivered, dropped).
i64 $cli; $cli = udp_client.Open("OnReply", -1, 0); print.Line(udp_client.Info($cli));
id=1 state=open stateCode=1 callback=OnReply timeout=-1 localPort=<auto> delivered=0 dropped=0
For detailed diagnostics in a single call. The keys are stable; easy to break down further with str.* operations.
udp_client.OpenCount
udp_client.OpenCount() -> intReturns the number of currently open UDP clients (plugin-level, global view).
This function takes no arguments.
The number of open clients (0 if none).
printf("%d\n", udp_client.OpenCount()); i64 $cli; $cli = udp_client.Open("OnReply", -1, 0); printf("%d\n", udp_client.OpenCount()); udp_client.Close($cli); printf("%d\n", udp_client.OpenCount());
0 1 0
For resource control and diagnostics: after Close it should return to 0 if everything was properly released. Useful for early detection of resource leaks.
udp_client.EnableAnsiColors
udp_client.EnableAnsiColors() -> intEnables ANSI tools (colored logging) on the current standard output.
This function takes no arguments.
1 if enabled or already supported (Linux/macOS); 0 if not (e.g. non-console output on Windows).
printf("%d\n", udp_client.EnableAnsiColors());
1
For colored output in logs or diag messages. On Linux/macOS it's natively supported (always 1); on Windows it tries to enable the virtual-terminal mode (ENABLE_VIRTUAL_TERMINAL_PROCESSING).
Practical notes
What the udp_client plugin is good for
Sending requests to UDP server(s) on loopback (test environment, mock client, debug tool).
Request-reply pattern: after a Send*, the background thread forwards incoming replies to a callback, so the script does not block on the recv.
Both text and binary payloads are supported (SendText / SendBlob).
Simple telemetry with Delivered/Dropped counters, and a readable Info string for the full state.
Processing the callback blob
The incoming reply blob is the raw payload, exactly as in
udp_server; the peer's host and port come as extra callback
parameters rather than inside the blob. (The earlier 8-byte header
convention — port + hostLen + host string prepended to the payload —
has been retired, along with the struct UdpPacket { i32 port; i32 hostLen; } + offset-slicing pattern it required.) The two plugins
together build a complete request-response channel.
Timeout strategy
timeoutMs=-1 (infinite) gives the callback the full run — the background thread only resumes recv when the callback returns. For high-throughput flows, a shorter (e.g. 50-200 ms) timeoutMs is recommended: the callback must fit in the time window, or the packet is DROPPED (the Dropped counter goes up). If Dropped grows, either raise timeoutMs or speed up the callback.
Loopback-only binding
The plugin intentionally binds only to loopback (127.0.0.1) — a
security default, optimized for sandbox-style, tested use. For sending
to the external network (e.g. a service on another host), you will need
another tool; this plugin is for local communication. The host
argument must resolve to a loopback address — “127.0.0.1”, “localhost”,
or sandbox loopback aliases all work.
Error handling
A wrong argument count or type, an out-of-range port (0..65535) or
timeoutMs, an invalid or closed clientId, a host resolution failure, a
failed socket/thread startup, and a sendto failure are reported by the
runtime as a runtime error, and the script stops. A successful Send*
returns the sent byte count (normally exactly the payload size for
UDP).