π Nylon Ring
Fast, explicit hostβplugin ABI for Rust
Build native plugins with a small C-compatible boundary, async routing, and safe lifecycle controls.
Features β’ Quick Start β’ Usage β’ Performance β’ System Overview
π Features
π Explicit ABI boundary
- C-compatible wire types and vtables with stable discriminants.
- ABI version and plugin-structure-size validation during loading.
- Panic containment at generated Rust plugin entry points.
- A documented ABI evolution policy.
- A buildable reference C plugin.
β‘ Flexible call patterns
- Fire-and-forget calls.
- Async unary request/response calls.
- A synchronous thread-local fast path.
- Bounded bidirectional streams with explicit backpressure.
- Optional response timeouts with automatic pending-request cleanup.
π§ Production lifecycle controls
- Multiple named plugins in one host.
- Unload and reload gates that reject new calls while draining active calls.
- Grace-period variants for controlled shutdown and hot reload.
- Metrics for loaded plugins, pending requests, state sessions, and in-flight calls.
- Lazily allocated request-routing shards to keep an idle host small.
π§ͺ Release-focused validation
- Workspace tests for lifecycle, callback routing, TLS re-entry, cancellation, and C ABI loading.
- Loom models for concurrency-sensitive state transitions.
- Miri coverage for the core ABI types.
- MSRV, formatting, Clippy, docs, packaging, and C compilation in CI.
π¦ Project structure
nylon-ring/
βββ crates/
β βββ nylon-ring/ # Core ABI types, vtables, and macros
β β βββ src/
β β βββ benches/
β βββ nylon-ring-host/ # Dynamic loading and request routing
β βββ src/
β βββ benches/
βββ docs/
β βββ ABI_EVOLUTION.md # Compatibility and versioning policy
βββ examples/
βββ ex-nyring-plugin/ # Rust cdylib plugin
βββ ex-nyring-host/ # Rust host and benchmark driver
βββ c-plugin/ # C header and reference plugin
The workspace publishes two crates:
nylon-ringcontains only the ABI types, vtables, anddefine_plugin!macro.nylon-ring-hostloads dynamic libraries and provides the higher-level host API.
Version 0.1.0 implements ABI version 1. The minimum supported Rust version is
Rust 1.88.
π Quick start
Install
Plugin crates need the ABI crate:
[]
= ["cdylib"]
[]
= "0.1.0"
Host applications need the host crate:
[]
= "0.1.0"
Build and run the workspace example
The host example builds and loads ex-nyring-plugin, then exercises the
supported call patterns and optional benchmarks.
Run benchmarks
Benchmark results depend on the machine, target, allocator, plugin behavior, and payload. The repository intentionally reports measurements from the machine running Criterion instead of presenting fixed throughput claims.
π Performance
The following snapshot was measured with Criterion in a release build on an Apple M1 Pro (10 cores). Values are the center estimates from one local run; they are a reference point, not a cross-platform guarantee.
ABI types
| Operation | Time | Notes |
|---|---|---|
NrStr::new |
0.77 ns | Create a borrowed string view |
NrStr::as_str |
6.79 ns | Validate and read UTF-8 |
NrBytes::from_slice |
0.48 ns | Create a borrowed byte view |
NrBytes::as_slice |
0.32 ns | Read a validated byte view |
NrKV::new |
2.67 ns | Create a key-value pair |
NrVec::from_vec |
21.96 ns | Wrap an owned vector with its drop callback |
NrVec::into_vec |
31.66 ns | Copy into the receiver allocator and release the source |
NrVec::as_slice |
0.32 ns | Read a vector view |
Host round trips
| Operation | Time | Throughput | Notes |
|---|---|---|---|
| Fire-and-forget | 50.98 ns | 19.62M calls/s | No response registration |
| Synchronous fast path | 76.63 ns | 13.05M calls/s | Thread-local response slot |
| Standard unary | 162.06 ns | 6.17M calls/s | Async request/response routing |
| Unary + 128-byte payload | 196.40 ns | 5.09M calls/s | Payload round trip |
| Unary + 1 KiB payload | 242.63 ns | 4.12M calls/s | Payload round trip |
| Unary + 4 KiB payload | 321.19 ns | 3.11M calls/s | Payload round trip |
The allocator-safe NrVec::into_vec path deliberately copies into the receiving
module's allocator before invoking the producer's drop callback. This trades the
old cross-allocator zero-copy claim for defined ownership behavior at the ABI
boundary.
To reproduce the snapshot, run the benchmark commands above and open the HTML
reports under target/criterion/.
π» Usage
Host: plugin management
use NylonRingHost;
use Duration;
# async
Use the dynamic-library suffix for the target platform:
- Linux:
.so - macOS:
.dylib - Windows:
.dll
Host: fire-and-forget
let status = plugin.call.await?;
Host: unary response
use NrStatus;
let = plugin.call_response.await?;
assert_eq!;
assert_eq!;
For a bounded wait, use call_response_timeout:
use Duration;
let result = plugin
.call_response_timeout
.await?;
Host: synchronous fast path
let = plugin.call_response_fast.await?;
The fast path is for handlers that invoke send_result synchronously on the
same thread. Re-entry while a fast slot is occupied is rejected instead of
aliasing the response slot.
Host: streaming
use NrStatus;
let = plugin.call_stream.await?;
plugin.send_stream_data?;
while let Some = stream.recv.await
plugin.close_stream?;
Streams use a bounded queue. When it is full, the host callback returns
NrStatus::Backpressure so the plugin can retry, coalesce, or drop work.
Dropping a StreamReceiver unregisters its pending request.
Plugin: implement handlers
use ;
use c_void;
use ;
static HOST_CTX: = new;
static HOST_VTABLE: = new;
unsafe
unsafe
define_plugin!
The define_plugin! macro:
- Exports
nylon_ring_get_plugin_v1. - Publishes package name and version metadata.
- Routes calls by UTF-8 entry name.
- Converts unwinding panics into
NrStatus::Panicbefore they cross the FFI boundary.
π System overview
+----------------------------------------------------------------+
| Host (nylon-ring-host) |
| |
| NylonRingHost |
| βββ LoadedPlugin A ββ call gate ββ dynamic library guard |
| βββ LoadedPlugin B ββ call gate ββ dynamic library guard |
| βββ Shared HostContext |
| βββ thread-local synchronous response slot |
| βββ lazy sharded pending-request maps |
| βββ bounded stream queues |
| βββ session state |
+----------------------------+-----------------------------------+
| C ABI v1
| NrPluginVTable / NrHostVTable
+----------------------------+-----------------------------------+
| Plugin |
| |
| define_plugin! entry point |
| βββ init / shutdown |
| βββ named handlers |
| βββ send_result callback βββββββββββββββββββββββββββββββββ |
+------------------------------------------------------------|---+
|
callback router <βββββββββββββββββββββββββββββ
- The host loads
nylon_ring_get_plugin_v1and validates the ABI version andNrPluginInfosize before callinginit. - A
PluginHandlecreates a session ID and registers any expected response. - The plugin handles the named entry and returns data through
NrHostVTable::send_result. - The callback router completes one unary request or advances one stream state atomically for that session ID.
- Call guards keep the dynamic library alive until active handles and streams finish, even after the host starts unloading it.
1. Host layer (nylon-ring-host)
NylonRingHost owns the plugin registry and shared callback context. Each
loaded library has an acceptance gate and in-flight counter. Normal calls stop
being accepted as soon as unload or reload starts, while existing call guards
keep the old library mapped until their work finishes.
The callback router uses two paths:
- A thread-local slot for synchronous
call_response_fastresponses. - Lazily initialized sharded maps for async unary and streaming responses.
Stream transitions happen while holding the relevant map entry, preventing the old lookup/remove/reinsert race. Queues are bounded and report backpressure to the plugin instead of growing without limit.
2. ABI layer (nylon-ring)
The ABI crate defines the wire types shared by both sides. Borrowed types expose
validated views, while owned vectors carry their producer's release callback.
NrStatus is a transparent integer wrapper with fixed ABI v1 values and a
reserved range for future additions.
3. Plugin layer
A plugin implements named handlers and uses define_plugin! to export the ABI
v1 entry point. Handlers can return immediately, send one response, or keep a
session ID and emit bounded stream frames later. The plugin remains responsible
for stopping its own workers during shutdown.
Core types
ABI crate (nylon-ring)
| Type | Purpose |
|---|---|
NrStatus |
Stable numeric status value with reserved future values |
NrStr |
Borrowed UTF-8 string view |
NrBytes |
Borrowed byte-slice view |
NrVec<T> |
Owned or borrowed vector representation with producer drop callback |
NrKV / NrMap |
ABI-compatible key-value data |
NrAny |
Typed erased payload with clone and drop callbacks |
NrHostVTable |
Callbacks exposed by the host |
NrPluginVTable |
Entry points exposed by a plugin |
Host crate (nylon-ring-host)
| Type | Purpose |
|---|---|
NylonRingHost |
Plugin container, loader, lifecycle manager, and metrics source |
PluginHandle |
Cloneable call interface that keeps its plugin loaded |
StreamFrame |
Status and bytes returned for one stream frame |
StreamReceiver |
Bounded response receiver with cancellation cleanup |
HostMetrics |
Snapshot of host lifecycle and routing counts |
π― Use cases
β Good fit
- Trusted native plugin systems.
- Hot-reloadable service or application logic.
- High-throughput request routing within one process.
- Rust hosts with Rust or C-compatible plugins.
- Systems that need unary, synchronous, and streaming call patterns together.
β οΈ Consider alternatives
- Untrusted third-party code: use process or sandbox isolation.
- Cross-machine calls: use an RPC protocol.
- A boundary that must be portable across arbitrary targets and data models: use a serialized wire format.
- Plugins that cannot obey the ownership, threading, and shutdown contracts.
π¬ Design principles
| Principle | Implementation |
|---|---|
| Explicit compatibility | Versioned entry point, fixed status values, and size checks |
| Allocator ownership | Owned vectors carry the producer's drop callback |
| Bounded resource use | Bounded stream queues and configurable response timeouts |
| Race-resistant routing | Entry-locked pending transitions and terminal delivery once |
| Safe lifecycle | Call gates, library guards, and graceful drain operations |
| Observable operation | Host metrics and distinct lifecycle/timeout errors |
| Regression protection | Host tests, Loom models, Miri, C validation, and package checks |
π‘ Safety and compatibility
Nylon Ring makes native-plugin invariants explicit, but it cannot make an arbitrary dynamic library safe. Plugin authors must uphold these rules:
- Host and plugin must target the same operating system, architecture, data model, and ABI version.
NrStrandNrBytesare borrowed views. Copy their contents before retaining them after a callback returns. Their accessors are unsafe because a C ABI cannot express their Rust lifetimes.NrVec<T>carries explicit ownership and a producer-side drop callback. The receiver copies elements into its allocator before invoking that callback. Borrowed foreign buffers useowned = 0; owned buffers provide the matchingdrop_fn.Tmust have a compatible C layout.- Plugin shutdown must stop worker threads and callbacks before the library can be released.
- Panic containment applies to unwinding panics.
panic = "abort"still aborts the process. #[repr(C)]stabilizes field layout; it does not make arbitrary Rust types or standard-library internals portable across compiler versions.
Loading third-party native code is not a security boundary. Only load trusted plugins.
π§° Development
Run the same checks as CI:
RUSTDOCFLAGS="-D warnings"
The core unsafe code can also be checked with Miri:
π€ Publishing
The crates are published in dependency order:
# Wait until the matching version is available from the crates.io index.
Publishing is automated by .github/workflows/publish.yml. Publishing a GitHub
Release whose tag matches the workspace versionβfor example, v0.1.0βdeploys
both crates. The workflow can also be started manually and reads the crates.io
token from the organization-level RUST_TOKEN Actions secret.
License
Licensed under the MIT License.
Acknowledgments
Built with Tokio, DashMap, libloading, and Criterion.rs.