Guide list | Previous: Overview | Next: C++
.NET Binding Guide (Zlink package)¶
Contract-owning document for this chapter — the .NET bindings spec covers it. This chapter shows that contract as working sample code.
Covers how to use the Zlink package in .NET in one chapter — installation,
core types, ownership, error handling, and deployment. For a deep dive into
messaging concepts (socket patterns, services, operations), see the core guide
links under See Also.
Installation¶
Ships as a single NuGet package, Zlink, with the native core bundled.
- .NET 8.0 or later (
net8.0). - No native install step needed — per-RID binaries load automatically. (see Native Library / Deployment)
5-Minute Example¶
A minimal example with a Pair socket where one side sends PING and the other
replies with ACK. The server binds, the client connects.
// Server
using var ctx = Zlink.CreateContext();
using var server = ctx.CreatePairSocket();
using var mon = server.MonitorOpen(SocketEvent.ConnectionReady);
server.Bind("tcp://127.0.0.1:5555");
mon.Recv(); // wait for connection
using var received = Received.Create();
server.Recv(received);
Console.WriteLine(received.FirstPart().GetString()); // PING
using var reply = Message.From("ACK");
server.Send().Message(reply).Submit();
// Client
using var ctx = Zlink.CreateContext();
using var client = ctx.CreatePairSocket();
using var mon = client.MonitorOpen(SocketEvent.ConnectionReady);
client.Connect("tcp://127.0.0.1:5555");
mon.Recv();
using var ping = Message.From("PING");
client.Send().Message(ping).Submit();
using var received = Received.Create();
client.Recv(received);
Console.WriteLine(received.FirstPart().GetString()); // ACK
Core Types¶
The 4 fundamental types every feature shares.
1. Context¶
The runtime entry point for a process. Usually you create one and build every socket/service from it.
using var ctx = Zlink.CreateContext();
ctx.Options.IoThreads = 4; // number of I/O threads
ctx.Options.MaxSockets = 1024; // max socket count
// set options before creating sockets.
IContext is IDisposable/IAsyncDisposable. Shutdown() can interrupt
in-flight operations on close, and using disposes it automatically.
2. Message¶
A single payload frame. Built from a string, bytes, or a pre-allocated buffer.
byte[] buffer = GetPayload();
using var fromText = Message.From("payload"); // string (UTF-8)
using var fromBytes = Message.From(buffer); // copies a byte[] / ReadOnlySpan<byte>
using var sized = new Message(1024); // pre-allocate, fill via AsSpan()
int size = fromText.Size;
string text = fromText.GetString(); // UTF-8 decode
ReadOnlySpan<byte> view = fromText.AsReadOnlySpan(); // read without copying
byte[] copy = fromText.ToArray(); // copy out
Message owns native storage, so it's IDisposable. The span returned by
AsSpan()/AsReadOnlySpan() is only valid while the message is alive. See the
message API for the
message model concept.
The binding doesn't provide object codec packages such as JSON, Protobuf, or
MessagePack. This layer keeps only a low-level API that exchanges raw Message
and byte payloads. If you need object serialization, register a framework codec
extension during the framework's configuration stage. On surfaces that exchange
raw Message directly — such as the framework's actor join callback — the
application layer explicitly builds and interprets the byte payload.
HWM-managed sends provide synchronous Submit() and asynchronous Async()
terminals. Submit() blocks in Core until local HWM admission. In asynchronous
code, use await ...Async(); it submits with DONTWAIT and settles from the
socket completion queue.
socket.Send().Message(message).Submit(); // synchronous Core admission
await socket.Send().Message(message).Async(); // asynchronous completion
Request provides Submit() to block until the reply and Async() to return a
Task<IReadOnlyList<Message>> settled from the socket completion queue. The
reply is the 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 limits cover pending SEND and REQUEST, and no
send-only pending names exist. Completion confirms local admission, not peer
delivery or an application acknowledgement.
A CancellationToken can prevent a pre-submit call or stop the managed waiter.
After Core accepts the payload, cancellation does not cancel Core admission or
the request; the socket owner still drains a late completion and releases it.
Set stream.Options.ReceiveMode to StreamReceiveMode.Raw or .Packet before
bind/connect, then use Recv or RecvPacket respectively.
When a public poller owns PollEventFlags.PollCompletion for a socket, another
thread must keep Wait() looping while a blocking request or Task is pending.
Wait() drains native completions and settles or cleans managed state; invoking
a blocking terminal between waits on the same thread can stall completion.
3. Received¶
A reusable envelope that holds a receive result. Build it once on the hot
path and reuse it across a Recv(...) loop to eliminate allocation.
using var received = Received.Create();
socket.Recv(received);
Message first = received.FirstPart(); // first part (no ownership transfer)
string body = first.GetString();
RoutingId? from = received.RoutingId; // present if a routing path exists
ReplyToken? token = received.ReplyToken; // present on a ROUTER request
IReadOnlyList<Message> parts = received.Parts; // full multipart set
4. RoutingId¶
A binary-safe value type identifying a peer, spot, or actor. Built only through static factories. See routing ID for the concept and policy.
RoutingId a = RoutingId.From("order-client"); // UTF-8 string
RoutingId b = RoutingId.From(0xC0FFEEu); // uint32 (big-endian)
RoutingId c = RoutingId.From(Guid.NewGuid()); // 16-byte UUID
RoutingId d = RoutingId.FromHex("0a1b2c"); // raw hex
string s = a.ToString(); // display string
string h = a.ToHex(); // preserves raw bytes
Ownership And Lifetime¶
IContext, sockets, Message, and Received all wrap native resources and
implement IDisposable (and mostly IAsyncDisposable). Whatever you create,
dispose of it — always with using (or await using).
- Dispose sockets before the context that created them.
- The reply parts returned by
Request().Async()/Join(...).Async()(IReadOnlyList<Message>) are owned by the caller — dispose them after use. - To hold onto a span, copy it first with
ToArray()/AsReadOnlyMemory().
Share / Move / Clone (Copy / Move / Clone)¶
Three explicit Message payload operations, with the same name and meaning across every
binding, mapping 1:1 to the Core C API (zlink_msg_copy/zlink_msg_move).
| Operation | Signature | Meaning | When |
|---|---|---|---|
Copy() |
Message Copy() |
ref-count share — new Message on the same buffer, original stays valid |
keep the same payload while still using the original |
Move(dest) |
void Move(Message dest) |
ownership transfer — hands off to dest, caller left empty |
re-send a received message with no copy (relay/echo) |
Clone() |
Message Clone() |
deep copy — independent buffer | mutate the duplicate independently |
using Message shared = msg.Copy();
socket.Send().Message(shared).Submit(); // shared is consumed
// msg is still valid
var outMsg = new Message();
receivedPart.Move(outMsg); // receivedPart becomes empty
socket.Send(routingId).Message(outMsg).Submit();
using Message dup = msg.Clone();
Copy()is a ref-share and does not guarantee mutation isolation — useClone()for an independently mutable payload. .NET'sCopyTo(Span<byte>)/CopyTo(IBufferWriter<byte>)are span-fill methods (they write the payload into a buffer, separate from theClonedeep copy) and stay unchanged.
For thread-safety rules, see thread safety.
IContext is safe to share across threads. Sockets are not — never call the
same socket from more than one thread concurrently.
Error Handling¶
Hard failures surface as per-operation typed exceptions. All inherit from
ZlinkException and expose Code (integer code) and a per-operation Result
(enum).
try
{
socket.Bind("tcp://127.0.0.1:5555");
}
catch (ZlinkBindException ex) when (ex.Result == ZlinkBindException.ErrorCode.AddrInUse)
{
Console.Error.WriteLine("Port already in use.");
}
catch (ZlinkException ex)
{
Console.Error.WriteLine($"zlink error {ex.Code}: {ex.Message}");
throw;
}
| Exception | Raised by |
|---|---|
ZlinkSubmitException |
Send/publish (Submit) |
ZlinkRequestException |
Request/reply (Request) — includes TimedOut |
ZlinkRecvException |
Receive (Recv) |
ZlinkBindException / ZlinkConnectException |
Bind/connect |
ZlinkConfigException |
Option/config |
ZlinkCloseException / ZlinkHandlerException |
Close/callback |
A non-blocking receive returns false from Recv(...) when no data is available.
The asynchronous send uses Core DONTWAIT and reports failure through its Task.
if (!socket.Recv(received, RecvFlags.DontWait)) { /* no data */ }
try { await socket.Send().Message(m).Async(); }
catch (ZlinkSubmitException ex) when (ex.Result == SubmitResult.Backpressured) { /* back-pressure */ }
C API Mapping¶
A compressed mapping for anyone coming from the C core (zlink.h) or comparing
against another language binding. .NET wraps raw functions in objects and
fluent builders, so this isn't 1:1, but it corresponds at the concept level. See
the core C API guide
for the full list of C functions.
| Area | C API (zlink_*) |
.NET |
|---|---|---|
| Context | zlink_ctx_new / zlink_ctx_term |
Zlink.CreateContext() / IContext.Dispose() |
| Context options | zlink_ctx_set / zlink_ctx_get |
IContext.Options (IoThreads, MaxSockets, …) |
| Socket creation | zlink_socket(ctx, TYPE) |
ctx.Create<Type>Socket() (e.g. CreatePairSocket()) |
| Bind / connect | zlink_bind / zlink_connect |
socket.Bind(...) / socket.Connect(...) |
| Disconnect | zlink_disconnect / zlink_disconnect_rid |
socket.Disconnect(string) / socket.DisconnectRid(RoutingId) |
| Socket options | zlink_set_option / zlink_get_option |
strongly typed per-socket properties (socket.Options) |
| routing id | zlink_set_routing_id / zlink_get_routing_id |
socket.SetRoutingId(RoutingId) / socket.GetRoutingId() |
| Message creation | zlink_msg_init / _init_size / _init_data |
new Message(size) / Message.From(...) |
| Message access | zlink_msg_data / zlink_msg_size |
Message.AsReadOnlySpan() / Message.Size |
| Message release | zlink_msg_close / zlink_multipart_close |
Message.Dispose() / Zlink.MultipartClose(parts) |
| Synchronous send | zlink_send / zlink_send_rid (part array + count, NONE) |
socket.Send().Message(...).Submit() |
| Asynchronous send | DONTWAIT send + completion pull | await socket.Send().Message(...).Async() |
| Receive | zlink_recv (output array + capacity + count) |
socket.Recv(Received) |
| Request / reply | zlink_request / zlink_reply |
dealer.Request()....Async() / router.Reply(rid, token) |
| Subscribe | zlink_set_subscription / zlink_subscribe |
socket.SetSubscription(...) / socket.Subscribe(TopicMessage) |
| Monitor | zlink_socket_monitor_open / _recv |
socket.MonitorOpen(...) / monitor.Recv() |
| Poller / timer | zlink_poller_* / zlink_timer_* |
Zlink.CreatePoller() / Zlink.CreateTimer() |
| Proxy | zlink_proxy |
Zlink.Proxy(...) |
Naming convention: C's
snake_casebecomesPascalCasein .NET. C's whole-message array and count are represented in .NET as accumulated.Message(...)calls on a fluent builder. The public shape follows language convention, but the semantic contract is the same.
Native Library / Deployment¶
Zlink bundles the native core under runtimes/<rid>/native, so no
extra setup is needed for a normal build. The ZLINK_LIBRARY_PATH environment
variable can override the load path. For self-contained/single-file/Native
AOT publishing, make sure the target RID's assets are included in the output
(dotnet publish -r <rid>).
Threading: IContext is thread-safe and shareable across threads. Sockets are
single-thread-owned — see thread safety
for the full rules.
Submit() stops the calling thread while it waits for HWM admission. This is
safe on a plain thread because only that thread waits. Use Async() when the
caller must remain available.
Samples¶
bindings/dotnet/samples/ has runnable examples organized by feature.
| Sample | Covers |
|---|---|
PairRecv |
PAIR send/receive |
DealerRouterRecv |
DEALER/ROUTER routing |
RequestReplyAsync |
Async request/reply |
PubSubRecv |
PUB/SUB topics |
MonitorRecv |
Socket monitor |
StreamRecv, StreamPacketCallback |
STREAM RAW/PACKET pull (legacy sample-directory name) |
SPOT/Actor examples are covered by the framework samples, not the core binding — see the Spot · Actor guides.
Run: ./samples/run_samples.sh (or run_samples.ps1).
See Also¶
Socket patterns - Socket pattern overview - PAIR - PUB/SUB - DEALER - ROUTER - STREAM - Proxy
Services - Framework service overview - Spot - Actor
Operations - Socket options - TLS security - Monitoring - Thread safety - Message API - Routing ID