Skip to content

C++ Common Runtime Per-Language Interface

C++ per-language interface table of contents

Spec table of contents | Previous: C++ System Structure | Next: C++ HTTP Hosting

Spec table of contents

This document is the C++ formal public interface contract of ZLink Framework. This document follows the common Framework policy under framework/doc/framework/common/spec as the higher standard, and designs the framework layer on top of the C++ binding's public library surface.

1. Contract Standard

C++ framework doesn't replace the C++ binding. The framework sits on top of the C++ binding, and uses the typed public API the binding provides as the internal runtime substrate.

Capability and usability concepts are aligned to the framework common spec as the standard. That is, app/host, DI scope, handler registry, channel messaging, STREAM, SPOT, ActorGateway session relay, monitoring, and graceful shutdown provide the same model, and the C++ public API only changes its expression to fit C++20 coroutine, callback, and RAII ownership.

The binding standard follows the documents below.

The framework public API is placed under the zlink::framework namespace. The installed public header includes only the formal contract and explicit extension points. The application must be able to compose the framework without knowing the transport implementation.

2. Binding Public Dependency Boundary

The Framework package only depends on the C++ binding's public API. Only values defined by the framework contract, such as ChannelName, topic, typed payload, timeout, and lifecycle, appear in the public handler and client.

The place a user can pass a binding value directly is limited to a payload boundary the formal signature specifies, such as message_t. No other binding type appears in a framework public signature.

3. Header And Namespace

The recommended public header layout is below. The header under contracts/* is the actual public contract owner corresponding to .NET's Contracts/*, and zlink/framework.hpp is a facade that lets a user include the whole framework surface at once. The public header layout doesn't include one-line wrappers under zlink/framework/*.hpp.

zlink/framework.hpp
zlink/framework/version.hpp
zlink/framework/contracts/actors/*.hpp
zlink/framework/contracts/channels/*.hpp
zlink/framework/contracts/codecs/*.hpp
zlink/framework/contracts/configuration/*.hpp
zlink/framework/contracts/dispatch/*.hpp
zlink/framework/contracts/errors/*.hpp
zlink/framework/contracts/eventing/*.hpp
zlink/framework/contracts/handlers/*.hpp
zlink/framework/contracts/http/*.hpp
zlink/framework/contracts/locations/*.hpp
zlink/framework/contracts/messaging/*.hpp
zlink/framework/contracts/monitoring/*.hpp
zlink/framework/contracts/spots/*.hpp
zlink/framework/contracts/streams/*.hpp
zlink/framework/contracts/timers/*.hpp
zlink/framework/contracts/workers/*.hpp

A public header such as zlink/framework/runtime.hpp isn't provided. The public API only exposes contract names a user understands, such as app_t, request_client_t, spot_context_t.

This structure doesn't mean every .NET public interface is moved to a C++ pure virtual class. The C++ public API can actively use a concrete facade and value type. But the facade's member, constructor, and method signature must not expose a runtime implementation type. Only a user extension point is kept as an abstract interface or concept contract.

3.1 Public Contract Boundary

A C++ public header only defines the type and result a user configures or calls. Even if a public facade keeps state, the user shouldn't need to know that state's data structure or processing order.

The public route_client_t and route_send_call_t provide a typed call targeting a node and global Spot ID. User Spot and Instance Spot use the same ID-only call surface, and don't provide a separate handle/resolver/logical address type. The request family returns channel_request_call_t. The user doesn't pass a target MeshNode, location owner token, or generation, and the framework handles routing envelope, location claim, and serializer selection.

An ordinary request receives a typed reply through request_to_node(...).timeout(...).async<TReply>(). A value set with .metadata(key, value) is snapshotted per the application metadata contract, and transport detail and correlation state aren't exposed in the public API.

The underlying transport and remote error envelope convert to the following public error meaning.

Underlying error meaning C++ error kind
timed_out, timeout deadline_exceeded
not_connected, route_not_connected unavailable
not_found, request_target_not_found, handler_not_found not_found
Admission or filter rejection with no typed result rejected
Local queue capacity shortage capacity_exceeded
Target queue capacity shortage a remote error envelope reported unavailable
busy One of the two lines above depending on owner location. If the underlying error alone can't tell the location, unavailable
protocol_error, request_protocol_error protocol_error

This table applies with the same meaning to both request completion and error envelope reply.

A DTO message name preferentially uses static constexpr const char *packet_name. Framework handler registration and the Stream Connector's send, request, and on default name read this value. A type with no name can use the C++ type name, but a public sample and formal DTO must have an explicit packet name.

3.2 C++ Public Header Constraint

Since an installed header is directly the public surface in C++, the following rules apply.

  • A template header only has type check and public facade forwarding.
  • A public class's state only uses a public contract type.
  • An optional dependency type, such as JSON, MessagePack, or Protobuf, can appear only in that codec extension's public contract.
  • A contract test only includes an installed public header.
  • A public inline function doesn't manipulate transport state beyond public validation and forwarding.

Every framework type is placed under the zlink::framework namespace. Each type's declaration is owned by exactly one category document specified in the per-language interface table of contents.

4. Common Result, Coroutine, And Message

namespace zlink::framework {

template <typename T>
class result_t {
public:
 static result_t success(T value);
 static result_t failure(
 framework_error_kind_t kind,
 std::string message);
 bool has_value() const noexcept;
 explicit operator bool() const noexcept;
 const T &value() const;
 T &value();
 const framework_exception_t *error() const noexcept;
 framework_error_kind_t error_kind() const;
};

template <>
class result_t<void> {
public:
 static result_t success();
 static result_t failure(
 framework_error_kind_t kind,
 std::string message);
 bool has_value() const noexcept;
 explicit operator bool() const noexcept;
 void value() const;
 const framework_exception_t *error() const noexcept;
 framework_error_kind_t error_kind() const;
};

template <typename T>
class task_t {
public:
 struct promise_type {
 task_t get_return_object();
 std::suspend_never initial_suspend() noexcept;
 std::suspend_never final_suspend() noexcept;
 void unhandled_exception();
 void return_value(result_t<T> result);

 template <typename U>
 void return_value(U &&value);
 };

 explicit task_t(result_t<T> result);
 task_t(task_t &&) noexcept = default;
 task_t &operator=(task_t &&) noexcept = default;
 task_t(const task_t &) = delete;
 task_t &operator=(const task_t &) = delete;
 ~task_t() = default;
 bool await_ready() const noexcept;
 void await_suspend(std::coroutine_handle<> continuation);
 T await_resume();
 const result_t<T> &result() const;
};

template <>
class task_t<void> {
public:
 struct promise_type {
 task_t get_return_object();
 std::suspend_never initial_suspend() noexcept;
 std::suspend_never final_suspend() noexcept;
 void unhandled_exception();
 void return_void() noexcept;
 };

 explicit task_t(result_t<void> result);
 task_t(task_t &&) noexcept = default;
 task_t &operator=(task_t &&) noexcept = default;
 task_t(const task_t &) = delete;
 task_t &operator=(const task_t &) = delete;
 ~task_t() = default;
 bool await_ready() const noexcept;
 void await_suspend(std::coroutine_handle<> continuation);
 void await_resume();
 const result_t<void> &result() const;
};

class message_t {
public:
 message_t() = default;

 template <typename TValue>
 static message_t from(TValue value);

 template <typename TValue>
 TValue decode() const;

 bool encoded() const noexcept;
 bool empty() const noexcept;
};

} // namespace zlink::framework

The first decode<T>() on a received message_t fixes either a value or a failure. Later calls do not run the serializer again. The retained value is returned when the same T is copy-constructible. Another T, or a type whose retained value cannot be returned again, ends with protocol_error. If the first call failed, its stored exception is delivered again.

5. Serialization

The Framework uses a typed JSON serializer as its default path. The handler and messaging API receive a payload type, and the application doesn't handle a registry, type-erased pointer, encoder callback, or raw dispatch table. A payload that can't be expressed as JSON selects a codec extension package through options.codecs().use(...). The extension package's registry wiring and payload conversion are a runtime-internal contract and aren't exposed in the application public header. Even when the Framework, connector, and HTTP client change codec, the handler's and client's typed API doesn't change.

A codec extension passes a parameter-free ASCII type/subtype as its contentType. At startup, the registry removes leading and trailing SP and TAB and converts ASCII uppercase letters to lowercase. The result is the canonical content type used as the registry key. A parameter, whitespace inside the value, or a non-ASCII token is rejected with framework_error_kind_t::protocol_error. If the same canonical content type is registered more than once, the last registration replaces the earlier one.

A value received from the framework service wire must already be canonical. The receive path does not transform another representation and instead completes it with protocol_error.

target_link_libraries(app PRIVATE zlink::cpp)

# Add only when Protobuf is needed.
target_link_libraries(app PRIVATE zlink::framework_codec_protobuf)

6. C++-Specific Contract

6.1 Backpressure

SPOT and STREAM backpressure is only observed through the public call object, timeout, and result error kind.

  • An application handler isn't given an API that directly controls the framework queue.
  • The default policy isn't an unlimited queue. Queue bound, submit timeout, and overflow policy are closed by framework runtime configuration, and exceeding the bound returns a failed result.
  • The error kind of an exceeded bound differs by operation family and queue location. It follows the §error mapping table above and Spot Messaging §5.3 — it isn't uniformly capacity_exceeded. Source-local saturation of one-way/send is deadline_exceeded, a request's local queue saturation is capacity_exceeded, and a remote queue saturation is unavailable.

This rule applies to ordinary SPOT and STREAM execution queues. Payloads temporarily retained by Message Follow relay do not have a separate message-count or byte bound. The per-wire-message size limit and the ordinary execution-queue, transport, and deadline limits after relay still apply.

6.2 Handler Filter

A filter reads the current dispatch kind and public metadata through handler_filter_context_t. The descriptor and raw message storage are kept Framework-internal. The filter doesn't return a result, and can't build or change a request reply.

Not calling next() only ends the current handler for send and Classic Fanout. A request completes as rejected. next() can be called only once, and a second call is an invalid_operation error.

The filter's registration order, next meaning, and scope are owned by Framework API §8.1.

6.3 Public Surface Boundary

The handler public contract is owned by contracts/handlers/*. The application only uses the handler signature, public metadata, and result, and doesn't control handler lookup, DI resolve, or serializer execution order.

6.4 Timer Execution

Timer callback, packet, and Actor turn are ordered in the same owner's serial execution queue. The application only uses logical timer registration and callback metadata.

A handler that's CPU-bound or might block is handed off to the Framework runtime's offload execution (§7.3 worker).

6.5 Actor Gateway Decision

Item Decision
actor_ref_t public shape A C++ value type holding node routing id, actor id, and generation
Session creation The session implementation is resolved from DI. The handler registry callback is kept only as a low-level extension surface
Remote ActorGateway The application only sees the actor_ref_t and session actor surface
Actor factory duplicate policy A duplicate same actor id is reported as already_exists, and an actor id/type mismatch as type_mismatch

actor_ref_t's node_rid/actor_id/generation is preserved across the bind/relay/push round trip. Local actor relay and remote actor relay use the same public surface.

7. Public Type Catalog

This section fills in the public type not covered by the sections above. A *_state_t/*_snapshot_t not here is runtime-internal state and isn't a public contract.

7.1 Dispatch Error Contract

A dispatch failure isn't expressed as a public event type. The Framework writes structured log/trace/metric records to standard providers the application configured, and doesn't expose a callback observer, error sink, or raw event DTO. A provider failure doesn't change the original dispatch result. The declaration of diagnostic levels is owned by Monitoring §2.

7.2 Dispatch Execution Policy

handler_execution_t distinguishes how a handler executes. The declaration of dispatch and message-flow diagnostic levels is owned by the Monitoring interface.

7.3 Worker

template <typename TResult> class worker_call_t
{
public:
 using executor_t = std::function<task_t<TResult>(
 std::stop_token)>;

 worker_call_t() = default;
 explicit worker_call_t(executor_t executor);
 worker_call_t &timeout (std::chrono::milliseconds value);
 task_t<TResult> async ();
 task_t<TResult> yield ();
};

class worker_options_t {
public:
 std::size_t min_threads() const noexcept;
 worker_options_t &min_threads(std::size_t value);
 std::size_t max_threads() const noexcept;
 worker_options_t &max_threads(std::size_t value);
 std::chrono::milliseconds idle_timeout() const noexcept;
 worker_options_t &idle_timeout(std::chrono::milliseconds value);
 std::size_t max_queue_length() const noexcept;
 worker_options_t &max_queue_length(std::size_t value);
};

A worker is work that runs outside a spot/session execution context. The rule for resuming completion in the original execution context is owned by Async Execution Policy. The worker function is passed a std::stop_token combining timeout, host shutdown, and caller cancellation. One-way async() completes without a result, while result-bearing async() keeps the current turn and waits for the result. yield() returns that turn and waits for the result only in a SpotWide User Spot's or Instance Spot's shared turn. In a different execution context, it completes with invalid_operation without submitting the worker or returning the turn. worker_options_t's min/max thread count, idle timeout, and queue bound are set only before host start.

7.4 Error Boundary

An API that returns synchronous validation and an explicit result object returns failure as result_t<T>. An async call's async() throws framework_exception_t carrying the same error information on failure. The application's error branch uses kind(). code() adds diagnostic information when there's a platform cause such as timeout or transport, but doesn't replace the common error classification.

The same Spot's dispatch serialization and yield()'s allowed scope are owned by Stage Wrapper §3 and Async Execution Policy.


Spec table of contents | Previous: C++ System Structure | Next: C++ HTTP Hosting