// STRYKE-ZMQ — ZEROMQ CLIENT FOR STRYKE // REQ/REP + PUB/SUB + PUSH/PULL + DEALER/ROUTER

stryke package · cdylib libstryke_zmq (libzmq vendored) · loaded on use Zmq · opt-in (kept out of stryke core)

Report GitHub Issues
// Color scheme

>_STRYKE-ZMQ

Brokerless messaging, no daemon to babysit. ZeroMQ client for stryke — request/reply, publish/subscribe, push/pull pipeline, and dealer/router, over TCP, IPC, or in-process transports. Opt-in package; libzmq is vendored into the cdylib, so there is no system library requirement on the consumer machine.

Install

# from a release tarball (no rustc/cmake on the consumer machine)
s pkg install -g github.com/MenkeTechnologies/stryke-zmq

# from a local checkout
cd ~/projects/stryke-zmq
cargo build --release        # first build vendors libzmq via cmake (~1-2 min)
s pkg install -g .           # cdylib lands in ~/.stryke/store/zmq@<version>/

# one-liner
make install

There is no CLI launcher — this is a library. The cdylib is dlopened in-process on the first use Zmq; a shared zmq::Context plus a socket-handle registry are held in OnceCell, so sockets persist across calls.

Quick start

use Zmq

# REQ/REP over TCP
val $server = Zmq::socket("rep", bind => "tcp://*:5555")
val $client = Zmq::socket("req", connect => "tcp://localhost:5555")

Zmq::send($client, "ping")
val $request = Zmq::recv($server, timeout_ms => 1000)   # "ping"
Zmq::send($server, "pong")
val $reply = Zmq::recv($client, timeout_ms => 1000)     # "pong"

Zmq::close($client)
Zmq::close($server)

PUB/SUB with a topic filter and a one-shot request:

val $sub = Zmq::socket("sub", connect => "tcp://localhost:5556", subscribe => "weather")
val $msg = Zmq::recv($sub, timeout_ms => 500)

val $reply = Zmq::request("tcp://localhost:5555", "hello", timeout_ms => 2000)

PUSH/PULL pipeline and multipart frames over inproc (from examples/):

# PUSH/PULL — fan-out work distribution
val $puller = Zmq::socket("pull", bind => "inproc://work")
val $pusher = Zmq::socket("push", connect => "inproc://work")
Zmq::send($pusher, $_) for ("task-a", "task-b", "task-c")
val $task = Zmq::recv($puller, timeout_ms => 1000)

# multipart — atomic frame group
Zmq::send_multipart($dealer, ["header", "body", "trailer"])
val @frames = Zmq::recv_multipart($router, timeout_ms => 1000)

API — complete reference

The whole Zmq namespace, grouped by purpose. Every function below exists in lib/Zmq.stk. Functions split into two classes: socket ops (take a handle, touch a live libzmq socket) and pure helpers (validation, endpoint, topic, and table utilities — no socket created, no FFI socket call).

Introspection

Zmq::version()package (crate) version string
Zmq::lib_version()vendored libzmq { major, minor, patch, version }
Zmq::has($capability)probe one capability: curve gssapi ipc pgm tipc norm draft → bool | undef
Zmq::capabilities()probe every capability at once → { ipc, pgm, tipc, norm, curve, gssapi, draft }
Zmq::io_threads($value?)get (no arg) or set the shared context's ZMQ_IO_THREADS pool size (default 1, process-wide)

Socket lifecycle

Zmq::socket($type, %opts)create socket, return integer handle. type: req rep pub sub push pull dealer router pair xpub xsub stream. opts: bind, connect, subscribe + any settable option
Zmq::socket_pair(%opts){ a, b, endpoint } — a connected inproc PAIR in one call (a = bound, b = connected). opt: endpoint
Zmq::socket_count(){ count, handles } — how many sockets the cdylib holds open + their handles (leak detection); reads the local registry, no libzmq call
Zmq::close($handle)close + remove the socket
Zmq::bind / connect / unbind / disconnect ($handle, $endpoint)dynamic endpoint management; bind returns the concrete endpoint (resolves tcp://*:*)

Send / receive

Zmq::send($handle, $data, %opts)send a frame; more => 1; encoding => utf8|hex|base64
Zmq::send_multipart($handle, $parts, %opts)send an arrayref of frames atomically; encoding opt
Zmq::recv($handle, %opts)receive a frame; timeout_ms, encoding; returns undef on timeout
Zmq::recv_multipart($handle, %opts)receive all frames of a message as a list; empty list on timeout
Zmq::recv_more($handle, %opts)drain the trailing frames of a message a prior recv started (while RCVMORE is set)
Zmq::drain($handle, %opts)non-blocking drain — every whole multipart message already queued (each an arrayref), stopping at the empty queue (ZMQ_DONTWAIT); opts: encoding, max
Zmq::sendrecv($handle, $data, %opts)send then recv one turn on a kept handle (REQ/DEALER/PAIR) — one round-trip without an ephemeral socket; undef on reply timeout
Zmq::request($endpoint, $data, %opts)one-shot REQ round-trip; timeout_ms (default 5000), encoding; returns reply or undef on timeout

Pub/sub topic filters

Zmq::subscribe / unsubscribe ($handle, $topic)SUB topic filter ("" = all)

Socket options + readiness

Zmq::set($handle, $opt, $value)full socket-option table: timeouts, buffers, hwm, tcp_keepalive*, heartbeat*, ipv6, immediate, conflate, router/req flags, CURVE keys, plain auth, identity…
Zmq::get($handle, $opt)read back any option (type, last_endpoint, mechanism, fd, CURVE keys as z85, keepalive counters, gssapi principals…)
Zmq::poll($handle, %opts){ readable, writable }; opt: timeout_ms
Zmq::poll_many($handles, %opts)one zmq_poll over many handles → { handle, readable, writable, error } per handle
Zmq::events($handle){ readable, writable, events } — zero-wait readiness peek via ZMQ_EVENTS (not a zmq_poll call)

Monitor + proxy device

Zmq::monitor($handle, $endpoint, %opts)publish lifecycle events to an inproc endpoint; opt: events
Zmq::monitor_recv($handle, %opts)decode one event { event, value, endpoint } from a monitor PAIR
Zmq::proxy($frontend, $backend, %opts)backgrounded zmq_proxy device; opts: capture, control (steerable)

CURVE + Z85 codec

Zmq::curve_keypair(){ public, secret } z85 keys (needs libsodium-enabled libzmq)
Zmq::curve_public($secret){ secret, public } — derive the public key from a z85 secret
Zmq::valid_curve_key($key){ key, valid, reason } — a CURVE key is exactly 40 Z85 chars (32 bytes)
Zmq::z85_encode / z85_decode ($data, %opts)z85 codec; opt: encoding
Zmq::z85_valid($z85){ z85, valid, reason } — structural RFC-32 check (length ÷5 + Z85 alphabet)

Endpoint helpers (pure)

Zmq::parse_endpoint($endpoint){ transport, address, known_transport, host?, port? } — no socket
Zmq::build_endpoint(%opts){ endpoint, known_transport } — inverse of parse_endpoint; opts: transport, address | host+port
Zmq::endpoint_bind_to_connect($endpoint, %opts){ endpoint, bind_endpoint, changed } — rewrite a wildcard bind host (*/0.0.0.0/::) to a connectable one (opt: host, default localhost)
Zmq::endpoint_connect_to_bind($endpoint, %opts){ endpoint, connect_endpoint, changed } — inverse: rewrite a concrete host to a bind wildcard (opt: host, default *)
Zmq::valid_endpoint($endpoint){ endpoint, valid, reason, transport } — strict transport-syntax check (stricter than parse_endpoint)

Topic + subscription helpers (pure)

Zmq::topic_match($subscription, $topic)ZMQ SUB prefix match (empty subscription matches all) → 1 | ""
Zmq::topic_match_any($topic, \@subs){ topic, match, matched } — which subscriptions prefix-match the topic (XPUB set routing)
Zmq::topic_overlaps($a, $b){ a, b, overlaps, subsumes } — true when one prefix subscription subsumes the other
Zmq::prune_subscriptions(\@subs){ pruned, removed } — reduce a subscription set to its minimal cover
Zmq::subscription_diff(\@current, \@desired){ add, remove, unchanged } — the set difference to reconcile a SUB filter
Zmq::build_subscription($topic, %opts){ frame, subscribe, topic } — SUB/XSUB wire frame: leading \x01 (or \x00) + topic
Zmq::parse_subscription($frame){ subscribe, topic, frame } — inverse: split the action byte from the topic

Socket-type + table helpers (pure)

Zmq::valid_socket_type($type){ valid, canonical } — aliases collapse (publishpub)
Zmq::socket_types()every canonical socket-type name
Zmq::socket_type_id($type)the libzmq ZMQ_* integer for a type (PAIR=0 … STREAM=11)
Zmq::socket_peers($type)socket types $type can validly connect to
Zmq::socket_types_compatible($a, $b)1 | "" — whether two types can be connected as peers (REQ↔REP, PUB↔SUB…)
Zmq::socket_caps($type){ type, pattern, can_send, can_recv } — messaging pattern + directionality
Zmq::socket_caps_all()the socket_caps table for every type
Zmq::socket_option_names()every option set/get accept → { name, settable, gettable, kind }
Zmq::valid_socket_option($opt){ opt, known, settable, gettable, kind } — single-name lookup to guard a set/get
Zmq::encoding_names()every payload encoding → { name, aliases, binary_safe }
Zmq::socket_event_names()every monitor event → { name, flag, is_error }
Zmq::monitor_event_flag($name)the ZMQ_EVENT_* flag for an event name (inverse of parse_monitor_event; accepts all)
Zmq::parse_monitor_event(event => $id, …){ event, name, is_error, value_meaning, value?, endpoint? } — decode a zmq_socket_monitor event

Endpoints follow ZeroMQ transport syntax: tcp://host:port, ipc:///path, inproc://name, pgm://, epgm://. The pure helpers create no socket. Full prose reference in the README API section.

Socket types

Twelve types are accepted by Zmq::socket, one per ZeroMQ messaging pattern. Aliases (e.g. publish) collapse to the canonical name via Zmq::valid_socket_type.

req / reprequest/reply — synchronous RPC-style exchange (strict send→recv state machine)
pub / subpublish/subscribe — topic-filtered fan-out (SUB is recv-only, PUB send-only)
xpub / xsubraw pub/sub that carry subscription frames on the wire (for proxy/broker topologies)
push / pullpipeline — PUSH load-balances to PULL workers (fan-out work distribution)
dealer / routerasync, identity-addressed routing (the building blocks behind REQ/REP)
pairexclusive 1:1, typically inproc thread coordination
streamraw non-ZMTP TCP peers

Encodings & binary-safe payloads

Payloads default to UTF-8 framing. For arbitrary bytes (NULs, high bytes), pass encoding => "hex" or encoding => "base64" on both the send and the matching recv so the message round-trips intact. Zmq::encoding_names() lists every accepted encoding with its aliases and a binary_safe flag.

# round-trip a NUL-containing byte string as hex
Zmq::send($a, "\x00\xff\x01", encoding => "hex")
val $bytes = Zmq::recv($b, timeout_ms => 1000, encoding => "hex")

Socket options

Zmq::set/Zmq::get cover the full libzmq option surface, grouped by value kind. Zmq::socket_option_names() returns the live table ({ name, settable, gettable, kind }); guard one lookup with Zmq::valid_socket_option($opt).

i32 tuningsndhwm rcvhwm linger sndtimeo rcvtimeo sndbuf rcvbuf rate recovery_ivl reconnect_ivl reconnect_ivl_max backlog multicast_hops tos connect_timeout handshake_ivl heartbeat_ivl heartbeat_ttl heartbeat_timeout tcp_keepalive tcp_keepalive_cnt tcp_keepalive_idle tcp_keepalive_intvl
wider intsmaxmsgsize (i64), affinity (u64)
booleansipv6 immediate conflate probe_router router_mandatory router_handover req_relaxed req_correlate xpub_verbose plain_server curve_server gssapi_server gssapi_plaintext
byte stringsidentity subscribe unsubscribe
CURVE keyscurve_publickey curve_secretkey curve_serverkey (40-char z85 or raw bytes)
plain / misc stringsplain_username plain_password socks_proxy zap_domain xpub_welcome_msg gssapi_principal…
val $s = Zmq::socket("dealer")
Zmq::set($s, "rcvtimeo", 500)
Zmq::set($s, "identity", "worker-1")
val $id = Zmq::get($s, "identity")

Monitoring socket lifecycle

Zmq::monitor wires a socket's connect/disconnect/handshake events to an inproc PAIR; Zmq::monitor_recv decodes one event at a time. Zmq::socket_event_names() lists every event with its flag and is_error bit, and Zmq::parse_monitor_event maps a raw 16-bit event id to its symbolic name plus what the 32-bit value carries (fd, errno, reconnect interval, protocol error code, ZAP status).

Zmq::monitor($sock, "inproc://mon", events => "all")
val $ev = Zmq::monitor_recv($mon, timeout_ms => 1000)   # { event, value, endpoint }

A backgrounded Zmq::proxy($frontend, $backend, %opts) runs a zmq_proxy device with optional capture and steerable control sockets.

Socket handles

ZeroMQ sockets are stateful and long-lived, so the API is handle-based: Zmq::socket returns an integer handle the caller keeps; every later op takes that handle. This is deliberate — a SUB socket must stay open to keep receiving and a REQ/REP exchange is a send→recv state machine. A fork-per-call model would drop messages between calls and re-pay connection setup each time.

The cdylib holds the sockets in a registry behind a mutex. ZeroMQ requires one-thread-at-a-time access per socket; the mutex provides exactly that, plus the cross-thread memory fence libzmq mandates when a socket migrates between threads (zmq::Socket is Send, not Sync).

Why a package, not a builtin

ZeroMQ integration requires libzmq, the C library every ZMQ binding wraps. The Rust binding (zmqzmq-syszeromq-src) compiles libzmq and libsodium from source and links them statically. The artifact is big enough that it doesn't belong in stryke core — opt in once, get all four messaging patterns.

Because libzmq is vendored into the cdylib, there is no system libzmq.so/.dylib requirement on the consumer machine. A from-source build needs cmake plus a C/C++ compiler; the published release tarball is prebuilt per host triple.

FFI layer

Each zmq__* export in the cdylib is a JSON-string-in / JSON-string-out function. stryke's FFI bridge resolves the symbols listed in stryke.toml's [ffi] table on first use Zmq, passes a JSON-encoded args dict per call, and copies the returned JSON into a stryke string. Allocations are freed via stryke_free_cstring.

# a handler error becomes a stryke die
{"error": "Zmq::recv: ..."}

# a receive timeout becomes undef / empty-list in the wrapper
{"timeout": true}

A handler that panics is caught at the boundary and returned as an error rather than crossing the FFI boundary and aborting the host shell.

Troubleshooting

recv returns undefthat is a timeout, not an error — the wrapper maps {"timeout": true} to undef/empty-list. Raise timeout_ms or check the peer is connected.
SUB drops early messagesZMQ's slow-joiner behaviour: a SUB must subscribe before the PUB sends, and the connection takes a moment to settle. Send a few messages, or use a higher-level handshake. See examples/pub_sub.stk.
binary data garbledpass the same encoding => hex|base64 on both send and recv; UTF-8 framing corrupts NULs and high bytes.
curve_keypair errorsneeds a libsodium-enabled libzmq — probe Zmq::has("curve") first.
leaked socketsZmq::socket_count() reports how many handles the cdylib still holds; close what you no longer need.
unknown socket handlethe handle was closed or never created — the cdylib returns {"error":"unknown socket handle: N"}, which becomes a die.

Layout

stryke-zmq/
├── Cargo.toml             # cdylib crate (zmq -> vendored libzmq)
├── src/lib.rs             # zmq__* exports + socket registry + tests
├── stryke.toml            # package manifest + [ffi] table
├── lib/Zmq.stk            # stryke wrapper (use Zmq)
├── examples/              # req_rep, pub_sub, push_pull, multipart
├── t/                     # stryke assertion suites
├── tests/                 # docs/readme/polish lint gates
├── Makefile               # `make install` builds + installs
└── docs/                  # this site (GitHub Pages)

Sibling packages

Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo: