>_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 (publish → pub) |
| 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 / rep | request/reply — synchronous RPC-style exchange (strict send→recv state machine) |
| pub / sub | publish/subscribe — topic-filtered fan-out (SUB is recv-only, PUB send-only) |
| xpub / xsub | raw pub/sub that carry subscription frames on the wire (for proxy/broker topologies) |
| push / pull | pipeline — PUSH load-balances to PULL workers (fan-out work distribution) |
| dealer / router | async, identity-addressed routing (the building blocks behind REQ/REP) |
| pair | exclusive 1:1, typically inproc thread coordination |
| stream | raw 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 tuning | sndhwm 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 ints | maxmsgsize (i64), affinity (u64) |
| booleans | ipv6 immediate conflate probe_router router_mandatory router_handover req_relaxed req_correlate xpub_verbose plain_server curve_server gssapi_server gssapi_plaintext |
| byte strings | identity subscribe unsubscribe |
| CURVE keys | curve_publickey curve_secretkey curve_serverkey (40-char z85 or raw bytes) |
| plain / misc strings | plain_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 (zmq → zmq-sys → zeromq-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 undef | that 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 messages | ZMQ'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 garbled | pass the same encoding => hex|base64 on both send and recv; UTF-8 framing corrupts NULs and high bytes. |
| curve_keypair errors | needs a libsodium-enabled libzmq — probe Zmq::has("curve") first. |
| leaked sockets | Zmq::socket_count() reports how many handles the cdylib still holds; close what you no longer need. |
| unknown socket handle | the 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:
- stryke-arrow — Apache Arrow / Parquet / Feather
- stryke-aws — S3, DynamoDB, SQS, Lambda, STS
- stryke-azure — Blob, Queues, Cosmos DB, Key Vault, Entra
- stryke-clickhouse — ClickHouse
- stryke-demo — live demos for every connector
- stryke-docker — Docker daemon API
- stryke-duckdb — embedded DuckDB SQL engine
- stryke-email — email + campaign client
- stryke-fleet — parallel expect/PTY automation
- stryke-gcp — Cloud Storage, Pub/Sub, Secret Manager, BigQuery, Firestore
- stryke-grpc — reflection-based gRPC client
- stryke-gui — mouse / keyboard / screen / clipboard automation
- stryke-k8s — Kubernetes
- stryke-kafka — Apache Kafka
- stryke-mcpd — MCP servers without a runtime
- stryke-mongo — MongoDB
- stryke-mssql — Microsoft SQL Server
- stryke-mysql — MySQL / MariaDB
- stryke-neo4j — Neo4j graph (Bolt)
- stryke-office — Office docs / PDF / images
- stryke-parquet — Parquet file inspector
- stryke-polars — Polars + ndarray + linalg + FFT
- stryke-postgres — PostgreSQL
- stryke-redis — Redis / Valkey
- stryke-scrape — web scraping / crawling
- stryke-scylla — ScyllaDB / Cassandra
- stryke-search — Elasticsearch / OpenSearch
- stryke-selenium — browser automation (WebDriver)
- stryke-spark — Spark Connect (no JVM)
- stryke-utils — pure-stryke utility belt