Skip to content

Guide list | Previous: Go

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.

[dependencies]
zlink = "11.2"
  • Rust 1.85 or later (edition 2024).
  • The native core is linked in at build time.
use zlink::{Context, Message, Received, RecvFlags, SendFlags};

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

let rid = RoutingId::from(b"server-01");
socket.set_routing_id(&rid).unwrap();

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.

cd bindings/rust
cargo run --example pair_recv_sample

Generating the API reference:

cd bindings/rust
cargo doc --no-deps --open

See Also

Socket patterns - Socket pattern overviewPAIR · PUB/SUB · DEALER · ROUTER · STREAM · Proxy

Services - Framework service overview

Operations - Socket options · TLS security · Monitoring · Thread safety · Message API · Routing ID