# xabi
`xabi` generates a stable native ABI from Rust traits.
The intended use case is a host application that wants to load third-party Rust
implementations from a dynamic library without asking users to hand-write C ABI
vtables, exported symbols, panic guards, async polling glue, or host-side
handles.
`xabi` is not a plugin framework. A plugin registry, package format, discovery
protocol, permission model, or product-specific lifecycle belongs in the host
project. `xabi` only owns the contract boundary.
## What It Generates
Given a Rust trait:
```rust
pub const TRAIT_ID: &str = "dev.example.index";
pub const ABI_VERSION: u32 = 1;
#[xabi::data]
#[derive(Clone, Copy)]
pub struct TrainInput {
pub rows_seen: u64,
}
#[xabi::xabi(id = TRAIT_ID, version = ABI_VERSION)]
pub trait IndexPlugin {
fn name(&self) -> String;
fn version(&self) -> u32;
async fn train(&self, input: TrainInput) -> xabi::Result<Vec<u8>>;
fn details_as_json(&self, details: &[u8]) -> xabi::Result<Option<String>>;
}
```
`xabi` generates:
- a versioned C-compatible vtable,
- export thunks for sync and async methods,
- panic guards that convert unwinds to ABI status codes,
- host-side owned and borrowed handles,
- `xabi_manifest` integration for dynamic modules,
- typed error payload encoding,
- composable optional payload encoding through `Option<T>` where `T: XabiType`,
- exact-version wire layouts for `#[xabi::data]` values.
Generated ABI artifacts use an explicit `XabiV1` prefix, for example:
```rust
XabiV1AbiTraitIndexPlugin
XabiV1VtableTraitIndexPlugin
XabiV1HandleTraitIndexPlugin
XabiV1BorrowedTraitIndexPlugin
XabiV1OwnedTraitIndexPlugin
```
These names are ABI artifacts. Domain crates should usually re-export only the
handles or helper APIs they want users to see.
## Export A Module
An implementation crate exports one or more implementations with
`#[xabi::module]`:
```rust
#[derive(Default)]
pub struct DemoPlugin;
#[xabi::module]
mod exports {
use super::*;
#[xabi::xabi(name = "demo", version = 1)]
impl IndexPlugin for DemoPlugin {
fn name(&self) -> String {
"demo".to_string()
}
fn version(&self) -> u32 {
1
}
async fn train(&self, input: TrainInput) -> xabi::Result<Vec<u8>> {
Ok(input.rows_seen.to_le_bytes().to_vec())
}
fn details_as_json(&self, details: &[u8]) -> xabi::Result<Option<String>> {
let value = std::str::from_utf8(details)
.map_err(|err| xabi::Error::Export(err.to_string()))?;
Ok(Some(format!(r#"{{"details":"{value}"}}"#)))
}
}
}
```
The exported crate is built as a `cdylib`. The module macro emits the manifest
symbol that hosts load. The implementation `version` is stored as the export
version; the trait ABI version is stored separately and checked before the
generated host calls the export constructor.
## Load From A Host
The host loads a trusted dynamic library and asks the generated handle to find a
matching export:
```rust
let module = unsafe { xabi::load(path)? };
let plugin = XabiV1HandleTraitIndexPlugin::xabi_load(&module)?;
let name = plugin.name()?;
let bytes = plugin.train(TrainInput::new(42)).await?;
```
Loading native code is unsafe. The host must trust the library and must define
the higher-level registration policy. Once a module is loaded, generated handle
loaders validate the xabi manifest, trait id, contract version, and generated
ABI prefixes before returning safe Rust handles.
## Data, Handles, And Returns
Use `#[xabi::data]` for values that cross the ABI by value or as typed error
payloads:
```rust
#[xabi::data]
pub struct BuildError {
pub message: String,
}
```
Each field is lowered through its own `XabiType::Wire`, so nested xabi data,
strings, owned bytes, callback refs, and opaque handles follow one recursive
rule.
Generated data layouts are exact-version contracts. A consumer rejects both
shorter and larger data wires and owned payloads. Appending even a scalar field
therefore requires incrementing the version of every `#[xabi::xabi]` trait that
uses the data type and updating that contract's layout snapshots. Older and
newer contract versions must not exchange the data value: an older consumer
cannot safely destroy an unknown tail field that owns a string, byte buffer, or
handle.
Use `XabiOwnedBytesOwner` when a contract needs to retain and read one
producer-owned contiguous byte buffer without copying it at the boundary:
```rust
#[xabi::xabi(id = "xabi.example.Reader", version = 1)]
pub trait Reader {
async fn read(&self) -> xabi::Result<xabi::XabiOwnedBytesOwner>;
}
let bytes = reader.read().await?;
consume(bytes.as_slice());
let copied: Vec<u8> = bytes.into_vec();
```
The generated path lowers this value to the raw `XabiOwnedBytes` wire
descriptor, validates and adopts it, and calls the producer's free callback
exactly once when the non-`Copy` owner is dropped. `Vec<u8>` remains available
when an explicitly Rust-owned copy is preferable. A generated module handle
also keeps the producer library loaded until the owner is dropped. Re-encoding
such a retained foreign owner across another ABI boundary makes a defensive
copy because the fixed raw descriptor has no field for module-lifetime context.
Segmented buffers and stream protocols remain domain contracts outside xabi.
`u128` and `i128` are supported with their native Rust representations. Host
and module must target the same platform and use ABI-compatible Rust toolchains;
xabi carries 128-bit integer arguments behind pointers and returns them through
owned payloads.
Use `#[xabi::opaque]` for non-null pointer handles owned by another standard or
domain:
```rust
#[xabi::opaque]
#[derive(Clone, Copy)]
pub struct ArrowStreamHandle {
stream: *mut ArrowArrayStream,
}
```
Trait object returns are represented as `impl Trait`:
```rust
#[xabi::xabi(id = FACTORY_ID, version = 1)]
pub trait Factory {
async fn open(&self, name: &str) -> xabi::Result<impl IndexPlugin + 'static>;
}
```
The exporter turns the concrete Rust value into the returned trait's vtable.
The host decodes it into the generated handle while preserving the dynamic
module lifetime.
Borrowed callback traits use the same mechanism. A host can export a local
callback as `XabiV1OwnedTrait*`, pass `xabi_borrow()` to the plugin, and the
plugin calls the generated borrowed handle. The borrowed type carries the
owner lifetime as `XabiV1BorrowedTrait*<'a>`, so a synchronous result or async
future cannot retain it after the owner is dropped. Implementations should use
`XabiV1BorrowedTrait*<'_>` in method signatures.
Borrowed callbacks can also be grouped with other call inputs without erasing
that lifetime:
```rust
#[xabi::data]
pub struct CallbackInput<'a> {
pub callback: XabiV1BorrowedTraitCallback<'a>,
}
```
`#[xabi::data]` accepts lifetime parameters for this borrowed-input shape. Its
wire struct remains lifetime-free and pointer-based; decoding the raw wire is
unsafe, while the generated safe call path keeps the owner borrowed through
completion or cancellation.
Generated owned trait handles can also cross a method boundary by ownership.
This is the contract shape used by layers and decorators that must retain an
inner service after the call returns:
```rust
#[xabi::xabi(id = LAYER_ID, version = 1)]
pub trait Layer {
fn apply(
&self,
inner: XabiV1OwnedTraitIndexPlugin,
) -> xabi::Result<impl IndexPlugin + 'static>;
}
```
The safe generated method consumes `inner`. `XabiV1OwnedRefTrait*` is only the
single-use wire representation: generated caller glue guards it until the
export thunk claims it, and generated export glue immediately places a claimed
vtable under the `XabiV1OwnedTrait*` RAII owner. Contract authors do not copy or
adopt the raw owned-ref token themselves.
## ABI Stability Model
Extensible ABI descriptors and generated data wire structs start with:
```rust
size: usize,
abi_version: u32,
```
Hosts validate the required prefix of extensible descriptors, and generated
handles do not read descriptor fields beyond the reported size. Vtable methods
live after the stable release prefix, so a shorter vtable reports an ABI
mismatch instead of reading unavailable tail fields. Additive descriptor fields
are appended to the tail. Breaking descriptor changes require a new ABI version.
`#[xabi::data]` wires use the same leading metadata for validation but are not
prefix-extensible. Direct wire decoding and owned payload decoding both require
the exact generated size. Any field change requires a new contract version for
every trait that references the data type; the version mismatch is rejected
before a method can transfer arguments or results with incompatible ownership.
Small primitive wire carriers such as `XabiStr`, `XabiSlice`, `XabiBytes`, the
raw `XabiOwnedBytes` descriptor, and `XabiResult` have fixed layouts. The safe
`XabiOwnedBytesOwner` is a Rust RAII wrapper whose wire representation is
`XabiOwnedBytes`; it has no separate ABI layout. Extending the carrier field
sets is a breaking runtime ABI change.
The contract identity is:
- a stable trait `id`,
- a contract ABI version carried in `XabiExport::contract_version`,
- the generated vtable and wire layouts.
The Rust trait name is not the runtime identity. It is used to generate Rust
API artifacts.
The repository checks fixture layouts through `xabi-assert` tests:
```sh
cargo test --workspace
```
When an ABI change is intentional, update snapshots with:
```sh
XABI_UPDATE=1 cargo test --workspace
```
Review prefix-layout snapshots with the append-only rule and generated data
snapshots with the exact-layout rule.
Provider crates that declare xabi contracts can assert their generated contract
layout in tests with `xabi-assert`:
```toml
[dev-dependencies]
xabi-assert = "0.1.2"
```
```rust
#[cfg(test)]
mod tests {
#[test]
fn abi_is_stable() {
xabi_assert::assert_abi!(super::XabiV1AbiTraitAsyncPlugin);
}
}
```
The assertion reads snapshots from
`xabi/snapshots/<contract-id>/<target>.txt` by default. Create or
intentionally update the snapshot with:
```sh
XABI_UPDATE=1 cargo test
```
## Examples
- `examples/async-plugin`: minimal async trait export and dynamic loading.
- `examples/scalar-index`: a richer fixture with nested data, callbacks, an
opaque Arrow stream handle, an object return, provider-side registration, and
a Python package wrapper that registers the native library back into the
provider.
- `examples/access-like`: an OpenDAL `Access`-shaped fixture with all accessor
operations, returned reader/writer/lister/deleter/copier handles,
provider-side registration, and a Python package wrapper for the native
plugin.
Run the main end-to-end fixture:
```sh
cargo test -p scalar-index-plugin
cargo test -p access-like-plugin
```
Run all workspace tests:
```sh
cargo test --workspace
```
## How xabi Differs From Existing Choices
`xabi` sits in a narrow space: native, in-process, Rust-authored contracts with
explicit ABI stability.
| Existing choice | Difference |
| --- | --- |
| Hand-written C ABI with `libloading` | `xabi` still uses native dynamic loading, but users write Rust traits and data structs. Vtables, manifests, error payloads, async polling, panic guards, and host handles are generated. |
| Rust stable-ABI libraries | `xabi` does not try to make Rust's general ABI stable. It generates a small C-compatible ABI per contract and keeps the public model close to Rust trait definitions. |
| Wasm Component Model | Wasm gives portability, sandboxing, and a language-neutral component boundary. `xabi` chooses trusted native dynamic libraries and direct FFI for hosts that need the Rust implementation to run in-process. |
| Protobuf, gRPC, Arrow Flight, or IPC | Those are serialization and transport boundaries. `xabi` is an in-process ABI boundary; it avoids a service process and leaves persistence or network transport to the host. |
| PyO3, N-API, JNI, or other language bindings | Those expose Rust to a specific language runtime. `xabi` defines the native host/plugin contract; a PyO3 package can be used only as distribution glue for the native library. |
| A full plugin framework | `xabi` intentionally does not define discovery, registries, configuration, permissions, or lifecycle policy. The host project owns those product decisions. |
The tradeoff is intentional. `xabi` provides less runtime infrastructure than a
component system and less language reach than an RPC protocol, but it gives a
small, auditable ABI for Rust hosts that want dynamic native extension points.
### Compared With `abi_stable` And `stabby`
Community crates such as [`abi_stable`](https://docs.rs/abi_stable/) and
[`stabby`](https://docs.rs/stabby/) solve an adjacent problem: they provide
reusable ABI-stable Rust-like types and trait-object machinery. `xabi` takes a
different axis: contract-first generation from host-owned Rust traits.
| Project | Primary model | Where `xabi` differs |
| --- | --- | --- |
| [`abi_stable`](https://docs.rs/abi_stable/) | Interface, implementation, and user crates built around `StableAbi`, `#[sabi_trait]`, prefix types, runtime layout checks, and ffi-safe standard-library replacements. | `abi_stable` is a good fit when a project wants to adopt its module model and ABI-safe type ecosystem. `xabi` keeps the public contract as ordinary Rust traits and data structs, then generates the contract-specific vtable, manifest export, panic guards, typed error path, async polling glue, and host handles. |
| [`stabby`](https://docs.rs/stabby/) | ABI as a library: `IStable`, `#[stabby::stabby]`, `repr(stabby)`, stable dyn pointers, closures, futures, and compact ABI-stable representations for options, results, strings, vectors, and enums. | `stabby` is a good fit when preserving a rich Rust-like ABI type universe and compact layout rules is the main goal. `xabi` intentionally avoids making a general Rust data-layout scheme the user-facing API; it lowers only the selected `#[xabi::xabi]` and `#[xabi::data]` boundary surface and snapshots those generated layouts per contract. |
The practical distinction is ownership of the abstraction. With `abi_stable` or
`stabby`, the domain API is usually shaped by the chosen stable-ABI type system.
With `xabi`, the host crate owns the domain trait, and the generated ABI is an
implementation detail with explicit names and snapshot fixtures.
## Current Status
`xabi` is experimental. The repository is still iterating on generated API
shape, naming, and ABI fixtures. Treat the ABI snapshot checks as part of the
design process, not as a release promise.