Runtime Events and Scopes
The native runtime handle lets plugin middleware participate in Relay’s
event hierarchy
without linking the host runtime crate. The example’s runtime group emits a configured
mark, opens and closes a child scope, and optionally creates an isolated
scope stack.
Preserve Stack Ownership
A scope stack determines visible scope-local middleware, event parentage, and cleanup.
Typed async middleware captures the callback’s stack and restores it around every future
and stream poll. The example inspects the current scope, then uses the isolated stack’s
with_current helper to bind it for a synchronous closure and restore the previous
thread binding afterward. Child tasks created with tokio::spawn do not automatically
inherit Relay scope context, so a plugin that deliberately moves work into a new task or
thread must use the explicit capture and binding helpers.
An isolated stack begins with its own root and is appropriate only when the emitted work should not be a child of the managed application call. The example creates the stack, binds it, pushes and pops its child scope, restores the previous binding, and drops the isolated stack through the typed guards. Those guards perform the same cleanup if the closure returns an error.
runtime.scope returns an owning guard. Calling close supplies the end-event output;
dropping an unclosed guard still follows the SDK cleanup path. with_current restores
the prior stack after either Ok or Err, and dropping isolated releases the stack.
Emit Typed Telemetry and Read Diagnostics
Use emit_mark_with_options when a mark needs a data schema or log severity. Use
emit_metric to create a validated Relay metric mark rather than constructing the
reserved metric schema yourself. The following code adds metadata to a policy mark and
then reads one runtime diagnostic:
The diagnostic snapshot is bounded and ordered by code. It is a host-level view, so it does not identify the plugin that recorded a diagnostic. These operations require the native host ABI v4 extensions.
Control Global Runtime Registrations
Native host ABI v4 adds activation-owned registration discovery and conditional
middleware guardrail control. PluginRuntime::list_runtime_registrations
returns structured global identities. A background plugin task can call
register_conditional_middleware_guardrail, retain the returned opaque handle,
and call deregister_conditional_middleware_guardrail when the target should
be eligible for future snapshots again.
The host evaluates a constant reason callback locally. It does not enter the
native library while resolving the target. The plugin can remove only gates
owned by its activation, and Relay removes remaining gates during activation
teardown. Use PluginContext::register_conditional_middleware_guardrail for a
gate that must exist as part of the initial registration transaction.
The following registration uses the example’s validated, disabled-by-default
registration_control group:
The native SDK installs the constant reason in the host. Relay does not enter the native library while it evaluates future snapshots. Refer to Conditional Middleware Guardrails for the complete discovery, ordering, snapshot, and cleanup contract.
Configure the SDK Executor
NativePlugin::executor_config supplies the plugin-wide default. The SDK default and the
example default are two worker threads. executor_config_for_component can derive a
component override; the standard implementation recognizes a positive
executor.worker_threads integer. Because the example schema rejects unknown values, it
declares the executor object even though the object is owned by the SDK contract.
More workers help only when measured async I/O leaves callbacks queued. They do not make CPU-bound or blocking callbacks safe. A host with many active native components has one executor per component, so an unnecessarily large value multiplies thread use.
With no component override, this implementation creates two executor workers. With
executor.worker_threads = 4, the standard component resolver creates four. Zero,
negative, fractional, and nonnumeric values become validation errors before the SDK
creates an executor.
Verify Runtime Cleanup
Use the following procedure to verify parentage, restoration, executor settings, and failure cleanup:
- Activate
runtime.emit_marks = trueandemit_isolated_scope = truewith two executor workers. - Run a managed call inside a named scope. Confirm the plugin mark and ordinary child scope use that call as their parent.
- Confirm the isolated scope uses its new root and has no accidental parent from the caller.
- In a host integration test, make one runtime operation fail after a push. Confirm the
scope guard closes the scope,
with_currentrestores the former stack, and dropping the isolated stack releases its host handle. - Run unrelated work on the same runtime thread and confirm its current stack is unchanged. Then clear the plugin and wait for active callbacks to finish before unloading the library.
Success means event parentage is intentional, no scope or isolated stack leaks on failure, later work regains its original context, and unload begins only after callback ownership has ended.