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.Raw bindings follow Core runtime boundary §3 for the service C ABI boundary; public Spot/Actor shapes follow Framework API.
| 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 | Framework public contract link |
| 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 | Framework public contract link |
| 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 | Framework public contract link |
| 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 | Framework public contract link |
| 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 | Core runtime boundary link |
| Spot Route Bridge API | Framework public contract link |
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 |
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. 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 raw operation, result, and callback helper types in the domain that defines their meaning. Send/request/reply results and callbacks belong in the messaging contract. Public Spot/Actor shapes are owned by Framework API.
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 |
| socket receive readiness handler registration | setReadableHandler |
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. |
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/
errors/
runtime/
native/
sockets/
messaging/
eventing/
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, or a handler/callback 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, 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 — 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
socketsoreventingholds 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, and timer 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,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, 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.
- 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 the eventing 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 |
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¶
The public Actor/Spot route surface is owned by Framework 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 (dealer and router variants)
- reply (router variant)
- publish
- subscribe (raw SUB/XSUB)
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¶
Core input-slot consumption and whole-record atomicity follow Core whole-message send. The binding projects those results through its language ownership contract. It adds no socket-local gate for a part sequence. Independent concurrent records follow Core same-handle concurrency.
Spot Get-Or-Create Mapping¶
The Framework API owns public Spot and Actor GetOrCreate behavior.
Raw Core bindings assume no service C ABI
(Core runtime boundary).
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,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.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
| | +-- Errors
| +-- <private runtime/internal area>
| | +-- Core
| | +-- Messaging
| | +-- Sockets
| | +-- Eventing
| | +-- 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 — 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, 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. - Raw
send, routed send,publish,request, andreplyreturn an operation builder. Public Spot/Actor builder shapes are owned by Framework API. - 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 public raw resources throughIContext.Create...factories. Public Spot/Actor factories follow Framework API. - 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 |
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). Public Spot/Actor receive shapes are owned by Framework API.
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 raw ROUTER receive, 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 across raw socket kinds. Public Spot/Actor result shapes are owned by Framework API.
The rules below are Required.
- The raw receive results of PAIR / DEALER / ROUTER / STREAM 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— 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(),
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 raw send/request/reply/publish family has several combination axes. Spreading
target path, payload part count, flags, timeout, and the native-suspension
completion mode across plain method overloads makes a
socket 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 raw send, request, reply, and publish surface. The name is converted to fit language convention.
Spot facade (Spot / spot_t)¶
Public Spot operation start points are owned by Framework API.
Raw socket facade¶
PubSocket.publish(topic)/XPubSocket.publish(topic)DealerSocket.send()/DealerSocket.request()RouterSocket.send(rid)/RouterSocket.request(rid)/RouterSocket.reply(rid, 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¶
Public Actor/SpotNode operation start points are owned by Framework API.
Common Builder Rules¶
- A start point does not send immediately — it returns a per-language
operation builder such as
SendOp,RequestOp, orReplyOp. 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), andpublish(topic, message)are forbidden. Express 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 a raw send, request, reply, or publish operation where payload is required, 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. 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,send,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 raw request 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.
Common Flow Example¶
A raw builder is used as routerSocket.request(rid).message(part).submit(). Public Spot/Actor examples are owned by Framework API.
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 raw send/request/reply/publish 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
STREAM. - 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 rawSUBorXSUBreceive. 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.- A binding projects the
ROUTERselected-route snapshot (zlink_router_routes_snapshot()) andZLINK_POLLROUTEwith the meaning defined in Core ROUTER §10.1. A route generation is a nonzero opaque equality token; its sign or size does not imply order. - 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¶
Public SPOT channel and dispatch operations are owned by Framework API.
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.- A binding does not forward managed heap limits (GC, JVM, V8, and so on) to Core. Memory-input precedence and connection admission follow Core Auto HWM §2. A binding does not apply 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
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, 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.
SendSubmission and RequestSubmission stage transitions and WRITABLE resubmission outcomes follow async execution model §5. The table below owns staging lifetime.
| Native submission result | High-level binding outcome |
|---|---|
SEND OK, ID 0 |
Follow the SEND stage outcome in async execution model §5. |
REQUEST OK, nonzero ID |
Follow the REQUEST stage outcome in async execution model §5. |
BACKPRESSURED, EAGAIN, nonzero wait token |
Before calling Core, the binding retains an independently owned staging record and passes a separate native array to each attempt. Only after a ZLINK_SEND_ADMITTED WRITABLE does the binding resubmit the same operation from staging. Staging is released once on admission, terminal failure, or socket/context lifecycle cleanup. ZLINK_SEND_NOT_FOUND and ZLINK_SEND_NOT_CONNECTED project to NOT_FOUND and NOT_CONNECTED, respectively; any other value, including the ABI-preservation value ZLINK_SEND_TIMED_OUT that Core never produces, projects to INTERNAL_ERROR with EPROTO. Stage outcomes follow async execution model §5. |
| 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 also retains the source under its language ownership contract, independently of the staging record above. 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¶
SpotNode HWM options are owned by Framework API.
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¶
The Framework API owns the public Actor service contract. Raw bindings follow the Core runtime boundary.
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. - Public Spot/Actor perf and sample policy follows Framework API.
- 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,disconnecton connectable base only;disconnectRidon every raw socketclose/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, 206, 306, 404, 505, 604, 704, and so on), the internal raw errno can be looked up withzlink_errno(). - The errno representation of a failed REQUEST completion result
(1xx, including 105) follows the Core
Request completion result
contract. Its original internal errno is not retrieved with
zlink_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. - Except for the C ABI, bindings do not expose a public raw
zlink_errno()accessor; callers use the error type’sinternalErrno/internal_errnofield.
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 Core errors.
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 raw method spans multiple function families, declare the shared parent
ZlinkException/ZlinkErrorand list the subtypes that can 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 raw data-path function has a flags option expressed as a per-language signature parameter or a builder 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 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.
// router: RouterSocket
router.request(rid)
.message(part)
.timeout(Duration::from_secs(3))
.submit() // returns the language completion object
// BAD: split names for the same operation.
request(rid, parts, timeout)
request_async(rid, 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¶
Public Spot/Actor target names are owned by Framework API.
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 raw DealerSocket/RouterSocket, the request start point is request/request(peer). The per-language final execution method follows the bindings async execution surface policy.
- On success, return only the reply payload's
List<Message>. Do not wrap request-target metadata inReceivedor introduce a separateReplytype. - Because multipart reply is possible, it returns
List<Message>, not a singleMessage. A single-part reply is retrieved withlist[0].
Shared¶
- For the full
zlink_request_result_tdefinition, see Core errors. - 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¶
Raw SUB/XSUB receive returns the complete payload array from Core zlink_subscribe()
as a per-language multipart object. Public Spot subscribe results follow Framework API.
| 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.
topicis a UTF-8string. It is not exposed asbytes/byte[]/Vec<u8>(even if it arrives internally as raw bytes, the public API decodes it).- Keep only a single typed
RoutingIdfield. Do not create a dual property such asRoutingId: stringplusRoutingIdValue: 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 raw PAIR / DEALER / ROUTER
/ STREAM 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 and ROUTER, 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 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) |
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.
- 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. Public Spot receive results follow Framework API.
- 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 raw subscribe/unsubscribe event XPub receives. Public Spot subscription events are owned by Framework API.
| 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).
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. The result of a monitor registration or modification whose mask contains any bit other than POLLIN is defined by the "Poller registration" items of Core 05-polling §9; the binding reports the Core result as a typed error. 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¶
Public Spot/Actor snapshot shapes are owned by Framework API.
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.
- Public Spot/Actor combined surfaces are owned by Framework API.
- 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 | Y |
disconnectRid is the peer-rid disconnect surface of a raw socket. STREAM
is a bind-only socket and does not expose connect or disconnect, but it
must provide disconnectRid to close an accepted connection. Public Spot/Actor peer management is owned by Framework API.
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 |
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. - Public Spot/Actor capabilities follow Framework API.
- Watch especially for these frequent violations:
- No plain
send(a send without routingId) onRouterSocket/StreamSocket— it must besend(routingId, ...). - No
connectordisconnectonStreamSocket—STREAMis a bind-only socket.connectanddisconnectare provided on every raw socket except STREAM (capability table above).disconnectRidis also provided onStreamSocket. - 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¶
Service layer policy is owned by Framework API.
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. Raw socket request-reply is provided by Router/Dealer sockets. Public Spot paths are owned by Framework API.
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 |
Public Spot request/reply paths are owned by Framework API.
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.
Public Spot/Actor API shapes are owned by Framework API.
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 is owned by Framework 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¶
Public SPOT request and reply operations are owned by Framework API.
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¶
SPOT messaging policy is owned by Framework API.
SPOT Event Dispatcher Policy¶
SPOT event dispatch policy is owned by Framework API.
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 is owned by Framework 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¶
SPOT snapshot query operations are owned by Framework API.
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, 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 raw facade's option items follow the Core transport liveness boundary
and the public option enum 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. Public Spot/Actor internal policy is owned by Framework API. A
DONTWAITcall, 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
- Flag-specific weight-0 submit results follow Core whole-message send and Request and reply.
- The socket monitor event ZLINK_EVENT_PEER_WEIGHT_CHANGED (bit 15)
Binding rules:
- weight is exposed through a per-language typed option/property
surface. It applies to ROUTER and DEALER. Spot and Actor options follow Framework API.
- Project NOT_ADMITTED as SubmitError only when Core returns it.
- Project PEER_WEIGHT_CHANGED through the Core socket monitor as a typed value.
Service observability follows Framework API.
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, protocol-state, and peer-weight submit results. The binding projects Core results and errno without reclassifying by state.
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 errors 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). A REQUEST completion result follows theINTERNAL_ERRORdetail item above.
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. Which errno each value comes from is owned by Core result and errno mapping.
zlink_submit_result_t (send, request submit, reply submit)¶
| Value | Constant | Category | Meaning |
|---|---|---|---|
| 0 | OK |
success | submit succeeded |
| 1 | BACKPRESSURED |
control flow | Core capacity refusal; see Core Errors §2 |
| 2 | NOT_CONNECTED |
control flow | the target peer/path is not connected |
| 3 | NOT_FOUND |
control flow | the target peer/route does not exist |
| 13 | NOT_ADMITTED |
control flow | Admission refusal returned by Core. Flag-specific weight-0 results follow Core whole-message send and Request and reply. |
| 4 | TERMINATED |
runtime/lifecycle | the context has terminated |
| 5 | INVALID_HANDLE |
caller contract violation | a NULL handle / invalid pointer |
| 6 | INVALID_ARGUMENT |
caller contract violation | an invalid argument |
| 7 | NOT_SUPPORTED |
caller contract violation | not supported on that socket type |
| 8 | INVALID_STATE |
caller contract violation | a socket/handle state error |
| 9 | THREAD_VIOLATION |
caller contract violation | accessed from the wrong thread |
| 10 | OUT_OF_MEMORY |
internal failure | a memory allocation failure |
| 11 | SEQ_EXHAUSTED |
internal failure | the request seq space is exhausted |
| 12 | INTERNAL_ERROR |
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 | Meaning |
|---|---|---|
| 0 | OK |
successfully received the reply payload |
| 101 | TIMED_OUT |
reply did not arrive within timeout_ms |
| 102 | NOT_FOUND |
target not found; completed with an error reply |
| 103 | TERMINATED |
the owner lifecycle ended |
| 104 | PROTOCOL_ERROR |
the reply metadata or error-reply payload is malformed or incompatible |
| 105 | INTERNAL_ERROR |
an internal request failure without another public bucket |
| 106 | REJECTED |
the target or admission path rejected the request |
| 107 | CONFLICT |
a conflict on the request target or state |
| 108 | BUSY |
the request-processing path is temporarily busy |
| 109 | NOT_CONNECTED |
the target peer/path is not connected |
| 110 | INVALID_ARGUMENT |
a request argument or metadata error |
| 111 | INVALID_STATE |
the handle state cannot accept a request |
| 112 | NOT_SUPPORTED |
request is not supported on this target |
| 113 | BACKPRESSURED |
the request-processing path cannot proceed because capacity is unavailable |
zlink_recv_result_t (recv, subscribe, subscription event, monitor recv, timer recv)¶
| Value | Constant | Meaning |
|---|---|---|
| 0 | OK |
receive succeeded |
| 201 | NO_DATA |
non-blocking recv has no data / the source is exhausted |
| 202 | BUSY |
a handler is already attached |
| 203 | TERMINATED |
the context has terminated |
| 204 | INVALID_HANDLE |
a NULL / invalid handle |
| 205 | NOT_SUPPORTED |
recv is not supported on this socket type |
| 206 | INTERNAL_ERROR |
an internal recv failure (see zlink_errno() for detail) |
| 207 | BUFFER_TOO_SMALL |
caller output capacity is insufficient |
| 208 | INVALID_STATE |
receive lifecycle state error |
zlink_handler_result_t (handler registration)¶
| Value | Constant | Meaning |
|---|---|---|
| 0 | OK |
handler registration succeeded |
| 301 | INVALID_ARGUMENT |
a NULL handler |
| 302 | BUSY |
a handler is already attached |
| 303 | NOT_SUPPORTED |
an unsupported subject |
| 304 | DEADLOCK |
reentrant handler replacement in callback scope |
| 305 | INVALID_HANDLE |
a NULL / invalid handle |
| 306 | INTERNAL_ERROR |
an internal handler-registration failure (see zlink_errno() for detail) |
zlink_close_result_t (close, destroy)¶
| Value | Constant | Meaning |
|---|---|---|
| 0 | OK |
close/destroy succeeded |
| 401 | BUSY |
an in-flight callback / API call |
| 402 | SHUTDOWN |
already closed |
| 403 | INVALID_HANDLE |
a NULL / invalid handle |
| 404 | INTERNAL_ERROR |
an internal close failure (see zlink_errno() for detail) |
zlink_bind_result_t (bind)¶
| Value | Constant | Meaning |
|---|---|---|
| 0 | OK |
bind succeeded |
| 501 | INVALID_ARGUMENT |
an invalid endpoint |
| 502 | ADDR_IN_USE |
the address is already in use |
| 503 | NOT_SUPPORTED |
an unsupported transport |
| 504 | INVALID_HANDLE |
a NULL / invalid handle |
| 505 | INTERNAL_ERROR |
an internal bind failure (see zlink_errno() for detail) |
zlink_connect_result_t (connect, disconnect, unbind)¶
| Value | Constant | Meaning |
|---|---|---|
| 0 | OK |
connect/disconnect/unbind succeeded |
| 601 | INVALID_ARGUMENT |
an invalid endpoint |
| 602 | NOT_SUPPORTED |
an unsupported transport |
| 603 | INVALID_HANDLE |
a NULL / invalid handle |
| 604 | INTERNAL_ERROR |
an internal connect/disconnect failure (see zlink_errno() for detail) |
| 605 | NOT_FOUND |
the endpoint or peer routing id does not exist |
| 606 | CONFLICT |
the peer routing id conflicts with two or more pipes |
| 607 | BUSY |
the lifecycle owner rejected a manual change |
| 608 | AUTH_FAILED |
transport peer authentication failed |
zlink_config_result_t (option set/get, message lifecycle, snapshot, poller mutation, proxy, timer config)¶
| Value | Constant | Meaning |
|---|---|---|
| 0 | OK |
configuration succeeded |
| 701 | INVALID_HANDLE |
a NULL / invalid handle |
| 702 | INVALID_ARGUMENT |
an invalid argument, or a config-layer conflict |
| 703 | NOT_SUPPORTED |
an unsupported option |
| 704 | INTERNAL_ERROR |
an internal config failure (see zlink_errno() for detail) |
| 705 | INVALID_STATE |
the lifecycle state rejects the config |
| 706 | NOT_FOUND |
the local lookup target does not exist |
| 707 | CONFLICT |
duplicate identity, endpoint, or registration value |
| 708 | BUFFER_TOO_SMALL |
caller output capacity is insufficient; no partial output |
| 709 | BUSY |
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() |
|---|---|
| pre-admission backpressure | WRITABLE wait under submit result projection. The reply timeout starts at admission under Core Request and reply. |
| 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
setReceiveHandlertosocketReceiveHandler→ 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 |
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 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 raw send/request/reply/publish 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.
This table and the Required surface test apply to the Core raw socket ABI. Framework API defines public Spot/Actor APIs and their verification.
| 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 |
| 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 |
| 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 provided |
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,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¶
Public Spot/Actor API verification is defined by Framework API.
- 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 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. - Verify a readiness event value does not replace the data-plane contract.
Conditional: Service Tests¶
Public Spot/Actor test contracts are owned by Framework API.
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.
monitor_recv_samplecovers monitor-event receive, including the socket monitor.- 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 → 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 socketReceiveHandler→setReceiveHandleron_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, 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. - Public Spot/Actor verification follows Framework API.
-
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 raw callback surface exposes
setPacketHandlerfor its role. -
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.
- Public Spot/Actor samples follow Framework API.
- 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¶
- Raw socket RID disconnect follows the Core socket contract.
- Spot and Actor target control follows Framework API.
Related Documents¶
bindings/cpp/bindings/dotnet/bindings/java/bindings/go/bindings/rust/bindings/node/bindings/python/
Core API Surface 6.0.0 Alignment¶
Core exposes no native service ABI under the Core runtime boundary.
Spot Route Bridge API¶
The public Spot route bridge surface is owned by Framework API.
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.