Bindings API Policy¶
What this chapter defines — the public API policy that applies across all of
bindings/. The per-language documents (c/,cpp/,java/,dotnet/,node/,python/,go/,rust/) align to this policy.The implementation baseline for request-reply, SPOT routed, and Actor dispatch follows the current public contract in
core/include/zlink.h. Actor dispatch is an independent public service-layer capability, like SPOT, and the public surface described in each per-language document aligns to this shared contract as well. Seec/,cpp/,java/,dotnet/,node/,python/,go/,rust/for the per-language interface signatures and usage examples.
| Section | Covers |
|---|---|
| Purpose | This document's scope and the meaning of the Required/Target notation |
| Binding Contract Category Policy | Contract category classification |
| Binding Runtime Category Policy | Runtime category classification |
| Actor/Spot Route Surface | Route lookup result types, and Actor-targeted send/request |
| High-Performance Binding Policy | Hot-path constraints |
| Core Whole-Message API and Public Binding Surface | The boundary between Core's array API and the public surface |
| Whole-Message API Usage Requirement (Required) | The internal array-and-count rule |
| Spot Get-Or-Create Mapping | The zlink_spot_node_spot_get_or_new mapping rule |
| Public vs Internal API Boundary | The contract/runtime separation principle and its test |
| Core Alignment Rules | Alignment rules against the core contract |
| Actor Dispatch Binding Contract | The public Actor dispatch surface |
| Document Interpretation Rules | How to read the Required/Target notation |
| Core Principles | The core principles that run through this whole policy |
| Monitor Ready Contract | The meaning of monitor readiness |
| POSD Structure Policy | The deep-module, low-change-amplification structural standard |
| Public Surface Rules | Operation naming, builder, and terminator rules |
| Domain Object Policy | The value-type-vs-interface test |
| Socket Type Capability Policy | The capabilities each socket family exposes |
| Per-Language Spec File Compliance Rule | The relationship between the per-language documents and this document |
| Service Layer Policy | The public contract for the SPOT/Actor service layer |
| Core API Additions | Binding coverage for recently added core capabilities |
| Option Policy | Rules for exposing socket/context options |
| Performance Policy | Shared performance standards across all bindings |
| Boundary Cost Policy | The FFI/marshalling boundary-cost standard |
| Peer Weight Policy | The peer-weight contract |
| Monitor Policy | The monitor event/snapshot contract |
| Error Policy | Error representation and domain mapping |
| Length And Range Boundary Policy | Value validation and boundary limits |
| Ownership Policy | Message/handle ownership rules |
| Naming Policy | Shared naming rules across languages |
| Compatibility Policy | The ban on compatibility shims and deprecated wrappers |
| Cross-Language Alignment | How to verify consistency across languages |
| Test Policy | Test scope and standards |
| Test Matrix | A language-by-capability coverage table |
| Sample Policy | Sample code standards |
| Perf Policy | Perf-runner standards |
| Script Location Policy | Where test/sample/perf scripts live |
| Review Checklist | Checks to make during PR review |
| POSD-Based Implementation Completeness Policy | The test for declaring an implementation complete |
| Implementation Review Checklist | Checks before declaring an implementation done |
| Binding Requirements | Requirements every binding must satisfy |
| API Reference | Standards for generating API reference documentation |
| Disconnecting A Peer By Routing ID | The routing-id-based peer disconnect contract |
| Related Documents | Links to related documents |
| Core API Surface 6.0.0 Alignment | Alignment status for the 6.0.0 core API surface |
| Spot Route Bridge API | The route bridge API contract |
Purpose¶
This document defines the public API policy for all of bindings/.
Its purpose is to prevent each language binding from growing its own
surface and its own exception rules, and instead to enforce a shared
contract that can always be explained in terms of
core/include/zlink.h.
This document does not claim that every binding is already in this state
today. It sets the .NET binding's contract/runtime separation and file
granularity as the standard target, and gives the remaining wrapper
bindings a baseline to align to in stages. Items marked Required apply
immediately in the current review; structures and surfaces marked
Target apply as the goal of the work that aligns that binding. The C
binding is the native ABI baseline, so it follows a separate set of
exceptions.
The documents under c/, cpp/, java/, dotnet/, node/, python/,
go/, rust/ define the public API contract each binding implementation
must actually provide externally. What these documents govern is the
public types, methods, signatures, return values, and error semantics —
the public interface a binding implementation exposes must not diverge
from this contract. Every wrapper binding except C also separates the
public contract from the runtime implementation. This separation is fixed
by responsibility, but the physical directory and package/module path
follow each language's conventions. The actual path each per-language
README specifies is the implementation baseline for that binding.
This document is not a simple style guide. It is a design-standard document for: - public API design standards - review standards - refactoring standards - sample and test standards
Its intent is to: - eliminate APIs that look similarly named across languages but carry different meaning - eliminate shallow surfaces that expose the same capability through multiple redundant paths - reduce raw option bags, unnecessary convenience wrappers, implicit ownership, and hidden error paths - let a binding user avoid needing to know internal sequencing, native detail, or hidden transport switches - drive deep modules and low change amplification, per POSD principles - tie correctness together with the cost model, sample quality, and testability under one shared standard
The baseline is always core/include/zlink.h. Each binding follows the
core contract, and may choose its representation to fit language
convention — but the semantic contract must not change.
This document defines "what each language must guarantee," not "how each language may look."
Binding Contract Category Policy¶
Every binding must split its public contract into the same semantic categories. The actual folder, package, namespace, or module name can be adjusted to fit language convention, but which category a given public type belongs to must be judged by the same standard across bindings.
The point of this policy is not to make file placement look tidy. It is to let a user find a concept they learned in one language, at the same location and with the same meaning, in another language. So the contract subcategories follow the conceptual boundaries of the public API, not the implementation file structure.
| Category | Purpose | Includes |
|---|---|---|
core |
The library-wide foundational contract | Public types not tied to a specific socket or service, such as Context, ContextOptions, RoutingId, and version/capability lookup |
messaging |
The message data and receive-result contract | Payload types independent of socket kind, such as Message, Received, topic message, subscription event, and multipart payload helpers |
sockets |
The socket-kind and socket-operation contract | PairSocket, DealerSocket, RouterSocket, PubSocket, SubSocket, StreamSocket, socket interfaces, send/recv/publish/request/reply builders, socket options |
eventing |
The waiting, event-source, and observation contract | Poller, PollEvent, timer, monitor socket, monitor event, monitor snapshot |
service |
The core service-layer contract | Public types that belong to a service domain, such as Spot and Actor dispatch |
errors |
The public error and failure-representation contract | Base exception, bind/connect/send/recv/submit/config/request exceptions, public error-code/result mapping |
Contract Category Rules¶
- The
contract/runtimesplit is the split between the public API and implementation detail. A contract subcategory must not mirror the runtime's internal structure as-is. - Keep
coresmall. A type that can only be explained by knowing a specific domain belongs in that domain's category, not incore. servicecan have subdomains such asspotandactor. Create a subdomain only when a user must learn it as an independent concept.eventingdoes not mean monitoring alone. It also holds public contracts for waiting on or observing events, such as poller, timer, and monitor.errorsholds error surfaces shared across multiple domains. A type whose meaning is strongly domain-specific, such as the result of a particular socket operation, belongs in that domain instead.- Do not make a representation-format category such as
enumsa canonical category. Enums, flags, and results belong in the category of the public concept that interprets their value. - Put operation, result, and callback helper types in the domain that defines their meaning. For example, send/request/reply results and callbacks belong in the messaging contract, while Actor join/session/management results and callbacks belong in the service contract. A snapshot entry stays with the service model that returns that snapshot.
Handler Registration Naming Policy¶
A callback/handler registration function's name must reveal what it actually does. A name that reads like a function invoked when an event occurs, used for a registration function, can make a user unsure whether it's a hook they must implement or an API that stores a handler.
- A public API that stores a single handler for one subject, or replaces
the existing handler, uses a
set...Handler-family name. Spell it per language convention:Set...Handler,set...Handler, orset_..._handler. - A public binding's
set...Handlerkeeps only one active handler per subject. Calling the same setter again replaces the current handler. A raw native attach conflict or a recv-mode conflict may still be reported as a separate error, but the public setter name does not imply cumulative registration. - Only a public API that accumulates multiple handlers uses an
add...Handlerorregister...Handler-family name. - An
on...-family name is reserved for a protected/internal hook or a framework-level handler method invoked when an event occurs. It is not the canonical name for a handler-registration function. - An API that changes protocol state, such as a topic subscription, may
use
subscribe/unsubscribe. A function that simply stores a callback does not usesubscribe...Handler. - Do not create a surface that unregisters by setting a callback to
null/None. When unregistration is needed, handle it through the close/lifecycle rules instead.
The representative canonical semantic names are:
| Meaning | Canonical name |
|---|---|
| Registering a raw STREAM packet handler | setPacketHandler |
| Registering a SPOT dispatch event handler | setDispatchHandler |
| socket receive readiness handler registration | setReadableHandler |
| SPOT routed receive | recvRouted |
| SPOT Actor lifecycle receive | recvActorLifecycle |
The representative enum/result/flags placement rules are:
| Example type | Category | Reason |
|---|---|---|
SendFlags, RecvFlags, SubmitResult, RecvResult |
sockets |
Describes the input to or result of a socket operation. |
PollEventFlag, PollSourceKind, MonitorEventType |
eventing |
Describes an event-wait or observation result. |
SpotDispatchEvent, SpotPeerKind |
service.spot |
Its meaning is defined only inside the Spot service domain. |
ConfigResult, ErrorCode |
errors |
Describes a failure meaning shared across multiple domains. |
Every wrapper binding shares the same architecture map. This map is not a baseline for copying one language's folder names literally — it lets a concept learned in one language be found at the same responsibility location in another language's code. The actual file names, directory names, and package/module/import paths follow per-language convention and public API stability.
The shared architecture map is:
contracts/
core/
messaging/
sockets/
eventing/
service/
errors/
runtime/
native/
sockets/
messaging/
eventing/
service/
errors/
buffers/
options/
handles/
In this map, contracts is the public contract surface a user reads
first. contracts holds the types a user directly depends on: public
interfaces, public value objects, public result/flag/error types, and
public builders/facades. Implementation detail stays hidden in runtime.
Do not overuse public interfaces, however. Value objects and plain data
types such as Message, RoutingId, Received, TopicMessage, and
enum/result/flags are not split into separate interfaces. Interfaces
belong only where a user needs to receive something polymorphically —
for example, a common socket role, a poll target, a monitor target, a
codec, a handler/callback, or a SPOT client role.
runtime is the implementation area that executes the public contract.
It hides implementation decisions such as the socket send/recv flow,
message materialization, the poller/timer/monitor loop, the service
runtime, native interop, and buffer/handle/error mapping. A runtime type
is not recommended as public API, and a user should not depend on it
directly without going through the contract surface.
Interface / Implementation Separation Policy¶
Every wrapper binding except C follows the .NET binding's direction of
separating the public interface/contract from the runtime implementation.
Separating "like .NET" here does not mean every language copies names
such as IContext or Contracts/Runtime literally. It means the
contract a user sees and the implementation detail — native calls, handle
owners, callback bridges, request pumps — live in separate areas of
responsibility.
The separation rules are:
- Types, interfaces, traits, protocols, abstract roles, factories, builder start points, DTOs, value objects, enums, and error/result types that a user depends on go in the public contract source.
- A type that directly owns a native handle, calls the core helper substrate, manages a callback trampoline and request progress, or performs marshalling goes in the runtime or native bridge source.
- For a resource type where it's more natural for a user to depend on a
role than on an implementation —
Context, socket, poller, timer, SpotNode, Spot, Actor — separate the contract role from the default implementation in whatever way the language supports. - Value-centric types such as
Message,RoutingId,Received,TopicMessage, snapshot DTOs, and enum/flags/result are not wrapped in a meaningless interface/trait/protocol. Keep a value type as a concrete public type, and hide implementation detail inside the type itself when it needs internal native-backed storage. - Even in a language where a runtime concrete class must be publicly exposed, the behavior contract a user needs to understand must be explained in the public contract source first.
- Write samples, perf, and framework adapters against the public contract projection, not against a runtime concrete type or native bridge.
This separation is not just a naming split. A file on the contracts
side must be readable without knowing the native handle, native function
names, request pump, callback trampoline, or buffer-marshalling sequence.
Conversely, a file on the runtime side implements the public contract,
but must not itself become a public surface a user has to import.
The same standard applies to file structure.
- Use a category aggregate file only as a small re-export barrel or for
factory wiring. If a single category file such as
sockets,service, oreventingholds the actual behavior of several public resources, the contract/runtime split has not happened. - Put a native-backed resource's implementation in its own per-resource file. For example, each socket family, poller, timer, SpotNode, Spot, and Actor should have its own implementation file. The file name follows language convention but must reveal the resource or operation name.
- A shared helper file is not a substitute location for a public
resource's implementation. A helper file should hold only lower-level
functionality shared across multiple implementations, such as a native
call wrapper, handle validation, a marshalling helper, or error
mapping. Do not collect a resource's actual behavior — for
Context,RouterSocket,SpotNode,Poller— into a helper file. - A contract file and a runtime file do not need a strict 1:1 mapping, but for any given public resource, its contract owner and runtime owner must each be clear.
File Granularity Policy¶
Every wrapper binding also matches a similar granularity when splitting files. The goal is not to replicate file count or file names 1:1, but to let a reader of any language find the same conceptual grouping in a similarly sized file.
The baseline is the .NET binding's Contracts organization level. One
file holds either one independent public concept, or a small, tightly
coupled group of contracts that share the same reason to change.
The file-splitting rules are:
- A resource contract a user looks up directly —
Context, a socket family,SpotNode,Spot,Actor, poller, timer — can have its own file even if it's thin. - A type where ownership, storage, value validation, or cost model
matters —
Message,Received,TopicMessage,RoutingId— gets its own file. - A type with a weak independent reason to change — a marker interface, delegate, small enum, one-line record — merges into the nearest contract file. For example, a socket marker role merges with the socket base contract, and a stream packet handler delegate merges with the stream socket contract.
- Staged operation builder contracts such as send/request/reply share the same domain-level reason to change, so they can be grouped into one operation contract file.
- A group with a clear service subdomain — Actor join, actor management, SpotNode snapshot models — gets its own domain file. Split it further only once the model file grows large enough that distinct reasons to change appear, such as peer/status/socket/actor snapshots.
- The same principle applies to runtime implementation files. One implementation file holding several native-backed resources' lifecycle, send/recv/request flow, callback registration, and snapshot mapping all at once is too broad. Split such a file into per-resource implementation files and a shared helper file.
- Keep a category barrel small. As a rough guideline, once a barrel holds hundreds of lines of resource implementation beyond re-exports and simple factory wiring, treat that as a split failure. The real criterion is the reason to change, not the line count — if editing different public resources always means opening the same file, split it.
- Do not create a file based only on a representation format or a
catch-all name such as
Enums,Types,Models,Common, orUtils. A file name must reveal the domain concept a user is looking for, or the reason it changes. - Each language follows its own casing, suffix, and package convention.
For example, C# might spell it
OperationContracts.cs, Rustoperation_contracts.rs, and TypeScriptoperation-contracts.ts— but the same grouping of responsibility must be preserved.
When applying this standard, the alignment approach declared by the
per-language README takes priority. If a per-language README declares a
breaking alignment, aligning to the canonical surface takes priority over
preserving the existing public surface. Prefer to cleanly rearrange the
namespace, package export, crate re-export, package exports, or
generated declaration surface when moving files. Do not create a new
public wrapper or a shallow compatibility shim just to match the file
structure.
The per-language application follows these principles:
.NETtreatsContracts/<Category>andRuntime/<Category>as its standard structure. Public interfaces and public value objects go inContracts; implementation classes and native interop helpers go inRuntime.- Because a Java package is close to public API, Java puts public
interfaces/value objects under
systems.zlink.contracts.<category>, and implementation classes and native bridges undersystems.zlink.runtime.<category>orsystems.zlink.runtime.nativeapi. It does not create aFooContractinterface just to list methods. - C is the native ABI baseline, so it does not create separate contract/runtime folders. It expresses the same categories through header files, header sections, and documentation sections.
- C++, Go, Rust, Python, and Node follow each language's module/package/ export convention, but must still distinguish the public-facing surface from the runtime implementation. When a language does not naturally support interfaces, the same distinction must be made explicit in the documentation and the export surface.
If an existing binding has a monitoring or Monitoring category, treat
eventing as the canonical category. The name monitoring was
sufficient while only the monitor API existed, but eventing is the
broader and more accurate concept for a public contract that also covers
poller and timer. When cleaning up structure, describe new documents and
new files as eventing responsibility. An already-public monitoring
import/export path may be kept as a temporary alias only when the
matching per-language README explicitly commits to preserving
compatibility. A binding that has declared a breaking alignment cleans up
to eventing and does not keep a monitoring alias.
A representation-format folder such as enums is not a top-level
category in the shared architecture map. Enums, flags, results, and
literal unions belong in the domain category that interprets their
value. For example, RecvFlags belongs to the sockets contract,
PollEventFlags to eventing, and SpotPeerKind to the service
contract.
Binding Runtime Category Policy¶
A wrapper binding separates the public contract from the runtime implementation. Runtime is the implementation layer that performs the actual behavior behind the contract surface. The public contract shows a user what they can call, and runtime handles that call against the native substrate and the language's own execution model.
A runtime subcategory does not need to be strictly 1:1 with a contract subcategory. But the implementation responsibility and reason to change must be clear, and it must not grow into a thin pass-through class that merely repeats the public contract.
The recommended runtime categories are:
| Category | Responsibility |
|---|---|
native, or a per-language equivalent name |
P/Invoke, JNI, FFI, native function declarations, ABI type conversion, native symbol loading |
handles |
Native handle ownership, dispose/close, lifetime, reference tracking |
messaging |
Native message part assembly, multipart handling, message conversion, request progress |
sockets |
Socket operation execution, send/recv/publish/request/reply flow |
eventing |
Poller, timer, monitor, event dispatch loop |
service |
Spot, Actor service runtime |
options |
Public option validation, native option mapping |
errors |
Converting native errno/result into public exception/result |
buffers |
Byte buffer, direct buffer, pooled buffer, pinned memory, copy/borrow policy |
The name may differ because of a language reserved word. For example,
because native is a keyword in Java, it can use runtime/nativeapi
instead. Names can differ, but the responsibility must still be
explainable.
Runtime Category Rules¶
- Runtime does not promise public API stability. The public contract is defined by the contract documentation and the per-language public surface.
- A runtime implementation class hides behind a public contract interface or a public facade. If a user has to construct or call a runtime class directly, the contract design is leaking.
- If a contract interface only repeats the runtime implementation's method list 1:1, that's a shallow-module warning sign. Create an interface only where there's a real role abstraction, and never for a value object.
- Split runtime categories by reason to change. For example, adding a
native symbol should change
native; a send/recv flow change should changesockets; a message-ownership change should changemessagingorbuffers. - Do not use a catch-all name such as
core,common,utils,internal, ormiscas a canonical runtime category. These names make it easy to mix unrelated reasons to change into one place.
If an existing runtime has a monitoring or Monitoring category, the
canonical category is eventing, the same as for contract. Even a file
that only holds a monitor implementation belongs under eventing if it
shares a reason to change with the poller, timer, or event dispatch loop.
From a POSD perspective, this standard aims to get both public-surface readability and implementation information hiding at once. The contract gives users a small, clear surface to learn, while runtime absorbs implementation decisions such as how native calls are made, handle ownership, buffer pooling, and error mapping. Do not create an abstraction, however, when it doesn't actually reduce a real role and only repeats a method list — that adds complexity instead of reducing it.
Actor/Spot Route Surface¶
Every binding must expose the core's Actor route and Spot route results without loss. Per-language type names can differ, but the following meaning must be preserved.
- An Actor route exposes the Actor ref's node rid, current Spot rid, and current Spot kind.
- A Spot route exposes the looked-up Spot rid, owner node rid, and Spot kind.
- Spot kind distinguishes Entry Spot, user Spot, and an invalid value.
- A binding does not create a new direct
router -> actororactor -> routerAPI. A user combines a route lookup result with the existing Spot routed API.
High-Performance Binding Policy¶
zlink is a high-performance messaging library. A binding may add per-language convenience, but it must not hide or worsen the hot path's cost model. The public API and internal implementation must follow the principles below.
- Do not use reflection-based dynamic dispatch on the message send/recv, publish/subscribe, request/reply, dispatch callback, poller, or timer path. Even when a language runtime requires reflection, restrict it to initialization or binding-registration time, and never use it in the message-processing loop.
- Reflection is not a workaround for a missing API. A high-performance binding must use a typed facade, a direct native downcall, or a direct internal bridge, and must not add a reflective lookup to the hot path just to satisfy the public contract.
- Do not create unnecessary allocation. A repeated call must not construct a new temporary array, wrapper, closure, or boxed object of the same size every time.
- Do not create unnecessary copies. Move a message part received from
core into the language's own
Message-owned object as directly as possible, and do not copy the byte buffer again without a decode step or an explicit user request. - Do not put a global lock, coarse lock, avoidable mutex contention, or a shared-executor serialization point on the hot path. Limit necessary synchronization to the minimum scope that protects per-subject state.
- Do not perform a hidden blocking wait, sleep, busy wait, or thread join on the callback, dispatch, poller, timer, or request-completion progress path. Only a call explicitly documented as a blocking API may wait.
- A binding passes a reusable native array and count to the Core whole-message
API. It moves received slots directly into language-owned
Messageobjects without creating another intermediate collection with the same content. - Perf, sample, and test code used for performance verification must also use only the public binding entrypoint, and must not break the cost model above.
This section is not an implementation-detail optimization recommendation — it's a public binding conformance requirement. If review finds a reflection hot path, unnecessary allocation/copy, thread contention, or a hidden wait, that binding is considered non-compliant.
Core Whole-Message API and Public Binding Surface¶
A binding runtime uses the whole-message API in core/include/zlink.h as its
internal foundation. A send, request, reply, or publish builder submits every
collected part in one call with an array and count. Receive, routed receive,
and subscribe pass a caller-provided array, capacity, and count to receive the
complete record. When the array is too small, the runtime grows it to the
required count after ZLINK_RECV_BUFFER_TOO_SMALL and retries the same record.
The public Received, Message, collection, and request/reply builder shapes
for Java, .NET, Go, Rust, Python, Node, and C++ stay unchanged. Native arrays,
capacity growth, and retry remain inside the runtime. The C binding uses the
array-based zlink.h ABI directly.
Request-Reply Protocol Metadata Boundary¶
A binding passes only application payload arrays to the request and reply whole-message APIs. Core owns the ZMP kind, wire sequence, and header extension, so a binding neither constructs a protocol envelope nor header nor adds one as a payload part. If an application supplies N multipart request or reply parts, its handler and completion also observe exactly N application parts.
In a language where a binding creates a native copy or view from a public Message, request-reply
metadata is attached only to that Core-consumed native message. A binding must not write metadata
to the public source or allow a failed submit's source to regain a request-reply kind when reused
for raw send. Whether the source is preserved or consumed continues to follow that language's
existing ownership contract.
Public receive and request completion aggregate only payload after Core removes the internal metadata. A binding neither reads ZMP kind from a raw message nor exposes it as a public metadata field.
Whole-Message API Usage Requirement (Required)¶
The internal implementation of send, request, reply, publish, and receive
families must make one Core whole-message call per record. This is a
Required rule.
Scope¶
This applies to every binding-internal implementation path in the following families.
- send (including single-part, multi-part, and routed)
- recv (including single-part, multi-part, and routed)
- request (including dealer, router, and SPOT variants)
- reply (including router and SPOT variants)
- publish
- subscribe (including SPOT subscribe)
Array Handling¶
- A send family materializes builder payloads in a reusable native array and submits it once.
- A receive family passes a reusable caller array. If capacity is insufficient, it grows the array to the required count and retries the unconsumed record.
- Successful receive slots move into language-owned objects without copying an intermediate aggregate.
The Public API Shape Stays The Same¶
This rule is about the internal implementation foundation. The public API shape a binding user sees stays whatever each language's spec defines, regardless of this rule.
- A user still uses a language-friendly API such as
send(List<Message>),recv(), orrequest(...). - A public binding's receive surface offers only an aggregate
result-storage API such as
recv,subscribe, orrecvRouted.
Multipart Submission¶
One Core call atomically submits the complete record, so a binding does not add a socket-local gate for a part sequence. Independent concurrent records follow Core's same-handle concurrency contract.
Spot Get-Or-Create Mapping¶
Core provides zlink_spot_node_spot_get_or_new(...) for the atomic
"get a local logical Spot by routing id, or create it if absent"
contract.
Every higher-level binding must map its public get-or-create SpotNode API
directly onto that C function. It must not compose spot_lookup() and
create_spot() to emulate the same behavior, because doing so loses
core's atomicity contract and reintroduces the lookup/create race.
The per-language names are:
- C++:
spot_node_t::get_or_create_spot(...) - .NET binding:
SpotNode.GetOrCreateSpot(...) - Java:
SpotNode.getOrCreateSpot(...) - Node:
SpotNode.getOrCreateSpot(...) - Go:
SpotNode.GetOrCreateSpot(...) - Rust:
SpotNode::get_or_create_spot(...) - Python:
SpotNode.get_or_create_spot(...)
Each wrapper returns both the owned Spot facade and whether this call
created the logical spot. The returned facade follows that language's
normal Spot lifetime rules.
Compliance Check¶
Confirm the following during implementation review and verification.
- Raw-socket paths in binding source use
zlink_send,zlink_send_rid,zlink_request,zlink_reply,zlink_publish,zlink_recv,zlink_router_recv,zlink_subscribe, andzlink_xpub_recvaccording to their contracts. - Send families make one native call per record. Receive families pass an array
and capacity for the complete record and grow to the required count before
retrying on
ENOBUFS. - Non-compliance blocks the review.
Public vs Internal API Boundary¶
Every binding must separate the public contract from the internal
implementation surface. This document and each per-language README
define the public API's boundary and the library's shape. The exact
function, method, and type list is owned by the public contract source
that each wrapper binding's per-language README designates, except for
C. For C++ and .NET, that location is a literal Contracts/ folder; for
Java, Node, Python, Go, and Rust, it's the package, module, or export
surface each README designates. The installed header, package
entrypoint, .d.ts, __init__.py, and lib.rs re-export are the
projections that expose this contract to the user. C is the exception —
core/include/zlink.h is the single baseline for the public C ABI.
The following principles apply to every binding in common.
- Any type, function, method, module, package, or namespace not included in the per-language public contract source is treated as internal implementation detail.
- A per-language README does not repeat every public member. Instead it defines the public contract source location, source layout, API-change procedure, runtime/internal boundary, and performance policy.
- An internal API is not enough to merely look internal by name. Where a
language supports it, use a language-native boundary — package export,
module export, assembly visibility, crate re-export, package
exports, aninternal/directory — to actually restrict access. - In principle, perf, sample, and test code must also use only the public binding entrypoint. Being in the same repository does not license a direct import of or reference to an internal helper.
- Public contract verification is judged against the entrypoint a deployed binding consumer actually sees. The mere existence of an internal symbol inside the source tree does not make it public.
- A binding that ships an installed header alongside a compiled binding
library, like C++, keeps a public
Contracts/inside the installedinclude/tree, and hides the implementation as private files underbindings/cpp/src/Runtime/. An aggregate header may still exist, but it must not become the only entry point for finding a public class. - The freedom to refactor internal structure is guaranteed, but only within the scope that preserves the public contract.
In other words, this document's purpose is not only to define the public API boundary and library shape — it also includes enforcing that boundary so a non-public API cannot be used as if it were public.
Per-Language Contract/Runtime Separation¶
Every wrapper binding except C must separate the public contract from the
runtime implementation. However, how it separates them must follow that
language's package, module, and import-path rules. A per-language README
must specify both the actual repository path and the actual
package/module path. Contracts and Runtime are shared logical
category names — they do not mean every language must turn that literal
word into a public package or import path.
C++ is a C++20 binding; its public contract root is
bindings/cpp/include/zlink/Contracts/ and its runtime implementation
root is bindings/cpp/src/Runtime/.
Because a Java package path is itself the source folder, Java reveals the
role structure through lower-case Java packages such as
systems.zlink.contracts.* and systems.zlink.runtime.*.
Languages such as Go, Rust, and Python, where the folder path connects
directly to the package/module/import path, also separate the public
contract from the runtime implementation inside the actual
package/module tree.
For Node/TypeScript, package.json exports set the public boundary, but
because the source folder name can also cause deep-import confusion, it
follows the actual source path and package-export rules its per-language
README specifies.
C is the native C ABI baseline. C's public contract is
core/include/zlink.h, and bindings/c aligns its sample, test, perf,
packaging, and any necessary mapping policy against that C API. C is not
forced to have a separate Contracts//Runtime/ layering.
Contracts is the role for the public contract source a user must check.
Runtime is the role for implementation detail such as a native handle,
callback bridge, request progress pump, helper substrate call, or object
lifetime correction. Native is the role reserved for the native
bridge — FFI, P/Invoke, JNI/Panama, N-API, cgo. In a language the
document names explicitly, such as C++ and .NET, these role names are
used as the actual folder names; in other languages, the same role is
expressed through the package/module/export structure the per-language
README specifies.
Contracts and Runtime are shared role names. That does not mean they
are the public package, namespace, module, or import-path name. A
language where directory structure directly affects the package/module
path does not expose Contracts or Runtime as a public import path.
Instead, it places the contract at an actual path inside the public
package/module tree, and keeps the runtime implementation inside a
language-specific private boundary such as internal, a private module,
an unexported module, or a pub(crate) module.
Contract / Runtime Placement Rules¶
The following criteria apply to every wrapper binding except C. Check this table first when adding a new public API or moving an implementation.
| Item | Location |
|---|---|
| A public behavior contract a user calls or references by type | The matching category in public contract source |
| The contract for a public constructor, factory, or builder start point | The matching category in public contract source |
| A public free function, static facade, extension helper, or module function | The matching category in public contract source |
| A public builder convenience method or helper | The matching category in public contract source |
| A DTO, value object, enum, or public error/result type | The matching category in public contract source |
| A runtime concrete class, socket kernel, or handle owner | The matching category in runtime/internal source |
| A request progress pump, callback trampoline, or whole-message array helper | The matching category in runtime/internal source |
| A native handle wrapper, FFI declaration, struct mirror, or marshalling helper | Native bridge source |
| Generated native loading code, platform artifact lookup | Native bridge source |
The judgment rules are:
- A public contract type's public signature does not reference a native bridge type.
- If runtime/internal source needs a user-facing method, add the contract to public contract source first. The runtime implementation implements or projects that contract.
- If a helper a user calls directly is public — whether it's shaped as a class method, static method, free function, extension method, or module function — put the contract in public contract source. Do not leave it in a runtime-only location just because it's a simple convenience function.
- A public factory may return a runtime concrete type. But its construction behavior and the user-observable behavior of the returned type must be explainable in public contract source.
- Do not expose the runtime/internal folder name or the module/package
path itself as public API. However, a basic implementation class or
type such as
Context, socket,SpotNode,Poller, orTimermay be exposed as a per-language public projection. In that case, the public behavior a user observes must still be explainable in public contract source. - A public contract type's public signature does not reference a native bridge type. Even when a concrete value object's internals must use native-backed storage, keep the P/Invoke/JNI/N-API/cgo declaration and the marshalling-only struct mirror in native bridge source.
- Keep a value-only DTO/value/enum/error/result type concrete. Do not wrap it in a meaningless interface, trait, or protocol for the sake of symmetry.
The fixed categories are:
Core/: context, version, roles, and utility resources.Messaging/: message, routing id, received, topic message, multipart.Sockets/: socket contracts, socket implementations, socket options.Eventing/: monitor, poller, timer, readiness events.Service/: SPOT, actor, SPOT topology.Errors/: public error/result/exception domains and runtime mapping.Native/: the native bridge category, kept only under runtime/internal source.
These category names are fixed in the documentation and review standard.
The actual file and folder names follow the convention each per-language
README specifies. If a new category is needed, change this shared policy
together with the per-language README structure (except C) before using
it. Do not create a Native category in public contract source — the
native bridge always lives under runtime/internal source.
The wrapper binding's shared structure is fixed to the following role structure. Except for C, each per-language README must show this structure again using that language's actual repository path and package/module/import path, and the implementation must match that structure.
bindings/<lang>/
+-- <public-package-or-module-root>/
| +-- <public contract categories>
| | +-- Core
| | +-- Messaging
| | +-- Sockets
| | +-- Eventing
| | +-- Service
| | +-- Errors
| +-- <private runtime/internal area>
| | +-- Core
| | +-- Messaging
| | +-- Sockets
| | +-- Eventing
| | +-- Service
| | +-- Errors
| | +-- Native
+-- codecs/
+-- tests/
+-- samples/
+-- perf/
+-- native/
+-- runtimes/
The shared standard is:
- The public API contract a user must check should be gathered in an easy-to-find location.
- Implementation detail such as a native handle, callback bridge, request progress pump, helper substrate call, or object lifetime correction must not mix with the public contract.
- Keep a DTO, value object, enum, or error/result object as a concrete type. Do not wrap a value-only type in a meaningless interface or trait.
- A type that hides a native resource and its behavior — socket, context, monitor, timer, service node, spot, actor — may have an abstraction boundary that fits the language's convention.
- In principle, write perf, sample, and framework adapters against the public contract too. Depending on a runtime-internal type just because it's in the same repository weakens the public/internal boundary.
The per-language application direction is:
| Binding | Application standard |
|---|---|
| C | core/include/zlink.h is the single baseline for the public C ABI. bindings/c does not add a separate contract/runtime layer, and aligns only the C-API-based mapping, sample, test, perf, and packaging policy. |
| C++ | bindings/cpp/include/zlink/Contracts/ is the public C++ contract location. bindings/cpp/src/Runtime/ is the private implementation location. It prefers C++20, RAII classes, and concrete values, and does not over-wrap a public class in a virtual interface. |
| .NET | Detailed standards follow the .NET binding blueprint. This document does not duplicate .NET's detailed file structure. |
| Java | The public contract package under bindings/java/src/main/java/systems/zlink/contracts/ is the public contract location. Because Java follows URL-based package layout, it reflects the lower-case contracts and runtime packages in the actual folders. The native bridge lives under the non-exported systems.zlink.runtime.nativeapi. |
| Node | bindings/node/src/index.ts and the package.json exports are the public contract projection. Contract source lives at a lower-case source path such as bindings/node/src/zlink/contracts/, and the runtime/native addon implementation is hidden under bindings/node/src/zlink/runtime/. |
| Python | bindings/python/src/zlink/contracts/ is the public contract source. The zlink root package is the projection that re-exports this contract, and the native/FFI implementation lives under private packages such as _runtime/ and _native/. |
| Go | The bindings/go/contracts/ public package is the Go public contract source. Currently the runtime/native implementation is owned by unexported implementation files at the root and by cgo bridge files. If it is split into a separate package later, it should be hidden under Go's internal/ convention. |
| Rust | bindings/rust/src/contracts/ serves as the public contract source. lib.rs re-exports the necessary types as a crate-root/domain projection, and bindings/rust/src/runtime/ and bindings/rust/src/runtime/native/ stay private modules. |
A review is not judged by simply "does an interface exist," but by the following questions.
- Can a user understand the usable API just by looking at the public contract?
- Does the public contract avoid directly requiring a runtime concrete type, native handle, or helper bridge type?
- Does the file hold an independent concept, or a group that shares the same reason to change?
- Could a thin file that holds only a marker, delegate, small enum, or one-line record be merged into a nearby contract file instead?
- Has abstracting a value type blurred equality, ownership, or the cost model instead of helping?
- Does it use the language ecosystem's natural encapsulation mechanism?
Per-Binding Target Physical Layout¶
Each per-language README treats the path and role below as the target for
new alignment work. If the current implementation still differs from
this structure, align it in stages together with that binding's API,
sample, and perf work during its structural cleanup. This does not mean
the public package, namespace, module, or import path directly exposes
the Contracts or Runtime name below.
| Binding | Contract root | Runtime root | Public projection |
|---|---|---|---|
| C++ | bindings/cpp/include/zlink/Contracts/ |
bindings/cpp/src/Runtime/ |
#include <zlink.hpp> and installed include/zlink/... headers |
| .NET | See dotnet/README.md | See dotnet/README.md | See dotnet/README.md |
| Java | bindings/java/src/main/java/systems/zlink/contracts/ |
bindings/java/src/main/java/systems/zlink/runtime/ |
exported systems.zlink.contracts.* JPMS packages and Maven artifact |
| Node | bindings/node/src/index.ts and bindings/node/src/zlink/contracts/ |
bindings/node/src/zlink/runtime/ |
package root export, generated .d.ts, and package.json exports |
| Python | bindings/python/src/zlink/contracts/ |
bindings/python/src/zlink/_runtime/ and bindings/python/src/zlink/_native/ |
zlink package exports from __init__.py |
| Go | bindings/go/contracts/ public package |
current root unexported implementation files and cgo bridge files; future split should use bindings/go/internal/... |
exported identifiers in zlink.systems/zlink/contracts |
| Rust | bindings/rust/src/contracts/ |
private bindings/rust/src/runtime/ and bindings/rust/src/runtime/native/ modules |
lib.rs re-exports and public rustdoc projection |
Each per-language README must show where the Core, Messaging,
Sockets, Eventing, Service, and Errors roles are actually placed
in its source. Native exists only as a runtime/native bridge role and
is never made a public contract role.
Package / Namespace Identity Policy¶
The official library domain is zlink.systems. Any per-language package,
namespace, module, or artifact name being newly fixed or changed must
start from this domain, and must not put a prior organization name or a
repository owner's name into a canonical public identifier.
| Binding | Canonical public identity |
|---|---|
| C | The public header is zlink.h; the symbol prefix is zlink_ |
| C++ | The namespace is zlink; the installed header root is include/zlink/ |
| .NET | The NuGet package id and root namespace are Systems.Zlink |
| Java | The Maven group id, JPMS module, and root package are systems.zlink |
| Node | The npm package is @zlink-systems/zlink; the public entrypoint is the package root |
| Python | The distribution name and import package are zlink |
| Go | The module path is zlink.systems/zlink; the public package is zlink |
| Rust | The crate name and public crate root are zlink |
- A framework extension package and namespace stays under that
framework language's canonical identity. For example,
.NETusesZlink.Framework.*, and Java usessystems.zlink.framework.*. - The raw payload distribution scope defines the boundary between bindings and codecs.
- A Node extension package's name follows ecosystem convention, but its
public identity must not drift outside the
zlinkandzlink.systemsdomain. - New documents, samples, and tests use only the canonical identity.
- Even if an old
Zlinkroot namespace or package id remains for implementation compatibility, it is not the canonical public identity, and no new public API is added under it.
Core Interface Shape Rules¶
This section summarizes the required public interface shape for every
wrapper binding except C. See the recv section and operation builder
section further below for the detailed contract. C keeps the functional
ABI of core/include/zlink.h as-is, so this wrapper rule does not apply
to it.
- The data-plane
recvandsubscribefamilies take caller-provided output storage. The caller creates a result object such asReceived,TopicMessage, orSubscriptionEvent, and the binding updates that object. - A data-plane receive's return value expresses only "was data
received." A hard error is delivered as a typed exception,
error, orResult, per language convention. - A control-plane API such as
Monitor.recvorTimer.recvis called infrequently and returns a small result, so a per-language nullable, optional, or value-return form is allowed. - A service control/admission receive such as
Spot.recvActorJoinis also not a data-plane drain path, so a per-language nullable, optional, or result-value form is allowed. However, no-data and a hard error must still be separated, and the public contract must clearly document this exception. send, routed send,publish,request,reply, SPOT send/request/reply, and the Actor location/session-attach family return an operation builder.- A builder start point's arguments take only the operation's target — destination, topic, channel, routing id, or request sequence. Payload, flags, and timeout are expressed at the builder stage.
- Multipart payload accumulates through repeated
message(...)calls on the builder. Amessages(...)convenience may exist per language convention, but the canonical path is the builder. If such a convenience is public, it is part of the builder contract and belongs inContracts/. - Do not multiply operation-start names such as
sendNoWait,publishWithFlags,requestAsync, orrequestCallback. Keep the same operation name, and let the builder stage absorb the variation. The per-language final execution method for native suspension completion surface follows the bindings async execution surface policy. - Resource creation is not scattered across public constructors on
several runtime classes. A per-binding root facade or context factory
owns construction responsibility. For example, the .NET binding
creates a context with
Zlink.CreateContext(), and creates socket and service resources throughIContext.Create...factories. - A runtime concrete type must not appear directly in a public contract signature. A public method's arguments and return value must be explainable through a contract interface, value object, DTO, enum, or result/error type.
- Samples, perf, and framework adapters use only this canonical interface. Do not write new code against a runtime-internal helper or a legacy overload.
The Send/Recv Public Shape Is Fixed¶
The public send/recv shape of the bindings is not something to
redecide every time the Core ABI changes. It is fixed to
the public shape this document and each per-language binding spec
define.
In other words, even if Core's whole-message array, count, and capacity shape changes, the binding's public API must keep the following principles.
- A binding user sees the
send,recv, request/reply, and callback shape defined in the language document. - Multipart can continue to be offered through whatever aggregate convenience model each language document defines.
- A binding's public
send/recvshape must not be changed just because the Core ABI changed. - Changing the public shape must be treated as a public API change
separate from a Core ABI change, and the
doc/spec/bindings/document must be updated first.
In other words, even if the Core C API changes, a
binding's send/recv is "the implementation foundation changing," not
"the shape the user sees changing automatically."
Canonical Recv: Caller-Provided Storage¶
For a high-level binding (C++ / .NET / Java / Node / Python / Go / Rust), the data-plane recv surface's canonical form is a ref-out shape that takes a caller-pre-built result storage as a parameter and updates its internal state. A shape that allocates and returns a new result instance on every call forces hot-path allocation overhead, so it is not used as the canonical surface.
This rule is Required. When building a new binding or updating an
existing one, the canonical recv surface must satisfy this section.
Scope (all data-plane recv)¶
| Surface | Result type (caller storage) |
|---|---|
MessageSocketBase.recv (PAIR / DEALER) |
Received |
RoutedMessageSocketBase.recv (ROUTER) |
Received |
StreamSocket.recv |
Received |
SubscriberSocketBase.subscribe (SUB / XSUB) |
TopicMessage |
XPubSocketBase.receiveSubscriptionEvent |
SubscriptionEvent |
Spot.subscribe |
TopicMessage |
Spot.recv (routed) |
Received |
Monitor.recv (MonitorEvent) and Timer.recv (uint64) are
control-plane calls, called infrequently and with a lightweight value
result, so they are not in scope for this section. They keep a
return-form (or a per-language Optional/nullable/Option). A service
control-plane API that receives an Actor join admission request, such as
Spot.recvActorJoin, can apply the same exception. In that case, the
public contract must document the no-data representation and the hard
error representation separately.
Base Contract¶
- The
recvcaller pre-builds a long-lived result storage and passes the same instance on every call. The binding reuses its internal part collection, routing-id storage, and topic buffer as much as possible, driving per-recv allocation toward zero. - The return value carries only "was something received" — a boolean, or an equivalent representation that distinguishes success from no-data. A hard error is delivered as an exception or error code, per language convention.
- When a call with a non-blocking flag such as
recv_flags_t::dontwaitfinds no data, it returns a no-data representation used together with caller-provided storage, such asfalse,recv_result_t::no_data,(false, nil), orOk(false). It does not signal EAGAIN as an exception. - A multipart result accumulates into the caller's result storage. The binding must not build a temporary collection and cache it separately from the caller's result storage — that allocation would not go away.
- For routed recv (router / spot), the routing id must be filled into
storage inside the caller-provided
Received. A path that allocates a new byte array per routing id does not belong on the internal hot path.
Canonical Per-Language Signature¶
Apply the same ref-out pattern to each surface in the table above. Below
are examples keyed on the Received result type; TopicMessage and
SubscriptionEvent follow the same pattern.
| Binding | Canonical signature |
|---|---|
| C++ | int recv(received_t& out, recv_flags_t flags = recv_flags_t::none); 0 = success; failure or no data returns a recv_result_t integer value. If a local failure such as message initialization happens inside the binding, it returns -1 and sets errno. Multipart results fill out.parts. subscribe(topic_message_t& out, int flags) and receive_subscription_event(subscription_event_t& out, int flags) follow the same rule. |
| .NET | bool Recv(Received result, RecvFlags flags = RecvFlags.None); bool Subscribe(TopicMessage result, RecvFlags flags = RecvFlags.None); bool ReceiveSubscriptionEvent(SubscriptionEvent result, RecvFlags flags = RecvFlags.None); A Received storage is created with Received.Create(). true = received, false = no data (DontWait). A hard error is ZlinkException. |
| Java | boolean recv(Received result, RecvFlags flags); boolean subscribe(TopicMessage result, RecvFlags flags); boolean receiveSubscriptionEvent(SubscriptionEvent result, RecvFlags flags); |
| Node | recv(received: Received, flags?: RecvFlag): boolean; subscribe(topic: TopicMessage, flags?: RecvFlag): boolean; receiveSubscriptionEvent(event: SubscriptionEvent, flags?: RecvFlag): boolean; |
| Python | def recv_into(self, received: Received, *, flags: int = 0) -> bool: ... def subscribe_into(self, topic: TopicMessage, *, flags: int = 0) -> bool: ... def receive_subscription_event_into(self, event: SubscriptionEvent, *, flags: int = 0) -> bool: ... |
| Go | func (s *Socket) Recv(out *Received, flags RecvFlags) (bool, error) func (s *Socket) Subscribe(out *TopicMessage, flags RecvFlags) (bool, error) func (s *Socket) ReceiveSubscriptionEvent(out *SubscriptionEvent, flags RecvFlags) (bool, error) |
| Rust | pub fn recv(&self, out: &mut Received, flags: RecvFlags) -> Result<bool, RecvError>; pub fn subscribe(&self, out: &mut TopicMessage, flags: RecvFlags) -> Result<bool, RecvError>; pub fn receive_subscription_event(&self, out: &mut SubscriptionEvent, flags: RecvFlags) -> Result<bool, RecvError>; |
A C ABI binding is not in scope for this section. The C binding exposes
zlink.h's typed substrate (zlink_router_recv,
zlink_subscribe, and so on) as-is.
Unifying Received Envelope Meaning¶
For a high-level binding (C++ / .NET / Java / Node / Python / Go / Rust),
Received is a shared envelope that holds the result of one
data-plane recv call. The meaning of request, reply, routed source, and
payload lifecycle must stay the same regardless of socket kind or
service kind.
The rules below are Required.
- The receive results of PAIR / DEALER / ROUTER / STREAM / SPOT routed
recv all use the same
Receivedmeaning. - A request-reply receive result must not fork into a separate
protocol-specific result type. A surface that splits request meaning by
socket kind into separate public types —
DealerReceived,RouterReceived,SpotReceived— is not canonical. - Request meaning is independent of socket kind. If
request_seqis present, the receive result has request-reply context; if not, it's an ordinary receive result. - The reply target, send-back target, and source routing metadata are
encapsulated inside
Received's own context. A user must not need to know a socket-kind-specific frame format or internal dispatch rule to handle a request. - Per-language names and optional representations (
null,None,Optional,Option, a zero value plus ahasflag, and so on) can differ, but the canonical field/method meaning must match the Domain Object Canonical Shape section.
The C ABI binding is an exception. C does not build a managed/object
result storage; it exposes the same envelope components through typed
out-params such as zlink_router_recv(), zlink_spot_recv_part(),
and zlink_dealer_recv_part(). Do not add a public aggregate object such
as zlink_received_t to C — doing so would grow message-part ownership,
init/close/reset, and reply-context retention into a new public lifetime
contract. If a C helper is needed, keep it only as a sample/perf/internal
helper.
A per-language detail document may separately list a deprecated overload kept for backward compatibility. The table above lists only the canonical path new code and samples/perf must follow.
Result-Storage Reuse Contract¶
- A result storage (Received / TopicMessage / SubscriptionEvent)
automatically resets its internal state before receiving a new recv
result. Passing the same instance to
recvrepeatedly is normal usage. - .NET's
Receiveddoes not expose a public constructor. A caller usesReceived.Create()to build the storage to be filled.Receivedis a concrete receive buffer and is not split into a separate read interface. - If a caller calls the next recv without separately
move-ing the previous recv's part messages, the previous message must be closed appropriately. The binding provides a separate helper (such astakeFirstPart) that hands ownership of a partMessageto the caller. - Thread safety does not guarantee that multiple threads can pass the same result storage into recv concurrently. The existing policy that a socket is recv'd by a single thread still holds.
Operation Builder Policy¶
zlink's send/request/reply/publish family, and the Actor
location/session-attach family, all have many combination axes. Spreading
target path, payload part count, flags, timeout, and the native-suspension
completion mode across plain method overloads makes a
socket or service handle a shallow, wide interface, and forces multipart
payload to be wrapped in an external List/Vector container. A high-level
binding hides this combinatorial complexity inside an operation object,
and multipart naturally accumulates through repeated message(...) calls
on the builder.
This policy does not apply to the C ABI binding. The C binding keeps the
functional contract that matches zlink.h. It applies to the canonical
public API of high-level bindings such as C++ / Java / .NET / Node /
Python / Go / Rust.
Start Points In Scope¶
An operation builder start point is exposed with the same pattern across every send, request, reply, publish, Actor location, and Actor session-attach surface. The name is converted to fit language convention.
Spot facade (Spot / spot_t)¶
publish(topic)sendToChannel(channelName)/send_to_channel(channel_name)sendToSpot(destNodeRid, destSpotRid)/send_to_spot(...)requestToChannel(channelName)/request_to_channel(...)requestToSpot(destNodeRid, destSpotRid)/request_to_spot(...)requestToRouter(peerRid)/request_to_router(...)replyToSpot(destNodeRid, destSpotRid, requestSeq)/reply_to_spot(...)replyToRouter(peerRid, requestSeq)/reply_to_router(...)replyActorJoin(request, accepted)(Actor join admission reply)
Raw socket facade¶
PubSocket.publish(topic)/XPubSocket.publish(topic)DealerSocket.send()/DealerSocket.request()RouterSocket.send(rid)/RouterSocket.request(rid)/RouterSocket.reply(rid, requestSeq)RouterSocket.sendToSpot(destNodeRid, destSpotRid)/requestToSpot(...)/replyToSpot(destNodeRid, destSpotRid, requestSeq)PairSocket.send()(PAIR send)StreamSocket.sendTo(rid)(STREAM peer send)- Any other raw send-capable socket's send entrypoint exposes an operation builder start point the same way.
SpotNode/StreamSocket Actor surface¶
SpotNode.joinActor(actor, destNodeRid, destSpotRid)/join_actor(...)SpotNode.leaveActor(actor, currentSpotRid)/leave_actor(...)SpotNode.destroyActor(actor)/destroy_actor(...)SpotNode.remoteActorGetRef(targetNodeRid, actorId)/remote_actor_get_ref(...)StreamSocket.bindActor(sessionRid, actor)/bind_actor(...)StreamSocket.unbindActor(sessionRid, actorId)/unbind_actor(...)StreamSocket.sendBoundActor(sessionRid, actorId)/send_bound_actor(...)SpotNode.sendBoundSessionMsg(actor)/send_bound_session_msg(...)
Common Builder Rules¶
- A start point does not send immediately — it returns a per-language
operation builder such as
SendOp,RequestOp,ReplyOp,ActorJoinOp,ActorLeaveOp,ActorDestroyOp,ActorLookupOp,ActorBindOp, orActorUnbindOp. Regardless of which start point is used, multipart payload is always expressed through repeated.message(...)calls. - A builder convenience such as
.messages(...),.flags(...),.timeout(...), or the final execution method of an async completion is part of the builder contract if it is public. It must not be defined only as a runtime-internal shortcut. The per-language name and meaning of the async-completion final execution method belongs in the bindings async execution surface policy. - Payload accumulates through repeated
message(part)calls on the builder. A single payload and a multipart payload are not split into separate start-point overloads. Multipart is not wrapped in an external List/Vector container. - Do not create a single-payload shortcut overload with the same name as
a start point. For example, public overloads such as
send(message),send(routingId, message),publish(topic, message),sendToChannel(channelName, message), andsendToSpot(nodeRid, spotRid, message)are forbidden. Express all of these through builder steps, such assend(...).message(message).submit(). - When a language can naturally express an explicit move/consume name, it
can add an ownership-transfer step (
moveMessage,MoveMessage,move_message, and so on) inside the same builder. This step is not a new operation start point, and the name and documentation must clearly state that the caller cannot reuse that message even after a submit failure. This does not change the existingmessage(...)step's contract of preserving the original on failure. - For an operation where payload is semantically required — send,
request, reply, publish, Actor join, ActorReplyJoin, and so on — a
submitwith zero messages is forbidden. A language whose type system can prevent this blocks it at compile time; other languages block it with a validation error atsubmittime. - For an operation with no payload — Actor
leave,destroy,bindActor,unbindActor,remoteActorGetRef— the builder can submit immediately without amessage(...)step. But it still exposes the same builder shape and option steps (flags(...),timeout(...), and the async-completion final execution method). flagsandtimeoutare optional builder steps, not start-point parameters. A start point takes only semantically key arguments, such as the target address or request sequence.- A messaging call in a sample or documentation example does not repeat a
default value. For a message-sending function such as
request,requestToChannel,send,sendToChannel,reply, orpublish, use the packet name inferred by default from the request object or the registered packet type. Use a packet-name override such as.packetName(...),.packet_name(...), or.PacketName(...)only when the packet actually being sent differs from the request type's default packet name. The same way, use a per-call timeout such as.timeout(...)or.Timeout(...)only when that call needs a value different from the default timeout configured on the socket or framework. This rule is not about making an example look shorter — it's a sample contract that keeps a user from mistaking an unnecessary option for standard usage. - A helper that directly builds request/reply ZMP header metadata does
not belong on the public binding surface. An API such as
requestFrame(...)exposes the wire sequence and frame layout to the caller, so it must stay a runtime/internal helper. - A reply must start from the received request context. The public
binding API offers only a surface that reveals a reply-capable
context, such as
received.reply()orrouter.reply(peerRid, requestSeq). An API such asdealer.reply(requestToken, parts), where DEALER starts a reply with an arbitrary token, does not belong on the public binding surface. A DEALER cannot designate a specific peer routing id, so reply-routing decisions would leak into a protocol helper, and the user would need to understand token semantics. - An async request or async Actor operation does not take submit flags. The detailed completion surface follows the bindings async execution surface policy.
- A builder cannot be submitted again once it has been submitted. A language that offers a move-only or ownership type blocks this by type; otherwise it is blocked by a runtime state check.
- Because an Actor join start point's admission completion shape differs,
its builder exposes a dedicated completion result (
ActorJoinResult) that captures both the reply payload and the final Actor ref together. lookup/destroy/leave/bind/unbind use the ordinary reply completion shape (RequestResult).
Common Flow Example¶
Names are converted to fit language convention.
spot.publish(topic)
.message(part1)
.message(part2)
.flags(SendFlags.DONTWAIT)
.submit();
routerSocket.requestToSpot(destNodeRid, destSpotRid)
.message(reqPart)
.timeout(Duration.ofSeconds(3))
.submit();
spotNode.joinActor(actor, destNodeRid, destUserSpotRid)
.message(joinStatePart)
.timeout(Duration.ofSeconds(3))
.submit();
streamSocket.bindActor(sessionRid, actorRef)
.timeout(Duration.ofSeconds(2))
.submit();
Per-Language Async Execution Surface Standard¶
The per-language final execution method for native suspension completion belongs in the bindings async execution surface policy.
This rule is Required under the POSD standard. When adding or cleaning up a new send/request/reply/publish or Actor location/attach public API, use this operation builder shape and the async execution surface policy as the baseline, and do not grow the existing overloads into more canonical APIs.
Core Alignment Rules¶
This section is a summary of the core contract that takes priority over
the detail examples in per-language documents. If there is a mismatch
between core/include/zlink.h and a per-language document, this section
is the baseline.
Direct Receive Callback Constraints¶
- The direct receive callback install surface exists only for raw
STREAMand SPOT routed receive. - A binding must not publicly expose an
onReceive-style direct data callback for rawPAIR,DEALER, orROUTER. - A binding must not publicly expose an
onSubscribe-style direct topic callback for rawSUB,XSUB, or SPOT subscribe receive. ROUTERinbound routed traffic is received through a single routed recv surface. The binding runtime useszlink_router_recv()internally, and exposes only aggregate routed recv and the common request completion surface. It does not provide a direct receive callback.- Core's raw
STREAMis an exceptional type that picks one of three modes:recv, the raw callback (zlink_recv_handler()), or the packet callback (zlink_stream_packet_handler()). A high-level binding's canonical public contract exposes only therecvand packet-callback surfaces. The raw direct callback is used only as an internal binding primitive — adding it as public API requires changing this policy document and the matching language spec together first, splitting it out as a separate raw/low-level surface.
SPOT Channel And Dispatch Surface¶
- SPOT is a channel-aware model. A binding must provide
create_route_bridge(...)or an equivalent typed bridge,create_publisher(...)or an equivalent publisher handle,send_to_channel,send_to_spot,request_to_channel, the channel-aware send/request operation builder start points, and the SPOT topic publish/subscribe surface. Legacy surfaces that attach an external channelDEALER, route meshROUTER, or rawPUBsocket directly to aSpotNodeare not part of the public contract. - A SPOT subscribe result exposes topic/parts. The channel name is not repeated as a message result field.
zlink_spot_dispatch_event_handler()is the canonical readable notification surface for the SPOT topic/routed/channel-reply/timer/actor planes.- The Actor dispatch surface is a public service-layer capability, same
as SPOT. Every binding exposes it through public types that fit its
own language convention, and the shared meaning follows the
Actor Dispatch Binding Contractsection and theActor Dispatch Policysection below.
Auto-HWM And SpotNode Options¶
- Core owns HWM (the queue byte threshold) calculation and queue admission. A binding only maps Core options and monitoring results to language-specific types; it does not count messages or recalculate an HWM.
ZLINK_OPT_SNDHWMandZLINK_OPT_RCVHWMlimit the accounted bytes actually retained by each directional pipe. Their manual default is4,096,000 bytes, and0means unlimited. The values are not queue message-count limits.ZLINK_CTX_OPT_AUTO_HWM_MEMORY_LIMIT_BYTES,ZLINK_CTX_OPT_AUTO_HWM_CORE_BUDGET_BYTES, andZLINK_CTX_OPT_AUTO_HWM_PROFILEare typed context options in every binding. A byte value of0means that the explicit memory limit or manual Core budget is absent. A binding does not calculate profile ratios or per-queue HWM values.- Input precedence is manual Core budget, explicit memory limit, a runtime memory limit detected by the binding, then Core fallback. When the user sets either of the first two values, the binding does not auto-detect a runtime hint. Candidate hints are .NET GC available-memory limit, Java maximum JVM heap, Node.js V8 heap limit, and a finite Go runtime memory limit. C++ and Rust add no runtime hint, while Python supplies one only when a distinct VM hard limit is unambiguous. A binding neither combines the hint with Core's hard limit nor applies a profile ratio itself.
- If Core detects a finite hard limit and an explicit memory limit or manual
Core budget exceeds it, the binding preserves Core's
EINVALand does not clamp the value. - The context budget snapshot includes Core ABI v1 version and structure size
and projects this canonical range without unit conversion: configured,
runtime, and resolved memory limits; configured and effective Core budgets;
planned, applied, and manual-reserved HWM; Core-queue, application, current,
peak, and provisional accounted bytes; completion current, peak, pending,
and total-messaging values; monitor-queue and instance aggregates; active
application and completion queue counts; outstanding application-lease
count, retired-queue count, and deferred origin-credit bytes; oversize,
blocked, budget-insufficient, and aggregate flags;
budgetGeneration; andmeasurementEpoch. Metrics reset is an operation on the same context. It preserves current, pending, and queue-count gauges, rebases both peaks to their current values, clears epoch counters, and incrementsmeasurementEpoch.
The canonical snapshot fields are below. A language binding changes only naming convention; it does not omit or merge fields, units, or meanings.
abiVersion, structSize
configuredMemoryLimitBytes, runtimeMemoryLimitBytes, resolvedMemoryLimitBytes
configuredCoreBudgetBytes, effectiveCoreBudgetBytes
totalPlannedHwmBytes, totalAppliedHwmBytes, manualReservedHwmBytes
coreQueueAccountedBytes, applicationAccountedBytes
currentAccountedBytes, peakAccountedBytes, provisionalAccountedBytes
completionCurrentAccountedBytes, completionPeakAccountedBytes
completionPendingMessageCount, totalMessagingAccountedBytes
monitorQueueAppliedHwmBytes, monitorQueueAccountedBytes
totalInstanceAppliedHwmBytes, totalInstanceAccountedBytes
activeDirectionalQueueCount, activeSendQueueCount, activeReceiveQueueCount
activeCompletionDirectionalQueueCount
outstandingApplicationLeaseCount, retiredQueueCount, deferredOriginCreditBytes
oversizeAdmissionCount, largestOversizeMessageBytes, blockedRatioPpm
unlimitedManualQueueCount, aggregateHwmValid, aggregateOverflow
budgetInsufficient, budgetGeneration, measurementEpoch
applicationAccountedBytes is ABI-reserved and always 0. Therefore
currentAccountedBytes = coreQueueAccountedBytes, and
totalMessagingAccountedBytes =
currentAccountedBytes + completionCurrentAccountedBytes. Completion values
are diagnostic only and change neither HWM admission nor the Core budget.
Send and receive counts are perspective-specific and must not be added to
derive the physical directional queue count. outstandingApplicationLeaseCount,
retiredQueueCount, and deferredOriginCreditBytes are also ABI-reserved and
always 0. A binding does not use these reserved fields for application
lifetime or Framework queue accounting.
- Project MonitorStatus version, size, and fields according to the
Core monitoring ABI.
Remove legacy message-unit, slot, size-cap, and connection-bucket planner properties without aliases.
HWM Calculation And Admission¶
Core Auto HWM owns per-queue planning, manual overrides, and recalculation; Core HWM admission owns write acceptance. A binding preserves Core ABI byte values and flags without converting inputs into message counts or planning units.
Receive ownership and routed completion¶
Terminal replies and error replies sent to a DEALER peer use the current ready
Application pipe and are subject to SNDHWM, RCVHWM, peer PAUSED, and
SNDTIMEO admission, so BACKPRESSURED is possible. Replies sent to a ROUTER
peer use HWM-free admission on the current ready Completion pipe. A binding
passes through Core's native result for the peer type and adds no separate retry
or readiness adapter.
- A receive result owns its payload under the language's lifetime rules without reconnecting retention, reuse, or cleanup to the HWM charge that ended at Core dequeue.
Core queue occupancy and application payload lifetime are separate responsibilities.
Core receive accounting
owns the charge and its end point. Ordinary
recvandsubscribepreserve parts and routing, reply, and topic metadata in aReceived/TopicMessage-family result. Each language README defines its cleanup APIs. Public and internal paths expose no separate retained receive, raw lease handle, application byte capacity or allowance, or duplicate accounting state.
A binding does not add Framework application-job queue counts to Core byte snapshots.
Receive-flow projection¶
- Every binding projects Core receive-flow setter transitions, results, and monitor observations without loss, leaving flow-state frame processing to Core. Language-specific state and error representations must preserve Core flow-control semantics. The Core receive-flow setter owns state setting, repeated setting, and unsupported-socket results; Core monitoring owns events, flags, and status fields.
Submit result projection¶
A high-level binding connects native submission results to the language outcomes below. Native results, IDs, part consumption, and wait-token conditions belong to Core whole-message send and Core REQUEST DONTWAIT.
Async terminals return a result object (SendSubmission/RequestSubmission). result is a submit-time
snapshot (OK|BACKPRESSURED) and the admitted stage (SEND and REQUEST) plus the reply stage (REQUEST)
carry completion. Synchronous terminals (submit_sync(), …) are unchanged. Wait rules and per-language
signatures belong to async-coroutine-policy §6.
| Native submission result | High-level binding outcome |
|---|---|
SEND OK, ID 0 |
End the terminal with result == OK and a completed admitted stage. |
REQUEST OK, nonzero ID |
Return result == OK with a completed admitted, and connect the corresponding REQUEST completion to the reply stage. |
BACKPRESSURED, EAGAIN, nonzero wait token |
Retain the input for resubmission and wait for WRITABLE. Continue the same operation according to Core's drain and resubmission contract; the terminal returns result == BACKPRESSURED and an admitted stage that completes on resubmission admission. |
| Submit failure without a wait token | Complete the terminal with that submit error (raised as an exception/error, not through the result object). |
- The binding delivers a WRITABLE found by socket-local context and token to its waiter without rechecking Core's guaranteed submit RID echo. The Core completion record contract owns that RID echo.
Completion consumption and submit races follow the async execution model's join contract.
A binding that uses Core's whole-message API adds no lock or gate of its own on the send path. Core whole-message send owns record atomicity, consumption of the complete array, and concurrent-submission results; Core thread safety owns the race between close and an in-flight submission. A binding whose public API preserves a message on failure submits an independently owned staging copy to Core to implement that contract. A binding adds no multipart ABI or public transaction abstraction.
Request reply completion, timeout, and language wait cancellation follow Timeout and the async execution model. The common terminal interfaces own each language's awaitable and blocking send/request signatures.
Each binding maps HWM values as follows.
| Binding | Public representation | Core mapping rule |
|---|---|---|
| C | uint64_t |
Passes an exact 8-byte option value. |
| C++ | byte_count_t |
Passes its internal uint64_t byte value as an exact 8-byte option value. |
| .NET | ulong |
Preserves the full uint64_t range. |
| Java/Kotlin | long |
Accepts 0 through Long.MAX_VALUE; a larger Core value read back is an overflow error. |
| Node.js | bigint |
Accepts 0 through 2^64-1 and passes exactly 8 bytes. |
| Python | int |
Accepts only the non-negative uint64_t range and passes exactly 8 bytes. |
| Go | uint64 |
Preserves the same range as Core uint64_t. |
| Rust | u64 |
Preserves the same range as uint64_t. |
SpotNode HWM Options¶
- A SPOT node option name follows core's public enum as-is. A binding
does not expose a directional HWM option or a delivery-queue
hard-limit option. What it exposes is the four admission options
ZLINK_SPOT_NODE_OPT_ROUTER_HWM_PROFILE,ZLINK_SPOT_NODE_OPT_ROUTER_HWM,ZLINK_SPOT_NODE_OPT_PUBSUB_HWM_PROFILE,ZLINK_SPOT_NODE_OPT_PUBSUB_HWM, and the two dispatch-worker optionsZLINK_SPOT_NODE_OPT_DISPATCH_WORKERS_MINandZLINK_SPOT_NODE_OPT_DISPATCH_WORKERS_MAX. The removedZLINK_OPT_AUTO_HWM_MSG_UNIT_BYTESandZLINK_CTX_OPT_AUTO_HWM_MSG_UNIT_BYTESdo not remain as aliases on a raw socket or on a socket/SpotNode/Spot facade. A binding exposes only the byte-valued context memory limit, Core budget, profile, and ABI v1 budget snapshot as the canonical Auto HWM surface. A dispatch-worker option adjusts only the size of the callback worker pool owned bySpotNode, and does not meanZLINK_IO_THREADSor a data-plane thread count.minmust be at least 1, andmaxmust be at leastmin. Absent explicit configuration, it maps tomin=max=1when there is 1 CPU, and otherwise tomin=2,max=cpu_count.
SPOT Status And Snapshot Names¶
- A SPOT binding status object must expose core's
disconnected_sub_target_countanddisconnected_routed_target_countunder a name that fits language convention. Because core currently does not disconnect a target from delivery-queue growth alone, both values currently report0. - When a SPOT binding exposes or documents an internal socket snapshot
name, it uses the public snapshot name core returns as-is. The current
names are
mesh-pub,mesh-xsub,peer_ctrl_pub,peer_ctrl_sub,routed-router,local-pub, andinternal_receiver.local-pubis the local fanout socket that sends to a subscriber inside the same node. (ingress-sub,pub-ingress-tx,internal-router, andinternal-router-txhave been removed and are not part of the snapshot.)
Dispatch Readiness Meaning¶
zlink_spot_dispatch_event_handler()is the single entry point for SPOT routed receive and Actor lifecycle readiness. A binding does not expose a direct routed callback as public API.ZLINK_SPOT_DISPATCH_EVENT_SUBSCRIBE_READABLEandZLINK_SPOT_DISPATCH_EVENT_ROUTED_READABLEare readiness notifications, not message-count notifications. A binding must not describe or implement them as edge-triggered one-shot events.ZLINK_SPOT_DISPATCH_EVENT_ACTOR_READABLEandZLINK_SPOT_DISPATCH_EVENT_ACTOR_JOIN_READABLEbelong to the same dispatch-readiness axis. An Actor readable event must let the caller know which Actor to drain, and an Actor join readable event must be drained throughSpot's join receive surface.- A SPOT dispatch consumer must reflect, in its documentation and
samples, the rule of draining
subscribe/recv_routeduntil each language's no-data representation appears. For example, C++ uses therecv_result_t::no_datareturn value, and Java/Node/Python usefalsefrom the API that fills caller-provided result storage. - The first SPOT routed recv must not perform hidden activation, hidden queue open, or hidden target registration. A binding assumes the same premise and does not layer lazy bootstrap logic on top.
Send Completion, Peer Weight, And STREAM Receive Modes¶
- Send outcomes follow Submit result projection;
PollCompletionand completion delivery follow the async execution model. Core whole-message send owns the relationship between rawZLINK_POLLOUTandZLINK_POLLCOMPLETION. - A binding must expose the peer-weight surface as a per-language typed
option/property. It applies to
ROUTERandDEALER; the value range is0..10000, and the default is100.0means exclusion from new outbound selection. The matching submit failure code isZLINK_SUBMIT_NOT_ADMITTED(value 13), and it must be included in every binding'sSubmitErrormapping. - Core's raw
STREAMcan select only one of three receive modes at a time: (a) blocking/non-blocking recv based onzlink_recv(), (b) the raw direct callbackzlink_recv_handler(), or (c) the packet callbackzlink_stream_packet_handler(), which uses big-endianu16 header_size + u32 body_size + header + bodyframing. A second attach returnsEBUSY. A high-level binding must keep the same mutual-exclusion rule among theSTREAMreceive surfaces it exposes publicly. A separate public release API such asdetachStream,streamDetach, or a callback detach is not in core's public contract, so it is not added to the canonical binding surface. Releasing the receive mode and cleaning up the callback is handled by socket close. zlink_recv_handler()is exclusive to rawSTREAM. Attaching it toPAIR/DEALER/SUB/XSUB/ROUTERfails withZLINK_HANDLER_NOT_SUPPORTED.- Socket defaults:
ZLINK_ROUTER_OPT_MANDATORY=1,ZLINK_OPT_RID_DUPLICATE_POLICY=ZLINK_RID_DUPLICATE_REJECT, andZLINK_PUB_OPT_NODROP=0. A binding's examples are written against these defaults.
Actor Dispatch Binding Contract¶
This section is the Actor public contract that applies to every language binding in common. A per-language document must spell out the contract below using names and types that fit its own language convention.
Actor dispatch is not an add-on to SPOT messaging — it's an independent
public service-layer capability. Because SpotNode, Spot, and
StreamSocket share ownership of lifecycle and routing, each public
type exposes its own share of the responsibility. The exact surface
placement follows the Actor Dispatch Policy section below.
Actor Id, Ref, And Lifecycle Entry Points¶
- An Actor id is a non-empty UTF-8 string up to 255 bytes. A NUL character is not allowed.
- An Actor ref carries
node_rid,actor_id, andgeneration.generation == 0is an unchecked remote ref, and is not treated as an invalid value. - Creating an unchecked remote Actor ref is owned by
SpotNode. It may be expressed as a static method or a factory function per language convention, but the canonical documentation and samples are based on theSpotNode-owned surface. Do not add a separate duplicate unchecked factory as public API directly onActorRefitself. - A local Actor is created by
SpotNode. An Actor can join only one Spot at a time, and leaving does not drain unread messages. Actor.close, or an equivalent lifecycle method, destroys the local Actor owned by that Actor handle.SpotNode.destroyActor(actorRef), or an equivalent method, is a ref-based destroy surface for a caller that holds only an Actor ref, without an Actor handle. These are not the same responsibility repeated under two names — they are two entry points with different owners, and a per-language spec must document this difference.Actor.join/Actor.leaveare the surface for a caller holding a local Actor handle.SpotNode.joinActor(actorRef, ...)/SpotNode.leaveActor(actorRef, ...)are the surface for a caller holding only an Actor ref. Providing only one of the two makes either the ref-only flow or the owned-handle flow unnecessarily complicated.
STREAM Session Binding¶
- One STREAM session can bind multiple Actors. Bind/unbind is keyed on the session routing id and either the actor id or the Actor ref.
- A public API that sends from STREAM to an Actor uses the bound session and actor id as its selector. A removed lookup/send helper name is not kept in the public API.
Actor.sendBoundSessionandActor.closeBoundSessiondo not take a session routing id as an argument. An Actor hides its current bound-session selection internally. When a caller must select explicitly by session routing id, it usesStreamSocket.sendBoundActor(...)instead.- Actor recv info's
source_node_ridandsource_session_ridare value fields of the core struct, so they are not documented as nullable/optional. No-data is delivered only through the recv result's own representation, such asfalse, a no-data result, orOk(false).
Dispatch And Join Results¶
- An Actor readable dispatch event must let the caller know which Actor to drain. A language that hands the callback off to a different execution context must non-blockingly pre-drain the Actor part at callback-entry time, so the public dispatch info can return that part.
- A Spot join request carries a message. A join reply must also return a
message to the caller together with the accept/reject result. Join
completion must deliver the final Actor ref (for a remote join, the
target node's ref) and the joined Spot rid to the application through
a dedicated
actor joinresult type. - The request-reply surface exposes only the payload part the core reply function supports. Because the core reply function has no send-flag argument, a binding does not add a no-op flag-setting step to the reply builder.
Removed APIs¶
- Remote Actor creation and the admission handler have been removed from
the public surface. An Actor that must start on a remote node is
created by the application directly on that SpotNode with
actor_new. When a checked ref for a remote Actor is needed, use the asyncremote_actor_get_reflookup. - Actor location is updated through the Actor creation, Spot join/leave, and Actor destroy flows. STREAM session bind/unbind neither creates nor removes an Actor location.
- Session attach and Actor location movement are different state transitions. Joining a user Spot does not require a bound STREAM session. Moving an Actor's location does not automatically change the session mapping.
- There is no per-Actor queue-limit option. A binding must not make this a public option.
- A removed Actor ref function, a stream actor lookup/send helper, or a session-actor-key design name is not kept in the public surface or documentation.
Document Interpretation Rules¶
- This document's policy body is a normative document by default.
- The terms below carry the following meaning.
Required: an item that must be followed in the current review and implementation. Non-compliance blocks the review.Recommended: an item strongly recommended, but which can be applied in stages depending on the binding's characteristics. Non-compliance requires a reason during review but does not block it.Target: a goal item to align to over the long term. It applies only once a given binding decides to implement that component. If a binding decides not to implement it, review does not require it.Internal-only: an item that can be used inside a binding's implementation, but must not be exposed through the public API, samples, guides, or spec signatures.- Absent a separate marker, treat the policy body as
Required. - If a section title is marked
(Target)or(Recommended), that whole section is interpreted at the marked level. This takes priority over the unmarked default (Required). - The
Implementation Review Checklistsection is not a design draft for adding a new API — it's the standard for confirming an implementation follows an already-defined public API contract. - A checklist item does not replace the semantic contract defined in the document body.
Core Principles¶
- The core contract's single baseline is the whole-message API in
zlink.h. - The internal implementation of the send/recv/request/reply/publish/ subscribe family calls Core once per record with an array and count. A receive family grows an undersized array to the required capacity and retries the same unconsumed record.
- The public API is designed around a multipart model.
- Blocking and non-blocking can be distinguished by name.
- The same capability is not exposed redundantly through multiple paths.
- Value meaning is raised into an enum, boolean, or value object, not
left as
int. - A raw option bag is not exposed publicly.
- A binding does not infer core's state errors.
- A binding blocks an input value's format, range, overflow, and truncation risk up front.
- Structure follows POSD principles, prioritizing deep modules, information hiding, and low change amplification.
- This document defines the semantic contract first.
- A per-language surface can differ to fit each language's convention, but the semantic contract must be the same.
Monitor Ready Contract¶
- The
valueof a*_READY_CHANGEDmonitor event is not an aggregate ready-count contract. - A binding's public API must not assume a monitor snapshot has a ready-count surface.
- When a readiness gate is needed, use the low-cost event edge directly.
- Raw perf/samples use
CONNECTION_READYevent counting. - SPOT perf/samples do not use a separate service event gate.
- SPOT perf uses an explicit
READY/STARTbarrier protocol. - Do not turn a delivery-ready/count-family monitor event into a new gate contract.
POSD Structure Policy¶
- Binding design follows John Ousterhout's POSD principles.
- A public API must reduce the number of concepts a user needs to know.
- Internal implementation complexity must be hidden behind a facade, value object, or domain object.
- Avoid a shallow wrapper.
- Do not multiply a public wrapper that only renames a native function without adding new meaning.
- Do not repeatedly expose the same capability through multiple types and multiple names.
- Gather a rule whose change must end in one place into a single module.
- Example: a routing-id length limit
- Example: the send-failure contract
- Example: typed-option ownership
- A role, owner, no-data, error, or naming rule shared across languages is owned exactly once by this policy document. A per-language spec does not redesign the same rule — it expresses this document's contract in that language's convention.
- If a per-language spec needs a rule that differs from this document, do not change the individual document first. First write the exception reason and scope into this policy document, then update that language document. This keeps the same design decision from scattering across multiple documents.
- Reduce temporal decomposition.
- Example: forbid an API where a user must remember the order in which
to combine
setOptioncalls - A public API reveals "what it can do," not "how it's wired internally."
- Treat a value object and a result object as deep modules.
- Give the caller a small interface, while encapsulating validation, ownership, and shape rules together internally.
Public Surface Rules¶
Base Type Exposure¶
- Where possible, let a user directly use only a concrete socket type at compile time.
- Avoid a structure where a user directly uses a generic root base, a raw compat base, or a shared base instead of a concrete socket type.
- A statically typed binding must enforce this rule using public type/export/visibility.
- A dynamic binding must enforce the same rule with export restrictions and surface tests.
- A generic root base or raw compat base exposes only the common lifecycle and common management functionality externally.
- A role-specific shared base may expose externally only the capability every descendant has in common.
- A socket-type-specific role must not be promoted to a generic root base or a raw compat base.
- Example common functionality a public base may allow external access to:
bind,unbindconnect,disconnect,disconnectRidon connectable base onlyclose/dispose- common typed options
monitorOpen, or an equivalent monitor entry pointsetTlsServer,setTlsClient, or an equivalent TLS helper- Functionality a generic root base or raw compat base must not allow external access to:
send(...)send(routingId, ...)sendParts(...)sendFrom(...)recv()recv(flags)/recv(size, flags)recvInto(...)recvMsgInto(...)- a routed-receive alias (
receiveRouted, and so on) publish(...)setSubscription(...)unsetSubscription(...)subscribe()receiveSubscriptionEvent()- raw direct receive handler registration
onSubscribe(...)setRoutingId(...),getRoutingId()attachStreamRaw(...),detachStream()streamAttach(...),streamAttachRaw(...),streamDetach()streamPeerRoutingId(...),streamSend(...)- a raw option bag (
setOption,getOption,setSockOpt,getSockOpt, and so on) - a topic/socket-type-specific option facade
- a legacy alias that bypasses the canonical name
- example:
recvHandler(...),subscribeHandler(...)
- example:
- A role-specific shared base can allow a role only when it's common to every descendant.
- example:
setSubscription,unsetSubscription,subscribeon a subscriber-only base - example:
publishon a publisher-only base - The roles above must exist as public only on a concrete socket type
marked
Yin the role matrix. - A base-mediated bypass call must not be possible for a socket type
marked
—in the role matrix. - Perf, sample, helper, and compat layers must not treat a base entry that bypasses the canonical public surface rule as a new baseline either.
- Even when a deprecated compat API is needed, isolate it into a compat namespace or internal surface separate from the canonical public API.
- A structure where a user must remember a
SocketTypeand a raw flag combination to pick the right send/recv family is treated as a POSD violation.
Multipart Only¶
- Unify the send/receive public surface around multipart.
- Do not put a single-message-receive convenience overload in the public surface.
- A single-part send convenience method can be allowed.
- example:
send(Message part)as a convenience overload ofsend(List<Message> parts) - A receive result is returned as a language-appropriate domain object or an equivalent multipart representation.
Error Handling Policy¶
Every data-path function (send, recv, request, reply,
subscribe, publish) follows the same error-handling principle.
Principles¶
- A language with exceptions does not deliver an error through a return value.
- Applies to: C++, Java, .NET, Node, Python.
- Returns a result, or void, on success.
- Throws an exception on failure.
- The exception carries an
int code(in the 0–709 range) so the caller can distinguish the failure cause. - Failures allowed by an operation's contract, including
BACKPRESSURED,NOT_CONNECTED, andNOT_FOUND, are delivered as exceptions. These are never return values. A raw terminal reply admitsBACKPRESSUREDwhen it uses the current ready Application pipe to a DEALER peer, but does not admit HWM backpressure when it uses the current ready Completion pipe to a ROUTER peer. - When no current ready reply route exists, a terminal reply fails with
NOT_CONNECTED(ENOTCONN). The same rule applies when an already selected reply route disappears while the reply is being committed. This is not backpressure, so readiness (POLLOUT) does not make a retry viable. A binding never retains such a one-shot reply nor resubmits it automatically, and it does not hand parts that were already consumed back to the caller. This meaning is identical across every language binding; only the idiom differs (an exception carrying the code in exception languages, a result value in return-based languages). - C / Go / Rust have no exceptions, so they follow a return-based contract. A binding handles it in the style each language's idiom fits.
- C: returns a per-function typed result enum
(
zlink_submit_result_t,zlink_recv_result_t,zlink_handler_result_t,zlink_close_result_t,zlink_bind_result_t,zlink_connect_result_t,zlink_config_result_t). - Go: returns
(T, error). The error object carries anintcode. - Rust: returns
Result<T, E>.Eis, where possible, a concrete per-function-family error (BindError,SubmitError, and so on), promoted toZlinkErroronly at a boundary where multiple function families mix. An error value carries anintcode. Callers use the?operator to propagate. - Express blocking-vs-non-blocking through
flagsand the return rule, instead of aTry*name. - C keeps the C ABI functional contract.
- Go / Rust keep return-based error delivery, but still apply the wrapper binding's ref-out recv and operation builder rules.
.NET/Java/Node/Python/C++do not add a publictrySend,tryRecv, ortryRequest.- The C ABI expresses blocking vs. non-blocking through a function
argument
flags. - A wrapper binding's send/publish/request family, and any reply family
whose Core ABI accepts send flags, expresses a non-blocking submit through
the builder's
.flags(...)step. It does not add a separateflagsargument to the operation start-point signature. A raw terminal reply has no send flag in the Core ABI and therefore exposes no flag step. - A wrapper binding's data-plane
recv, routed recv, andsubscribefill caller-provided result storage, and the return value expresses only "was data received." - When a non-blocking receive currently has no data, it returns a
per-language no-data representation such as
false,nil, false, orOk(false), and only a real error is delivered as an exception or a return error. - An async request is selected through the same
requestoperation builder's completion-object-return step, and it does not take submit flags. - A transport-style name such as
sendNoWait,recvNoWait, orpublishNoWaitdoes not belong on the public surface. - Looking up
INTERNAL_ERRORdetail. - When the result code is in the
INTERNAL_ERRORfamily (12, 105, 206, 306, 404, 505, 604, 704, and so on), the internal raw errno can be looked up withzlink_errno(). - The binding's error type (an exception object for exception
languages, an error value for return-based languages) exposes this
through an
internalErrno/internal_errnofield (for debugging only). - For every other result code, calling
zlink_errno()is unnecessary.
Per-Language Error Representation¶
| Language | Handling | Error type | Code access | Internal errno |
|---|---|---|---|---|
| C | return | per-function result enum | the enum value itself | zlink_errno() |
| C++ | return / throw | caller-provided recv returns int; other failures use zlink_error_t |
recv: the return value; exception: .code() |
recv: errno when -1; exception: .internal_errno() |
| Java | throw | ZlinkException |
.getCode() |
.getInternalErrno() |
| .NET | throw | ZlinkException |
.Code |
.InternalErrno |
| Go | return | error |
.Code() |
.InternalErrno() |
| Rust | return (Result) |
ZlinkError |
.code() |
.internal_errno() |
| Node | throw | ZlinkError |
.code |
.internalErrno |
| Python | throw | ZlinkError |
.code |
.internal_errno |
- The
returngroup (C / Go / Rust) has the caller explicitly check the return value. Go usesif err != nil; Rust uses thematch/?operator idiom. - The
throwgroup (C++ / Java / .NET / Node / Python) propagates an exception. The caller handles it with a per-languagetry/catchor by propagating further up.
Error Codes¶
- The C API returns a per-function typed result enum.
- Every enum shares the success value
0; nonzero values are unique across enums in the 1–709 range. - A binding includes this code in its per-language error type's
int code(an exception object for exception languages, a return error value for return-based languages). - For the full enum definition, see errno-map.md.
Per-Function Error Type Hierarchy¶
Every binding inherits the C API's per-function typed-result-enum
structure as-is. With only a single ZlinkException/ZlinkError, a
caller could not know the set of possible errors from the signature
alone.
Each binding provides 8 function-family error types as subtypes of
ZlinkException/ZlinkError. A method signature must expose that
function family's concrete error type.
| C result enum | Function family | Subtype (semantic contract) |
|---|---|---|
zlink_submit_result_t |
send / publish / request submit / reply submit | SubmitError |
zlink_request_result_t |
request completion (callback) | RequestError |
zlink_recv_result_t |
recv / subscribe / subscription event / monitor recv / timer recv | RecvError |
zlink_handler_result_t |
handler registration | HandlerError |
zlink_close_result_t |
close / destroy | CloseError |
zlink_bind_result_t |
bind | BindError |
zlink_connect_result_t |
connect / disconnect / unbind | ConnectError |
zlink_config_result_t |
option set/get, snapshot, poller mutation, proxy, timer config | ConfigError |
Per-Language Naming¶
| Language | Top-level type | Subtype naming | Base type | Example signature |
|---|---|---|---|---|
| C | — | the per-function typed enum as-is | — | zlink_bind_result_t zlink_bind(...) |
| C++ | zlink_error_t |
zlink::<category>_error_t (snake_case + _t) |
the std::runtime_error family |
void bind(...) /* @throws bind_error_t */ |
| Java | ZlinkException |
<Category>Exception |
unchecked (RuntimeException) |
void bind(...) /* @throws BindException */ |
| .NET | ZlinkException |
Zlink<Category>Exception |
System.Exception (unchecked; every .NET exception is unchecked) |
void Bind(...) /* throws ZlinkBindException */ |
| Node | ZlinkError |
<Category>Error |
Error |
bind(ep): void /* @throws BindError */ |
| Python | ZlinkError |
<Category>Error |
Exception |
def bind(ep): ... # raises BindError |
| Go | error (interface) |
*<Category>Error (typed error struct) |
implements the error interface |
func (s) Bind(ep) error // returns *BindError |
| Rust | ZlinkError (enum) |
<Category>Error (a variant or a separate type) |
implements std::error::Error |
fn bind(ep) -> Result<(), BindError> |
Categoryis one of 8:Submit/Request/Recv/Handler/Close/Bind/Connect/Config.ZlinkException/ZlinkErrorstays the parent of every subtype, preserving the "catch-all" idiom. A caller catches the subtype when it needs granularity, or the parent otherwise.- Each subtype error has its own dedicated
ErrorCodenested enum for that function family. Another function family's codes are not expressed in that type. - Java / .NET follow the unchecked-exception system. A method
signature does not force a
throwsclause. A possible exception is documented with Javadoc@throws/ XML doc/// <exception cref="...">. - Rust / Go declare the concrete subtype error as the return type. A
dynamic language (Node/Python) provides the same information with
TSDoc
@throws/ a Python docstringRaises:.
Signature Declaration Rules¶
- When a method can throw/return only a single function family's error, declare only that concrete subtype.
- Java:
@throws BindException(Javadoc; nothrowsclause needed in the signature) - .NET:
/// <exception cref="ZlinkBindException"> - C++:
/// @throws bind_error_t(do not mark itnoexcept) - Node: TSDoc
@throws {BindError} - Python: docstring
Raises: BindError - Go: document the return type as
returns *BindError - Rust: return type
Result<T, BindError> - When a method spans multiple function families (for example, a service
layer's combined call), declare the shared parent
ZlinkException/ZlinkErrorand list the subtypes that can actually occur in the doc. - A validation exception (a language-native
IllegalArgumentException, and so on) is separate from the system above and does not enter theZlinkException/ZlinkErrorhierarchy.
Flags Policy¶
Every data-path function has a flags option. An ordinary socket
function expresses it as a per-language signature's flags parameter,
and a function targeting a SPOT operation builder expresses it as the
builder's flags(...) step.
| Function family | flags usage |
|---|---|
send, publish, reply |
DONTWAIT — non-blocking submit |
recv, subscribe, receiveSubscriptionEvent |
DONTWAIT — non-blocking receive |
request (callback) |
DONTWAIT — non-blocking submit |
request (async completion) |
No flags — uses the per-language completion-object-return path |
- The default
flagsvalue is0(blocking). - A non-blocking call's temporary state is delivered per each language's public contract.
.NET/Java/Node/Python- caller-provided
recv,subscribe,receiveSubscriptionEvent:falsewhen there is currently no data - Any other failure: a typed exception
- caller-provided
- C++
- caller-provided
recv/subscribe/receive_subscription_event: returns therecv_result_t::no_datainteger value when there is currently no data - only a binding-local failure returns
-1and setserrno
- caller-provided
- Return-based languages (C/Go/Rust): return the error (C = result
enum, Go =
error, Rust =Err(E)). - Per-language
flagsrepresentation: - C:
int flags = 0(the C ABI does not apply the builder policy) - C++ / Java / .NET / Node / Python / Go / Rust send/request/reply/
publish/Actor-attach surfaces: expressed through the builder's
.flags(...)step. No separateflagsargument or_with_flagsvariant is added to the operation start-point signature. - C++ / Java / .NET / Node / Python / Go / Rust data-plane
recv/subscribe surfaces: take a
flagsargument together with caller-provided output storage.
Naming Policy¶
Creation Function Naming¶
A public function that builds a new object from an input value, such as
Message and RoutingId, aligns to the .NET binding's From(...)
meaning. Avoid putting the input type into the function name — doing so
splits the same concept into multiple names across languages.
- Gather ordinary construction under a single
from(...), or that language's equivalent name. A type-suffixed name such asfrom_bytes,from_string,from_u32, orfrom_uuidis not used as canonical public API. - Hex decoding is allowed as an exception, under the
from_hexfamily, because it carries a distinct meaning — decoding a human-readable string. The per-language spelling follows idiom:FromHex,fromHex,from_hex,NewRoutingIDFromHex. - Python uses
from_(...)becausefromis a reserved word. - Rust uses the standard
Fromimplementation for a routing id, and can use thetry_fromidiom for a message construction that can fail. It does not add an input-type-named public helper such asfrom_bytes/from_string. - Because Go has no overloads, it allows a typed constructor such as
NewRoutingID(...),NewRoutingIDString(...),NewRoutingIDUint32(...), andNewRoutingIDUUIDBytes(...). This exception exists to preserve Go's static-typing style, and it does not repeat bothFromand the type name together, as inNewRoutingIDFromString. - Allocation is not a source conversion, so it uses
allocate(...)or a per-language constructor idiom (NewMessageWithSize(...), and so on).
One Entrypoint, Variation Expressed As Builder Steps¶
Variations of the same operation — single/multipart,
with or without flags, with or without a timeout — use the same
entrypoint name, and the variation is expressed as a builder step. Do not
create a separate name such as request_callback, send_nonblocking, or
send_with_flags.
// GOOD: one name, builder absorbs the form.
spot.request_to_channel(channel)
.message(part)
.timeout(Duration::from_secs(3))
.submit() // returns the language completion object
// BAD: split names for the same operation.
request_to_channel(channel, parts, timeout)
request_to_channel_async(channel, parts, timeout)
Shared Result Type Names¶
A shared result type does not repeat its owner's name. The type name reveals the domain concept the value represents, directly.
- The canonical name for a poller wait-result type is
PollEvent. C++ usespoll_event_t; Java/.NET usePollEvent; Node/TypeScript usePollEvent. A name that appends the owner a second time, likePollerEvent, is not used as canonical public API. - Timer, monitor, and dispatch results follow the same rule. When the owner is already clear from the return type or namespace, the type name does not repeat it.
SPOT Target Naming¶
SPOT routed naming separates pub/sub from targeted messaging.
- Channel-aware path
send_to_channel(channel_name) -> SendOprequest_to_channel(channel_name) -> RequestOp- SPOT topic path
publish(topic) -> SendOp- The receiver is already a publish-capable socket or
Spot, so it does not repeat owner or parameter meaning, as inpublish_spotorpublish_to_topic.
- The receiver is already a publish-capable socket or
- Direct routed path
send_to_spot(dest_node_rid, dest_spot_rid) -> SendOprequest_to_spot(dest_node_rid, dest_spot_rid) -> RequestOprequest_to_router(peer_rid) -> RequestOp- Reply path
reply_to_spot(dest_node_rid, dest_spot_rid, request_seq) -> ReplyOpreply_to_router(peer_rid, request_seq) -> ReplyOp
The payload and options of SendOp, RequestOp, and ReplyOp are
expressed through the message(...), flags(...), timeout(...), and
submit... steps the Operation Builder Policy section defines. So a
new canonical SPOT surface does not add a Message/List<Message>/
flags/timeout combination overload on the same start point.
On a new SPOT binding surface, send_to_channel/request_to_channel/
publish(...) are treated as the default path, instead of the old
send_service/request_service. A direct address-targeted path can be
separately supported as core's typed routed surface.
Convert to camelCase / PascalCase / snake_case per each language's convention.
Request Policy¶
A request offers one language-native suspension terminal on the same
RequestOp operation builder the request entrypoint returns. Do not create a
separate name (request_callback, requestAsync, submit_async, and so on).
For a SPOT operation builder target, the work start point is
requestToChannel/requestToSpot/requestToRouter; for a raw
DealerSocket/RouterSocket, the work start point is
request/request(peer). Regardless of the start point, the completion
mode is selected through the per-language final execution method the
bindings async execution surface policy
defines.
- On success, return only the reply payload's List<Message>. Do not wrap request-target
metadata in Received or introduce a separate Reply type.
- Because multipart reply is possible, it returns List<Message>, not a
single Message. A single-part reply is retrieved with list[0].
Shared¶
- For the full
zlink_request_result_tdefinition, see errno-map.md. - Because Go / Rust have no exceptions, suspension failures use their return-based error representation.
Domain Object Policy¶
- Java, C#, Go, Rust, Node, and Python prefer a domain object over an
outparameter or a raw tuple where possible. - The minimum core domain model:
MessageRoutingIdReceivedTopicMessageSubscriptionEventSubmitResult(exposed by every language through the async terminal's result objectresult(OK|BACKPRESSURED). Other submit failures surface as an error in return-based languages and as the exception object's.codein exception languages)- A result object must describe payload shape, ownership, and optional routing metadata together.
- A convenience feature is a method on the result object.
- example:
singlePartOrThrow()
Domain Object Canonical Shape (Shared By Every Binding)¶
Every domain object exposes the canonical field/method set below as is. Only the naming convention (camelCase / snake_case / PascalCase) is converted per language — the field type and method meaning do not change. A per-language idiomatic convenience method can be added, but it must not replace or partially omit a canonical method.
Message¶
A single message part that carries a transport payload. Every send/
request/reply/publish builder accumulates one or more Messages to
build a multipart payload.
| Member | Type | Meaning |
|---|---|---|
| empty constructor | ctor/static | Creates a zero-length message |
allocate(size) |
static/ctor | Creates a size-byte payload buffer |
from(bytes) |
static/ctor | Copies bytes-like input into message-owned storage |
from(string) |
static/ctor | Encodes a user string as a UTF-8 payload |
copy() / from(Message) |
Message |
Copies the source payload into a new message |
move() / consume path |
Message / builder step |
An explicit ownership transfer; the source cannot be reused after the call |
size |
int / usize |
Payload byte length |
is_empty() |
bool |
size == 0 |
to_bytes() |
bytes / byte[] / Vec<u8> |
A snapshot copy of the payload |
data / as_bytes() |
view | A read view of the payload; no lifetime guarantee after close |
mutable_data / as_mut_bytes() |
mutable view | A mutable view for filling an allocated payload |
copy_to(destination) |
int / bool |
Copies the payload into a caller-provided buffer |
to_string() / as_str() |
string / result |
A UTF-8 decode convenience |
get_property(name) |
string? / result |
Looks up a native message string property |
ref_count() |
int |
A native-storage reference-count diagnostic value |
close() / Dispose() / Drop |
— | Cleans up native storage, per each language's lifecycle idiom |
The name follows per-language idiom. The meaning fits the slots below.
| Meaning | .NET | Java | Node | Python | Rust | C++ | Go |
|---|---|---|---|---|---|---|---|
| Empty message | new Message() |
new Message() |
Message.from(Buffer.alloc(0)) or equivalent |
Message() |
Message::new() |
message_t() |
NewMessage(nil) |
| Sized allocation | Allocate(size) |
allocate(size) |
allocate(size) |
allocate(size) |
with_size(size) / allocate(size) |
allocate(size) |
NewMessageWithSize(size) |
| Bytes copy | From(bytes) |
from(byte[]) |
from(BufferLike) |
from_(buffer) |
try_from(bytes) |
from(...) |
NewMessage(data) |
| UTF-8 string | From(string) |
from(String) |
from(string) |
from_(str) |
TryFrom<&str> or equivalent |
from(std::string) |
NewMessageString |
| External buffer copy | — | from(ByteBuffer) / from(ByteBuf) |
from(BufferLike) |
from_(buffer) |
try_from(bytes) |
from(...) |
NewMessage(data) |
| Message copy | Copy() |
from(Message) |
copy() or from(Message) |
copy() |
Clone or try_clone() |
copy constructor | Clone() / Copy() |
| Explicit move | Move() / MoveMessage(...) |
move() / moveMessage(...) |
moveMessage(...) |
move_message(...) |
move-by-value | move constructor / rvalue builder | MoveMessage(...) |
| Bytes snapshot | ToArray() |
toByteArray() |
toBytes() |
to_bytes() |
to_vec() |
to_bytes() |
BytesCopy() or equivalent |
| Read view | AsReadOnlySpan() |
dataBuffer() |
data() |
data |
as_bytes() |
bytes() |
Data() |
| Mutable view | AsSpan() |
mutableDataBuffer() |
data() |
data |
data_mut() |
bytes() / data() |
Data() |
| UTF-8 decode | GetString() |
toUtf8String() |
toString() / getString() |
to_string() / decode helper |
as_str() |
to_string() |
String() / Text() |
| Property | GetProperty(name) |
getProperty(name) |
getProperty(name) |
get_property(name) |
get_property(name) |
property(name) |
GetProperty(name) |
| Refcount | RefCount |
refCount() |
refCount() |
ref_count() |
ref_count() |
ref_count() |
RefCount() |
Rules:
- The from(bytes) family always copies into message-owned storage. The
caller must be free to change or release the input buffer afterward.
- Java's from(ByteBuf) copies a Netty ByteBuf's readable bytes
without changing readerIndex. copyTo(ByteBuf) writes into the
destination's writable region and advances writerIndex.
- A borrowed/zero-copy constructor is not part of the canonical public
contract. Even if a particular binding uses one as an internal
optimization, the public API must not push lifetime responsibility
onto the caller.
- The message(...) builder step follows the original-preservation
contract. The caller must be able to reuse the message it passed even
after a submit failure.
- Ownership transfer is allowed only through a separate path whose name
reveals consume semantics — move, MoveMessage, move-by-value. This
path must document that the original message cannot be reused even
after a submit failure.
- to_bytes() is a snapshot copy. Allocation-free payload access is kept
separate as a read-view API (data, as_bytes, AsReadOnlySpan, and
so on).
- A read/mutable view is valid only until the message is closed,
disposed, or dropped. A binding does not guarantee view usage after
close.
- get_property(name) is a diagnostic/interop API for reading native
message metadata. A property-write API is not part of the shared
required contract.
- ref_count() is a diagnostic value. Do not build a public contract
that judges ownership policy or send eligibility from the reference
count.
- An RAII language (C++, Rust) does not have to explicitly expose
close(). Resource close/dispose in an explicit-lifecycle language
(.NET, Java, Python, Go) is idempotent by default. An exception applies
when the language exact contract states that deterministic cleanup returns
a public Message wrapper identity to a pool. The reference becomes invalid
when cleanup returns, and no operation, including repeated close/dispose, is
allowed through that reference.
- A pooled Message wrapper identity may represent a later receive. An
application must not retain a returned wrapper as a Map, WeakMap, or
metadata key, and must not assume custom properties survive into later
ownership. Pool capacity and reuse order are not public contract.
- The behavior of size, data, and get_property on a closed or
moved-from message follows per-language convention, but must document
whether it returns an empty value or throws an exception/error.
TopicMessage¶
The recv result for raw SUB/XSUB and Spot subscribe. Raw pub/sub wraps
the complete payload array returned by C API zlink_subscribe(), while Spot
subscribe wraps the result of zlink_spot_subscribe_part(). The binding's
public API returns either result as one per-language multipart object.
| Member | Type | Meaning |
|---|---|---|
routing_id |
RoutingId? (optional) |
The sender's routing id; null/None/empty if the transport doesn't carry one |
topic |
string (UTF-8) |
The matched topic. Not bytes. |
parts |
List<Message> / Vec<Message> |
The multipart payload |
is_single_part() |
bool |
parts.size() == 1 |
first_part() |
Message |
parts[0]; error/exception if empty |
single_part_or_throw() |
Message |
Returns the part if is_single_part(), else error/exception |
close() / Dispose() / Drop |
— | Cleans up held parts, per each language's lifecycle idiom |
Rules:
- Do not create Subscribed or a similar subclass. Expose only
TopicMessage.
- A Spot subscribe result exposes topic + parts together. The channel
is state that already lives on the SpotNode the Spot handle is
bound to, so it is not repeated as a message result field.
- topic is a UTF-8 string. It is not exposed as bytes/byte[]/
Vec<u8> (even if it arrives internally as raw bytes, the public API
decodes it).
- Keep only a single typed RoutingId field. Do not create a dual
property such as RoutingId: string plus RoutingIdValue: RoutingId?.
The .NET binding additionally exposes TopicMessage.ReleaseForReuse(). It releases the
current parts and metadata while retaining the internal topic receive buffers for a later
Subscribe call. It is valid only while the object is open; after terminal Dispose() it
throws ObjectDisposedException and does not reopen the object. This is a .NET lifecycle
operation, not a requirement that every binding expose the same method name.
Received¶
The single canonical domain object that carries a PAIR / DEALER / ROUTER
/ STREAM / SPOT routed recv result. Other than lacking a topic field, it
has the same convenience-method set as TopicMessage. A routed recv
result provides a send() operation builder for sending an ordinary
response, and a request-reply result also provides a reply() builder.
Both entrypoints accumulate payload and options through builder steps,
per the Operation Builder Policy.
Received is not a per-socket-kind message wrapper. Request meaning is
the same across DEALER, ROUTER, and SPOT, and is expressed only through
request_seq and reply context. A binding must not add a
protocol-specific public result type such as DealerReceived/
RouterReceived/SpotReceived as a new canonical surface. If an
existing binding has such a type, remove it, and new code, samples,
perf, and framework integrations must use Received.
| Member | Type | Meaning |
|---|---|---|
routing_id |
RoutingId? |
The sender's routing id (router = peer_rid, spot = source_node_rid) |
spot_rid |
RoutingId? |
Set only for SPOT routed recv (source_spot_rid) |
request_seq |
uint64? |
Set in request-reply mode; otherwise null |
parts |
List<Message> |
The multipart payload |
is_single_part() |
bool |
Same as above |
first_part() |
Message |
Same as above |
single_part_or_throw() |
Message |
Same as above |
send() |
SendOp |
An operation builder that sends an ordinary routed message back to this Received's sender; SubmitError at submit time if there's no routed source context |
reply() |
ReplyOp |
A reply operation builder valid only when this was a request; SubmitError at submit time if request_seq is absent or the reply context is invalid |
close() / equivalent |
— | Same as above |
In .NET, Received.Create() is the canonical construction path for
caller-provided recv storage. Received stays a public concrete
contract type.
request_seq rules:
- null/None/an empty Optional/hasRequestSeq == false means an
ordinary receive result.
- 0 is not exposed as "has a request" on the public high-level
Received. A high-level binding converts a core out-param's
request_seq == 0 into absent.
- A non-zero request_seq means a receive result that has request-reply
context. This meaning is the same across DEALER / ROUTER / SPOT.
- A substrate-level distinction such as a request/reply message type must
not split the public Received meaning. If such a value is genuinely
needed as a public contract, expose it only as Received's shared
metadata, not as a protocol-specific result type.
send() rules:
- Independent of whether it was a request. It can be called as long as
routed source context exists, even without request_seq.
- send() has no request-reply meaning. It simply sends an ordinary
routed message back toward whoever sent this Received.
- A ROUTER and STREAM receive result sends by peer routing id. A
SPOT routed receive result sends by source node rid and source spot
rid.
- Payload accumulation and options such as flags(...) are expressed
through SendOp builder steps, and a non-blocking submit flag such as
DONTWAIT is also delivered through the builder's .flags(...) step.
reply() rules:
- Calling it is forbidden when request_seq is null. Calling it
anyway is handled as a SubmitError-family failure at the builder's
submit step. An invalid reply context — request_seq == 0, an invalid
(routing_id, request_seq) combination, and so on — is treated as the
same submit domain.
- Received internally holds a reference to the source socket (injected
by the binding when it builds Received inside recv/handler).
- Calling reply().submit() after the socket has closed returns
SubmitError(TERMINATED).
- The server-side user does not need to separately store
(peerRid, requestSeq) — Received alone is self-contained.
- A separate router.reply(peerRid, seq).message(...).submit() path is
also kept for pull-mode compatibility, but the recommended path is
received.reply().message(...).submit().
SubscriptionEvent¶
The subscribe/unsubscribe event XPub receives, and the recv result for a Spot subscription event.
| Member | Type | Meaning |
|---|---|---|
routing_id |
RoutingId? |
The subscriber's routing id |
topic |
string (UTF-8) |
The subscribed/unsubscribed topic |
subscribed |
bool |
true = subscribe, false = unsubscribe |
Rules:
- Expose it only as a value object (no methods, fields only).
- No lifecycle such as close() (it's a value type).
- A Spot subscription event result exposes topic + subscribed.
RoutingId¶
A routing-id value object. Binary-safe (1–255 bytes).
| Member | Type | Meaning |
|---|---|---|
bytes / data |
bytes / byte[] / Vec<u8> / Buffer |
The raw bytes (an immutable view) |
size |
int (1–255) |
The byte length |
from(bytes) |
static/ctor | Builds from raw bytes |
from(value: string) |
static/ctor | Encodes a user string as UTF-8 bytes |
from_hex(value) |
static/ctor | Rebuilds from a hex string produced by to_hex() |
from(value: uint32) |
static/ctor | Builds a 4-byte big-endian uint32 routing id |
from(value: guid) |
static/ctor | Builds a 16-byte UUID routing id |
to_bytes() |
bytes |
Returns the original bytes |
to_hex() |
string |
Displays the raw bytes as a hex string |
| equality / hash | — | Per-language idiom (equals/hashCode, __eq__/__hash__, PartialEq+Eq+Hash) |
The name follows per-language idiom. The meaning fits the slots below.
| Meaning | .NET | Java | Node | Python | Rust | C++ | Go |
|---|---|---|---|---|---|---|---|
| User string | From(string) |
from(String) |
from(string) |
from_(str) |
From<&str> |
from(std::string) |
NewRoutingIDString |
| Raw bytes | From(bytes) |
from(byte[]) |
from(Buffer) |
from_(bytes) |
From<&[u8]> |
from(bytes) |
NewRoutingID |
| Hex round-trip | FromHex |
fromHex |
fromHex |
from_hex |
from_hex / try_from_hex |
from_hex |
NewRoutingIDFromHex |
| uint32 | From(uint) |
from(long) |
from(number) |
from_(int) |
From<u32> |
from(uint32_t) |
NewRoutingIDUint32 |
| UUID | From(Guid) |
from(UUID) |
16-byte from(Buffer) |
from_(uuid.UUID) |
From<[u8; 16]> |
from(std::array<uint8_t, 16>) |
NewRoutingIDUUIDBytes |
Rules:
- A binary-safe value type. Because a user-set routing id is usually
a human-readable string, the string overload from(value) means UTF-8
encoding. Arbitrary bytes received from native/core are preserved
through the bytes overload from(bytes).
- A from_hex(value) input allows only hex characters. A hex string is
at most 510 characters, and it must fail with a per-language exception
or error code if the decoded routing id exceeds 255 bytes.
- A value core treats as uint32_t, such as a 4-byte STREAM routing id,
is handled through a typed API such as from(value: uint32)/
try_to_uint32(out value).
- A 16-byte UUID value is handled through a typed API such as
from(value: guid)/try_to_guid(out value).
- to_string()/String() is a per-language display string. It's
recommended to show printable UTF-8 as-is, a 4-byte uint32 as a
numeric string, a 16-byte UUID as a UUID string, and anything else as a
hex display prefixed with hex:. Use to_hex()/from_hex(value) for
round-trip storage.
- Immutable. Once created, its content cannot change.
- Caching is not an observable contract. A binding can internally use a
hash, a native struct, or a short-lived cache on the recv hot path if
needed, but equality must always be judged by the bytes value, and a
cache hit must not change API behavior.
- On Node, expose the RoutingId wrapper type as-is instead of a raw
Buffer.
MonitorEvent¶
An event a socket monitor emits. Required for every binding to expose.
| Member | Type | Meaning |
|---|---|---|
event |
MonitorEventType (enum) |
The event kind (CONNECTION_READY, CONNECTED, DISCONNECTED, and so on) |
value |
uint64 |
A per-event detail value (for example, the reason code on DISCONNECTED) |
routing_id |
RoutingId? |
The matching peer routing id (null for an event without one) |
local_addr |
string |
The local endpoint |
remote_addr |
string |
The remote endpoint |
connection_id |
uint64 |
Identifier of one physical transport attempt (Core 06-monitoring §3.1). READY/DISCONNECTED/CLOSED of the same attempt carry the same nonzero value |
transport_lane |
TransportLane (enum: Application=0, Completion=1) |
The lane of the physical connection the event belongs to |
flags |
uint32 (bit flags) |
Auxiliary event flags such as the READY edge (Core zlink_monitor_event_t.flags) |
Bindings that mirror the native layout by hand (.NET, Java, Python, Rust) pin the Core public struct size of 800 B and the connection_id/transport_lane/flags offsets 784/792/796 with a regression test.
Monitor sources in Poller¶
Every binding's public Poller must be able to add/modify/remove a socket monitor as a source in addition to raw sockets, timers and FDs (Core 05-polling §3, "socket monitor" row). A monitor only reports POLLIN readiness and is drained with Monitor.recv (DONTWAIT) after it is ready. A monitor registration or modification whose mask contains any readiness bit other than POLLIN (POLLOUT, POLLCOMPLETION, ...) is rejected by the binding with a typed ConfigResult.INVALID_ARGUMENT (the Core accepts the registration, so this is binding input validation). The registration surface follows the language's conventions (an overload, a dedicated add_monitor family, or the monitor satisfying the socket source type), the language spec names it, and the Poller test verifies monitor registration and drain.
MonitorStatus¶
The runtime status snapshot a socket monitor provides. Required for every binding to expose.
| Member | Type | Meaning |
|---|---|---|
source_kind |
enum | The kind of monitored target |
state_flags |
enum flags | The state bitmask |
detail_flags |
enum flags | The detail bitmask |
snd_pending_msgs |
uint64 |
The number of messages pending in the send queue |
rcv_pending_msgs |
uint64 |
The number of messages pending in the receive queue |
snd_pending_bytes, rcv_pending_bytes |
uint64 |
Current pending accounted bytes in the send and receive queues |
auto_hwm_* diagnostic fields |
enum / number / bigint | Exposes canonical zlink_monitor_status_t current Core monitoring ABI fields unchanged: enabled, profile, role, policy class, planned/applied/deferred SND and RCV HWM bytes, effective buffer bytes, last recalculation time and reason, blocked ratio, SND and RCV in-flight bytes, minimum Core charge, and oversize diagnostics |
is_ready() |
bool |
A convenience method that checks the ready bit in state_flags, for a raw socket monitor source only |
Monitor open uses only monitor_hwm_bytes. Zero selects the Core default;
a positive value is passed unchanged as the exact byte limit for the source
worker and both directions of the internal PAIR. No legacy monitor message-count
option or count-to-byte alias is exposed.
Service-Layer Entry Objects¶
The following are value objects returned from service-layer snapshot/ query calls. Every binding must spell out the field list in its spec (a raw C struct must not be exposed as-is — wrap it in per-language named fields).
SpotNodeStatus— a spot node status snapshotSpotNodePeerEntry— a spot node peer entry. Must includeweight.SpotNodeSubjectEntry— a spot node subject entry
Each spec spells out these types' fields as a table or code block. C++
wraps the raw zlink_*_t struct as class <name>_t { ... } rather than
exposing it directly on the binding API surface.
An extra method/field beyond the canonical set above is a policy violation. When a per-language spec is found missing one, fill it in against the canonical baseline, and remove any added non-standard method.
Socket Type Capability Policy¶
- Expose a per-socket-type capability only on that type itself.
- An unrelated socket must not have access to an unrelated function.
- example: no publish/subscribe/xpub control surface on
PairSocket - example: no general connect surface on
StreamSocket - Also expose a per-socket-type option only through that type's own role facade.
Socket Class Naming/Structure Rule (Important)¶
- A socket class name follows the core C API's socket-type name
as-is:
PairSocket,PubSocket,SubSocket,XPubSocket,XSubSocket,DealerSocket,RouterSocket,StreamSocket. A binding must not rename it arbitrarily or add a synonym (ClientSocket,BrokerSocket, and so on). - A socket's capability functions (
send,recv,request,reply,publish,subscribe,on*handlers, and so on) are exposed directly as methods on the socket class. Do not create a separate wrapper/ "helper" class for a single function or a narrow role such as request-reply (RequestDealer,RequestRouter,DealerClient,RouterRequester, and so on). - Reason 1: the C API's contract places
zlink_dealer_request_part()/zlink_router_request_part()/zlink_router_reply_part()directly on the raw socket handle. The binding surface must preserve this structure to keep the core ↔ binding mapping 1:1. - Reason 2: a wrapper class creates a duplicate lifecycle — "having to carry around another wrapped socket."
- Reason 3: the role is easy to misread as inverted from the name
(
RequestDealercan be misread as "dealing requests"). - Keep implementation state such as a future/promise completion linkage (a pending map, and so on) inside the socket class, and expose only methods externally.
- The only exception is a service-layer surface that combines different socket types — these are independent service contracts, not a single-socket-function wrapper.
- This rule applies identically across every binding (C++/Java/.NET/Node/Python/Go/Rust), and a violation found in a spec file is fixed immediately.
Socket Capability Matrix¶
- This table defines the capability each socket type must have, based on
the
core/include/zlink.hC API. - This table is a public capability contract shared across every language binding. Functionality must not differ between bindings.
- A per-language difference is allowed only in how the same capability is expressed for that language's convention — casing, overloads, nullable, exception/error representation.
- Every binding treats this table as the answer key when writing surface tests.
Ymeans every binding must expose that capability as public API.—means no binding may expose that capability as public API.
Connection Capabilities¶
| Capability | Pair | Dealer | Router | Pub | Sub | XPub | XSub | Stream |
|---|---|---|---|---|---|---|---|---|
bind |
Y | Y | Y | Y | Y | Y | Y | Y |
unbind |
Y | Y | Y | Y | Y | Y | Y | Y |
connect |
Y | Y | Y | Y | Y | Y | Y | — |
disconnect |
Y | Y | Y | Y | Y | Y | Y | — |
disconnectRid |
Y | Y | Y | Y | Y | Y | Y | — |
disconnectRid is the peer-rid disconnect surface of a connectable raw
socket. STREAM is a bind-only socket and does not expose connect,
disconnect, or disconnectRid as public API. Spot also does not
expose a raw peer-rid disconnect — disconnecting a SPOT node peer is
handled by the SpotNode.disconnectPeerRid family.
Send Capabilities¶
| Capability | Pair | Dealer | Router | Pub | Sub | XPub | XSub | Stream |
|---|---|---|---|---|---|---|---|---|
send |
Y | Y | — | — | — | — | — | — |
send(routingId) |
— | — | Y | — | — | — | — | Y |
publish |
— | — | — | Y | — | Y | — | — |
Receive Capabilities¶
| Capability | Pair | Dealer | Router | Pub | Sub | XPub | XSub | Stream |
|---|---|---|---|---|---|---|---|---|
recv |
Y | Y | Y | — | — | — | — | Y |
subscribe |
— | — | — | — | Y | — | Y | — |
receiveSubscriptionEvent |
— | — | — | — | — | Y | — | — |
Subscription Management¶
| Capability | Pair | Dealer | Router | Pub | Sub | XPub | XSub | Stream |
|---|---|---|---|---|---|---|---|---|
setSubscription |
— | — | — | — | Y | — | Y | — |
unsetSubscription |
— | — | — | — | Y | — | Y | — |
Callback Capabilities¶
| Capability | Pair | Dealer | Router | Pub | Sub | XPub | XSub | Stream |
|---|---|---|---|---|---|---|---|---|
setPacketHandler |
— | — | — | — | — | — | — | Y |
onReceive |
— | — | — | — | — | — | — | — |
onSubscribe |
— | — | — | — | — | — | — | — |
The STREAM public surface must provide recv and setPacketHandler.
The raw direct callback onReceive is not canonical public binding API.
Attempting to attach a different receive mode while one is already
active returns HandlerResult::BUSY (or the equivalent EBUSY). A
public detachStream/streamDetach-family release API is not provided.
Typed Option Capabilities¶
| Option Facade | Applies to |
|---|---|
| Common options (linger, HWM, timeout, and so on) | All |
| Router options (mandatory, handover, probe, connectRoutingId) | Router |
| Dealer options (probe) | Dealer |
| Stream options (notify) | Stream |
| Pub options (verbose, verboser, noDrop, manual, and so on) | Pub, XPub |
| Sub options (topicsCount) | Sub, XSub |
| RoutingId (set/get) | Dealer, Router, Stream |
disconnectRid, unbind, and close are blocked.
Per-Language Spec File Compliance Rule¶
Each per-language spec file (doc/spec/bindings/{lang}/README.md) must
follow the rules below. Apply this checklist when writing or reviewing a
spec file.
Capability Matrix Consistency¶
- A per-language spec must not omit or add to the functionality in the Socket Capability Matrix above.
- Each socket-type class must provide every capability marked
Yin the Socket Capability Matrix above, as a public surface that fits that language's convention. - A capability marked
—must not exist on that socket-type class in any language binding. - Service-layer functionality the Socket Capability Matrix doesn't cover may be exposed as public API only when it's specified in a separate role matrix or policy section.
- Watch especially for these frequent violations:
- No plain
send(a send without routingId) onRouterSocket/StreamSocket— it must besend(routingId, ...). - No
connect,disconnect, ordisconnectRidonStreamSocket—STREAMis a bind-only socket. Allowed only on Dealer, Router, Pub, Sub. - No
onSubscribecallback onXPubSocket— onlyreceiveSubscriptionEventis allowed on XPub. - No
STREAMraw direct callbackonReceive, and nodetachStream-family release API — the canonical surface is therecv/setPacketHandler/closecombination. - Do not omit the shared socket TLS helper (
setTlsServer,setTlsClient, or an equivalent name) — TLS configuration is shared transport functionality, so it must sit at the same location on every raw socket type.
Required Argument For Routed Send¶
RouterSocket's andStreamSocket's send must take routingId as a required argument.- Making routingId an optional/default parameter is forbidden, because it would allow a plain send.
Send/Publish Return Value¶
- Send and request submission results and failures follow Submit result projection.
- Publish return types and failure reporting follow each language README's publish contract.
Per-Language Naming Consistency¶
- A naming convention must not be mixed within one binding.
- Python: every public API uses
snake_case(including properties). - Java:
camelCasemethods,PascalCaseclasses. - C#:
PascalCasethroughout. - Go:
PascalCaseexported identifiers. - Rust:
snake_casemethods,PascalCasetypes. - C++:
snake_casemethods. Keep type naming consistent within one binding. A_tsuffix can be used for a handle/value wrapper type, or where a type name would otherwise collide with a method name, but it is not forced onto every enum/class. Do not add a separate alias to the same type just to match a suffix rule. - Node/TypeScript:
camelCasemethods,PascalCaseclasses.
Full C API Coverage¶
- Each per-language spec file must describe a binding interface that
covers every
ZLINK_EXPORTfunction incore/include/zlink.hand the public headers undercore/include/zlink/**, without omission. - The mapping does not have to be 1:1 (for example, a group of option functions can consolidate into a single typed facade).
- But no C API capability may be missing from a binding spec.
- When a new C API is added to a public header, every per-language spec file must be updated together.
Service Layer Policy¶
- This section defines the public API policy for the service layer (Spot, Actor) that sits on top of the socket layer.
- The service layer follows the same POSD principles, naming policy, error policy, ownership policy, and testing policy as the socket layer.
- The service layer's baseline is the Spot/Actor C API in
core/include/zlink.h.
Spot / SpotNode Lifecycle (POSD Principles)¶
SpotNodeis the lifecycle owner.Spotis a pub/sub facade on top of it, valid only whileSpotNodeis alive.Spotis not built with an independent constructor. It is created through a factory method such asSpotNode.createSpot(...). The name follows language idiom (spot_node.new_spot,spotNode.createSpot, and so on).- The "get if it exists, otherwise create" flow keyed on an explicit
Spot routing id is exposed through a
SpotNode.getOrCreateSpot(...)family method that directly maps tozlink_spot_node_spot_get_or_new(...). A binding must not emulate this meaning by combining lookup and create. Spot's life is bound to its parentSpotNode.spot.close()— ends only the Spot; the node stays alivespotNode.close()— cleans up the node and every live Spot under it together (cascading close)- This removes the need for a user to manually combine the close order
of
SpotandSpotNode. The binding pre-processes child spots insideSpotNode.close()before tearing down the node. - The C API's raw
zlink_spot_new(...)+zlink_spot_node_new(...)combination is not exposed as a binding public constructor as-is. It must be wrapped in aSpotNode-centered factory pattern.
Service Layer Introspection Surface Tiers¶
The service layer's introspection/snapshot/entry types are split into two tiers by usage frequency. A binding spec reflects this split.
- Primary (core): a snapshot/query surface an ordinary user uses
frequently. Described in
bindings/<lang>/README.md's upper section. -
SpotNodeStatus(spot node status) -
Advanced/Diagnostic: for special purposes such as debugging or operational monitoring. Described in a separate "Advanced" or "Diagnostic" subsection in the spec.
SpotNodePeerEntry,SpotNodeSubjectEntrySpotNodeSocketEntry,SpotNodeSpotEntry,SpotNodeActorEntry- Various filter types (
SpotNodePeerFilter,SpotNodeSubjectFilter,SpotNodeSocketFilter)
The Primary types alone must be enough for a basic usage scenario. The "register / discover / connect a service" flow must complete without learning the Advanced types.
Public Exposure Of zlink_errno()¶
- A binding does not expose the raw
zlink_errno()/zlinkErrno()function publicly. Error detail is accessed only ever through the error type'sinternalErrno/internal_errnofield. - Do not create a dual path where a user investigating an error
"sometimes uses
ZlinkException.getCode()and sometimes usesZlink.errno()" — unify it to one entry point. - It's allowed for a binding's internal implementation to call
zlink_errno()to fill in the exception object (for internal interpretation). The ban applies only to the public surface. - A message-lookup utility such as
Zlink.strerror(errno)can remain as a convenience, but the rawerrno()accessor should be private or removed.
Service Layer Architecture¶
- The service layer's current public axes are
SpotNode,Spot,Actor,StreamSocket's Actor binding surface, and the SPOT route bridge/publisher surface. The public Discovery/Registry handle was removed from the core contract in core 8.4.3, so it must not be revived as a new binding surface.
SpotNode
|-- bind
|-- raw mesh: connectPeer, disconnectPeer
| createPublisher
|-- actor: create, lookup, remote create, join, leave
|-- introspection: status, peers, peers(filter),
| subjects, spots, actors
`-- TLS: setTlsServer, setTlsClient
Spot
|-- publish, subscribe
|-- sendToChannel, requestToChannel
|-- sendToSpot, requestToSpot, requestToRouter
|-- replyToSpot, replyToRouter
|-- actor join: recvActorJoin, replyActorJoin, actors
|-- actor lifecycle: recvActorLifecycle
|-- setSubscription, unsetSubscription
|-- setDispatchHandler
`-- close facade only
Actor
|-- ref: nodeRid, actorId, generation
|-- receive: recvPart
|-- bound session: send, close
`-- close lifecycle handle
StreamSocket
|-- bindActor, unbindActor
`-- sendBoundActor
|-- connect
|-- snapshot
`-- close
Actor Dispatch Policy¶
Actor dispatch is a formal service-layer contract that currently exists
in core's public header. A binding does not hide Actor as an internal
SPOT detail — it organizes it as a separate public capability spanning
SpotNode, Spot, Actor, and StreamSocket.
If the language provides a unit for splitting public surface — a header, module, package, namespace — Actor must have its own independent entrypoint. This entrypoint must not be a thin forwarding file that merely re-includes/imports/exports the whole SPOT header or module. The Actor entrypoint must substantively own the public types and function declarations that make up the Actor contract — the Actor value object, the Actor lifecycle handle, the Actor recv/join helper. A structure where the SPOT entrypoint reuses the Actor entrypoint is allowed, but a structure where the Actor entrypoint exists only by leaning on the whole SPOT implementation is non-compliant.
The baseline core public types and functions are as follows.
- Types:
zlink_actor_ref_t,zlink_actor_route_t,zlink_actor_recv_info_t,zlink_actor_join_info_t,zlink_actor_join_result_t,zlink_actor_join_entry_spot_result_t,zlink_actor_lookup_result_t,zlink_spot_actor_lifecycle_info_t,zlink_actor_join_spot_handler_fn,zlink_actor_join_entry_spot_handler_fn,zlink_actor_lookup_handler_fn,zlink_spot_node_spot_entry_t,zlink_spot_node_actor_entry_t SpotNodeaxis:zlink_spot_node_actor_new,zlink_spot_node_actor_lookup,zlink_remote_actor_get_ref(async lookup),zlink_spot_node_actor_destroy(async submit),zlink_spot_node_actor_join_spot(async submit + a dedicated completion typedef),zlink_spot_node_actor_join_entry_spot(async submit + a dedicated completion typedef),zlink_spot_node_actor_leave_spot(async submit),zlink_spot_node_actor_recv_part,zlink_spot_node_actor_send_bound_session_msg,zlink_spot_node_actor_reply_no_bind,zlink_spot_node_actor_close_bound_sessionSpotaxis:zlink_spot_actor_join_recv,zlink_spot_actor_join_reply,zlink_spot_recv_actor_lifecycle,zlink_spot_actorsStreamSocketaxis:zlink_stream_bind_actor(async submit),zlink_stream_unbind_actor(async submit),zlink_stream_send_bound_actor_part,zlink_stream_bound_actors- Snapshot axis:
zlink_spot_node_spots,zlink_spot_node_actors,zlink_spot_actors
The binding surface follows this split of responsibility.
| Public owner | Actor role |
|---|---|
SpotNode |
Local Actor create/lookup, async remote Actor lookup, async destroy, async join/leave, node-level Actor snapshot |
Actor |
Holds the Actor ref, Actor recv, bound STREAM session message send, bound session close |
Spot |
Actor join request recv/reply, Actor lifecycle event receive, a snapshot of Actors currently joined to this Spot |
StreamSocket / session facade |
Async STREAM session Actor bind/unbind, send targeted at a bound Actor, session-attach list lookup |
A binding must provide the following domain objects as a public contract. The name can be converted to fit language convention, but the field meaning does not change.
| Object | Required meaning |
|---|---|
ActorRef |
node_rid, actor_id, generation |
ActorRoute |
The routed target Actor, current Spot routing id, current Spot kind |
ActorRecvInfo |
The receiving Actor, source node/session routing id, flags |
ActorReceived |
ActorRecvInfo plus payload parts. The name can change per language convention, but the native array, capacity, and count are not exposed as public fields. In a language that owns the payload parts, expose it as a disposable envelope, not a cloneable record/value |
ActorJoinInfo + join message |
The source_actor, target_actor, source_node_rid, source_spot_rid, target_node_rid, target_spot_rid, join_epoch, flags, and join message needed to judge and respond to a join request. Can be grouped into an ActorJoinRequest wrapper or a tuple/pair per language convention. A wrapper that owns the join message must be disposable. The native reply context is kept only inside the binding and is not exposed as a public field |
ActorJoinResult |
Delivered on join completion. result, the final actor ref (the target node's ref for a remote join), joined_spot_rid, join_epoch, flags |
ActorJoinEntrySpotResult |
Delivered on Entry Spot join completion. result, the final actor ref, target_node_rid, join_epoch, flags. No join message or reply payload |
ActorLookupResult |
Delivered on remote Actor lookup completion. result, the checked actor ref, flags |
SpotActorLifecycleEvent |
The result of draining a Spot lifecycle readable event. kind, info. In a language where it also owns request parts, expose it as a disposable envelope, not a cloneable record/value |
SpotActorLifecycleInfo |
Included in a Spot lifecycle event. previous_actor, current_actor, previous_spot_rid, current_spot_rid, join_epoch, flags |
SpotNodeSpotEntry |
Spot routing id, Entry/User Spot kind, whether a dispatch handler is set, joined/pending Actor count, route sync state, change timestamp |
SpotNodeActorEntry |
Actor ref, current Spot routing id, current Spot kind, route sync state, pending message count, change timestamp |
The detailed rules are as follows.
- An Actor id is a non-empty UTF-8 string up to 255 bytes. A NUL character is not allowed.
generation == 0is an unchecked remote ref, and is not treated as an invalid value.- A local Actor is created by
SpotNode, and its lifecycle handle is exposed as the per-languageActortype. An Actor can join only one Spot at a time. leaveis an async submit API. It does not drain unread Actor messages. It always returns to the Entry Spot of the same node — ifleavesucceeds from a user Spot, a source-left event and an Entry-Spot-joined lifecycle event fire, and the active route is updated to the Entry Spot location.- Entry Spot join is an async submit API. The target argument is the SpotNode rid, not an Entry Spot rid. Because a SpotNode has only one Entry Spot, the public API does not require a separate Entry Spot rid. An Entry Spot join does not send a join message and does not go through the application join queue. The completion handler returns only success/failure and the final Actor ref.
- An Actor that must start on a remote node is created by the
application directly on that SpotNode with
actor_new. A checked ref for a remote Actor is obtained through the asyncremote_actor_get_reflookup. Remote create-or-get and an admission handler are not on the public surface. - A Spot join request carries a message. A join reply must also return a
message to the caller together with the accept/reject result. Join
completion delivers the final Actor ref and joined Spot rid to the
caller as an
ActorJoinResultvalue. - The request-reply surface exposes only the payload part the core reply function supports. Because the core reply function has no send-flag argument, a binding does not add a no-op flag-setting step to the reply builder.
ActorJoinInfoexposing this does not mean it must expose every field of nativezlink_actor_join_info_tas a public field. A per-language binding keeps the native request context needed for the reply as opaque internal state. The public value object exposessource_actor,target_actor, the source/target node and Spot routing id,join_epoch,flags, and the message — what a user needs to judge and respond.- One STREAM session can bind multiple Actors. Bind/unbind is keyed on the session routing id and either the actor id or the Actor ref.
- When a language can naturally provide a session facade, it's better to expose STREAM Actor bind/unbind and send targeted at a bound Actor as operations on the session facade, rather than as socket-wide functions. This avoids repeatedly passing the session routing id.
- A public API that sends from STREAM to an Actor uses the bound session and actor id as its selector.
- Actor location is updated through the Actor creation, Spot join/leave, and Actor destroy flows. STREAM session bind/unbind does not change Actor location.
- There is no per-Actor queue-limit option. A binding must not make this a public option.
- A removed Actor ref function, a stream actor lookup/send helper, or a session-actor-key design name is not kept in the public surface or documentation.
An Actor dispatch event uses the same readiness model as the SPOT dispatch event handler.
ZLINK_SPOT_DISPATCH_EVENT_ACTOR_READABLEis a notification that an Actor part can be read. One callback does not mean one part.ZLINK_SPOT_DISPATCH_SUBJECT_ACTOR's subject is a native Actor ref valid only during the callback. A binding's public API does not expose a raw pointer.- A language that hands the callback off to a different execution context must non-blockingly pre-drain the Actor part at callback-entry time, so the public dispatch info can return that part.
ZLINK_SPOT_DISPATCH_EVENT_ACTOR_JOIN_READABLEis the readiness signal for a Spot's Actor join request plane. A binding must let it be drained throughSpot.recvActorJoinor an equivalent public surface until each language's no-data representation appears.
SpotNode Capability Matrix¶
| Capability | SpotNode |
|---|---|
bind |
Y |
connectPeer |
Raw mesh only |
disconnectPeer |
Raw mesh only |
disconnectPeerRid |
Raw mesh only |
createSpot |
Y |
entrySpot |
Y |
spotLookup |
Y |
setTlsServer |
Y |
setTlsClient |
Y |
status |
Y |
peers |
Y |
peers(filter) |
Y |
subjects |
Y |
internalSockets |
Diagnostic |
spots |
Y |
actors |
Y |
close |
Y |
- SpotNode does not directly expose the data-plane API (
send/recv/publish/subscribe). - The data plane is accessed only through the
Spotfacade. connectPeer/disconnectPeerare control paths exclusive to raw peer topology.createSpotis a public factory placed on top ofzlink_spot_new().entrySpotwrapszlink_spot_node_entry_spot()in a per-language typedSpotfactory.spotLookupwrapszlink_spot_node_spot_lookup()as a per-language typedSpotlookup surface. Treated as centered oncreatePublisher.
Actor Capability Matrix¶
Actor dispatch is an independent service-layer capability spanning
SpotNode, Actor, Spot, and StreamSocket. Each binding must expose
the roles below as a public surface that fits its own language
convention.
| Capability | Public owner | Core substrate |
|---|---|---|
| local Actor create | SpotNode |
zlink_spot_node_actor_new |
| local Actor lookup | SpotNode |
zlink_spot_node_actor_lookup |
| unchecked remote Actor ref | SpotNode |
zlink_remote_actor_get_ref |
| Actor destroy by ref | SpotNode |
zlink_spot_node_actor_destroy |
| owned Actor close/destroy | Actor |
zlink_spot_node_actor_destroy |
| Spot Actor lifecycle receive | Spot |
zlink_spot_recv_actor_lifecycle |
| Actor join by ref | SpotNode |
zlink_spot_node_actor_join_spot |
| Actor Entry Spot join by ref | SpotNode |
zlink_spot_node_actor_join_entry_spot |
| owned Actor join | Actor |
zlink_spot_node_actor_join_spot |
| owned Actor Entry Spot join | Actor |
zlink_spot_node_actor_join_entry_spot |
| Actor leave by ref | SpotNode |
zlink_spot_node_actor_leave_spot |
| owned Actor leave | Actor |
zlink_spot_node_actor_leave_spot |
| Actor recv | Actor |
zlink_spot_node_actor_recv_part |
| no-bind request reply | SpotNode |
zlink_spot_node_actor_reply_no_bind |
| bound session send | Actor |
zlink_spot_node_actor_send_bound_session_msg |
| bound session close | Actor |
zlink_spot_node_actor_close_bound_session |
| join request recv | Spot |
zlink_spot_actor_join_recv |
| join request reply | Spot |
zlink_spot_actor_join_reply |
| STREAM bind Actor | StreamSocket / session facade |
zlink_stream_bind_actor |
| STREAM unbind Actor | StreamSocket / session facade |
zlink_stream_unbind_actor |
| STREAM send bound Actor | StreamSocket / session facade |
zlink_stream_send_bound_actor_part |
| STREAM bound Actor snapshot | StreamSocket / session facade |
zlink_stream_bound_actors |
| node Spot snapshot | SpotNode |
zlink_spot_node_spots |
| node Actor snapshot | SpotNode |
zlink_spot_node_actors |
| Spot joined Actor snapshot | Spot |
zlink_spot_actors |
Spot Capability Matrix¶
| Capability | Spot |
|---|---|
publish(topic, ...) |
Y |
subscribe |
Y |
receiveSubscriptionEvent |
Y |
setSubscription / unsetSubscription |
Y |
sendToChannel / requestToChannel |
Y |
sendToSpot |
Routed ordinary send (spot → spot) |
requestToSpot |
Routed request initiation (spot → spot) |
requestToRouter |
Routed request initiation (spot → router) |
replyToSpot |
Routed reply surface (spot → spot) |
replyToRouter |
Routed reply surface (spot → router) |
setDispatchHandler |
Y |
recvActorLifecycle |
Y |
close |
Y |
- Spot is not a socket type — it's a channel-aware facade layered on top of SpotNode.
- Spot routed receive can be exposed as
recv_routedor an equivalent typed recv surface. - Spot has no
bind/connect(SpotNode owns that). - Spot's
closereleases only the facade — SpotNode stays alive.
Removed Discovery/Registry Capability¶
The public Discovery and Registry C API was removed from the core contract in core 8.4.3. A binding must not expose a Discovery/Registry factory, resolver method, sync option, registry query client, or compatibility alias as current API.
Service Observability Policy¶
- Public service-layer observation uses a snapshot/query surface instead of a separate monitor handle.
- SPOT (SpotNode, Spot) observation uses the
status,peers,peers(filter),subjects,spots, andactorsAPIs. A binding that needs internal socket diagnostics keepsinternalSocketsas a separate diagnostic surface. - When a state transition needs to be observed, compare successive snapshot/query results.
- The SocketMonitor callback release policy stays the same as before.
- When a callback registration API exists, release it only through
close()
Service Layer Domain Objects¶
- The service layer must also use domain objects.
- The minimum core domain objects:
MonitorStatus: a monitor status snapshotSpotNodeStatus: SpotNode status (state, peer count, and so on)- Advanced/Diagnostic domain objects:
SpotNodePeerEntry: peer informationSpotNodeSubjectEntry: subject informationSpotNodeSocketEntry: internal socket diagnostic information. Uses the sharedSocketTypeenum for the socket kind — does not create a separate SpotNode-only socket-type enum that repeats the same values.SpotNodeSpotEntry: node-owned Spot informationSpotNodeActorEntry: node-owned Actor route information- Filter objects:
SpotNodePeerFilter: a peer-lookup filterSpotNodeSubjectFilter: a subject-lookup filterSpotNodeSocketFilter: an internal socket diagnostic filter- Enum/value objects:
SocketType: the socket kind shared between an ordinary socket and SpotNode's internal socket diagnosticsSpotRole:PUB,SUBSubjectKind:NONE,TOPIC,PATTERNSpotNodeState:IDLE,CONNECTING,PARTIAL_READY,READY,ERRORMonitorSourceKind:SOCKET,SPOT_PUB,SPOT_SUBSpotPeerSource:MANUAL,DISCOVERY,MIXEDSpotPeerState:CONFIGURED,CONNECTING,CONNECTEDMonitorStatus.isReady()or an equivalent convenience accessor interprets ready meaning only for a raw socket monitor source. For aSPOT_PUB/SPOT_SUBsource, the ready bit must not be reinterpreted as extended SPOT readiness.
Service Layer Naming Policy¶
- The service layer also follows the Naming Policy.
- The allowed variation is the same three variations as the Naming Policy — casing variation, a minimal suffix for a language without overloads, and per-language property/getter convention only.
- Word substitution, omission, or replacement is forbidden.
- The detailed rules are the same as the Naming Policy body.
Service Layer Canonical Name Table¶
| Component | Canonical Name | Description |
|---|---|---|
| SpotNode | bind |
Binds an endpoint |
| SpotNode | connectPeer |
Connects a raw peer |
| SpotNode | disconnectPeer |
Disconnects a raw peer |
| SpotNode | createRouteBridge |
Registers a caller/channel-runtime-owned socket with the SPOT route bridge |
| SpotNode | createPublisher |
Creates a publisher handle used for SpotNode's topic-publish ingress |
| SpotNode | setTlsServer |
Configures TLS server |
| SpotNode | setTlsClient |
Configures TLS client |
| SpotNode | status |
A node status snapshot |
| SpotNode | peers |
A peer-list snapshot |
| SpotNode | peers(filter) |
A filtered peer lookup |
| SpotNode | subjects |
A subject-list snapshot |
| SpotNode | internalSockets |
An internal socket diagnostic snapshot |
| SpotNode | spots |
A node-owned Spot snapshot |
| SpotNode | actors |
A node-owned Actor snapshot |
| SpotNode | close |
Terminates the node |
| Spot | publish(topic, ...) |
Publishes a Spot topic |
| Spot | subscribe |
Receives a topic subscription |
| Spot | receiveSubscriptionEvent |
Receives a topic subscription event |
| Spot | setSubscription / unsetSubscription |
Manages a subscription filter |
| Spot | sendToChannel / requestToChannel |
A channel-targeted routed send/request |
| Spot | setDispatchHandler |
Registers the topic/routed/channel-reply/timer readable notification handler |
| Spot | recvActorLifecycle |
Receives an Actor join/leave lifecycle event |
| Spot | close |
Terminates the facade |
Service Layer Test Policy¶
- Because the service layer includes components not directly verified by a sample or perf, it must be tested for correct FFI mapping, lifecycle, and type conversion.
- The service layer is tested using the same categories as the Test Matrix.
Service Layer Surface Tests¶
- Confirm SpotNode role-matrix alignment
- Confirm Spot role-matrix alignment
- Confirm the service TLS helper exists
- Confirm the typed domain objects exist (SpotNodeStatus, SpotNodePeerEntry, SpotNodeSocketEntry, SpotNodeSpotEntry, SpotNodeActorEntry, and so on)
- Confirm the typed enums exist (SpotRole, SubjectKind, SpotNodeState, and so on)
Service Layer Contract Tests¶
- SpotNode: no leak across the create/bind/close lifecycle
- Spot: create/close lifecycle (SpotNode must stay alive)
- Confirm native resources are cleaned up on exception/error paths too
Service Layer Behavior Tests¶
- SpotNode bind → Spot publish → Spot subscribe path succeeds
- Spot subscribe → returns empty when there's no data (non-blocking)
- Confirm exception on Spot publish failure
- Confirm the Spot dispatch event callback fires
- Confirm the Spot receiveSubscriptionEvent path
- Confirm SpotRouteBridge attach/send/request/handleReceived paths work
- Confirm the SpotNode publisher handle publish path works
Service Layer Introspection Tests¶
- SpotNode status → verify SpotNodeStatus fields (state, peerCount, subjectCount, and so on)
- SpotNode peers → verify the SpotNodePeerEntry list
- SpotNode peers(filter) → verify the filtered result
- SpotNode subjects → verify the SpotNodeSubjectEntry list
Service Layer Test Scope¶
| Test Category | SpotNode+Spot | Actor | Stream Actor Binding |
|---|---|---|---|
| Surface | Required | Required | Required |
| Contract | Required | Required | Required |
| Behavior | Required | Required | Required |
| Introspection | Required | Required | Required |
- A binding without a service/spot family can exclude this test.
- Here, "monitor" refers to a socket monitor.
Service Layer Sample Policy¶
- Service-family samples defined in the Canonical Sample Set:
spot_recv_sample: Spot channel-aware subscribe/routed recvspot_callback_sample: Spot dispatch event callbackmonitor_recv_sample: monitor event receive (including socket monitor)- A binding without a service/spot family can exclude the
spot_*samples.
Per-Binding Service Layer Scope¶
- Not every binding has to implement the entire service layer.
- The minimum requirement:
| Component | Required level |
|---|---|
| SpotNode + Spot | Required if that binding has spot support |
Callback API Policy¶
- A callback registration API is exposed according to each socket type's role.
- The Callback Capabilities table above is the baseline.
- Canonical handler registration names:
setDispatchHandler: registers the SPOT unified readable notification callback- SPOT routed receive and Actor lifecycle do not expose a direct
callback registration API.
setDispatchHandlerannounces a readable event, and the user explicitly drains the queue withrecvRoutedorrecvActorLifecycle. onReceivemay be used only as the internal name for the rawSTREAMdirect fragment callback. It is not used as a canonical public binding API name.- Unregistering a callback by setting it to
null/Noneis not allowed. A callback is unregistered only by closing the socket.
Core API Additions¶
This section summarizes the core APIs added to core/include/zlink.h.
Each binding must expose these APIs as a per-language typed surface.
Request-Reply Policy¶
See
cpp/,java/,dotnet/,node/,python/,go/,rust/for per-language interface signatures and usage examples.
The Core ZMP specification owns the exact byte layout and validation for request-reply kinds, sequences, and ZMP headers. This section owns the binding contract that preserves application payload part count and ownership while keeping internal metadata out of the public surface.
Design Principles¶
The Core request contract owns native request submission, reply matching, and timeout. Binding completion delivery follows the async execution model; language-specific blocking and awaitable surfaces follow the terminal policy. Request-reply is a capability extension of Router/Dealer sockets and SPOT.
APIs Not On The Public Surface¶
The message-level request-reply marker API and the per-message metadata
API are not part of the public surface. A binding does not expose the
following functions or constants publicly, and does not keep request
marker state inside a Message object.
zlink_msg_set_request,zlink_msg_set_reply,zlink_msg_get_request_infozlink_msg_set_metadata,zlink_msg_get_metadata,zlink_msg_clear_metadata
Valid Request-Reply Combinations¶
Socket paths:
| Requester | Responder | Possible | Reply path |
|---|---|---|---|
| Dealer | Router | Y | Router replies using the Dealer's routing_id |
| Router | Router | Y | Both sides reply using routing_id |
| Dealer | Dealer | N | Neither side has a routing_id |
| Router | Dealer | N | Dealer cannot reply to a specific peer |
SPOT paths:
| Requester | Responder | Possible | Reply path |
|---|---|---|---|
| Spot | Spot | Y | Replies with the peer's address + request_seq |
| Spot | Router | Y | Spot requests Router; Router replies to Spot |
| Router | Spot | Y | Router requests Spot; Spot replies to Router |
DealerSocket.request() connection constraints:
- Every connected target must be a Router. If Router and Dealer are
mixed on a Dealer's connections, request can fail.
- A binding does not validate this constraint at runtime. It is the
user's responsibility, and it is documented in the API docs.
C API Surface¶
Raw socket request, reply, and ROUTER receive declarations follow Core request and reply and Core routed receive. Core completion pull and ownership owns completion records and the lifetime of returned reply parts.
SPOT service-layer operation names (binding-level concepts):
zlink_submit_result_t zlink_spot_send_channel_part(void *spot, ...);
zlink_submit_result_t zlink_spot_request_channel_part(void *spot, ...);
zlink_submit_result_t zlink_spot_send_spot_part(void *spot, ...);
zlink_submit_result_t zlink_spot_request_spot_part(void *spot, ...);
zlink_submit_result_t zlink_spot_request_router_part(void *spot, ...);
zlink_submit_result_t zlink_spot_reply_spot_part(void *spot, ...);
zlink_submit_result_t zlink_spot_reply_router_part(void *spot, ...);
zlink_submit_result_t zlink_router_request_spot_part(void *router, ...);
zlink_submit_result_t zlink_router_reply_spot_part(void *router, ...);
zlink_submit_result_t zlink_router_send_spot_part(void *router, ...);
zlink_submit_result_t zlink_spot_publish_part(void *spot, ...);
zlink_recv_result_t zlink_spot_subscribe_part(void *spot, ...);
zlink_recv_result_t zlink_spot_recv_part(void *spot, ...);
zlink_handler_result_t zlink_spot_dispatch_event_handler(void *spot, ...);
The list above describes operation names at the binding/service layer; it is not a list of Core C
function declarations. See core/include/zlink.h for the actual Core C request-reply signatures.
Receive Dispatch Model¶
Core routed receive owns ROUTER application record kinds, source RIDs, and reply tokens. Reply matching follows the Core request contract; delivery of matched results to the language follows the common completion owner. SPOT-specific routing context belongs to a separate service-layer API.
Request API Variants¶
A request exposes one language-native suspension terminal through the
RequestOp operation builder the request entrypoint returns. Timeout and
failure-delivery rules follow the bindings async execution surface
policy.
The C binding keeps the substrate shape
zlink_*_request_part(..., flags, part_flag, timeout, ...). The wrapper
builder policy does not apply to the C ABI.
- Error handling follows the Error Handling Policy. Exception languages use an exception and return-based languages use an error result for suspension failure.
- A reply result is delivered exactly once as the suspension's successful
List<Message>value.
SPOT Request-Reply¶
The same request-reply protocol is used on top of SPOT direct delivery. After SPOT routed control, the ZMP header of the first application payload carries the request-reply kind and sequence; no request-reply-specific payload part is added. A SPOT reply is also sent with the peer's address + request_seq, without a ctx. Multiple requests can be outstanding concurrently on the same Spot. A high-level request's completion ends with the first reply.
Timeout¶
- The request builder's timeout corresponds to Core's reply timeout, measured from admission.
Core owns the start point, defaults, the meaning of
0, and expiry, so a binding does not recalculate it with a separate timer. Cancellation of the language wait follows caller wait cancellation.
Pending Map¶
The Core request contract owns wire request correlation. A binding associates submit results and completions with language terminals; the async execution model owns their lifetime and join.
Wire Format¶
The Core ZMP specification owns ZMP kinds, sequences, header byte layout, and validation. A binding neither constructs nor interprets wire metadata and preserves the payload boundary in the Request-Reply policy.
Return Type¶
- On success,
request()returns only the reply payload'sList<Message>(a per-language list type such asVec<Message>,IReadOnlyList<Message>,Message[], ortuple[Message, ...]). - Do not wrap request-target metadata together with the reply payload in a
Received. - A separate
Replytype is not created. - Because multipart reply support is the goal, it's a list shape rather
than a single
Message. A single-part reply is retrieved withparts[0]. - Responder receive metadata follows Core routed receive
and the ReplyToken policy.
Do not introduce a separate
Requesttype or dedicatedonRequestcallback.
Ownership¶
Request and reply inputs follow the existing send ownership contract. Native reply-array and part cleanup follow Core completion ownership; language payload conversion and unfinished terminals on close follow the async execution model.
Language completion results¶
Request success payloads, typed errors, and completion after cancellation follow the common completion contract. Language completion types and terminal signatures follow the terminal interfaces.
SPOT Messaging Policy¶
See
cpp/,java/,dotnet/,node/,python/,go/,rust/for the per-language SPOT interfaces.
The SPOT public surface separates two naming axes. sendToChannel(...)
and requestToChannel(...) are the channel-aware direct messaging path,
while publish(topic, ...) publishes to the topic plane the Spot
itself belongs to. Direct address-targeted routed messaging is an
optional supplementary typed surface. Request-reply is layered on top
of routed messaging.
Pub/Sub Messaging¶
SPOT pub/sub is a publish/subscribe model based on the channel a Spot
handle belongs to and a topic. The publish caller does not pass the
channel name as a separate argument.
The binding runtime submits the payload collected by a publish builder to Core once as an array and count. Subscribe receive also gets the topic and complete payload array in one Core call, then fills the language-level object.
Binding rules:
- A binding does not have a separate no-wait function name for publish.
- A non-blocking publish is selected through the builder flag and classifies
errno into zlink_submit_result_t. A binding does not add a separate
tryPublish or publishNoWait.
- A subscribe receive is exposed as a typed receive surface that
returns topic + parts.
- Topic filter configuration is exposed as a typed subscription API.
- A channel-aware send/request or topic publish failure is promoted to
SubmitError.
- NOT_FOUND: for channel-aware send/request, no matching
channel_name or attach target exists; for topic publish, there is
no target on the topic plane to publish to.
- NOT_CONNECTED: an attachment exists, but there is no active send path
- BACKPRESSURED: a path exists, but the HWM has been reached
- NOT_ADMITTED: the target peer is draining, so a new submit is
rejected
Routed Direct Messaging¶
SPOT routed direct messaging sends a message directly to a specific Spot
or Router peer, or a routed reply target. All payload parts accumulated by
the builder are submitted to Core once as an array and count. Both the high-level
binding's Spot facade and RouterSocket's router-to-spot helper expose
this capability as an operation builder start point that fits the
Operation Builder Policy. A raw socket's ordinary send/request/reply
also follows the same builder pattern.
Binding rules:
- The binding runtime makes one Core whole-message call per operation.
- The high-level binding's Spot endpoint, RouterSocket's
router-to-spot helper, and every ordinary send/request/reply/publish
surface on raw DealerSocket/RouterSocket/PubSocket/StreamSocket
all follow this document's Operation Builder Policy.
- The destination address/request sequence is taken as a builder
start-point argument, and payload/flags/timeout/callback are expressed
as builder steps.
- Routed recv uses the handler/recv surface of the Event Dispatcher
below.
SPOT Lifecycle / Bridge / Deprecated Attachment¶
void *zlink_spot_new(void *node); /* create SPOT facade */
zlink_close_result_t zlink_spot_destroy(void **spot_p);
void *zlink_spot_node_new(
void *ctx,
const zlink_spot_node_options_t *options);
zlink_close_result_t zlink_spot_node_destroy(void **node_p);
zlink_bind_result_t zlink_spot_node_bind(void *node, const char *endpoint);
zlink_connect_result_t zlink_spot_node_connect_peer(void *node,
const char *peer_endpoint);
zlink_connect_result_t zlink_spot_node_disconnect_peer(void *node,
const char *peer_endpoint);
zlink_connect_result_t zlink_spot_node_disconnect_peer_rid(void *node,
const zlink_routing_id_t *peer_rid);
void *zlink_spot_route_bridge_new(
void *ctx,
void *spot_node,
const zlink_spot_route_bridge_options_t *options);
int zlink_spot_route_bridge_attach_router_channel(
void *bridge,
const char *channel_name,
void *router,
const zlink_spot_route_bridge_endpoint_options_t *options);
int zlink_spot_route_bridge_send(
void *bridge,
const char *channel_name,
const zlink_routing_id_t *target_node_rid,
const zlink_routing_id_t *target_spot_rid,
zlink_msg_t *parts,
size_t part_count,
zlink_send_flags_t flags);
int zlink_spot_route_bridge_request(
void *bridge,
const char *channel_name,
const zlink_routing_id_t *target_node_rid,
const zlink_routing_id_t *target_spot_rid,
zlink_msg_t *parts,
size_t part_count,
zlink_reply_handler_fn reply_handler,
void *userdata,
zlink_send_flags_t flags,
uint32_t timeout_ms);
int zlink_spot_route_bridge_handle_router_received(
void *bridge,
const char *channel_name,
const zlink_routing_id_t *source_node_rid,
uint64_t request_seq,
zlink_msg_t *parts,
size_t part_count,
bool *handled_out);
int zlink_spot_route_bridge_drain(void *bridge);
int zlink_spot_route_bridge_close(void *bridge);
void *zlink_spot_node_publisher_new(void *node);
int zlink_spot_node_publisher_publish(
void *publisher,
const char *topic,
zlink_msg_t *parts,
size_t part_count,
zlink_send_flags_t flags);
int zlink_spot_node_publisher_close(void *publisher);
options == NULL or options->mode == 0 turns on every SPOT
capability. A binding uses this default in each language's default
constructor, and where it exposes mode, it maps PUBSUB, ROUTED, and
ALL to the same meaning as the C contract. The internal socket
observation API is based on zlink_spot_node_internal_sockets(), and
returns only already-created sockets.
The SpotNode option facade must not omit core's six public options.
| Core option | Binding surface |
|---|---|
ZLINK_SPOT_NODE_OPT_ROUTER_HWM_PROFILE |
router admission HWM profile |
ZLINK_SPOT_NODE_OPT_ROUTER_HWM |
router admission HWM override |
ZLINK_SPOT_NODE_OPT_PUBSUB_HWM_PROFILE |
pub/sub admission HWM profile |
ZLINK_SPOT_NODE_OPT_PUBSUB_HWM |
pub/sub admission HWM override |
ZLINK_SPOT_NODE_OPT_DISPATCH_WORKERS_MIN |
minimum dispatch callback workers |
ZLINK_SPOT_NODE_OPT_DISPATCH_WORKERS_MAX |
maximum dispatch callback workers |
Dispatch worker min/max configures SpotNode's dispatch-callback
execution pool. It must not be described as an option that changes the
data-plane thread count or transport I/O thread count. Value validation
matches core: min >= 1, max >= min. A binding exposes these two
values as a per-language typed option/property, and must not revive a
raw option bag as the canonical path.
Binding rules:
- SpotNode and Spot are exposed as separate typed handles.
- Spot is a facade layered on top of SpotNode. When SpotNode is
released, Spot also becomes invalid.
- Use SpotRouteBridge to send from Spot to a different channel, or to
receive a Spot relay packet on a ROUTER channel. The ROUTER socket
registered with the bridge continues to be owned by the caller or the
channel runtime.
- A raw Core socket has no API to set or query logical channel metadata.
A channel name is used only as the logical routing value
SpotRouteBridge's typed operations accept.
- The bridge's handle_router_received() is called from the channel
runtime's receive loop. When handled == true, the bridge takes
payload ownership, and the caller does not process the same received
object again.
- SpotNodePublisher is a handle for publishing to SpotNode's topic
publish ingress without external code attaching a raw PUB socket to
SpotNode.
- Spot.publish(topic).message(...).submit() is the channel-aware topic
plane that enters SpotNode's own topic publish ingress queue. An
external channel call is described through SpotRouteBridge and the
channel-runtime-owned socket path.
- connect_peer/disconnect_peer are control paths exclusive to raw
peer topology. They must not be described as the central API of the
channel-aware public surface.
SPOT Event Dispatcher Policy¶
Core provides a callback-based event dispatcher model. It can handle multiple event sources (sub recv, routed recv, timer) without synchronization, inside a single I/O thread context.
Core principles: - Once a handler callback is registered, the core I/O thread calls the callback when the event occurs. - Because every callback runs in the same thread context, state can be shared without a lock. - Calling recv, send, or reply inside the callback has no synchronization issue. - The timer also runs in the same context.
Callback Registration API¶
/* raw STREAM direct recv callback */
zlink_handler_result_t zlink_recv_handler(void *s,
zlink_socket_msg_handler_fn handler, void *userdata);
/* raw STREAM packet callback */
zlink_handler_result_t zlink_stream_packet_handler(void *stream,
zlink_stream_packet_handler_fn handler, void *userdata);
/* install or replace the async-send completion callback */
zlink_handler_result_t zlink_send_complete_handler(void *s,
zlink_send_complete_handler_fn handler, void *userdata);
Rules:
- A receive-side core C attach function allows only one active handler per subject.
Attaching again while a native handler is already attached can return
EBUSY. A public binding's set...Handler surface does not directly
repeat this raw attach function — it provides the meaning of storing
or replacing the current public handler.
- zlink_send_complete_handler() is replace-only. It rejects NULL, and
replacing it from the same socket's completion callback fails with
EDEADLK. The callback only hands completion to application state; calling
send, publish, or request inside it fails with EDEADLK.
- zlink_recv_handler() is allowed only on raw STREAM.
- zlink_stream_packet_handler() is also allowed only on raw STREAM,
and the three modes — recv/raw callback/packet callback — are
mutually exclusive.
- Raw PAIR, DEALER, ROUTER, SUB, XSUB do not have a direct
receive callback install surface. PAIR, DEALER, ROUTER receive
only through a public recv method, and SUB, XSUB receive only
through the topic subscribe receive surface.
- After a callback is registered, a direct recv on the same subject and
registering that data-plane ZLINK_POLLIN can fail with EBUSY. The
exact scope follows STREAM/SPOT's per-type rules.
- A public callback setter is replace-only. Passing NULL is not
allowed.
Spot Dispatch Event Handler¶
Spot's core event dispatcher is zlink_spot_dispatch_event_handler().
Once this handler is registered, every event related to the Spot arrives
through a single callback. Callbacks for the same spot must be
delivered in order. The implementation must not call the same spot's
dispatch callback concurrently or reentrantly. Inside the callback, a
caller must be able to check the event kind and process Spot messaging
sequentially by calling recv.
This serialization is per-spot. It does not require global
serialization across different spots. The implementation must be able
to process different Spots in parallel, while still preserving the
sequential-processing contract for the same spot.
typedef enum zlink_spot_dispatch_event_t {
ZLINK_SPOT_DISPATCH_EVENT_SUBSCRIBE_READABLE = 1,
ZLINK_SPOT_DISPATCH_EVENT_ROUTED_READABLE = 2,
ZLINK_SPOT_DISPATCH_EVENT_TIMER_READABLE = 3,
ZLINK_SPOT_DISPATCH_EVENT_CHANNEL_REPLY_READABLE = 4,
ZLINK_SPOT_DISPATCH_EVENT_ACTOR_READABLE = 5,
ZLINK_SPOT_DISPATCH_EVENT_ACTOR_JOIN_READABLE = 6
} zlink_spot_dispatch_event_t;
typedef enum zlink_spot_dispatch_subject_kind_t {
ZLINK_SPOT_DISPATCH_SUBJECT_SPOT = 1,
ZLINK_SPOT_DISPATCH_SUBJECT_TIMER = 2,
ZLINK_SPOT_DISPATCH_SUBJECT_CHANNEL_DEALER = 3,
ZLINK_SPOT_DISPATCH_SUBJECT_ACTOR = 4
} zlink_spot_dispatch_subject_kind_t;
typedef struct zlink_spot_dispatch_info_t {
zlink_spot_dispatch_event_t event;
zlink_spot_dispatch_subject_kind_t subject_kind;
void *subject;
} zlink_spot_dispatch_info_t;
typedef void (*zlink_spot_dispatch_event_handler_fn)(
void *spot, const zlink_spot_dispatch_info_t *info, void *userdata);
zlink_handler_result_t zlink_spot_dispatch_event_handler(void *spot,
zlink_spot_dispatch_event_handler_fn handler, void *userdata);
Usage pattern:
- Register a dispatch event handler.
- When the callback fires, check info->event, info->subject_kind,
and info->subject.
- Inside the active dispatch callback for the same spot, the ordinary
recv surfaces can be used.
- On SUBSCRIBE_READABLE, drain the pub/sub plane with
zlink_spot_subscribe_part() or
zlink_spot_recv_subscription_event().
- On ROUTED_READABLE, recv the routed/request message with
zlink_spot_recv_part().
- On TIMER_READABLE, recv the timer fire with zlink_timer_recv()
against the info->subject timer handle.
- CHANNEL_REPLY_READABLE is only a readiness signal — there is no
separate public drain API. The reply is delivered automatically by
core through the zlink_reply_handler_fn registered when
zlink_spot_request_channel_part() was called. The info->subject
dealer handle is diagnostic information meaningful only on the
deprecated dealer attach path.
- On ACTOR_READABLE, drain zlink_spot_node_actor_recv_part() keyed on
the Actor subject delivered via info->subject. The public API does
not expose the raw subject pointer or the part loop — it returns an
ActorReceived or an equivalent aggregate value object.
- On ACTOR_JOIN_READABLE, drain the join request plane with
zlink_spot_actor_join_recv().
- A dispatch event is a readable notification. One callback does not
mean one message.
- Inside the callback, a caller must be able to drain that plane until
there's nothing left to read.
- The first call to zlink_spot_recv_part() must not perform hidden
activation, hidden queue open, or hidden registration.
- Because dispatch callbacks for the same spot are serialized, Spot
messaging can be processed sequentially.
- Because different spots can be processed in parallel, a
high-performance room execution model can be built.
Spot Timer API¶
A Spot-owned timer is created with zlink_spot_timer_new(spot), and
controlled afterward through the common zlink_timer_* functions.
void *zlink_spot_timer_new(void *spot);
/* use the common timer API after creation */
zlink_close_result_t zlink_timer_destroy(void **timer_p);
zlink_config_result_t zlink_timer_start(void *timer,
uint64_t interval_ns, uint64_t repeat_count);
zlink_config_result_t zlink_timer_stop(void *timer);
typedef void (*zlink_timer_handler_fn)(
void *timer, uint64_t fire_count, void *userdata);
zlink_handler_result_t zlink_timer_handler(void *timer,
zlink_timer_handler_fn handler, void *userdata);
zlink_recv_result_t zlink_timer_recv(void *timer, uint64_t *fire_count_out);
Rules:
- A timer is created dependent on a Spot, via
zlink_spot_timer_new(spot).
- After creation, it's controlled through the common
zlink_timer_start, zlink_timer_stop, zlink_timer_recv,
zlink_timer_handler, and zlink_timer_destroy APIs.
- interval_ns is in nanoseconds. repeat_count = 0 means infinite
repeat.
- A timer fire arrives at the dispatch event handler as
TIMER_READABLE.
- A timer handler callback can be registered directly, or polled with
zlink_timer_recv().
- Inside the dispatch callback, a pending fire can be processed
sequentially with zlink_timer_recv().
Binding rules:
- A timer is exposed as a typed wrapper.
- interval_ns is converted to that language's Duration type.
- Timer and dispatch event are unified, so a user must be able to handle
sub recv + routed recv + timer without synchronization, just by
registering a callback.
Dispatch Model Summary¶
zlink_spot_dispatch_event_handler callback
(serialized per spot, non-reentrant)
|-- SUBSCRIBE_READABLE -> zlink_spot_subscribe_part()
| or zlink_spot_recv_subscription_event()
|-- ROUTED_READABLE -> zlink_spot_recv_part()
|-- TIMER_READABLE -> zlink_timer_recv()
|-- CHANNEL_REPLY_READABLE -> readiness only; reply handler runs internally
|-- ACTOR_READABLE -> zlink_spot_node_actor_recv_part()
`-- ACTOR_JOIN_READABLE -> zlink_spot_actor_join_recv()
For the same spot, a caller must be able to process recv, send, and
reply sequentially inside this callback. Different spots must be able
to run in parallel where needed. Inside the callback, a caller must be
able to drain the plane the event announced.
Receive-Model Summary¶
| Socket type | Receive path |
|---|---|
PAIR / DEALER |
The runtime uses zlink_recv(); the public surface is aggregate recv |
SUB / XSUB |
The runtime uses zlink_subscribe(); the public surface is aggregate topic recv |
ROUTER |
Runtime uses zlink_router_recv() and the public surface is aggregate routed recv. Request completion follows the common execution model. |
STREAM |
One of three modes below (mutually exclusive). Raw recv / zlink_recv_handler() / zlink_stream_packet_handler() |
SPOT |
zlink_spot_recv_part() + zlink_spot_subscribe_part() + zlink_spot_recv_subscription_event() + zlink_spot_recv_actor_lifecycle() + zlink_spot_dispatch_event_handler(). Does not expose a direct routed callback |
A binding reflects the contract above in its implementation as-is. A public socket class exposes only the aggregate recv surface, and a forbidden callback install surface must not be reachable by a bypass through any base class.
Typed Receive Surface¶
SPOT receive provides several typed surfaces. A binding layers a per-language handler/callback surface on top of these typed surfaces.
Spot Receive¶
zlink_recv_result_t zlink_spot_recv_part(void *spot, ...);
zlink_recv_result_t zlink_spot_recv_actor_lifecycle(void *spot, ...);
request_seq = 0means an ordinary routed message.request_seq != 0means a request-reply message.source_rid + spot_ridis the sender's address, used as the reply target.- A binding's public API exposes an aggregate
Receivedor a per-language equivalent type, instead of the part helper. - Actor lifecycle is drained with
zlink_spot_recv_actor_lifecycle()after a dispatch event.
Router Receive (unified routed recv surface)¶
Raw ROUTER DATA/REQUEST receive signatures and metadata follow Core routed receive; request-result delivery follows the common completion owner. SPOT-specific routing context belongs to a separate service-layer API.
Pub/Sub Receive¶
- Raw
SUB,XSUBare receive-only topic sockets. - A binding exposes a per-language aggregate topic receive surface on
top of the
zlink_subscribe()typed receive substrate. - A direct topic callback install surface is not placed on the raw pub/sub family.
SPOT Snapshot Query¶
zlink_config_result_t zlink_spot_node_status(void *node,
zlink_spot_node_status_t *out);
zlink_config_result_t zlink_spot_node_peers(void *node,
zlink_spot_node_peer_entry_t *entries, size_t *count);
zlink_config_result_t zlink_spot_node_peers(void *node,
const zlink_spot_node_peer_filter_t *filter,
zlink_spot_node_peer_entry_t *entries, size_t *count);
zlink_config_result_t zlink_spot_node_subjects(void *node,
const zlink_spot_node_subject_filter_t *filter,
zlink_spot_node_subject_entry_t *entries, size_t *count);
zlink_config_result_t zlink_spot_node_internal_sockets(void *node,
const zlink_spot_node_socket_filter_t *filter,
zlink_spot_node_socket_entry_t *entries, size_t *count);
zlink_config_result_t zlink_spot_node_spots(void *node,
zlink_spot_node_spot_entry_t *entries, size_t *count);
zlink_config_result_t zlink_spot_node_actors(void *node,
zlink_spot_node_actor_entry_t *entries, size_t *count);
zlink_config_result_t zlink_spot_actors(void *spot,
zlink_actor_ref_t *entries, size_t *count);
Binding rules: - A snapshot result is converted into a per-language typed domain object array. - A filter query is exposed as a typed filter builder or struct. - A binding must properly release the memory of a returned array.
SpotNode Node-Level Options¶
SpotNode's node-level options are handled through the
zlink_set_spot_node_option() family.
Option Policy¶
Public Option Surface¶
- A public raw
setOption(key, value)/getOption(key)bag is forbidden. - A public raw
setsockopt/getsockoptbag is also forbidden. - Common options are exposed only through a per-language typed surface (facade).
- Specialized options are also exposed only through a per-language role surface (facade).
- If a spec still has a public path that rotates a raw enum key plus a
generic setter/getter, that's a policy violation. (A C contract such
as
set_option(ZLINK_OPT_*, value)must not surface as binding public API. A native call path used internally by the binding is allowed.) - Once a typed facade exists, the raw path is not exposed redundantly — a user should never have to choose between the two.
- Examples:
- Java/.NET:
CommonSocketOptions,RouterSocketOptions - Go: typed method set, role interface
- Rust: typed builder, method set, newtype
- Python/Node: property, namespace object, role object, typed method set
Option Facade Canonical Type Names¶
- Every binding must provide the canonical facade types below.
- The type name varies only in language casing convention.
| Facade | Contents | Applies to |
|---|---|---|
CommonSocketOptions |
linger, sendHighWaterMark, receiveHighWaterMark, sendTimeout, receiveTimeout, immediate, connectTimeout, ipv6, tcpNoDelay, tcpKeepAlive, heartbeatInterval/Ttl/Timeout, maxMessageSize, backlog, reconnectInterval/Max, submitRetryMode, submitRetryTimeout, submitRetryAttempts | All |
RouterSocketOptions |
mandatory (bool), handover (bool), probe (bool), connectRoutingId (RoutingId), requestTimeout (Duration), peerWeight (int, read/write) | Router |
DealerSocketOptions |
probe (bool), requestTimeout (Duration), peerWeight (int, read/write) | Dealer |
StreamSocketOptions |
notify (bool) | Stream |
PubSocketOptions |
verbose (bool), verboser (bool), noDrop (bool), manual (bool) | Pub, XPub |
SubSocketOptions |
topicsCount (int, read-only) | Sub, XSub |
- Each facade's option items are based on the matching option enum value
in
core/include/zlink.h. - The option value type inside a facade follows the Option Value Types policy.
- A submit retry option exposes off/0ms/0 attempts as the default on the
raw socket facade. A managed SPOT/service internal profile may use
LOCAL_FAILURE/100ms/2 attempts, but this does not change the raw socket option default. ADONTWAITcall, backpressure, admission rejection, and the reply timeout after a successful request submit are not subject to submit retry.
Option Value Types¶
- Expose an option value as a meaning-based type wherever possible.
- Policy:
- a
0/1option:boolean - a finite state set:
enum - time meaning:
Durationor that language's standard time type - a binary identifier: a value object such as
RoutingId - a genuinely numeric setting:
int/long - string/bytes:
String/byte[] - An option whose name alone is an enum while the value is a raw
intis not sufficient.
Performance Policy¶
- Performance is not a separate optimization item — it's part of the public API design.
- The canonical hot path must be the path with the fewest hidden costs.
- The following are forbidden by default on the hot path:
- a hidden payload copy
- a hidden array/list reallocation
- unnecessary UTF-8 encoding/decoding
- redundant wrapping at the binding layer
- unnecessary boxing/unboxing just to build a result
- A convenience API must be documented if it costs more than the default path.
- The callback path and the direct receive path must not diverge excessively — not just in payload shape, but in cost model too.
- If a zero-copy, borrowed, or owned path differs, the cost model must be documented together with ownership.
- The intensity of performance verification can vary by language and runtime characteristics.
- Still, every binding must adopt, as a baseline policy, reducing unnecessary copies, allocations, and conversions on the hot path.
High-Performance Buffer Ecosystem Policy (Recommended)¶
- The canonical public contract keeps
Message/List<Message>/Received/TopicMessageas its baseline. - On the send/publish/request/reply input path, however, it's recommended to support, as an adapter surface, a buffer ecosystem type that is effectively standard in that language and gives a large copy-reduction benefit.
- This support does not replace the canonical contract.
- It does not change a recv result into an external library type.
- It does not change a domain object's field type into an external library type.
- Even where supported, it's limited to an entrypoint such as
Messageconstruction, an input adapter, afrom_*helper, orimpl IntoMultipart. - Criteria for support:
- Is it widely used in that language's networking/IO ecosystem?
- Is the zero-copy or copy-reduction benefit substantial?
- Does it avoid forcing dependency on a specific framework across the entire public surface?
- Not criteria:
- a niche library
- a buffer type used mainly inside a specific company/project
- a wrapper that tries to replace the canonical type
Recommended priority:
| Language | Recommended support | Level | Notes |
|---|---|---|---|
| Java | Netty ByteBuf |
Recommended | Very common in network stacks; the direct/off-heap path has significant value |
| Java | Agrona DirectBuffer |
Optional | Useful in low-latency contexts, but lower priority than Netty |
| .NET | ReadOnlyMemory<byte> / ReadOnlySequence<byte> / IBufferWriter<byte> |
Recommended | The standard buffer ecosystem; large copy-reduction benefit |
| .NET | PipeReader / PipeWriter |
Optional | Useful for the System.IO.Pipelines user base |
| Rust | bytes::Bytes / BytesMut |
Recommended | Effectively standard in the async/network ecosystem |
| Python | buffer protocol / memoryview |
Recommended | Secures a zero-copy input path beyond bytes/bytearray |
| Node | Buffer / Uint8Array |
Baseline | Effectively the default supported category |
| Go | []byte / [][]byte |
Baseline | The language's default path is already the hot-path standard |
- Design rules:
- An adapter must be an input-side convenience. It does not change the canonical return type.
- A third-party buffer type that is not the language's standard
library or runtime should be split into a separate extension module
rather than the core binding, where possible. For example, Java's
ByteBuffercan stay in core, but Netty'sByteBufbelongs in a separate Netty extension. - Adapter support must not excessively widen the overload surface.
Absorb it, where possible, into one unified entrypoint such as
MessageLike,IntoMultipart, or the buffer protocol. - Even when accepting an external buffer type, the binding must clearly define the ownership/retain/release rules in documentation.
- A user must not be left to guess a framework-specific object
lifetime rule (
ByteBuf.retain/release, a pooled buffer, and so on). - Do not confuse "can be supported" with "zero-copy is guaranteed." When a zero-copy guarantee isn't possible, document the possibility of a copy.
Codec/Serializer Extension Module Policy¶
-
Bindings distribute only raw
Messageand byte-payload contracts and own no codec-extension distribution units. A raw-transport user must not need to install a particular serialization format or implementation. The transport contracts forMessage,List<Message>,Received, andTopicMessageremain unchanged. Binding core does not require protobuf, JSON, MessagePack, or codec extensions as dependencies. Each Framework's documentation owns serializer selection and typed conversion. -
Distribute third-party buffer adapters separately from core binding without making them core dependencies. They are optional integrations with a particular buffer library's lifetime APIs. Buffer support and lifetime descriptions follow the high-performance buffer ecosystem policy.
Language documents identify the package locations below and their language-specific public types.
| Language | Binding package source |
|---|---|
| C | bindings/c/include/zlink/, bindings/c/src/ |
| C++ | bindings/cpp/include/zlink/ |
| .NET | bindings/dotnet/src/Zlink/ |
| Java | bindings/java/src/main/java/systems/zlink/ |
| Node | bindings/node/src/ |
| Python | bindings/python/src/zlink/ |
| Go | bindings/go/ |
| Rust | bindings/rust/src/ |
External Buffer Attach/Release Hook Policy¶
- The C API's
zlink_msg_init_data(..., zlink_free_fn*, hint)provides the capability to attach an external buffer plus a release hook. - A binding exposes this capability publicly only when it fits that language's idiom and memory model.
- Base principles:
- A copy-based
Messageconstruction path is Required in every binding. - A VM- or GC-based language (Java, .NET, Go, Python, Node) does not provide a public API that hands a VM-managed buffer to the native queue borrowed/zero-copy.
- A borrowed zero-copy wrap API without a release hook does not belong on a managed language's public surface, default send path, or perf-only fast path.
- A VM language's performance path must build a native-owned
Message, fill its payload, and hand it to the part-based send/recv API — rather than lending the caller's buffer to the native queue. - External buffer attach is allowed only when the release point can be closed by the public contract.
- Allowed:
- C++
- Allows external attach in a shape such as
from_external(..., zlink_free_fn*, hint). - Because the release hook is explicit, it can be closed by the public contract.
- Allows external attach in a shape such as
- Discouraged/forbidden:
- Java / .NET / Go / Rust / Python / Node
- A generic public borrowed wrap (
wrapDirect,wrapNative,wrap_buffer, and so on) is forbidden. - A send/publish/request/reply fast path that hands a VM-managed
buffer to the native queue via
zlink_msg_init_data(..., NULL, NULL)is forbidden. - A public or default fast path that pins a VM-managed buffer and then releases it via a release callback is forbidden.
- Reason: it's hard to safely close, through a public contract, the backing buffer's lifetime after send, retain/release, arena/ session, and its interaction with the GC.
- A generic public borrowed wrap (
- Exception:
- Only a language where the caller explicitly owns the release hook and lifetime, like C++, can offer advanced external attach.
- Adding this exception in a VM- or GC-based language first requires a separate draft spec, a public lifetime contract, a regression test, and a perf comparison. It is not added directly to the formal spec and implementation.
Boundary Cost Policy¶
- Prefer performing boundary validation once, at the earliest safe location.
- If the same validation repeats across multiple layers, the reason must be clear.
- A value going into a fixed-size native struct returns an immediate error instead of truncating.
- A boundary value such as a string, topic, routing id, or metadata must consider all of the following together:
- the length limit
- the encoding cost
- the copy count
- the reallocation policy
- Length limits for binding input that map to core's fixed-size struct fields:
| Field | C struct size | Binding validation responsibility |
|---|---|---|
RoutingId |
data[255] |
Returns an immediate error when constructing the value object if it exceeds 255 bytes |
| topic / filter | a C string (null-terminated) | The binding returns an immediate error if it contains an embedded null character. Core handles the length limit, so the binding does not separately validate length |
| channel_name | char[256] |
Returns an immediate error if it exceeds 255 bytes |
| endpoint | char[256] |
Returns an immediate error if it exceeds 255 bytes |
| metadata | zlink_msg_t (variable) |
Handled by core; the binding validates only null |
- When a value going into a fixed-size field exceeds the limit, a binding returns an immediate exception/error without truncation.
- Avoid unnecessary intermediate collection construction when building a public domain object.
- A helper or sample must not make a slow path look like the canonical path.
Peer Weight Policy¶
Peer weight is the canonical surface that controls the peer-level outbound selection ratio and drain state. Every binding must expose this for the handles it implements.
Core API/contract:
- ZLINK_ROUTER_OPT_WEIGHT
- ZLINK_DEALER_OPT_WEIGHT
- Value range 0..10000, default 100
- The submit result ZLINK_SUBMIT_NOT_ADMITTED (value 13) — returned
when the target peer's weight is 0
- The socket monitor event ZLINK_EVENT_PEER_WEIGHT_CHANGED (bit 15)
- zlink_spot_node_peer_entry_t.weight /
zlink_member_peer_entry_t.weight
Binding rules:
- weight is exposed through a per-language typed option/property
surface. It applies to ROUTER and DEALER. A weight-setting surface
is not exposed on SpotNode or Spot.
- Include NOT_ADMITTED in the SubmitError family so a caller can
distinguish a weight-0 rejection.
- The PEER_WEIGHT_CHANGED event bit is exposed as a typed value on the
existing socket monitor/service monitor surface. value is the new
weight, 0..10000.
- The SpotNodePeerEntry/MemberPeerEntry domain object must include a
weight field.
Monitor Policy¶
- The monitor plane also follows the same rules.
- A public monitor receive is provided as a single
recv(). - Blocking/non-blocking is controlled by a flags parameter or per-language convention.
- A monitor event is separate from the data plane, but the way blocking/non-blocking is distinguished must be the same.
- Monitor is a separate plane for observing a socket's state changes, readiness changes, and lifecycle events.
- A monitor payload must not be confused with the message data-plane payload.
- A monitor event type must be exposed as a typed event surface or an equivalent meaning-carrying surface.
- A monitor consumer must be able to read the event's meaning, not just a raw integer mask.
- The monitor lifecycle's relationship to the observed socket's lifecycle must be explainable:
- when the monitor opens
- when the monitor closes
- what happens after the observed socket closes
- Monitor is not an API that replaces the data plane.
- A monitor's readiness/state event meaning must not conflict with the data-plane contract.
- A monitor sample and test must show:
- the successful event-receive path
- the non-blocking empty path
- the relationship between a socket state change and a monitor event
Error Policy¶
Binding Validation vs Native Error¶
- The binding immediately blocks a format/range error in an input value.
- Core decides socket-state, connection-state, transport-state, and protocol-state errors, and the binding delivers them to the caller as-is.
What A Binding Must Validate¶
- a value that risks truncation
- a value that risks overflow
- a value going into a fixed-size native struct
- a value with an obvious length limit
- an offset/length range error
- a non-nullable argument
- a value outside an enum's range
For these, use a binding exception.
- Java: IllegalArgumentException, IndexOutOfBoundsException,
NullPointerException
- .NET: ArgumentException, ArgumentOutOfRangeException,
ArgumentNullException
- Go: an immediate error return, or panic (a programmer error)
- Rust: a compile-time guarantee (NonZero, newtype), or
panic!/Result<T, E>
What Native Decides¶
- no peer
- backpressure
- insufficient readiness
- a conflict between callback mode and direct recv
- a socket type/state/runtime problem
- a transport, TLS, endpoint, or protocol error
For these, a binding converts the native error into that language's
idiom and delivers it to the caller. An exception language throws; a
return-based language returns an error value.
- C++: throw zlink_error_t
- Java: throw ZlinkException
- .NET: throw ZlinkException
- Node: throw ZlinkError (extends Error)
- Python: raise ZlinkError (extends Exception)
- Go: return err (ZlinkError or an equivalent typed error)
- Rust: Err(E) (Result<T, E>; ZlinkError only when multiple
function families mix)
Error Code Table¶
The codes zlink uses and their meaning. A binding maps these codes onto its per-language error type, so a caller can distinguish the cause.
Codes split into two layers.
- Public result enum codes (0–709) — the return enum value of a public C API function. A binding faces these directly and must expose them as a per-language error type. See core/errno-map.md for the full definition.
- Internal errno — the internal raw errno looked up with
zlink_errno(). Used to look up the detailed cause behind a coarse bucket such asINTERNAL_ERROR. A binding exposes this value through aninternalErrno/internal_errnofield (for debugging only).
Public Result Enum Catalog¶
A binding must map every value, without omission, of the 8 enums below into a per-language representation. OK (0) is shared by every enum and is not treated as an error.
zlink_submit_result_t (send, request submit, reply submit)¶
| Value | Constant | Internal errno | Category | Meaning |
|---|---|---|---|---|
| 0 | OK |
— | success | submit succeeded |
| 1 | BACKPRESSURED |
EAGAIN |
control flow | the send queue is saturated (HWM) |
| 2 | NOT_CONNECTED |
ENOTCONN, EHOSTUNREACH |
control flow | the target peer/path is not connected |
| 3 | NOT_FOUND |
ENOENT |
control flow | the target peer/spot/route does not exist |
| 13 | NOT_ADMITTED |
ECONNREFUSED family |
control flow | a new submit was rejected because the target peer's weight is 0 |
| 4 | TERMINATED |
ETERM |
runtime/lifecycle | the context has terminated |
| 5 | INVALID_HANDLE |
EFAULT |
caller contract violation | a NULL handle / invalid pointer |
| 6 | INVALID_ARGUMENT |
EINVAL |
caller contract violation | an invalid argument |
| 7 | NOT_SUPPORTED |
ENOTSUP |
caller contract violation | not supported on that socket type |
| 8 | INVALID_STATE |
EFSM, EBUSY |
caller contract violation | a socket/handle state error |
| 9 | THREAD_VIOLATION |
EMTHREAD |
caller contract violation | accessed from the wrong thread |
| 10 | OUT_OF_MEMORY |
ENOMEM |
internal failure | a memory allocation failure |
| 11 | SEQ_EXHAUSTED |
EBUSY |
internal failure | the request seq space is exhausted |
| 12 | INTERNAL_ERROR |
EPROTO, and so on |
internal failure | an internal submit failure (see zlink_errno() for detail) |
This enum is shared by the submit function family; that does not make every
value applicable to every function. BACKPRESSURED applies to HWM-managed
send, publish, and request submit. A raw ROUTER/Received reply can also return
BACKPRESSURED when the target peer is a DEALER because admission on the single
Application connection is subject to HWM, PAUSED, and SNDTIMEO. When the
target peer is a ROUTER, the reply makes one submission to the HWM-free
Completion connection and does not return HWM backpressure.
zlink_request_result_t (REQUEST completion)¶
| Value | Constant | Internal errno | Meaning |
|---|---|---|---|
| 0 | OK |
0 |
successfully received the reply payload |
| 101 | TIMED_OUT |
ETIMEDOUT |
reply did not arrive within timeout_ms |
| 102 | NOT_FOUND |
ENOENT |
target not found; completed with an error reply |
| 103 | TERMINATED |
ETERM, ESHUTDOWN |
the owner lifecycle ended |
| 104 | PROTOCOL_ERROR |
EPROTO, ENOCOMPATPROTO |
the reply metadata or error-reply payload is malformed or incompatible |
| 105 | INTERNAL_ERROR |
EIO, unclassified errno |
an internal request failure without another public bucket |
| 106 | REJECTED |
EACCES, ECONNREFUSED, ECANCELED |
the target or admission path rejected the request |
| 107 | CONFLICT |
ESTALE, EEXIST |
a conflict on the request target or state |
| 108 | BUSY |
EBUSY |
the request-processing path is temporarily busy |
| 109 | NOT_CONNECTED |
ENOTCONN, EHOSTUNREACH |
the target peer/path is not connected |
| 110 | INVALID_ARGUMENT |
EINVAL, EFAULT |
a request argument or metadata error |
| 111 | INVALID_STATE |
EFSM, EALREADY |
the handle state cannot accept a request |
| 112 | NOT_SUPPORTED |
ENOTSUP, EOPNOTSUPP |
request is not supported on this target |
| 113 | BACKPRESSURED |
EAGAIN, ENOBUFS |
the request-processing path cannot proceed because capacity is unavailable |
zlink_recv_result_t (recv, subscribe, subscription event, monitor recv, timer recv)¶
| Value | Constant | Internal errno | Meaning |
|---|---|---|---|
| 0 | OK |
— | receive succeeded |
| 201 | NO_DATA |
EAGAIN |
non-blocking recv has no data / the source is exhausted |
| 202 | BUSY |
EBUSY |
a handler is already attached |
| 203 | TERMINATED |
ETERM |
the context has terminated |
| 204 | INVALID_HANDLE |
EFAULT |
a NULL / invalid handle |
| 205 | NOT_SUPPORTED |
ENOTSUP |
recv is not supported on this socket type |
| 206 | INTERNAL_ERROR |
EPROTO, and so on |
an internal recv failure (see zlink_errno() for detail) |
| 207 | BUFFER_TOO_SMALL |
ENOBUFS |
caller output capacity is insufficient |
| 208 | INVALID_STATE |
EINVAL, ESTALE, ESHUTDOWN |
receive lifecycle state error |
zlink_handler_result_t (handler registration)¶
| Value | Constant | Internal errno | Meaning |
|---|---|---|---|
| 0 | OK |
— | handler registration succeeded |
| 301 | INVALID_ARGUMENT |
EINVAL |
a NULL handler |
| 302 | BUSY |
EBUSY |
a handler is already attached |
| 303 | NOT_SUPPORTED |
ENOTSUP |
an unsupported subject |
| 304 | DEADLOCK |
EDEADLK |
reentrant handler replacement in callback scope |
| 305 | INVALID_HANDLE |
EFAULT |
a NULL / invalid handle |
| 306 | INTERNAL_ERROR |
EPROTO, and so on |
an internal handler-registration failure (see zlink_errno() for detail) |
zlink_close_result_t (close, destroy)¶
| Value | Constant | Internal errno | Meaning |
|---|---|---|---|
| 0 | OK |
— | close/destroy succeeded |
| 401 | BUSY |
EBUSY |
an in-flight callback / API call |
| 402 | SHUTDOWN |
ESHUTDOWN |
already closed |
| 403 | INVALID_HANDLE |
EFAULT |
a NULL / invalid handle |
| 404 | INTERNAL_ERROR |
EPROTO, and so on |
an internal close failure (see zlink_errno() for detail) |
zlink_bind_result_t (bind)¶
| Value | Constant | Internal errno | Meaning |
|---|---|---|---|
| 0 | OK |
— | bind succeeded |
| 501 | INVALID_ARGUMENT |
EINVAL |
an invalid endpoint |
| 502 | ADDR_IN_USE |
EADDRINUSE |
the address is already in use |
| 503 | NOT_SUPPORTED |
ENOTSUP |
an unsupported transport |
| 504 | INVALID_HANDLE |
EFAULT |
a NULL / invalid handle |
| 505 | INTERNAL_ERROR |
EPROTO, and so on |
an internal bind failure (see zlink_errno() for detail) |
zlink_connect_result_t (connect, disconnect, unbind)¶
| Value | Constant | Internal errno | Meaning |
|---|---|---|---|
| 0 | OK |
— | connect/disconnect/unbind succeeded |
| 601 | INVALID_ARGUMENT |
EINVAL |
an invalid endpoint |
| 602 | NOT_SUPPORTED |
ENOTSUP |
an unsupported transport |
| 603 | INVALID_HANDLE |
EFAULT |
a NULL / invalid handle |
| 604 | INTERNAL_ERROR |
EPROTO, and so on |
an internal connect/disconnect failure (see zlink_errno() for detail) |
| 605 | NOT_FOUND |
ENOENT |
the endpoint or peer routing id does not exist |
| 606 | CONFLICT |
EADDRINUSE |
the peer routing id conflicts with two or more pipes |
| 607 | BUSY |
EBUSY |
the lifecycle owner rejected a manual change |
| 608 | AUTH_FAILED |
EACCES |
transport peer authentication failed |
zlink_config_result_t (option set/get, message lifecycle, snapshot, poller mutation, proxy, timer config)¶
| Value | Constant | Internal errno | Meaning |
|---|---|---|---|
| 0 | OK |
— | configuration succeeded |
| 701 | INVALID_HANDLE |
EFAULT |
a NULL / invalid handle |
| 702 | INVALID_ARGUMENT |
EINVAL, EBUSY |
an invalid argument, or a config-layer conflict |
| 703 | NOT_SUPPORTED |
ENOTSUP |
an unsupported option |
| 704 | INTERNAL_ERROR |
EPROTO, and so on |
an internal config failure (see zlink_errno() for detail) |
| 705 | INVALID_STATE |
EBUSY, ESHUTDOWN |
the lifecycle state rejects the config |
| 706 | NOT_FOUND |
ENOENT |
the local lookup target does not exist |
| 707 | CONFLICT |
EEXIST |
duplicate identity, endpoint, or registration value |
| 708 | BUFFER_TOO_SMALL |
ENOBUFS |
caller output capacity is insufficient; no partial output |
| 709 | BUSY |
EBUSY |
the same mutable object is used concurrently |
Non-OK Value Total¶
- 66 non-OK codes in total (submit 13 + request 13 + recv 8 + handler 6 + close 4 + bind 5 + connect 8 + config 9 = 66). The value ranges are: 1–13, 101–113, 201–208, 301–306, 401–404, 501–505, 601–608, 701–709.
- Because the value ranges don't overlap across enums, a single
intuniquely identifies the code. - A binding must provide a per-language error representation for all 66 values. If one is missing, the caller has no way to distinguish that cause.
See the Per-Language ErrorCode Mapping section below for per-language
enum/constant mapping style.
Standard POSIX errno¶
On a platform where POSIX doesn't define the matching constant, a
ZLINK_HAUSNUMERO-based substitute value is used. A binding must compare
by constant name and must not depend directly on the integer value.
| errno | Substitute value (when POSIX doesn't define it) | Meaning | Representative situation |
|---|---|---|---|
ENOTSUP |
HAUSNUMERO + 1 | an unsupported operation | an operation impossible on that socket type |
EPROTONOSUPPORT |
HAUSNUMERO + 2 | protocol not supported | a request for an unsupported protocol |
ENOBUFS |
HAUSNUMERO + 3 | insufficient buffer space | an internal buffer allocation failure |
ENETDOWN |
HAUSNUMERO + 4 | the network is down | a transport-layer failure |
EADDRINUSE |
HAUSNUMERO + 5 | the address is already in use | an endpoint conflict on bind |
EADDRNOTAVAIL |
HAUSNUMERO + 6 | the address cannot be used | an invalid endpoint format |
ECONNREFUSED |
HAUSNUMERO + 7 | the connection was refused | the target refused the connection |
EINPROGRESS |
HAUSNUMERO + 8 | the operation is in progress | an async connect in progress |
ENOTSOCK |
HAUSNUMERO + 9 | not a socket target | an invalid handle was passed |
EMSGSIZE |
HAUSNUMERO + 10 | the message size was exceeded | the message exceeds the configured max size |
EAFNOSUPPORT |
HAUSNUMERO + 11 | address family not supported | an unsupported address family |
ENETUNREACH |
HAUSNUMERO + 12 | the network is unreachable | routing is impossible |
ECONNABORTED |
HAUSNUMERO + 13 | the connection was aborted | the connection ended abnormally |
ECONNRESET |
HAUSNUMERO + 14 | the connection was reset | the peer forcibly closed the connection |
ENOTCONN |
HAUSNUMERO + 15 | not connected | a send/recv attempt before connecting |
ETIMEDOUT |
HAUSNUMERO + 16 | the operation timed out | a request-reply timeout, a connect timeout |
EHOSTUNREACH |
HAUSNUMERO + 17 | the target is unreachable | the peer isn't connected; routing is impossible |
ENETRESET |
HAUSNUMERO + 18 | the network was reset | the network connection dropped |
EAGAIN |
(POSIX standard) | the resource is temporarily unavailable | the HWM was reached on a non-blocking send (backpressure) |
EINVAL |
(POSIX standard) | an invalid argument | out of range, an invalid option value |
ECANCELED |
(POSIX standard) | the operation was canceled | the caller canceled the request |
The ZLINK_HAUSNUMERO value is 156384712.
zlink-Specific errno¶
zlink's own error codes. Uses a ZLINK_HAUSNUMERO-based offset so it
does not collide with POSIX errno.
| Substitute value | Constant | Meaning | Representative situation |
|---|---|---|---|
| HAUSNUMERO + 51 | EFSM |
a finite-state-machine error | an operation not allowed in the socket's state (for example, direct recv in callback mode) |
| HAUSNUMERO + 52 | ENOCOMPATPROTO |
an incompatible protocol | a peer connection using a different protocol version |
| HAUSNUMERO + 53 | ETERM |
context/socket terminated | an operation attempted while the context or socket is closed |
| HAUSNUMERO + 54 | EMTHREAD |
insufficient I/O threads | the context's I/O threads are insufficient |
Per-Language ErrorCode Mapping¶
Each binding maps the Public Result Enum Catalog's 66 non-OK codes onto a per-language enum/constant, providing type-safe branching.
| Language | Handling | ErrorCode type | Access |
|---|---|---|---|
| C | return | a per-function typed enum (zlink_*_result_t) |
the return value itself |
| C++ | throw | a unified ErrorCode enum |
zlink_error_t.code() |
| Java | throw | a unified ErrorCode enum |
ZlinkException.getCode() |
| .NET | throw | a unified ErrorCode enum |
ZlinkException.Code |
| Node | throw | a unified ErrorCode enum (or string constant) |
ZlinkError.code |
| Python | throw | a unified ErrorCode enum |
ZlinkError.code |
| Go | return | a unified ErrorCode typed int constant |
ZlinkError.Code() |
| Rust | return (Result) |
a unified ErrorCode enum variant |
ZlinkError.code() |
- Each variant of the unified enum maps 1:1 to one of the Public Result Enum Catalog's 66 values. A binding can either keep the original C enum split (submit / recv / handler / close / bind / connect / config / request), or unify it into a single enum per language idiom. Either style must express every value without omission.
- A constant/variant name can either keep the original
UPPER_SNAKE_CASEor convert to that language's style (PascalCase/camelCase). The numeric value and meaning are fixed. - An
internalErrno/internal_errnofield is provided separately, mainly for looking up the detailed cause behind a coarse bucket such asINTERNAL_ERROR.
Request-Reply Error Policy¶
Request-reply uses two subtypes from the Per-Function Error Type
Hierarchy: RequestError (request completion) and SubmitError
(request submit). RequestError maps to zlink_request_result_t, and
SubmitError maps to zlink_submit_result_t.
Error codes split into two layers.
Wire error reply codes — the first application part of a protocol-level error reply from a
peer contains a nonzero errno as a 4-byte Big Endian value. Core normalizes it to
zlink_request_result_t; no separate public binding sender constructs this header or errno part.
An unknown nonzero value normalizes to INTERNAL_ERROR. An absent part, a size other than 4 bytes,
or a zero value normalizes to PROTOCOL_ERROR.
API/completion normalization — Before invoking the callback, Core maps an internal or
wire errno to the zlink_request_result_t defined by
Request completion result in Core Errors.
A binding converts that result to its language-specific RequestError and does not redefine
the errno classification.
Request errors (RequestError):
| Situation | request() |
|---|---|
| backpressure | waits for writable (added to the timeout) |
| timeout | RequestError(TIMED_OUT) |
| target not found | RequestError(NOT_FOUND) |
| a remote error reply | RequestError(<matching code>) |
| the socket closed | RequestError(TERMINATED) |
| a protocol error | RequestError(PROTOCOL_ERROR) |
| a reply not in the pending map | ignored |
Reply errors (SubmitError):
Raw ROUTER/Received reply is a synchronous one-shot submit in every
high-level binding. It submits a terminal reply or error reply with one native
call. Admission to a DEALER peer is subject to Application HWM, PAUSED, and
SNDTIMEO; a ROUTER peer uses the HWM-free Completion connection. The following
results are delivered immediately.
| Situation | reply() |
|---|---|
| success | returns normally |
the HWM/PAUSED wait for a DEALER peer expires |
SubmitError(BACKPRESSURED) |
| not connected | SubmitError(NOT_CONNECTED) |
| socket/context terminated | SubmitError(TERMINATED) |
| invalid argument | SubmitError(INVALID_ARGUMENT) |
| any other failure | SubmitError(<matching submit code>) |
- An async request delivers a completion failure through the async completion path (a Future reject / an await error).
- Uses the per-function-family subtype error (see Per-Function Error Type Hierarchy).
- a submit failure:
SubmitException/SubmitError - a request completion failure:
RequestException/RequestError - Per-language representation:
- Java:
SubmitException/RequestException— distinguish the cause withgetCode()(unchecked) - .NET:
ZlinkSubmitException/ZlinkRequestException— theCodeproperty - Node:
SubmitError/RequestError— thecodeproperty - Python:
SubmitError/RequestError— thecodeattribute - C++:
submit_error_t/request_error_t— the.code()method - Go:
*SubmitError/*RequestError— theCode()method (interface) - Rust:
Err(SubmitError{..})/Err(RequestError{..}), or at a multi-function-family boundary,Err(ZlinkError::Submit(..))/Err(ZlinkError::Request(..))— the.code()method
Length And Range Boundary Policy¶
- Validation responsibility splits into two layers.
- For a type where a value object exists:
- Perform canonical validation at value-object construction time.
- Example:
RoutingId, a typed enum wrapper, a bounded identifier - For a type without a value object, or one that needs a call-context- dependent conversion:
- Validate immediately before the native call.
- Example:
Duration -> int millis, offset/length slicing, output buffer sizing - Re-validation immediately before the native call is required only in these cases:
- a raw path exists that bypasses the value object
- an additional conversion happens between value-object construction and the call
- overflow/truncation can occur from a non-value-object composite input combination
- Passing a truncated value to native is forbidden.
Examples:
- RoutingId must not exceed zlink_routing_id_t's data[255]
contract.
- A Duration -> int millis conversion must not allow overflow.
- A path involving a fixed output buffer, such as topic, subscription,
or metadata, must have a clear length and reallocation policy.
Ownership Policy¶
Messageownership must match the core contract.- Because every binding calls the C API internally, every language, including GC languages, must correctly manage the ownership of the native message.
- Ownership paths:
- send success: ownership moves to native. The binding must not access it afterward.
- send failure: don't confuse a restorable path with a consumed path.
- recv: the binding receives ownership of the message native created. The binding is responsible for releasing it.
- constructed but not sent: if the binding constructed a message directly and never sent it, it must be explicitly closed/released. GC only collects the managed wrapper — it doesn't release the native memory — so a leak occurs otherwise.
- Callback delivery and direct receive must have the same payload shape.
- Frame validity after the callback must be clear from the contract.
Naming Policy¶
- A method name reflects only language convention.
- Keep the concept name as identical as possible across bindings.
- The list below gives the canonical names by meaning.
- The actual binding method name allows only the following three variations.
- Casing variation: convert to camelCase/PascalCase/snake_case
per language convention. Word composition does not change.
- example:
connectPeer→ Go:ConnectPeer, Python:connect_peer, C++:connect_peer, Rust:connect_peer
- example:
- A minimal suffix for a language without overloads: in a
language without overloading, like Go and Rust, a minimal suffix is
allowed to distinguish a parameter variant of the same operation.
This suffix distinguishes the operation — it is not parameter
encoding.
- example:
send→ Go:Send/SendTo, Rust:send/send_to - allowed suffix scope: only up to a minimal operation-distinguishing
suffix at the
Tolevel. A suffix that spells out the parameter type or meaning is forbidden. - allowed:
SendTo,send_to - forbidden:
SendWithRoutingId,send_routed,send_multipart - suffix allowance applies only to a language with neither overloading nor keyword/optional parameters (Go, Rust).
- a language that can distinguish by signature without a suffix does not use one.
- overloading: Java, C#, C++
- keyword/optional parameter: Python
- optional/union type: Node/TypeScript
- example:
- Per-language property/getter convention: a value-reading
accessor can use a property or getter form that fits language
convention. However, the concept name must stay the same, and a new
operation name must not be created.
- example: canonical
getValue→ C++value(), .NETValueorGetValue(), Java/NodegetValue() - example: canonical
routingId/getRoutingId→ C++routing_id(), JavaroutingId(), NodegetRoutingId()
- example: canonical
- No other word substitution, word omission, or word replacement is allowed.
- forbidden example: changing
setDispatchHandlertospotDispatchHandler→ word substitution - forbidden example: shortening
querySnapshottosnapshot→ word omission; if this is desired, the canonical name itself must be defined assnapshot - Even when casing or the suffix differs, the role distinction and semantic contract must stay the same.
- example:
receiveSubscriptionEvent→ Python:receive_subscription_event, Go:ReceiveSubscriptionEvent - Recommended canonical names:
bind,connect,closesendrecvpublishsubscribereceiveSubscriptionEventsetSubscription,unsetSubscriptionsetPacketHandler,setDispatchHandler
Method Name Conciseness¶
- This rule applies strictly to the public API.
- An internal/private API is allowed to encode parameters when that improves readability.
- Internal code may read better with an explicit name, without overloading.
- example:
sendRouted(id, msg)is allowed in an internal helper - A method name expresses only the action.
- The presence, type, and count of parameters are not repeated in the name.
- Do not restate in the name what the signature already describes.
- When the operation itself differs (for example,
sendvspublish), the name must differ. - When only the input differs (for example, whether a routing id is present), the name is not lengthened.
Anti-pattern vs. correct pattern:
| Anti-pattern | Correct pattern | Reason |
|---|---|---|
send(message) |
send().message(message).submit() |
The start point takes only the send target; payload is a separate builder step |
sendWithRoutingId(id, msg) |
send(id).message(msg).submit() |
The builder separates RoutingId and payload into steps |
sendMultipartMessages(parts) |
send().message(p1).message(p2).submit() |
Multipart is expressed through repeated builder .message(...) calls |
publish(topic, message) |
publish(topic).message(message).submit() |
Topic and payload are not mixed into one start point |
publishToTopic(topic, msg) |
publish(topic).message(msg).submit() |
publish is already the topic-having operation; the builder separates payload into a step |
sendToChannel(channel, message) |
sendToChannel(channel).message(message).submit() |
The channel target and payload are separated into builder steps |
requestToChannel(channel, parts, timeout) |
requestToChannel(channel).message(p1).message(p2).timeout(timeout).submit() |
A channel request's payload and timeout are builder steps |
requestFrame(seq, parts) |
forbidden on the public surface | Request sequence and frame layout are runtime/internal helper detail |
dealer.reply(token, parts) |
received.reply().message(...).submit(), or a router/SPOT reply |
DEALER cannot designate a specific peer routing id, so an arbitrary-token reply doesn't conceptually fit |
recvWithTimeout(timeout) |
recv(timeout) |
The signature is enough |
setLingerTimeoutMilliseconds(ms) |
setLinger(duration) |
The type conveys the unit |
The send/request/reply/publish/Actor surface exposes only the builder
start point, per the Operation Builder Policy, and every variation
axis — payload, flags, timeout, callback — is expressed as a builder
step. A start-point name carries only the action, and does not repeat
the presence, type, or count of parameters.
For a non-builder public surface (for example, snapshot, lookup, getter/setter) where the parameter combination differs, use each language's own disambiguation mechanism instead of lengthening the name.
- Java/C#/C++: overloading
- one name; the signature distinguishes
- Go: variadic arguments/functional option/a separate method
- because there's no overloading, allow a minimal suffix only when the operation meaning differs
- do not put the parameter directly into the name
- Python: keyword argument/optional parameter
- one name; the keyword distinguishes
- Node/TypeScript: optional parameter/union type
- one name; the type distinguishes
- Rust: trait bound/
Option<T>/newtype - because there's no overloading, distinguish with
impl Into<T>,Option<T>, or a strong newtype - allow a minimal suffix only when the operation meaning differs
- do not put the parameter directly into the name
Per-language summary:
| Language | Disambiguation method | Parameter encoding in the name |
|---|---|---|
| Java | overloading | forbidden |
| C# | overloading | forbidden |
| C++ | overloading + strong type | forbidden |
| Go | separate method/functional option | forbidden; only an operation-distinguishing suffix is allowed |
| Python | keyword/optional | forbidden |
| Node/TS | optional/union | forbidden |
| Rust | trait bound/Option/newtype | forbidden; only an operation-distinguishing suffix is allowed |
Compatibility Policy¶
- A consistent public surface can be prioritized over compatibility.
- Remove a deprecated compatibility layer as soon as possible.
- Do not keep a bypass surface for the same capability publicly alongside the canonical path.
- Flag type policy:
- Whether and how public flags are exposed follows the
Flags Policysection above. - .NET's
SendFlags/RecvFlagspublic surface is the canonical contract. - Do not add a legacy flag type or duplicate flag path that isn't in the per-language spec.
Cross-Language Alignment¶
Shared Behavior Contract¶
- The blocking send/receive family delivers a per-language error path on failure (an exception for exception languages, a returned error for return-based languages)
- Non-blocking receive delivers "no data" through the same error path
too (distinguished by result code). No separate
try*API is provided. - Non-blocking send has an explicit outcome (a submit result code)
- multipart-only
- a typed option surface
Per-Language Return Style¶
- C API
- the raw contract and a per-function typed result enum
- a multipart-only baseline surface
- a blocking API plus an explicit non-blocking entry (a
flagsparameter) - C++
- RAII and typed wrappers
- a multipart-only baseline surface
- failure is
throw zlink_error_t(including theSubmitResultcode) - .NET
- a typed option surface plus
ZlinkException - a multipart-only baseline surface
- failure is
throw ZlinkException(includingCode) - Java
- domain objects plus
ZlinkException - a multipart-only baseline surface
- failure is
throw ZlinkException(includinggetCode()) - Go
(T, error)plus strong types and an explicit error check- a multipart-only baseline surface
- every failure returns
error(including theSubmitResultcode) - Rust
Result<T, E>plus a strong newtype and ownership- a multipart-only baseline surface
- a single function family uses a concrete error such as
BindError/SubmitError; multiple function families useZlinkError - Node/Python
- follows language convention, but the semantic contract is the same
- a multipart-only baseline surface
- every failure is
throw/raise(including theSubmitResultcode)
The surface can differ per language, but the semantic contract must be the same.
Cross-Language Capability Table (Target)¶
This table is a target role table organized around .NET. If an
already-implemented binding's current public surface differs from this
table, interpret that item as a goal for structural alignment or
breaking cleanup work. However, an Internal-only item is not exposed
in the public API, samples, guides, or spec signatures even in its
target state.
| Area | C API | C++ | .NET | Java | Go | Rust | Node | Python |
|---|---|---|---|---|---|---|---|---|
| Multipart-only public surface | Required | Required | Required | Required | Required | Required | Required | Required |
| Blocking API named directly | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Non-blocking receive uses flags + empty result | C raw DONTWAIT |
Required | Required | Required | Required | Required | Required | Required |
| Non-blocking send explicit outcome | Core enum/result | Required | Required | Required | Required | Required | Required | Required |
| Public flags surface | Raw C flags | int flags |
SendFlags / RecvFlags |
SendFlags overload |
flags SendFlags |
SendFlags via .flags(...) builder step |
flags?: SendFlags |
keyword flags |
| Typed option surface | N/A raw C options | Required | Required | Required | Required | Required | Required | Required |
| Socket TLS helpers | zlink_set_tls_* |
Required | Required | Required | Required | Required | Required | Required |
| Service TLS helpers | zlink_set_tls_* on service handles |
Required | Required | Required | Required | Required | Required | Required |
| Socket Capability Matrix compliance | Based on Core | Required | Required | Required | Required | Required | Required | Required |
onReceive callback |
STREAM raw fn ptr | Internal-only | Internal-only | Internal-only | Internal-only | Internal-only | Internal-only | Internal-only |
setPacketHandler callback registration |
STREAM packet fn ptr | Required | Required | Required | Required | Required | Required | Required |
setDispatchHandler callback registration |
SPOT raw fn ptr | Required once implemented | Required once implemented | Required once implemented | Required once implemented | Required once implemented | Required once implemented | Required once implemented |
recvActorLifecycle |
SPOT lifecycle queue | Required | Required | Required | Required | Required | Required | Required |
| HWM-managed send completion | Core-owned completion | Required | Required | Required | Required | Required | Required | Required |
StreamSocket connect blocked |
N/A | Required | Required | Required | Required | Required | Required | Required |
StreamSocket disconnectRid blocked |
N/A | Required | Required | Required | Required | Required | Required | Required |
Public detachStream not exposed |
N/A | Required | Required | Required | Required | Required | Required | Required |
| Poller result type name | N/A | poll_event_t |
PollEvent |
PollEvent |
PollEvent |
PollEvent |
PollEvent |
PollEvent |
| Monitor typed event surface | Raw struct | Required | Required | Required | Required | Required | Required | Required |
Test Policy¶
The purpose of binding tests is not to match a test count across languages. The purpose is to confirm that each binding guarantees, at the same level and without omission, the contract that matches its own public surface.
The test count can vary by per-language API surface, runtime ownership model, and packaging approach. So the test count is only a supporting signal for judgment — the real standard is whether the verification layers below and the Test Matrix's semantic contract are satisfied. If a particular binding's test count looks unusually high or low, first check whether a contract is missing, or whether a redundant test — such as one that re-verifies core correctness — has crept in, rather than trying to match the count itself.
A zlink binding is not a thin wrapper that simply calls a native function. Each language binding also provides a public facade, helper objects, domain objects, typed options, callback delivery, ownership management, a native loader, a package boundary, and hot-path optimization. So a simple round-trip test is not enough either — the additional meaning a public helper provides, and its optimization invariants, must also be verified as part of the binding contract.
Tests are classified into the layers below.
Required: a test every binding must have.Conditional: a test required only for a binding that provides the matching public API or distribution unit.Language-specific: a test that verifies a risk specific to a runtime — lifetime, exceptions, GC, borrow, cgo, native loader. This is not forced to be duplicated in another language.Sample smoke: a test that confirms a user-facing pattern runs through the public API. This does not expand into a large scenario set that re-verifies core correctness.Out of scope: core's own messaging correctness, a full re- verification of the transport matrix, one-off migration verification, and review items that can't be automated. These items are not kept as permanent binding tests. However, if a path involves a binding helper, facade, or optimization invariant, it stays a binding test even if it looks like it overlaps with core functionality.
The shared principles are:
- A public surface test fixes the canonical public API.
- A contract test verifies type conversion, error mapping, and handle lifecycle at the binding/native boundary.
- A behavior test verifies that the binding public API correctly relays the core contract.
- A helper/facade test verifies the semantic contract of the extra language-friendly functionality a binding provides.
- An ownership test must cover the send-success, send-failure, receive, callback, and multipart paths.
- An optimization guard test verifies the hot path hasn't regressed into a slow path the policy forbids.
- A path where callback mode and direct mode are not allowed together verifies the conflict rule.
- An option test verifies the typed option surface together with blocking incorrect role access.
- Performance regression verification is owned by the separate Perf Policy. A functional test must not replace a perf benchmark, and a perf benchmark must not replace a public contract test.
The test satisfaction criteria are:
- Each binding must verify every
Requireditem in the Test Matrix for the public API it provides. - If it provides a particular public API, extension package, or sample
suite, it must also verify the matching
Conditionalitem. - A risk that arises from the language runtime — ownership, lifetime,
loader, callback, GC, borrow, cgo — is verified with a
Language-specifictest. - Do not add a test for a public API the binding doesn't provide, just to match a count.
- A test that re-verifies core correctness is removed from the binding tests or moved to a core test, unless it directly relates to a binding helper, facade, package boundary, native loader, or optimization invariant.
- If multiple tests repeatedly verify the same contract, merge them into one deep test; if one test hides multiple distinct contracts, split it so each Matrix item is visible.
Required test rules on a policy change:
- a public surface change: accompanied by a public surface test
- a contract change: accompanied by a contract test
- a blocking/non-blocking contract change: accompanied by a behavior test
- an ownership/receive shape change: accompanied by a callback regression or ownership test
-
an option surface change: accompanied by a typed option surface test and a negative role test
-
a helper/facade change: accompanied by a helper/facade contract test
- a hot-path implementation change: accompanied by an optimization guard test or a perf regression gate
If existing code has a test outside the Test Matrix, clean it up by the following criteria.
- If it re-verifies core functionality, move it to a core test or delete it.
- If it's migration verification, mark it as a temporary test to delete once migration is complete.
- If it confirms a user-facing pattern, move it to sample smoke.
- If it verifies a binding helper, facade, package boundary, native loader, or optimization invariant, keep it, classified under the appropriate Test Matrix category.
- If it verifies a specific language runtime risk, keep it as a Language-specific test, and make the reason clear from the test name or file name.
Test Run Script Policy¶
- Each binding must provide a script that can run the entire test suite at once.
- The run script must be located in the
bindings/<language>/tests/directory. - The script must be repeatable and must summarize success/failure.
- Recommended forms:
tests/run_tests.shtests/run_tests.ps1- a language-specific test runner entry
Bug Discovery Policy¶
- When a bug is found while writing/running a test or perf benchmark, follow this procedure.
- A binding library bug:
- Fix it directly in that binding.
- Add a regression test together with the fix.
- A core library bug:
- Do not fix the core bug directly from the binding.
- Write a bug report in the
bindings/<language>/bug/directory. - The report must include at least the following:
- reproduction conditions (socket type, pattern, message size, transport, and so on)
- expected behavior
- actual behavior
- reproduction code or a test reference
- If a workaround is needed on the binding side, explicitly mark it as a workaround and reference the bug report.
Test Matrix¶
- This section lists the minimum test items each binding must have.
- Even though the surface differs per binding, every semantic contract below must be verified.
Surface Tests,Contract Tests,Behavior Tests,Failure Contract Tests,Helper/Facade Tests,Optimization Guard Tests,Boundary Validation Tests,Option Tests,Ownership Testsare baselineRequireditems for every binding.Callback Tests,Monitor Tests,Poller Tests,Service Tests,Codec Tests,Sample Smoke TestsareConditionalitems for a binding that provides the matching public API, extension package, or sample suite.Language Runtime TestsareLanguage-specificitems for a binding where runtime characteristics create a risk.
Required: Surface Tests¶
- a canonical public API surface test
- confirming socket-type role separation
- confirming a typed option surface exists
- confirming the shared socket TLS helper exists
- confirming the service TLS helper exists
- confirming a raw option bag is not exposed
- confirming a monitor canonical surface exists
recv()
Required: Contract Tests¶
- verifying FFI/native call mapping
- confirming a binding public API call maps to the correct C API function
- confirming parameter passing and return-value conversion are correct
- verifying type conversion at the managed ↔ native boundary
- confirming the language-type-to-C-type conversion is correct
- confirming the C-type-to-language-type conversion is correct
- verifying resource lifecycle
- confirming context/socket native handle construction and release work without leaking
- confirming native resources are cleaned up on exception/error paths too
Required: Behavior Tests¶
- Verify the binding layer correctly relays the core contract.
- The goal is confirming the correctness of the binding path, not re-verifying core messaging functionality.
- Blocking paths:
send→ successfully relays to core sendrecv→ successfully relays to core recvpublish→ successfully relays to core publishsubscribe→ successfully relays to core subscribe- routed
send→ successfully relays including the routing id - Non-blocking paths:
recvnon-blocking → returns empty when there's no datasubscribenon-blocking → returns empty when there's no datareceiveSubscriptionEventnon-blocking → returns empty when there's no data- confirms an exception or error path on
sendfailure - confirms an exception or error path on
publishfailure
Required: Helper/Facade Tests¶
- When a public helper or facade provides meaning beyond a simple native call, directly verify that meaning.
- Verify the invariants of binding-provided types such as
Message,Received, a multipart collection, a routing id value/codec, a typed option facade, a domain object, a request/reply helper, and a topology snapshot value object. - Confirm a helper does not leak native detail to the user.
- Confirm a helper keeps success/failure, an empty payload, one empty message, and multipart boundaries distinct.
- Confirm a helper does not require the user to perform internal sequencing that isn't in the public API.
- Confirm a convenience API does not create a meaning different from the canonical API.
Required: Optimization Guard Tests¶
- Verify the hot path continues to satisfy the High-Performance Binding Policy.
- Confirm the internal send/recv/request/reply/publish/subscribe path makes one Core whole-message call per record.
- Confirm hidden double materialization, an unnecessary eager copy, or a per-call closure/ boxing/allocation hasn't crept back in.
- Confirm a hidden blocking wait, sleep, busy wait, or thread join hasn't appeared on the callback, dispatch, poller, or request- completion path.
- This verification does not always need to be a micro benchmark. When it can be automated reliably, use whichever of a source-level/static check, a public API allocation check, a stress smoke test, or a perf gate costs the least.
- A perf benchmark owns numeric regression, while an optimization guard test owns keeping a forbidden structure out of the code.
Required: Failure Contract Tests¶
- confirm a blocking
sendfailure is delivered to the caller through an exception or the per-language error path - confirm a blocking
publishfailure is delivered to the caller through an exception or the per-language error path - confirm the
sendbackpressure exception - confirm the
sendnot-ready exception - confirm the
publishbackpressure or not-ready exception - confirm an error other than native
NO_DATAis not ignored - confirm an error is delivered per the native contract when callback mode and direct recv conflict
- confirm a state where direct recv is impossible is not hidden as empty/null
- confirm only native
NO_DATAis treated as an empty/non-success result
Required: Boundary Validation Tests¶
RoutingId's maximum length boundary (255 bytes OK)RoutingIdover the length returns an immediate error (256+ bytes → exception)- the
Duration -> int millisoverflow boundary - offset/length bounds validation
- non-nullable argument validation
- out-of-enum-range value validation
channel_nameover 255 bytes returns an immediate error (fixed-sizechar[256])endpointover 255 bytes returns an immediate error (fixed-sizechar[256])- an embedded null character in topic/filter returns an immediate error
Required: Option Tests¶
- common option typed getter/setter
- per-socket-type typed option getter/setter
- blocking an option-role access on the wrong socket type
- confirming an enum/boolean surface is provided instead of a raw integer
Required: Ownership Tests¶
- the ownership-transfer contract on send success (moves to native; the binding must not access it afterward)
- the language API's documented send-failure ownership contract, including cleanup of native staging parts consumed or left untouched by Core
- explicit close/release of a constructed-but-unsent message (native memory leaks without close)
- the ownership contract of a recv result (the binding receives it and is responsible for releasing it)
- the frame-validity contract after a callback
- whether the multipart receive shape and the callback delivery shape match
Conditional: Callback Tests¶
- If there's a public callback API, verify callback delivery.
- Verify the ownership of the message or multipart payload the callback receives.
- Confirm a per-language failure — a callback exception, panic, rejected promise, delegate exception — is delivered through the documented error path.
- Verify a callback delegate/function/object's lifetime doesn't outlive the native callback in the wrong direction and create a use-after-free.
- Confirm a forbidden blocking wait or hidden thread join doesn't occur inside the callback.
Conditional: Monitor Tests¶
- the successful blocking monitor
recvpath - the non-blocking monitor recv empty path
- whether the monitor callback/state change matches data-plane readiness
Conditional: Poller Tests¶
- Confirm raw socket readiness or fd readiness is delivered through the public poller API.
- Confirm a socket monitor can be registered with the poller and its events
drained after
POLLINreadiness. - Confirm the poller doesn't silently accept a service-specific handle it doesn't support.
- Verify a readiness event value does not replace the data-plane contract.
Conditional: Service Tests¶
- A binding that provides the spot/actor public API verifies that service's lifecycle through a minimal path.
- Confirm a lifecycle constraint such as close/connect/unbind is delivered through the public API per the native contract.
- For spot publish/subscribe, spot request/reply, and SPOT status/ snapshot, perform a round-trip or snapshot verification wherever a public surface exists.
- A service test's goal is verifying the service-layer binding contract. It does not re-run the entire core service matrix in every language.
Raw Payload Distribution Verification¶
Sending and receiving raw messages through public binding APIs without installing an additional serializer module preserves payload bytes and multipart part counts. The raw payload policy defines the distribution scope.
Conditional: Sample Smoke Tests¶
- A binding that provides a sample suite provides a run smoke test for the canonical sample set.
- Sample smoke is the minimal verification that confirms the public API is usable.
- Sample smoke does not replace the core transport matrix, stress testing, or perf measurement.
Language-specific: Runtime Tests¶
- .NET: verifies
IDisposable,SafeHandle, delegate lifetime,GCHandle, the native library loader, andZlinkExceptionmapping. - Java: verifies
AutoCloseable, JNI object lifetime, the checked/ unchecked exception policy, and the classloader/native loader boundary. - Go: verifies the cgo pointer rule, an explicit close that doesn't
depend on a finalizer, and
(T, error)mapping. - Rust: verifies ownership move, borrow lifetime,
Drop, whetherSend/Syncis exposed, and concrete error type mapping. - Python: verifies the buffer protocol, reference counting, the context manager, and exception mapping.
- Node: verifies native addon lifetime,
Bufferownership, the async callback error path, and the package export boundary. - C++: verifies RAII, move-only message ownership, the exception type, and the installed header boundary.
- C: verifies the raw ABI, errno/result codes, and caller-provided message lifecycle.
Note: Performance And Sample Verification¶
- Performance regression verification is owned by the Perf Policy
(
doc/perf/). It is not duplicated in the Test Matrix. - A sample/helper's compliance with the canonical API, avoiding ignoring a send failure, and avoiding a legacy-surface bypass are verified in the Review Checklist. These are not automated test items.
Sample Policy¶
- Sample-authoring rules use
doc/spec/sample/SAMPLE_POLICY.mdas the single baseline document. - This document covers
core/samples/andbindings/*/samples/together. - Adding, changing, or reviewing a binding sample is judged against that document.
Perf Policy¶
Perf code is not a demo — it's code for measuring and improving the binding library's performance. Perf's primary purpose is to reveal the binding layer's cost, identify bottlenecks and regressions, and measure before/after differences from improvement work.
The single baseline for perf policy is the doc/perf/ policy
documents. Every detailed specification — CLI options, defaults,
output format, RESULT line format, the pattern/transport matrix, phase
rules, result storage, failure handling, environment variables — follows
the documents below. This section does not redefine them.
doc/perf/PERF_POLICY.md— the shared perf policy (shared principles, directory structure, RESULT format, result storage, output format, failure handling, environment variables, refactoring principles, per-language scope)doc/perf/PERF_SINGLE_TEST_POLICY.md— the single-suite policydoc/perf/PERF_MULTI_TEST_POLICY.md— the multi-suite policy
Binding Perf Principles¶
- Perf code follows the
doc/perfpolicy. - It's based on the patterns and scenarios
core/perfprovides. - It keeps a scenario comparable to core perf, while writing it to fit each language's style.
- The measurement anchor point, phase meaning, metric set, and RESULT line meaning do not change.
- The perf policy is
Requiredfor a binding that officially provides a performance-measurement surface. A binding that doesn't yet provide perf code treats it asTarget.
Binding API Spec Documents¶
Each binding's API surface is documented in the file below.
The perf policy is managed across every language in a shared way, in
doc/perf/PERF_POLICY.md.
| Binding | API Spec |
|---|---|
| C | c/README.md |
| C++ | cpp/README.md |
| Java | java/README.md |
| .NET | dotnet/README.md |
| Node.js | node/README.md |
| Python | python/README.md |
| Go | go/README.md |
| Rust | rust/README.md |
Perf Review Checklist¶
- Does this perf measure the binding library's cost?
- Can the core send/recv/callback path be read directly in the perf file body?
- Is each pattern split into its own file?
- Is it aligned with
core/perfpatterns? - Does it follow the
doc/perfpolicy?
Script Location Policy¶
- A run script lives in the same directory as its target.
- It goes in each subdirectory, not the binding root.
| Purpose | Location | Example script |
|---|---|---|
| Tests | bindings/<language>/tests/ |
run_tests.sh |
| Samples | bindings/<language>/samples/ |
run_samples.sh |
| Perf | bindings/<language>/perf/ |
run_benchmarks.sh, run_benchmarks_multi.sh |
- When Windows support is needed, also provide a
.ps1. - Do not put a wrapper such as
run_samples.shat the binding root (bindings/<language>/). A wrapper at that location duplicatessamples/run_samples.shand creates confusion about which one is authoritative. - If an orchestration script is needed to run tests+samples+perf
together for CI or full verification, it can be placed under a name
such as
bindings/<language>/run_all.sh. This script is an entrypoint that calls the individualtests/run_tests.sh,samples/run_samples.sh, and so on — it does not replace the individual scripts.
Review Checklist¶
- Is the public API multipart-only?
- Are blocking/non-blocking not split into separate names?
- Is there no remaining public flag type or duplicate flag path that
isn't in the per-language
Flags Policy? - Does no raw option bag remain public?
- Have option values been promoted to enum/boolean/value objects?
- Are per-type roles properly closed off?
- Is a blocking send failure always delivered to the caller through an exception or error path?
- Does a
sendfailure deliver every error, including backpressure/ not-ready, as an exception? - Does the binding pre-validate truncation/overflow?
- Does the binding avoid arbitrarily inferring a native state error?
- Are the public surface test and behavior test present together?
- Is the division of responsibility between value-object validation and pre-call validation explainable?
- Has a legacy flag type not in the per-language
Flags Policybeen removed from the public contract? - Does sample code use only the canonical API?
- Does the helper avoid ignoring a blocking send failure?
- Does the helper avoid bypassing through a deprecated/legacy surface?
POSD-Based Implementation Completeness Policy¶
- This section defines the POSD-based procedure applied when completing or refactoring a binding implementation.
- A binding is completed against structural correctness, not a feature checklist.
- The completion criteria are the Socket Capability Matrix, Callback API Policy, Option Policy, Test Matrix, and Sample Policy.
- Refactoring is reducing system complexity, not moving code around.
Completion Order¶
- A binding implementation follows the order below.
- Each step depends on the previous step's result.
- Do not skip a step and move to the next.
Step 1: Align The Capability Matrix¶
- Review each socket type's public API against the Socket Capability Matrix.
- Add an API that should exist but doesn't.
- Remove, or move to internal, an API that's exposed but shouldn't be.
- Verification: the surface test must match the matrix.
- Representative violation examples:
connect()exposed on StreamSocket → removedisconnectRid()exposed on StreamSocket → removedetachStream()exposed on StreamSocket → remove- async send uses a binding retry queue instead of Core completion → wire it to Core completion
- publish/subscribe exposed on the wrong socket → remove
Step 2: Normalize Names¶
- Align to the canonical name per the Naming Policy and Callback API Policy.
- Unify an API that differs only in name but means the same thing under the canonical name.
- Remove a deprecated alias.
- Verification: confirm the canonical name exists in the surface test.
- Representative violation examples:
- a public
recvHandler/onReceive→ remove, or move to the internal raw STREAM bridge spotDispatchHandler→setDispatchHandleron_topic_message→subscribe
Step 3: Deep Module Structure¶
- Secure depth for public types, per the POSD deep-module principle.
- Confirm each public type isn't a simple pass-through — that it encapsulates validation, ownership, or shape rules internally.
- Criteria for identifying a shallow wrapper:
- Does it only wrap a native function 1:1 without adding new meaning?
- Can the caller use it only by knowing the native contract (sequence, size, encoding)?
- Is the same rule redundantly implemented across multiple socket types?
- Once a shallow wrapper is found:
- Move validation inside a value object or facade.
- Gather the duplicated rule into one module.
- Remove, or merge into internal, a public type that only does pass-through.
- Representative violation examples:
- RoutingId length validation duplicated per socket type → gather into one RoutingId value object
- a monitor event as raw int → promote to a typed event surface
- an option value as raw int → promote to enum/boolean/Duration
Step 4: Eliminate Change Amplification¶
- Find where the same rule is scattered across multiple places and gather it into one module.
- Criteria for identifying it:
- Does changing one policy require fixing 2 or more files?
- Does adding a new socket type require modifying existing code in N places?
- Representative violation examples:
- the send-failure contract rule implemented separately per socket type
- blocking/non-blocking branching implemented separately per socket type
- option validation implemented separately per option setter
Step 5: Strengthen Information Hiding¶
- Find where the public API exposes native detail and hide it behind a facade.
- Criteria for identifying it:
- Does the user need to know errno, flag constants, or a native struct size?
- Does the user need to remember internal sequencing (call order)?
- Is a native handle, raw pointer, or raw buffer exposed in the public API?
- Representative violation examples:
- a raw
setSockOptRaw/setOption(int, byte[])is public - a raw int mask is exposed as-is in a monitor event
- a legacy flag type not in the per-language
Flags Policyremains a public type
Step 6: Complete The Test Matrix¶
- Write or strengthen the Test Matrix's
Requiredcategory in every binding. - A binding that provides the matching public API, extension package, or
sample suite also writes or strengthens the matching
Conditionalcategory. - A binding with a language-runtime lifetime, exception, or native-
loader risk also writes or strengthens the matching
Language-specificcategory. - Completion criteria:
- a Surface test verifies the Socket Capability Matrix
- a Contract test verifies FFI mapping and lifecycle
- a Behavior test verifies the blocking/non-blocking paths
- a Helper/Facade test verifies the semantic contract of a binding-provided helper
- an Optimization Guard test verifies the hot-path optimization invariant
- a Failure Contract test verifies the send/receive error contract
- a Boundary test verifies value boundaries
- an Option test verifies the typed surface
- an Ownership test verifies send/recv ownership
- where the matching public API exists, a Callback, Monitor, Poller, Service, or Codec test verifies the public contract
- where a sample suite exists, a Sample Smoke test verifies running the canonical API
Step 7: Align Samples¶
- Complete samples against the Canonical Sample Set.
- Confirm each sample uses only the canonical API.
- If a name or API changed in steps 1–5, update the samples too.
Refactoring Judgment Criteria¶
- If the answer to the following question is "yes," that's a point that needs refactoring.
- Would the user lose nothing if this public type were removed? → shallow wrapper
- Would fixing this rule require touching 3 or more files? → change amplification
- Does the user need to know another API's internal behavior to use this one? → information leak
- Is the same capability exposed under 2 or more names? → duplicate surface
- Does correct behavior require the user to remember a call order? → temporal decomposition dependency
Refactoring Exit Conditions¶
- Refactoring repeats until every condition below is satisfied.
- If even one remains, it isn't done.
- The judgment is made from a POSD perspective.
- The exit-condition scope is limited to what that binding has decided to implement.
- a
Requireditem: applies to every binding - a
Conditionalitem: applies to a binding that provides the matching public API, extension package, or sample suite - a
Language-specificitem: applies to a binding with that runtime risk -
a
Recommendeditem (for example, samples): applies to a publicly distributed binding -
Full Capability Matrix Alignment
- Every
Yitem in the Socket Capability Matrix exists in the public API. - Every
—item in the Socket Capability Matrix is not exposed in the public API. - The Capability Matrix of any service-layer component that binding implements is aligned the same way. If a binding doesn't implement it, exclude it from the exit condition.
-
The surface test verifies this and passes.
-
Name Normalization Complete
- Every public API uses the Naming Policy's canonical name.
- No deprecated alias remains.
-
The Callback API Policy's canonical names (
setPacketHandler,setDispatchHandler) exist matching their roles. -
Shallow Wrappers Removed
- No public type merely wraps a native function 1:1.
- Every public type encapsulates at least one of validation, ownership, or shape rules.
RecvPart,RecvRoutedPart,SubscribePart, or a per-language equivalent name, does not exist in the public API. Part-by-part receive exists only as a runtime/internal substrate.- A helper that exposes request-reply ZMP header metadata as-is, such as
requestFrame(...), does not exist on the public surface. -
A reply helper that doesn't fit DEALER's send capability, such as
dealer.reply(requestToken, parts), does not exist on the public surface. -
Change Amplification Resolved
- The same rule is not redundantly implemented in 2 or more modules.
-
Only 1 file needs to change on a policy change.
-
Information Hiding Secured
- The public API does not expose a raw option bag, a legacy/raw flag outside the policy, a raw native struct, or a raw errno.
-
A user can use the API correctly without knowing internal sequencing.
-
Test Matrix Complete
- Every
Requiredtest exists and passes. - Every
Conditionaltest within that binding's scope exists and passes. -
Every
Language-specifictest needed for that runtime risk exists and passes. -
Sample Alignment Complete
- Every sample in the Canonical Sample Set exists.
- This includes a sample for any service-layer component that binding implements.
- A sample for an unimplemented
Targetcomponent is excluded. - Every sample uses only the canonical API.
-
No sample uses a deprecated/legacy path.
-
Dead Code Removal Complete
- Every unnecessary piece of code produced during refactoring has been removed.
- No deprecated alias, legacy wrapper, or unused import/using/ require remains.
- Implementation code for an API marked
—in the Capability Matrix doesn't needlessly remain in internal either. - No function/method/type under an old name replaced by name normalization remains.
- No uncalled private/internal helper remains.
- No unreferenced constant, enum value, or type alias remains.
- No commented-out code block (
// removed,// deprecated,// remove later) remains. - No empty file, empty class, or empty module remains.
- Dead code is not kept "in case it's needed later." Restore it from git history if needed.
Refactoring Iteration Rule¶
- After running through steps 1–7 once, recheck the exit conditions.
- Because an earlier step's change can affect a later step, if even one exit condition is unmet, redo starting from that step.
- Repeat until all 8 exit conditions are satisfied.
- The completion criterion is not "no more spots visibly need fixing" — it's "all 8 exit conditions pass."
Refactoring Prohibitions¶
- Do not change the semantic contract in the name of structural improvement.
- An internal refactor must not change the public API's signature.
- If the signature must change, that's an API change, not a refactor.
- Do not compromise correctness in the name of a performance improvement.
- Do not pre-build an abstraction "in case it's needed later."
- Do not pull code used only once out into a utility/helper.
Implementation Review Checklist¶
- This section is a review checklist for confirming the public API policy has been reflected in the implementation.
- The items below are not a proposal for new public API. They're the standard for confirming the implementation, tests, and samples follow the already-defined contract and boundary rules.
- Use these items as the base checklist for per-binding review and refactoring work.
Public vs. Internal Boundary Follow-Up¶
- Java:
- An internal-natured type that remains in a public package
(
SocketCore,MessagePlane, a request/reply support helper, and so on) must be moved to an internal or implementation package. - If JPMS is used, clean it up so only documented public packages are exported.
- .NET:
InternalsVisibleTomust be restricted to test-support scope only.- Assembly visibility must be closed back up so the perf project doesn't access the internal surface.
- C:
- Once the helper substrate and the public C binding header are
actually separated, the
core/include/zlink.h-centered explanation must be reorganized around the public C binding header. - The boundary between the installed public header and the private substrate header must be reflected in both the documentation and packaging.
Value Validation Follow-Up¶
RoutingId- length-limit validation at value-object construction
- re-validation immediately before the native call, if a raw path remains
Duration-based optionsint millisconversion overflow validation- explicitly stating the negative-value allowed/disallowed contract
- topic/filter/string identifiers
- check the reallocation policy on a fixed-size output buffer path
- check that the entire string is processed without truncation
- offset/length-based byte APIs
- unify bounds validation
- a raw integer option without an enum wrapper
- investigate enum or boolean promotion candidates
Public Surface Follow-Up¶
- a legacy flag type
- reconfirm removal of a public flag type or duplicate flag path not
in the per-language
Flags Policy - decide whether to move it to internal, if needed
- the monitor plane
- confirm the
recv()canonical surface is preserved - the callback API
- reconfirm the callback payload shape matches the direct-receive shape
- a single-message convenience method
- check for a remaining public receive/subscribe convenience overload
Option Surface Follow-Up¶
- investigate any remaining raw option bag
- investigate any option-role leak across socket types
- list an option value that still stays
int - review whether a context option should also get a typed facade under the same standard
Error Contract Follow-Up¶
- investigate a path where a binding validation exception and a native exception are mixed
- investigate a path where the binding arbitrarily interprets errno
- investigate code that wrongly hides an error other than native
NO_DATAbehind an empty/bool path - investigate a helper/sample that ignores a blocking send failure
Performance Follow-Up¶
- investigate a hidden copy on the hot-path send/recv path
- investigate unnecessary collection/array allocation while
constructing
Message,Received,TopicMessage - investigate the cost gap between the callback path and the direct path
- investigate the encoding/decoding cost of string/topic/routing-id conversion
- investigate whether a sample or helper exposes a slow alternative path as the default usage pattern
POSD Follow-Up¶
- investigate a public type that provides only a shallow wrapper
- investigate a change-amplification point where one rule is scattered across multiple modules
- investigate a temporal API that requires the user to know internal sequencing
- investigate a raw/native concept leak point that could be hidden behind a facade
Ownership And Callback Follow-Up¶
- check that the send-failure restore path and consume path match the documentation
- re-verify the frame-validity contract after a callback
- check that an error is delivered per the native contract when callback mode and direct recv conflict
Test Follow-Up¶
- confirm a public surface test exists for every public surface change
- add value-boundary verification tests
- example:
RoutingId's maximum length - example:
Durationoverflow - strengthen option negative-role tests
- confirm ownership/callback regression tests are kept
Binding Requirements¶
| Binding | Language version | Runtime/framework | Build tool |
|---|---|---|---|
| C++ | C++20 | — | CMake 3.10+ |
| .NET | C# 12 | .NET 8.0 | MSBuild |
| Java | Java 25 | JDK 25 | Gradle 9.3.0 |
| Go | Go 1.22+ | — | Go modules |
| Rust | Rust 2024 edition | MSRV 1.85+ | Cargo |
| Node | TypeScript 5.8 | Node 22+ | npm |
| Python | Python 3.9 | CPython 3.9+ | setuptools 68+ |
| - The exact version for each binding is governed by that project's | |||
| configuration file. | |||
- C++: CMakeLists.txt |
|||
- .NET: Zlink.csproj (PackageId/RootNamespace: Systems.Zlink) |
|||
- Java: build.gradle, gradle-wrapper.properties |
|||
- Go: go.mod |
|||
- Node: package.json, tsconfig.json |
|||
- Python: pyproject.toml |
API Reference¶
Each binding generates its API reference with that language's standard documentation tool.
| Binding | Doc tool | Generate command | Output location |
|---|---|---|---|
| C++ | Doxygen | doxygen Doxyfile |
cpp/doxygen/html/ |
| Java | Javadoc (Gradle) | ./gradlew javadoc |
java/build/docs/javadoc/ |
| Python | Sphinx + autodoc | sphinx-build -b html docs docs/_build/html |
python/docs/_build/html/ |
| Node | TypeDoc | npx typedoc |
node/typedoc/html/ |
| .NET | DocFX | docfx docfx.json |
dotnet/_site/ |
| Go | godoc / pkgsite | go doc ./... |
(a dynamic server) |
| Rust | rustdoc | cargo doc --no-deps |
rust/target/doc/zlink/ |
- Run the generate command from each binding's directory.
- Exclude the output directory from tracking with
.gitignore. - Each binding's
README.*.mdfile specifies the detailed generation procedure and scope.
Disconnecting A Peer By Routing ID¶
- Every binding exposes core's peer-rid disconnect surface for a connectable raw socket type.
- The raw socket API maps to
zlink_disconnect_rid(), and the SpotNode API maps tozlink_spot_node_disconnect_peer_rid(). StreamSocketis bind-only and does not expose peer-rid disconnect.- A Spot facade type also does not expose a separate peer-rid disconnect method, because peer mesh ownership belongs to SpotNode.
| Language | Raw socket name | SpotNode name |
|---|---|---|
| C | zlink_disconnect_rid |
zlink_spot_node_disconnect_peer_rid |
| C++ | disconnect_rid |
disconnect_peer_rid |
| Python | disconnect_rid |
disconnect_peer_rid |
| Node | disconnectRid |
disconnectPeerRid |
| Go | DisconnectRID |
DisconnectPeerRID |
| Rust | disconnect_rid |
disconnect_peer_rid |
| Java | disconnectRid |
disconnectPeerRid |
| .NET | DisconnectRid |
DisconnectPeerRid |
A binding must expose ZLINK_OPT_RID_DUPLICATE_POLICY,
ZLINK_RID_DUPLICATE_REJECT, ZLINK_RID_DUPLICATE_HANDOVER, and the
connect result values NOT_FOUND, CONFLICT, BUSY, using each
language's usual enum/error mapping style.
- C and higher-level bindings no longer expose the removed
ZLINK_OPT_AUTO_HWM_MSG_UNIT_BYTESvalue0x3034orZLINK_CTX_OPT_AUTO_HWM_MSG_UNIT_BYTESvalue18. They do not add a compatibility alias, deprecated wrapper, or raw bypass. - The canonical Auto HWM surface is the byte-valued context memory limit, Core budget, profile, ABI v1 budget snapshot, and metric reset. A socket, SpotNode, or Spot does not add a per-message-unit option.
Related Documents¶
bindings/cpp/bindings/dotnet/bindings/java/bindings/go/bindings/rust/bindings/node/bindings/python/
Core API Surface 6.0.0 Alignment¶
- Actor create and join payloads use an aggregate multipart payload.
- The public binding API takes a message collection for remote actor create, actor join, actor join receive, and actor join reply.
- A single-message convenience path is allowed only when that language's README explicitly decides to keep that convenience surface. Otherwise, it's removed while cleaning up toward the canonical multipart path during breaking alignment.
- Even when kept, it must call the multipart path internally, and an empty payload must remain distinguishable from a single empty message.
- An admission handler receives a borrowed payload view valid only during the callback.
The public Registry scalar setting was removed together with the public Discovery/Registry C API in core 8.4.3. A binding must not keep a registry option surface, a named registry setter, or a compatibility alias as current public API.
Spot Route Bridge API¶
- A binding must expose
SpotRouteBridge, or an equivalent typed handle, soSpotNodedoesn't own the channel socket. - The bridge references a
ROUTERsocket owned by the caller/channel runtime, sending a Spot route packet, or handing a SPOT relay packet received in the channel receive loop over to SpotNode. - Closing the bridge does not close the registered channel socket.
A per-language API must not omit the following meaning.
createRouteBridge(options)or an equivalent constructorattachRouterChannel(channelName, routerSocket)sendToSpot(targetNode, targetSpot, parts)requestToSpot(targetNode, targetSpot, parts, replyHandler, timeout)handleRouterReceived(channelName, received)closeordispose
timeout == 0 uses the bridge's default timeout. When
handleRouterReceived returns a handled result, the binding must
clearly express to the caller that payload ownership has moved to the
bridge.
The old C API that attaches a router channel peer directly to SpotNode is not part of the public contract. A framework adapter must not use that path in a new implementation.
Implementation and contract-test verification requirements¶
The following requirements are verified only through each language binding's public request/reply surface, public message ownership, and per-language error values. Each item maps to one contract test.
Payload and metadata boundary
- Passing the same application multipart through a binding request and reply produces the same part count, order, and bytes at the peer application; neither protocol parts nor ZMP metadata appear in the public payload.
- Even when a valid peer error reply contains payload after the errno part, public completion exposes only the language-specific error and no reply payload.
Ownership and error mapping
- If a C++ native request submit fails with
submit_error_t, the publicmessage_tremains with the caller, and raw-sending it does not produce a request-reply kind. - When a peer sends a valid wire error reply with errno
EAGAINorENOBUFS, public request completion produces the binding'sBACKPRESSUREDerror, and all 66 non-OK values in the Public Result Enum Catalog remain distinguishable. - When a raw reply to a DEALER peer is not admitted within
SNDTIMEObecause of Application HWM orPAUSED, each language returnsBACKPRESSURED. A raw reply to a ROUTER peer returns the HWM-free admission result of the current ready Completion pipe. - Every binding provides receive-flow setter, request/reply, completion-receive, and monitor signatures. No public API sets or queries the lane count.