Native ABI Reference

View as Markdown

Use the typed nemo-relay-plugin SDK for normal authoring. This page records the raw contract needed to audit compatibility or implement an escape hatch; unsafe *_raw functions are intentionally not tutorial examples.

Entry and Version Negotiation

The exported symbol receives a pointer to the v1 prefix of the host table and fills a v1 plugin descriptor. The implementation must inspect abi_version and struct_size before casting a longer table.

1extern "C" fn nemo_relay_register_plugin(
2 host: *const NemoRelayNativeHostApiV1,
3 out: *mut NemoRelayNativePluginV1,
4) -> NemoRelayStatus

The current host negotiates ABI v4, then a separately frozen v3 table, then the frozen legacy v2 table. ABI v4 extends the complete v3 prefix with completion-scoped codecs and pull-based downstream LLM streams, plus an activation-owned runtime capability for registration discovery and dynamic conditional middleware guardrail control. These table versions are independent of authored compat.native_api = "1".

The following table enumerates the operations introduced at each table level. The exact function signatures and field order are defined by the public nemo-relay-plugin declarations.

Table LevelOperations
Frozen v1/v2 prefixVersion and struct-size negotiation; host version; string allocation, access, and release; thread-local error reporting; callback-scoped LLM request decode and encode plus response decode; subscriber, five tool, six LLM, and three event-sanitizer registrations; current scope, scope push and pop, mark emission, isolated stack creation and release, thread-stack set, capture, and restore, captured-binding release, active-stack inspection, and scoped binding.
Frozen v3 extensionCompletion resolve, reject, cancellation inspection, and release; one-shot completion-coupled continuation invocation; continuation release; generic async middleware registration; bounded output-stream push, finish, reject, cancellation inspection, and release; downstream stream invocation; stream-middleware registration; and repeated or concurrent unary continuation invocation with independent result callbacks.
Current v4 extensionCompletion-scoped LLM request decode and encode plus response decode; pull-based downstream LLM stream open, pull, cancel, and release; completion retain for typed codec facades; output-stream backpressure inspection; extended mark emission; runtime diagnostics; activation-owned runtime capability creation, retain, and release; global runtime-registration discovery; owned conditional middleware guardrail registration and deregistration; and activation-time host-resident gate declaration.

The prefix and descriptor layout are explicit. A plugin fills the descriptor with its stable kind, component multiplicity, opaque state, callbacks, and destructor. The host table is immutable after negotiation.

1#[repr(C)]
2pub struct NemoRelayNativeHostApiV1 {
3 pub abi_version: u32,
4 pub struct_size: usize,
5 pub relay_version: *const c_char,
6 pub string_new: unsafe extern "C" fn(
7 data: *const u8,
8 len: usize,
9 out: *mut *mut NemoRelayNativeString,
10 ) -> NemoRelayStatus,
11 pub string_data: unsafe extern "C" fn(
12 value: *const NemoRelayNativeString,
13 ) -> *const u8,
14 pub string_len: unsafe extern "C" fn(
15 value: *const NemoRelayNativeString,
16 ) -> usize,
17 pub string_free: unsafe extern "C" fn(
18 value: *mut NemoRelayNativeString,
19 ),
20 // Registration, runtime, codec, and error operations follow in this table.
21}
22
23#[repr(C)]
24pub struct NemoRelayNativePluginV1 {
25 pub struct_size: usize,
26 pub plugin_kind: *mut NemoRelayNativeString,
27 pub allows_multiple_components: bool,
28 pub user_data: *mut c_void,
29 pub validate: Option<NemoRelayNativePluginValidateFn>,
30 pub register: Option<NemoRelayNativePluginRegisterFn>,
31 pub drop: NemoRelayNativePluginDropFn,
32}

The shortened host-table declaration shows the mandatory prefix, not a replacement definition that a raw plugin can copy. A raw implementation must compile against the complete public declarations in nemo-relay-plugin so field offsets match exactly.

Values and Ownership

Text and JSON cross the boundary as host-owned NemoRelayNativeString handles. ABI structs otherwise contain scalars, opaque handles, callback pointers, and plugin-owned user_data. Rust trait objects, futures, serde_json::Value, allocator-owned strings, and unwinding must never cross the boundary. Release host strings with the matching host operation and retain plugin state until the host releases the registration and every callback-owned reference derived from it.

A successful plugin_context_runtime call returns one owned runtime-capability reference. Each successful plugin_runtime_retain call creates another owned reference, and the plugin must pass every owned reference to plugin_runtime_release exactly once. Activation teardown makes runtime operations unavailable, but it does not release references retained by plugin code; those references must still be released.

1#[repr(i32)]
2pub enum NemoRelayStatus {
3 Ok = 0,
4 AlreadyExists = 1,
5 NotFound = 2,
6 ScopeStackEmpty = 3,
7 GuardrailRejected = 4,
8 Internal = 5,
9 NullPointer = 6,
10 InvalidJson = 7,
11 InvalidUtf8 = 8,
12 InvalidArg = 9,
13 StreamEnd = 10,
14 Backpressured = 11,
15}
16
17#[repr(C)]
18pub struct NemoRelayNativeString {
19 _private: [u8; 0],
20 _marker: PhantomData<(*mut u8, PhantomPinned)>,
21}

Ok means the output pointers required by that operation were populated according to its contract. Backpressured is retryable only for the bounded stream operation that returned it. Other errors should be propagated with the host’s thread-local error message set when additional context is available.

The table exposes registrations for subscriber; mark, scope-start, and scope-end sanitizers; five tool surfaces; and six LLM surfaces. Runtime operations cover current scope, mark emission, scope push and pop, isolated stack creation and drop, stack capture or binding, and restoration.

Async Completions and Continuations

PluginContext::register_async_middleware_raw registers non-stream middleware that can settle later. Return Complete only after resolving or rejecting the completion inside the callback. Return Pending only after retaining it. A retained completion must settle exactly once and then be released. Release every async next reference after its last use.

async_next_invoke_result supports repeated or concurrent unary continuation calls with independent result callbacks. The older completion-coupled async_next_invoke is one-shot because the continuation result settles the middleware completion. Settle the owner only after every started continuation call has finished. When the owner settles or is cancelled, the host rejects new continuation calls and cancels unfinished ones.

For a tool continuation, the result callback receives canonical nemo.relay.ToolExecutionResult@1 JSON with required result and optional opaque annotation. A raw tool-execution middleware completion returns nemo.relay.ToolExecutionInterceptOutcome@2: the same result and annotation fields plus optional pending_marks. Pending marks belong to the intercept outcome only. Release each host-owned result string with the matching host-table operation after decoding it.

The host can poll a callback on different Tokio workers, and separate invocations can run concurrently. Plugin code must synchronize user_data and opaque handles. Do not race final release with settlement, cancellation inspection, stream operations, codec operations, or next invocation.

Streaming

The generic v3 completion API rejects the LLM stream intercept kind. Register it through plugin_context_register_async_stream_middleware and invoke downstream streams through async_next_invoke_stream. Each repeated or concurrent invocation needs independent callback state.

The output queue is bounded. async_stream_push_json and async_stream_reject never block a callback thread. Backpressured means the same logical chunk or rejection must be retried after the consumer advances. InvalidArg means the stream is closed or cancelled and the operation must not be retried. async_stream_is_backpressured is only a point-in-time observation; it does not replace checking the result of a push.

A downstream terminal callback reports failure or consumer cancellation with a non-null error and reports clean completion with done = true. Reclaim its user_data in that terminal callback. If a chunk callback returns false, reclaim state before returning because the host does not call it again.

Codec Handles

LLM sanitizer contexts report codec kind None, BuiltIn, Runtime, or Opaque, an optional codec ID, and a borrowed callback-lifetime handle. Built-in IDs are openai_chat, openai_responses, anthropic_messages, oci_genai, and gemini_generate_content. Runtime and opaque codecs can still have a usable handle.

Request handles support decode to an annotated request and encode of normalized changes onto the original envelope. Response handles support decode to an annotated response. A successful null sanitizer result omits the observability payload and annotation; an error also omits them and records the callback failure. Neither outcome changes the real request or application response. Never retain a raw handle or resolved typed facade after the sanitizer callback ends.

Unload Ordering

Relay keeps the library loaded while any owned registration or callback can reference plugin code. Shutdown stops new invocations, cooperatively cancels unfinished async work, waits for completion and stream references to be released, deregisters component-owned surfaces, runs component cleanup, and only then unloads the shared library. A plugin that retains a completion, continuation, stream, codec, or runtime handle indefinitely can therefore delay safe unload.