Spec index | Previous: C | Next: C++
.NET Bindings Implementation Blueprint¶
What this chapter defines — the
Contracts/Runtimeshape the .NET library must have, and the baseline architecture map other wrapper bindings reference.
This document defines the shape the .NET library must have. It is not an
exhaustive list of every interface member. The actual public contract
source lives at bindings/dotnet/src/Zlink/Contracts/.
The .NET implementation is considered aligned once Contracts/, the
runtime implementation classes, tests, samples, the perf runner, and
package behavior all follow this blueprint and map core/include/zlink.h's
stable features onto a .NET-appropriate API.
This README describes the .NET binding shape and serves as the baseline guide for aligning other binding documents to the same architecture map. Even when another binding uses its own language-specific naming, the contract/ runtime ownership, public contract categories, file-splitting criteria, and verification intent described here still apply.
This binding follows the common bindings architecture map using .NET
naming. Contracts/<Category> owns the public contract source and
Runtime/<Category> owns the implementation. Another binding may use
different casing or package names, but this document is that same map
projected onto .NET.
The code a reviewer reads first should be the public contract under
Contracts/. A runtime file must implement that contract, and a new
user-facing behavior should never be discovered first in a runtime file.
| Section | Covers |
|---|---|
| Public contract source | Namespace, contract/runtime source locations, the API reference link |
| Repository layout | The aligned directory tree and folder ownership boundary |
| API change principles | Public mapping and contract/runtime boundary requirements |
| Library shape | Interface/concrete-type classification, builders, IDisposable, RoutingId helpers |
| Contract / Runtime placement rules | The boundary between public declarations and runtime implementation |
| Standard interface rules | recv signatures, builder start methods, naming constraints |
| Contract folder layout | The ownership scope of each category under Contracts/ |
| Runtime folder layout | The implementation scope of each category under Runtime/ |
| Creation entry points | The list of public factory methods |
| Required feature coverage | The user-facing features that must be guaranteed once aligned |
| Receive and Subscribe shape | Caller-provided storage and distinguishing no-data |
| Service and SPOT shape | The split of responsibility between ISpotNode/ISpot |
| Byte HWM and monitoring ABI v4 | ulong byte HWM and the monitor snapshot fields |
| Receive flow state | The receive-flow state type, setter, and monitor surface |
| Error and validation policy | Validation timing and exception mapping |
| Performance policy | Hot-path constraints |
| Implementation checklist | What to confirm before declaring alignment, and required verification commands |
| Actor and Spot Route results | The route-result record and Actor-directed send/request |
Public contract source¶
- Public namespace:
Systems.Zlink. - Package identity:
Systems.Zlink. - Public contract:
bindings/dotnet/src/Zlink/Contracts/. - Runtime implementation:
bindings/dotnet/src/Zlink/Runtime/. - Internal implementation: P/Invoke declarations,
SafeHandleor native handle ownership, callback trampolines, the request progress pump, native model converters, the socket kernel, option accessors, buffer codecs, validation helpers. - Documentation's role: this README defines the library shape and review rules.
Contracts/owns the exact public behavior surface. - API reference comments:
api-reference-comments.en.mddefines the XML comment authoring and review criteria forContracts/.
A runtime implementation file does not define a user-facing behavior that
can't be understood from Contracts/ or a documented creation entry
point alone.
Repository layout¶
Use the following paths consistently when changing the .NET binding.
- Public contract:
bindings/dotnet/src/Zlink/Contracts/. - Runtime implementation:
bindings/dotnet/src/Zlink/Runtime/. - Native bridge/artifacts:
bindings/dotnet/src/Zlink/Runtime/Native/,bindings/dotnet/native/. Inside the NuGet package, these files are laid out underruntimes/<rid>/native/. - Codec distribution scope: follow the common raw payload policy.
- Tests:
bindings/dotnet/tests/Zlink.Tests/. - Samples:
bindings/dotnet/samples/. -
Perf:
bindings/dotnet/perf/. -
Contracts/'s public signatures never include a P/Invoke declaration,SafeHandledetail, a native struct mirror used only for marshalling, or a request pump type. - A concrete value type may use native-backed storage internally for ownership, but .NET does not expose or use a zero-copy send path that borrows a VM-managed buffer as a public or default behavior.
- Native bridge declarations and marshalling-only mirrors still live under
Runtime/Native/. Contracts/andRuntime/are fixed repository folders.- The
Systems.Zlinknamespace and the NuGet package surface are that contract projected onto .NET. - A namespace segment named
ContractsorRuntimeis never exposed as the primary user-facing namespace.
The tree below is prescriptive about ownership and shows representative files — it is not the complete file list.
- A file that defines public behavior lives under
Contracts/. - A file that calls native code, owns a handle, marshals a struct, or runs callback/request progress logic lives under
Runtime/, and native bridge code lives underRuntime/Native/.
bindings/dotnet/
+-- src/
| +-- Zlink/
| | +-- Contracts/
| | | +-- Core/
| | | | +-- Context.cs
| | | | +-- ContextOptions.cs
| | | | +-- RoutingId.cs
| | | | +-- Zlink.cs
| | | +-- Messaging/
| | | | +-- Message.cs
| | | | +-- Received.cs
| | | | +-- TopicMessage.cs
| | | | +-- SubscriptionEvent.cs
| | | | +-- OperationContracts.cs
| | | +-- Sockets/
| | | | +-- ISocket.cs
| | | | +-- MessageSocketContracts.cs
| | | | +-- RoutedSocketContracts.cs
| | | | +-- PubSubSocketContracts.cs
| | | | +-- IStreamSocket.cs
| | | | +-- SocketOptionFacades.cs
| | | +-- Eventing/
| | | | +-- Monitor.cs
| | | | +-- Poller.cs
| | | | +-- PollEvent.cs
| | | | +-- Timer.cs
| | | | +-- ZlinkPoll.cs
| | | +-- Service/
| | | | +-- SpotNode.cs
| | | | +-- Spot.cs
| | | | +-- Actor.cs
| | | | +-- SpotNodeModels.cs
| | | +-- Errors/
| | | | +-- Errors.cs
| | +-- Runtime/
| | | +-- Core/
| | | +-- Handles/
| | | +-- Messaging/
| | | +-- Sockets/
| | | +-- Eventing/
| | | +-- Service/
| | | +-- Errors/
| | | +-- Buffers/
| | | +-- Options/
| | | +-- Native/
+-- tests/
+-- samples/
+-- perf/
+-- native/
+-- runtimes/
- The folder names
ContractsandRuntimeare a repository ownership boundary. They are not permission to exposeSystems.Zlink.ContractsorSystems.Zlink.Runtimeas a user-facing namespace. - Public creation returns a public contract such as
IContext, a socket interface,ISpotNode,IPoller, orIZlinkTimer, unless the public contract explicitly requires a concrete value type. - Runtime classes such as
Context, a socket class,SpotNode,Poller, orTimerare implementation owners, not the preferred consumer surface.
IPoller accepts a socket monitor as a source through void Add(ISocketMonitor monitor, PollEventFlags events, nuint slot),
void Modify(ISocketMonitor monitor, PollEventFlags events) and bool Remove(ISocketMonitor monitor) (common spec "Monitor
sources in Poller"); the one-shot ZlinkPoll.Poll(IReadOnlyList<ISocketMonitor>, ...) remains. A monitor mask accepts only
PollIn or None; any other bit is rejected with ZlinkConfigException (ErrorCode.InvalidArgument). Drain with
Receive/TryReceive (DONTWAIT) after readiness.
Runtime/Buffers, Runtime/Handles, and Runtime/Options are
implementation-support categories. These folders exist because real
native ownership, routing-id encoding, and option validation decisions
must be hidden inside the .NET binding. Another binding may use different
names for these support areas, but never moves that detail into a public
contract file.
API change principles¶
The public projection of a Core capability follows these principles:
- Add the user-facing behavior to the appropriate
Contracts/category. - Use a concrete DTO/value/record type unless the caller needs substitutable behavior.
- Add or modify the
Runtime/implementation without exposing a native bridge type. - Document a new creation entry point if the interface alone cannot construct the object.
- Add tests against the public contract, not an
internalmember. - Update samples and perf only through the public contract and public factories.
- Confirm a framework adapter does not reach the binding's private members via reflection or
InternalsVisibleTo.
The contract/runtime boundary meets these requirements:
- Move user-facing declarations to their matching
Contracts/category. - Move the native-backed implementation, handle ownership, request progress, marshalling, and option validation to
Runtime/. - Keep P/Invoke declarations and native struct mirrors in
Runtime/Native/. - A duplicate public entry point that exists only for compatibility is not part of the contract.
- Update samples, perf, and framework adapters only through the public contract and documented creation entry points.
- Add or update tests through the public
Systems.Zlinksurface.
The following .NET-specific shortcuts are not allowed.
- The public contract never mentions P/Invoke,
SafeHandle, a native struct, a raw option id, callback userdata, request pump state, or whole-message array handling. - A runtime class never introduces public behavior that can't be found in
Contracts/. - Framework adapters, samples, perf, and tests never use reflection,
NonPubliclookup, or a private runtime shortcut. - A compatibility-only wrapper is not part of the public shape.
Library shape¶
The .NET binding uses a contract/runtime split.
- A behavior contract is a public
I*interface inContracts/. An operation builder contract may use a domain name such asSendOperationorRequestOperation, following the public shape that package has settled on. - When a caller must create a resource through a public factory such as
Context,DealerSocket,RouterSocket,SpotNode,Poller, orTimer, the native-backed implementation is an internal sealed class inRuntime/. - A non-instantiable abstract base class may live in
Runtime/purely as implementation support for those runtime implementation classes above. These are not creation entry points, and their public behavior must still be covered by aContracts/interface or value type. - DTO, value, result, option, enum, and exception types stay concrete types. They use ordinary .NET convention —
record,sealed class,readonly struct,enum. An envelope that owns and must dispose a message part is asealed class, not arecord. - An operation builder is an interface, so it can hide staged native request state and multipart accumulation.
- A public static facade, extension method, or builder convenience helper is part of the contract when the caller can call it directly. Even when the implementation delegates to runtime code, its definition lives under the owning
Contracts/category. - A native handle, request pump, callback bridge state, whole-message array handling, or raw option id stays in
Runtime/or aninternalimplementation type. - A disposable native resource implements both
IDisposableandIAsyncDisposable.
DTOs such as Message, RoutingId, Received, and TopicMessage are not
turned into interfaces just for symmetry. These are concrete domain values
whose ownership and allocation behavior are clear. Received is a
caller-provided, reusable recv storage, so it is created with
Received.Create().
The standard interface classification other wrapper binding documents follow is defined by the following .NET types.
- Core resource:
IContext. - Socket resource roles:
ISocket,IMessageSocket, the routed socket contract, the pub/sub socket contract, and the pair/dealer/router/pub/sub/xpub/xsub/stream socket-family interfaces. A family interface exists only when that family has native-backed behavior. - Eventing resource roles: the monitor socket contract,
IPoller, the poll event source contract,IZlinkTimer.ISpotNode,ISpot, and, when an Actor handle is exposed,IActoror an equivalent actor resource contract. - Operation builder roles: send, request, reply, publish, channel send/request, SPOT send/request/reply, actor create, actor join, actor join reply operations.
- Handler roles: SPOT dispatch handler, route handler, and admission handler.
RoutingId string and binary helpers¶
RoutingId stays a binary-safe value type. The public .NET helpers have
the following meaning.
RoutingId.From(string value)encodes a user routing id string as UTF-8.RoutingId.From(byte[] value)andRoutingId.From(ReadOnlySpan<byte> value)preserve the routing id's raw bytes as-is.RoutingId.FromHex(value)restores the bytes thatToHex()printed.RoutingId.From(uint value)records a 4-byte big-endianuint32routing id.RoutingId.From(Guid value)records a 16-byte UUID routing id.ToString()is for display: printable UTF-8 text, thenuint32, then UUID, and ahex:-prefixed raw hex when no clearer representation applies.
For a durable raw-byte round trip, use ToHex() / FromHex(value).
RoutingId caching is purely an internal optimization. The binding may
cache a hash or a short-lived receive-path value, but equality and public
behavior are defined only by the immutable byte value.
Contract / Runtime placement rules¶
- A public interface, a concrete DTO/value type, an enum, and the public exception domain live in
Contracts/. - A public static facade, extension method, module-style helper, or builder convenience helper lives in
Contracts/. - The runtime implementation, the socket kernel, the request pump, callback bridge state, and lifecycle owners live in
Runtime/. - P/Invoke declarations,
SafeHandleimplementations, native struct mirrors, marshalling helpers, and platform loading code live inRuntime/Native/. Contracts/'s public signatures never mention aRuntime/Native/type.- Even when a runtime class is intentionally exposed for direct construction, its public behavior is still described by
Contracts/. This is the exception, not the default shape.
Standard interface rules¶
- Data-plane
Recv, routed recv,Subscribe, and subscription event receive fill a caller-providedReceived,TopicMessage, orSubscriptionEventinstance and returnbool. - A .NET caller creates a reusable receive storage with
Received.Create().Receivedhas no public constructor. Send, routed send,Publish,Request,Reply, SPOT operations, and Actor location/session operations return a fluent operation builder.- The terminal for PUB/XPUB
Publish(topic)is the synchronousPublishSubmitOperation.Submit() -> void. Default PUB semantics are lossy, so the publisher never waits at the high-water mark. UnderNODROPa full subscriber surfaces on the spot asZlinkSubmitException(Result == Backpressured), and the retry policy belongs to the application.TryPublish(topic)is the separate surface that observes the same back-pressure asfalseinstead of an exception. - A builder's start method takes only a target identity, topic, channel, routing id, or
ReplyToken. Payload, request timeout, and terminal choice are handled at the builder stage. - A reply builder collects its payload and ends with
Submit(). - The terminal for a raw ROUTER/
Receivedreply is the synchronous one-shotReplySubmitOperation.Submit() -> void. It submits a terminal reply or error reply with one native call. A DEALER peer is subject to Application HWM (the queue byte threshold),PAUSED, andSNDTIMEO, so the result can beBackpressured; a ROUTER peer uses the HWM-free Completion connection.NotConnected,Terminated,InvalidArgument, and other submit failures immediately throwZlinkSubmitException. - No single-payload shortcut overload is added under the same name as an operation's start method.
Send(Message),Send(RoutingId, Message),Publish(string, Message),SendToChannel(string, Message),SendToSpot(..., Message)are not public contract members. A caller uses the builder terminal for that role;Send(...).Message(message).Async()is canonical for a DEALER/ROUTER routed send. - A multipart payload accumulates via repeated
Message(...)calls. AMessages(...)-style convenience method is allowed, but since it is a public builder contract member, it lives inContracts/. IDealerSocketdoes not expose protocol envelope helpers such asRequestFrame(...)orReply(requestToken, parts). A dealer can start a request withRequest(), but has no API-level peer routing id, so it cannot reply to an arbitrary token. Reply starts from a received request context, or from an explicit router/SPOT reply surface when the target context requires it.- A message payload factory uses
Message.From(...)overloads. A source-type suffix such asFromBytes, or a value-style factory such asOf, is not part of the public contract. - No operation-start method family such as
SendNoWait,PublishWithFlags,RequestAsyncis added. Keep one operation name, and let the builder absorb variants. An awaitable terminal on an HWM-managed DEALER/ROUTER routed send/request builder is unified asAsync(...).
Send and request terminals¶
- Every socket's send factory returns a
SendOperationthat captures the target. - Send
Submit()uses CoreNONEadmission, andAsync(CancellationToken)uses CoreDONTWAITcompletion. - Request
Submit()andAsync(CancellationToken)wait through reply, timeout, or typed error. The builder'sTimeout(...)sets the Core request timeout. CancellationTokenrepresents either rejection before the native call or caller-wait cancellation after successful submit. Runtime drain releases the payload and operation state on late completion.- Publish retains synchronous
Submit()on a separate operation.
Contract folder layout¶
Contracts/ must be readable as a public API map.
Core/: context, context option, routing id, utility resource contracts.Messaging/: message, received metadata, topic message, subscription event, common send/request/reply operation contracts, message-domain convenience helpers.Sockets/: socket operation contracts, socket capability interfaces, typed option facades.Eventing/: monitor, monitor snapshot/event, poller, timer, poll event contracts. A static poll helper, when public, also belongs here.Service/: SPOT node, SPOT handle, the topology model, actor ref, actor lifecycle, service-only operation builders.Errors/: the exception hierarchy and error-domain mapping.
Files within each category are split by user-facing concept, not implementation order.
- Common messaging operations split into send, request, and reply; the service topology model splits into the SPOT node model and shared topology enums.
- A request result belongs to the messaging request contract, not a socket enum file.
- A received message kind stays with the received message metadata.
- SPOT node mode, socket snapshot, Spot snapshot, and actor snapshot belong to the SPOT node model.
SPOT stays a single handle contract, ISpot. It is not split into
per-role interfaces unless the caller genuinely needs to receive those
roles separately.
ISpotNodemay split node configuration, peer connection, Spot creation, Actor operations, and topology lookup roles into separate interfaces that compose. Even so, the default creation path and the user-facing return type remainISpotNode, and a role interface must never expose a runtime implementation type.- SPOT callback registration uses a named callback delegate, so the public signature describes the callback's meaning without adding a wrapper context object.
- A registration method uses the
Set...Handlername because it stores or replaces the current handler. AnOn...name is reserved only for a method invoked when the event occurs. - Since these delegates are used only in the SPOT handle contract, they are declared next to
ISpot. - A lifecycle data type lives with the actor model. A lifecycle event envelope that owns a message part is a sealed class, not a cloneable record.
- The Actor operation contract splits into join, management, and session binding.
If a user or a framework adapter needs a public API, that API must be discoverable in this folder without reading P/Invoke or runtime bridge code.
Runtime folder layout¶
Runtime/ follows the same standard map, but contains only
implementation.
Core/: context lifecycle, counter/stopwatch/thread helpers, runtime version/capability lookup.Handles/: native resource ownership, close state, lifetime checks, reference tracking.Messaging/: multipart message materialization, request/reply progress, request state, received handlers, topic encoding.Sockets/: the socket base class, the socket kernel, socket implementations, callback adapters, option accessors, receive helpers, operation implementation classes.Eventing/: poller, timer, monitor state, callback delivery, event materialization helpers.Service/: SPOT node, Spot, Actor, topology converters, service option support, service operation implementations.Errors/: boundary validation, native result mapping, errno conversion.Buffers/: the routing-id codec, payload buffer ownership, the copy/borrow policy, snapshot buffer helpers.Options/: context/socket option constants, validation, runtime option conversion.Native/: P/Invoke declarations, platform loading, native type mirrors, marshalling helpers.
Runtime code may depend on public contract types. A contract file may internally delegate to runtime code to wire up a public factory/static facade, but a public signature must never expose runtime implementation detail.
Creation entry points¶
An interface defines behavior; creation is provided by a public factory.
Zlink.CreateContext()creates the runtime context implementation.Zlink.CreateAtomicCounter(),CreateStopwatch(),CreateThread(...)create utility resources through the public contract.IContext.CreatePairSocket(),CreateDealerSocket(),CreateRouterSocket(),CreatePubSocket(),CreateSubSocket(),CreateXPubSocket(),CreateXSubSocket(),CreateStreamSocket()create runtime socket implementations.IContext.CreateSpotNode()andCreateSpotNode(SpotNodeMode)create the service-layer implementation.- A
Spothandle is obtained viaISpotNode.CreateSpot(),ISpotNode.EntrySpot(),ISpotNode.GetOrCreateSpot(...), orISpotNode.SpotLookup(...). Directly constructing aSpotis not public.GetOrCreateSpot(...)maps directly tozlink_spot_node_spot_get_or_new(...), and is never implemented by combining lookup and create in managed code. - An
Actorhandle is created withISpotNode.CreateActor(...). Directly constructing an Actor is not public. Zlink.CreatePoller(),Zlink.CreateTimer(),Zlink.CreateTimer(ISpot)create eventing resources.Zlink.Version(),Zlink.Has(...),Zlink.Strerror(...),Zlink.Proxy(...),Zlink.Sleep(...),Zlink.MultipartClose(...),ZlinkPoll.Poll(...)are public static facades. Even though their native calls remain inRuntime/, their callable behavior is part of the contract surface.
A factory's return type favors the public contract wherever the caller does not need the concrete runtime type.
Required feature coverage¶
The .NET public contract covers every stable, user-facing core feature. The shape may be narrower or more idiomatic than C, but the meaning stays the same.
- Context lifecycle, options, shutdown, auto-HWM recalculation, version, capability helpers, strerror.
- Message ownership, multipart payload, routing id, received metadata, topic message, subscription event. Payload share/transfer/duplicate follow the common contract's
Copy/Move/Clone:Message Copy()(ref-count share, internalzlink_msg_copy),void Move(Message dest)(ownership transfer, caller left empty,zlink_msg_move), andMessage Clone()(independent deep copy). .NET's existingCopyTo(Span<byte>)/CopyTo(IBufferWriter<byte>)are span-fill methods that write the payload into a caller buffer — separate from theClonedeep copy — so they stay unchanged. See the common Message ownership contract §"명시적 Copy / Move / Clone". - pair, dealer, router, pub, sub, xpub, xsub, stream sockets.
- Common options, typed socket options, TLS, bind/connect/disconnect, routing id, channel name, request/reply, publish/subscribe, callback surfaces.
- socket monitor, monitor event/snapshot, poller, poll event, timer, SPOT timer integration.
- SPOT node, SPOT handle, topology snapshot, actor ref, actor operations, actor lifecycle, stream actor binding.
- Typed exceptions for submit, request, recv, handler, close, bind, connect, config failures.
A native helper function that exists only to support whole-message array handling, callback userdata, interop marshalling, or request progress stays internal.
A Message receive wrapper may return to a bounded thread-local pool only
after its owner Received or TopicMessage removes part references and
completes Dispose(). The Message reference is invalid when deterministic
cleanup returns. The caller must not use it again, including repeated
Dispose(), payload access, or object-identity dictionary lookup. A wrapper
that has not been returned and a caller-created owned Message are not reused
for another ownership.
Receive and Subscribe shape¶
.NET's recv-family data-plane API uses caller-provided output storage for allocation-free draining.
- Message/routed receive fills a caller-provided
Receivedobject, created withReceived.Create(), and returnsbool. - Raw
SUB/XSUBand SPOT subscribe fill a caller-providedTopicMessageorSubscriptionEventobject and returnbool. TopicMessage.ReleaseForReuse()releases the current parts and metadata while retaining the internal topic receive buffers for a laterSubscribecall. It is valid only while theTopicMessageis open; after terminalDispose()it throwsObjectDisposedExceptionand does not reopen the object.ReceivedandTopicMessageown the managed lifetime of parts and metadata, released throughDispose(),ReleaseForReuse(), or storage reuse. Ordinary receive preserves the Router source RID and nullableReplyToken, and the SUB topic and source RID. The common receive ownership contract defines the boundary with receive accounting.falsemeans no data only for a non-blocking receive usingRecvFlags.DontWait.- A real receive failure (one that is not simply no-data) throws
ZlinkRecvException. - A control-plane API such as monitor recv or timer recv may keep a nullable return form when no-data is a natural value shape.
- A service control/admission API such as
RecvActorJoin(...)may also keep a nullable return form. These are not data-plane drain APIs, but they still distinguish no-data from a real receive failure (one that is not simply no-data).
SPOT's SubscribeReadable and RoutedReadable dispatch events are
readiness notifications. The caller drains the matching receive API until
no-data is reported.
Service and SPOT shape¶
SPOT is a service-layer API — it is never a leak of the raw socket.
ISpotNodeowns node lifecycle, route identity, peer connections, route bridge/channel coordination, external pub-ingress attachment, topology snapshot, spot creation, and actor creation.ISpotowns SPOT topic publish/subscribe, routed send/request/reply, routed receive, dispatch events, actor join receive/reply, and actor lifecycle callbacks.Spot.Publish(topic)enters the owning node's SPOT topic plane. It never exposes or selects a rawPUBsocket.Spot.Publish(topic)keeps its short publish name because the caller already holds a publishableSpot. The binding contract does not rename it toPublishSpotorPublishToTopic.- A channel-targeted SPOT operation uses
SendToChannel(...)andRequestToChannel(...), so the destination-bearing send/request names stay aligned withSendToSpot(...),RequestToSpot(...),RequestToRouter(...). - Actor location and stream session binding are independent of each other. An actor joining a user Spot does not require a bound stream session.
Byte HWM and monitoring ABI v4¶
- HWM is a limit on Core-computed accounted bytes, not a message count on the queue.
- The public type is
ulong, which does not shrink Core'suint64_trange. 0means unlimited, and the manual default is4_096_000 bytes.- The binding calls Core with an exact 8-byte value.
- The public surface provides no
intoverload, alias, or count-unit adapter.
public interface IContextOptions
{
ulong CoreHwmMemoryLimitBytes { get; set; }
ulong CoreHwmBudgetBytes { get; set; }
CoreHwmProfile CoreHwmProfile { get; set; }
}
public interface IContext
{
CoreHwmBudgetSnapshot GetCoreHwmBudgetSnapshot();
void ResetCoreHwmBudgetMetrics();
}
public partial class CommonSocketOptions
{
public ulong SendHighWaterMark { get; set; } // Outbound accounted-byte limit.
public ulong ReceiveHighWaterMark { get; set; } // Inbound accounted-byte limit.
}
Input precedence is manual Core budget, explicit memory limit, the available-
memory limit reported by .NET GC, then Core fallback. Setting either of the
first two values disables automatic GC-hint detection. The binding does not
combine the hint with Core's hard limit. If an explicit input exceeds a finite
hard limit Core detected, the binding preserves the existing configuration
exception corresponding to EINVAL and does not clamp the value.
HWM planning, manual overrides, admission, and value projection follow Core HWM calculation and admission.
0UL means unlimited.
MonitorOpen(events, monitorHwmBytes) accepts an exact ulong byte value for
the monitor queue. 0UL selects the Core monitor default; a positive value is
forwarded unchanged. There is no message-count overload or alias.
MonitorStatusprovides the same fields as the nativezlink_monitor_status_tABI version 4.- Planned, applied, and deferred HWM, and in-flight usage, are all
ulongbyte values. - A deferred value is valid only when the matching
AutoHwmDeferredSendHighWaterMarkValidorAutoHwmDeferredReceiveHighWaterMarkValidistrue. - A pending-message value stays a count diagnostic value,
SndPendingMsgsandRcvPendingMsgs, and never shares a name with a byte field. - Pending bytes are exposed separately as
SndPendingBytesandRcvPendingBytes. - If a snapshot's
AbiVersionis not4, or itsStructSizediffers from the binding layout, it throwsNotSupportedException. An older monitoring layout is not accepted.
CoreHwmBudgetSnapshot projects ABI version/size, configured/runtime/resolved
memory limits, configured/effective budgets, planned/applied/manual-reserved
HWM, Core-queue/application/current/peak/provisional accounted bytes,
completion current/peak/pending and total-messaging values, monitor/instance
aggregates, application/completion queue counts,
OutstandingApplicationLeaseCount, RetiredQueueCount,
DeferredOriginCreditBytes, oversize/blocked/aggregate flags,
BudgetGeneration, and MeasurementEpoch without unit conversion. Reset
preserves current, pending, and queue-count gauges, rebases both peaks to
current, clears epoch counters, and increments MeasurementEpoch.
ApplicationAccountedBytes and the three owner-lifecycle fields are
ABI-reserved and always zero. A budget snapshot ABI version/size mismatch
throws NotSupportedException.
Request/reply APIs take no HWM value as an argument. Timeout and waiting in Async(...) follow
the common request timeout and
completion execution model.
Receive flow state¶
The ReceiveFlowState enum provides Running = 0 and Paused = 1.
void ISocket.SetReceiveFlowState(ReceiveFlowState) throws a ZlinkConfigException whose
Result carries the failed native ConfigResult as ZlinkConfigException.ErrorCode.
State, result, and monitor projection follow the common receive-flow contract.
Error and validation policy¶
- A fixed-size native boundary value is validated before calling core.
- An invalid routing id, actor id, endpoint, channel name, or topic throws a .NET argument/config exception before truncation would occur.
- submit, request, recv, handler, close, bind, connect, and config errors map to a typed zlink exception.
- A typed zlink exception's public constructor must never accept the success value
Ok. TheOkenum member stays as a native result mirror, but the public constructor accepts only a failure code. A constructor that also accepts a native errno is for internal runtime conversion and is not public surface. - No-data and transient backpressure are never reported as an ordinary exception.
- The public API never requires a caller to inspect a native errno directly.
Performance policy¶
- The hot path never uses reflection, dynamic invocation, repeated boxing, avoidable allocation, avoidable buffer copies, hidden sleeps, busy waits, thread joins, or broad locks.
- Native interop creates
Message,Received, andTopicMessagevalues from the array filled by one Core whole-message receive call. A public, caller-ownedReceivedbuffer is created withReceived.Create(). - A caller that drains repeated publishes may call
TopicMessage.ReleaseForReuse()after the current consumers finish. This avoids reallocating the topic receive buffers without changing the ownership of the current message parts. - Request progress is shared per handle wherever possible. It does not create a new polling thread or timer per request.
- Perf, samples, and framework adapters use only the public contract and creation entry points.
Implementation checklist¶
Before declaring the .NET binding aligned:
Contracts/exposes every public behavior a user or framework adapter needs.Runtime/implements that contract without adding a hidden, user-facing API.- Concrete value types stay concrete.
- The default creation path is documented and tested.
- A public static facade, extension helper, or builder convenience method is discoverable in
Contracts/. - The recv/sub API uses the caller-provided storage shape.
- Any exception where service control/admission receive differs from the data plane's caller-provided storage is documented.
- Perf semantics match
bindings/c/perf. A private runtime shortcut never changes the meaning of a measurement. Contracts/'s public signatures never exposeRuntime/Native/, a raw handle, a native struct mirror, a request-progress type, or a runtime implementation class. An internal delegation from a static facade to runtime code is allowed.- A runtime class never becomes a second contract surface.
- A framework adapter calls the public binding API directly.
- The public surface has no compatibility-only alias, duplicate operation-start name, or deprecated wrapper.
Required verification after a .NET binding change. Run the following
commands from bindings/dotnet/.
- Run
dotnet test Zlink.sln, or the repository's current .NET binding test solution. - Run
./tests/run_tests.sh. - Run
./samples/run_samples.shwhen a public example or a generation path changed. - Run
./perf/run_benchmarks.shand./perf/run_benchmarks_multi.shas a smoke gate when hot-path, receive, send, request, poller, timer, or service behavior changed. - Search framework adapters, samples, perf, and tests for reflection,
NonPublic,InternalsVisibleTo,Runtime.Native, raw handle use, or direct request-pump access.
Actor and Spot Route results¶
.NET exposes route lookup results as a public contract record.
ActorRoutepreserves the resolvedActorRef,Actor.NodeRid,CurrentSpotRid,CurrentSpotKind.SpotRoutepreservesSpotRid,OwnerNodeRid,SpotKind.SpotKinddistinguishes an Entry Spot from a user Spot. An invalid kind is not a successful route result.-
SpotNodeSpotEntryandSpotNodeActorEntryexpose the same Spot kind/current Spot fields as the core snapshot. -
The binding exposes
ISpotNode.SendToActor(ActorRef)andISpotNode.RequestToActor(ActorRef), which take a resolved Actor ref. SendToActor, once submit succeeds, transfers ownership of one or more message parts, and completes once the Actor owner's mailbox takes them over.RequestToActor, once submit succeeds, transfers ownership of the request part and delivers the reply part the Actor handler produced, as a task or a callback.- The binding must not resurrect the removed Discovery route table or resolver API as a compatibility helper.
Pull completion public contract¶
.NET package information follows its distribution metadata; the Core ABI version follows Core release metadata.
.NET provides blocking Submit() and Async(CancellationToken) returning a result object (SendSubmission/RequestSubmission: Result and Admitted, plus Reply for a request).
The caller wait cancellation input is CancellationToken.
Native completion IDs, user_context, and raw drain are not public APIs.
Submission results follow the common result projection;
completion joins, lifetime, and progress conditions for PollEventFlags.PollCompletion follow the
async execution model.
ReplyToken is a sealed reference type created only by ROUTER REQUEST receive. It compares both owner
object identity and an opaque value, and does not expose the raw value even as a string. StreamPacket
is an empty reusable output created by a factory; Dispose() clears its payload for reuse. A token
provides no numeric constructor, raw accessor, ordering, serialization, or IDisposable. Concurrent recv
into the same output is invalid-state. Message references remain valid only until the next recv entry or
Dispose(). Before the first bind/connect, the receive-mode setter accepts only Raw and Packet and
rejects Unspecified.
Public interface¶
public readonly struct SendSubmission
{
public SubmitResult Result { get; } // OK | BACKPRESSURED, submit-time snapshot
public Task Admitted { get; } // completed when Result is OK
}
public readonly struct RequestSubmission
{
public SubmitResult Result { get; }
public Task Admitted { get; }
public Task<IReadOnlyList<Message>> Reply { get; } // completes after successful admission
}
public interface SendSubmitOperation
{
SendSubmitOperation Message(Message message);
void Submit();
SendSubmission Async(CancellationToken cancellationToken = default);
}
public interface RequestSubmitOperation
{
RequestSubmitOperation Message(Message message);
RequestSubmitOperation Timeout(TimeSpan timeout);
IReadOnlyList<Message> Submit();
RequestSubmission Async(CancellationToken cancellationToken = default);
}
public interface ReplySubmitOperation
{
ReplySubmitOperation Message(Message message);
void Submit();
}
public sealed class ReplyToken : IEquatable<ReplyToken>
{
private readonly object _owner;
private readonly ulong _value;
internal ReplyToken(object owner, ulong value)
{
_owner = owner;
_value = value;
}
public bool Equals(ReplyToken? other) => other is not null
&& ReferenceEquals(_owner, other._owner) && _value == other._value;
public override bool Equals(object? obj) =>
obj is ReplyToken other && Equals(other);
public override int GetHashCode() =>
HashCode.Combine(
System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(_owner), _value);
public override string ToString() => nameof(ReplyToken);
}
public enum StreamReceiveMode
{
Unspecified = 0,
Raw = 1,
Packet = 2
}
public sealed class StreamPacket : IDisposable
{
private StreamPacket();
public static StreamPacket Create();
public bool IsEmpty { get; }
public RoutingId? RoutingId { get; }
public Message? Header { get; }
public Message? Body { get; }
public void Dispose();
}
public interface IStreamSocket
{
SendOperation Send(RoutingId routingId);
bool Recv(Received result, RecvFlags flags = RecvFlags.None);
bool RecvPacket(StreamPacket result,
RecvFlags flags = RecvFlags.None);
}
The operation-start signatures are PAIR SendOperation Send(), DEALER
SendOperation Send() and RequestOperation Request(), ROUTER
SendOperation Send(RoutingId), RequestOperation Request(RoutingId), and
ReplyOperation Reply(RoutingId, ReplyToken), and STREAM SendOperation Send(RoutingId). A send
factory captures the target in the builder. Received.ReplyToken is nullable, and
StreamSocketOptions.ReceiveMode can be set to only Raw or Packet before the first bind/connect.
Received.Send() returns a SendOperation that captures the source target, and Received.Reply()
returns a ReplyOperation that captures the source RID and token.
The public .NET surface contains no send/request Flags or Submit(SendFlags), request callback
delegate/overload, IStreamSocket.TrySend, IStreamSocket.RecvPart, STREAM callback,
ISocketMonitor.OnEvent, IZlinkTimer.OnFire, or pair/generation property or method.
RoutedSendOperation and RoutedSendSubmitOperation are not public types.
Monitor provides MonitorEvent? Recv(RecvFlags = None), MonitorStatus Status(), and Close(). Timer
provides Start(TimeSpan, ulong), Stop(), ulong? Recv(RecvFlags = None), and Close(). The Core
connection_id projected into a monitor event is used only for diagnostics and correlation, not as a
send/reply target or reconnect fence. The internal native enum mirror uses only
ZLINK_OPT_PENDING_MAX_MSGS and ZLINK_OPT_PENDING_MAX_BYTES and adds no public option property.
Implementation and contract-test verification requirements¶
Verify the following using only the public .NET interface, Tasks, exceptions, and poller events. Each item maps to one contract test.
Operations and completion
- Send and request factories return
SendOperationandRequestOperationand expose only the terminal signatures in the Public interface section. - Common completion, cancellation, and poller observations follow the execution-model verification requirements.
- When HWM/
PAUSEDwaiting expires for a raw reply submitted to a DEALER peer,ZlinkSubmitExceptionreportsBackpressured; a reply submitted to a ROUTER peer retains the HWM-free result of the Completion connection.
ReplyToken and STREAM
- Tokens with the same owner and value are equal, while tokens with different owners are not. Reply started with a token from another socket fails before the native call.
StreamPacket.Create()returns an empty output. Afalseresult or error fromRecvPacket()leaves the output empty, and it can be reused afterDispose().
Pull eventing
- Monitor and timer DONTWAIT no-data is
null, and their pull methods expose events and fire counts without handlers.