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:
pub const TRAIT_ID: &str = "dev.example.index";
pub const ABI_VERSION: u32 = 1;
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_manifestintegration for dynamic modules,- typed error payload encoding,
- composable optional payload encoding through
Option<T>whereT: XabiType, - stable wire layouts for
#[xabi::data]values.
Generated ABI artifacts use an explicit XabiV1 prefix, for example:
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]:
;
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:
let module = unsafe ;
let plugin = xabi_load?;
let name = plugin.name?;
let bytes = plugin.train.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:
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.
Use #[xabi::opaque] for non-null pointer handles owned by another standard or
domain:
Trait object returns are represented as impl Trait:
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.
ABI Stability Model
Extensible ABI descriptors and generated wire structs start with:
size: usize,
abi_version: u32,
Hosts validate the required prefix and generated handles do not read 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 fields are appended to the tail. Breaking changes require a new ABI version.
Small primitive carriers such as XabiStr, XabiSlice, XabiBytes,
XabiOwnedBytes, and XabiResult have fixed layouts. Extending their 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:
When an ABI change is intentional, update snapshots with:
XABI_UPDATE=1
Review snapshot changes with the append-only layout rule in mind.
Provider crates that declare xabi contracts can assert their generated contract
layout in tests with xabi-assert:
[]
= "0.1.0"
The assertion reads snapshots from
xabi/snapshots/<contract-id>/<target>.txt by default. Create or
intentionally update the snapshot with:
XABI_UPDATE=1
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 OpenDALAccess-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:
Run all workspace tests:
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 and
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 |
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 |
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.