Rust Binding Guide (zlink)¶
Contract-owning document for this chapter — the Rust bindings spec covers it. This chapter shows that contract as working sample code.
Explains how to use zlink in Rust through working sample code. See the core guide for messaging concepts.
Installation¶
Add it to Cargo.toml.
- Rust 1.85 or later (edition 2024).
- The native core is linked in at build time.
5-Minute Example — PING/ACK¶
use zlink::{Context, Message, Received, RecvFlags};
// Server
let ctx = Context::new().unwrap();
let server = ctx.pair_socket().unwrap();
server.bind("tcp://127.0.0.1:5555").unwrap();
let mut received = Received::empty();
server.recv(&mut received, RecvFlags::NONE).unwrap();
println!("{}", received.parts()[0].as_str().unwrap()); // PING
let ack = Message::try_from(b"ACK").unwrap();
server.send().message(ack).submit_sync(SendFlags::NONE).unwrap();
// Client
let ctx = Context::new().unwrap();
let client = ctx.pair_socket().unwrap();
client.connect("tcp://127.0.0.1:5555").unwrap();
let ping = Message::try_from(b"PING").unwrap();
client.send().message(ping).submit_sync(SendFlags::NONE).unwrap();
let mut received = Received::empty();
client.recv(&mut received, RecvFlags::NONE).unwrap();
println!("{}", received.parts()[0].as_str().unwrap()); // ACK
Core Types¶
Context¶
let ctx = Context::new().expect("context creation failed");
// dropping ctx interrupts blocking operations on child sockets
Message¶
Message owns a single payload frame. Passing it to send moves ownership,
and the compiler prevents any further use.
// build from a byte slice
let msg = Message::try_from(b"payload").unwrap();
// pre-sized empty frame
let mut msg = Message::with_size(256).unwrap();
msg.data_mut().copy_from_slice(&data);
// send — msg is moved here
socket.send().message(msg).submit_sync(SendFlags::NONE).unwrap();
// reusing msg is a compile error → ownership safety is enforced by the type system
HWM-managed sends provide a result-object submit() and synchronous
submit_sync(SendFlags). submit() returns Result<SendSubmission, _>, where
SendSubmission carries result (OK|BACKPRESSURED) and an admitted future.
In async code, use socket.send().message(msg).submit()?.admitted.await? to wait
for admission (already complete when result is OK). On a plain thread,
submit_sync(SendFlags::NONE) is available; pass SendFlags::DONT_WAIT
when immediate back-pressure is required.
Request provides submit_sync() to block until the reply and submit() to return a
RequestSubmission (result, admitted, plus a reply future). When result is OK
you can await reply directly; the reply is that terminal result, not DATA received separately.
Core owns retry after accepting a pre-admission operation; do not add a caller
retry queue or resubmit its payload. The shared native
ZLINK_OPT_PENDING_MAX_MSGS/BYTES caps cover pending SEND and REQUEST, with no
send-only pending names. Completion means local admission, not peer delivery or
an application acknowledgement.
Dropping a Future can stop the Rust waiter. Before Core submit, abandon the
builder without calling Core; after Core accepts the payload, admission or
request work may continue and the socket owner drains a late completion. Call
stream.options().set_recv_mode(StreamRecvMode::Raw) or ::Packet before
bind/connect, then use recv or recv_packet respectively.
If a public poller owns POLLCOMPLETION for a socket, keep another thread
calling wait() while a blocking request or Future is pending. wait() drains
native completions and settles or cleans Rust state; calling a blocking
terminal between waits on the same thread can stall it.
Reading a received message:
let part = &received.parts()[0];
let bytes: &[u8] = part.as_bytes();
let text: &str = part.as_str().unwrap(); // UTF-8
let size = part.size();
Received — the receive envelope¶
let mut received = Received::empty(); // reusable
socket.recv(&mut received, RecvFlags::NONE).unwrap();
let parts = received.parts(); // &[Message]
let rid: Option<&RoutingId> = received.routing_id(); // ROUTER/SPOT
let token: Option<&ReplyToken> = received.reply_token();
Routing ID¶
Ownership And Lifetime¶
Rust's ownership system enforces most of this at compile time.
| Situation | Rule |
|---|---|
submit() succeeds |
Message was already moved — no further handling needed |
submit() fails |
returns Result::Err, the builder cleans up its internal state |
recv() |
receives in place into &mut Received, parts released on drop |
| Async request | owns the reply Vec<Message>, each Message released on drop |
// error-handling pattern
let msg = Message::try_from(b"data").unwrap();
match socket.send().message(msg).submit_sync(SendFlags::NONE) {
Ok(_) => { /* sent */ }
Err(e) => eprintln!("send failed: {e}"),
}
Error Handling¶
The Rust binding returns per-operation error types via Result.
match socket.send().message(msg).submit_sync(SendFlags::DONT_WAIT) {
Ok(_) => {}
Err(e) => match e.code() {
zlink::SubmitResult::Backpressured => { /* retry */ }
zlink::SubmitResult::NotConnected => { /* not connected */ }
_ => return Err(e.into()),
},
}
Error types: SubmitError, RequestError, RecvError, BindError,
ConnectError, ConfigError, CloseError, HandlerError.
Each exposes the result code enum via a code() method.
C API ↔ Rust Mapping¶
| C API | Rust API |
|---|---|
zlink_ctx_new() |
Context::new() |
zlink_ctx_term() |
drop(ctx) |
zlink_socket(ctx, type) |
ctx.pair_socket(), etc. |
zlink_bind(s, ep) |
socket.bind(ep) |
zlink_connect(s, ep) |
socket.connect(ep) |
zlink_send(..., parts, count, ...) / zlink_send_rid(..., parts, count, ...) |
socket.send().message(m).submit_sync(flags) |
| DONTWAIT send + completion pull | socket.send().message(m).submit()?.admitted.await |
zlink_recv(..., parts_out, capacity, count_out, ...) |
socket.recv(&mut received, flags) |
zlink_msg_data(msg) |
part.as_bytes() |
zlink_routing_id_t |
RoutingId |
zlink_socket_monitor_open(...) |
SocketMonitor::open(&socket) |
zlink_poller_new() |
Poller::new() |
zlink_timer_new() |
Timer::new() |
Native Library / Deployment¶
The native core links in automatically at build time. Checking the runtime version:
let (major, minor, patch) = zlink::version(); // (i32, i32, i32) tuple
println!("zlink {major}.{minor}.{patch}");
Threading rules:
| Item | Rule |
|---|---|
Context |
Sync — shareable across threads (Arc<Context>) |
| Sockets | Send, but single-thread use only. No concurrent access |
Message::as_bytes() |
valid only while the message lives |
submit_sync(SendFlags::NONE) stops its calling thread while waiting for
HWM admission. This only parks that plain thread. In an async executor that must
keep running other tasks, use submit()?.admitted.await; use
submit_sync(SendFlags::DONT_WAIT) for immediate back-pressure.
use std::sync::Arc;
let ctx = Arc::new(Context::new().unwrap());
let ctx2 = ctx.clone();
std::thread::spawn(move || {
let socket = ctx2.dealer_socket().unwrap();
// use socket only from this thread
});
Samples¶
Verified samples live under bindings/rust/samples/.
| File | Description |
|---|---|
pair_recv_sample.rs |
PAIR send/receive |
dealer_router_recv_sample.rs |
DEALER/ROUTER send/receive |
request_reply_future_sample.rs |
Future request/reply |
pubsub_recv_sample.rs |
PUB/SUB publish/subscribe |
stream_recv_sample.rs |
STREAM raw TCP |
stream_packet_recv_sample.rs |
STREAM PACKET pull |
monitor_recv_sample.rs |
Monitor event receive |
SPOT/Actor examples are covered by the framework samples, not the core binding. Rust doesn't have a framework binding yet.
Generating the API reference:
See Also¶
Socket patterns - Socket pattern overview — PAIR · PUB/SUB · DEALER · ROUTER · STREAM · Proxy
Services - Framework service overview
Operations - Socket options · TLS security · Monitoring · Thread safety · Message API · Routing ID