Skip to content

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.

dotnet add package Zlink
using Systems.Zlink;   // every public API lives in this namespace

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 — use Clone() for an independently mutable payload. .NET's CopyTo(Span<byte>)/CopyTo(IBufferWriter<byte>) are span-fill methods (they write the payload into a buffer, separate from the Clone deep 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_case becomes PascalCase in .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