ABI Specification¶
Wire protocol, binary layout, and ABI contract between Fidius hosts and plugins.
Source: crates/fidius-core/src/descriptor.rs, crates/fidius-core/src/status.rs, crates/fidius-core/src/wire.rs, crates/fidius-core/src/hash.rs, crates/fidius-core/tests/layout_and_roundtrip.rs
Magic Bytes¶
The first 8 bytes of every PluginRegistry. Used by the host to verify the registry pointer is valid before reading further fields.
Version Constants¶
| Constant | Value | Description |
|---|---|---|
REGISTRY_VERSION |
1 |
Layout version of the PluginRegistry struct. |
ABI_VERSION |
200 (at fidius-core 0.2.0) |
Layout version of the PluginDescriptor struct. |
Both are u32. The host rejects registries or descriptors with mismatched versions.
ABI_VERSION is derived from the fidius-core crate version per [ADR-0002]. Pre-1.0 releases use MAJOR * 10000 + MINOR * 100 (so every minor is a breaking change); post-1.0 releases use MAJOR * 10000 only (minor releases must be ABI-additive).
[ADR-0002]: https://github.com/colliery-io/fidius — see .metis/adrs/FIDIUS-A-0002.md in the source tree.
PluginRegistry Layout¶
Note: All sizes and offsets in this section assume a 64-bit platform (pointer size = 8 bytes). On 32-bit platforms, pointer fields are 4 bytes and total struct sizes differ.
#[repr(C)], 24 bytes, 8-byte aligned (on 64-bit platforms).
| Offset | Size | Field | Type | Description |
|---|---|---|---|---|
| 0 | 8 | magic |
[u8; 8] |
Must equal b"FIDIUS\0\0". |
| 8 | 4 | registry_version |
u32 |
Must equal REGISTRY_VERSION (currently 1). |
| 12 | 4 | plugin_count |
u32 |
Number of descriptor pointers in the array. |
| 16 | 8 | descriptors |
*const *const PluginDescriptor |
Pointer to array of plugin_count descriptor pointers. |
Each PluginRegistry is constructed once per dylib by build_registry() and cached in a OnceLock. The descriptors field points to a leaked Vec of pointers collected via the inventory crate.
PluginDescriptor Layout¶
#[repr(C)], 104 bytes, 8-byte aligned (on 64-bit platforms).
| Offset | Size | Field | Type | Description |
|---|---|---|---|---|
| 0 | 4 | descriptor_size |
u32 |
Size in bytes of this descriptor struct at plugin build time. Host reads this FIRST to detect fields added in later minor versions. See [ADR-0002]. |
| 4 | 4 | abi_version |
u32 |
Must equal ABI_VERSION (e.g., 200 for fidius-core 0.2.0). |
| 8 | 8 | interface_name |
*const c_char |
Null-terminated UTF-8 C string. Interface trait name. |
| 16 | 8 | interface_hash |
u64 |
FNV-1a hash of required method signatures. |
| 24 | 4 | interface_version |
u32 |
User-specified version from #[plugin_interface(version = N)]. |
| 28 | 4 | (padding) | Alignment padding before u64. |
|
| 32 | 8 | capabilities |
u64 |
Bitfield: bit N set means optional method N is implemented. |
| 40 | 1 | buffer_strategy |
u8 |
1 = PluginAllocated, 2 = Arena. (0 was CallerAllocated, reserved since 0.1.0.) |
| 41 | 7 | (padding) | Alignment padding before pointer. | |
| 48 | 8 | plugin_name |
*const c_char |
Null-terminated UTF-8 C string. Plugin implementation name. |
| 56 | 8 | vtable |
*const c_void |
Opaque pointer to the interface-specific #[repr(C)] vtable. |
| 64 | 8 | free_buffer |
Option<unsafe extern "C" fn(*mut u8, usize)> |
Buffer deallocation function. Must be Some for PluginAllocated. |
| 72 | 4 | method_count |
u32 |
Total number of methods in the vtable. |
| 76 | 4 | (padding) | Alignment padding before pointer. | |
| 80 | 8 | method_metadata |
*const MethodMetaEntry |
Array of method_count per-method metadata entries, or null if no methods declared #[method_meta]. See Method Metadata. |
| 88 | 8 | trait_metadata |
*const MetaKv |
Trait-level metadata array (#[trait_meta]), or null. |
| 96 | 4 | trait_metadata_count |
u32 |
Number of entries in trait_metadata. Zero when null. |
| 100 | 4 | (padding) | Alignment padding to 104-byte total. |
Additive layout evolution (post-1.0)¶
Fields added in later minor releases appear at offsets >= the current descriptor_size. Plugins built against an older minor release report a smaller descriptor_size, and host reads for newer fields must check descriptor_size before trusting the offset. See [ADR-0002] for the full discipline.
PluginDescriptor Helper Methods¶
PluginDescriptor provides the following convenience methods:
| Method | Return type | Description |
|---|---|---|
interface_name_str() |
&str |
Reads interface_name as a CStr and converts to &str. |
plugin_name_str() |
&str |
Reads plugin_name as a CStr and converts to &str. |
buffer_strategy_kind() |
Result<BufferStrategyKind, u8> |
Converts the buffer_strategy u8 to the enum. Returns Err(raw) for unknown discriminants (e.g. 0, reserved since 0.1.0). |
has_capability(bit: u32) |
bool |
Returns true if the given capability bit is set. |
BufferStrategyKind¶
#[repr(u8)], 1 byte.
| Discriminant | Variant | Description |
|---|---|---|
0 |
(reserved) | Was CallerAllocated; removed in 0.1.0. Any plugin reporting 0 is rejected with UnknownBufferStrategy. |
1 |
PluginAllocated |
Plugin allocates output. Host frees via free_buffer. |
2 |
Arena |
Host provides pre-allocated arena buffer. Plugin writes into it. Retry-on-too-small via STATUS_BUFFER_TOO_SMALL. Data valid until next call on the same thread. |
Both PluginAllocated and Arena are fully supported by the macro and host. See Buffer Strategies for usage guidance.
Layout sizes and offsets are regression-tested in crates/fidius-core/tests/layout_and_roundtrip.rs to catch accidental ABI drift.
Wire Format¶
All data crossing the FFI boundary is serialized as bincode via serde.
Wire format is a non-choice at runtime — there is no WireFormat enum and no
wire_format field in the descriptor. Build profile (debug vs release) does
not affect the bytes.
See Wire Format for the full rationale (including why the earlier debug-mode JSON path was removed in 0.1.0).
Serialization¶
Deserialization¶
Both functions are thin wrappers around bincode::serialize / bincode::deserialize.
VTable Function Pointer Signatures¶
The vtable layout differs by buffer strategy. PluginAllocated and Arena
use distinct C signatures; a plugin reports which one it uses via the
descriptor's buffer_strategy field. Required methods use the function
pointer type directly; optional methods use Option<unsafe extern "C" fn(...)>.
PluginAllocated¶
int32_t method(
const uint8_t* in_ptr, // serialized input (tuple-encoded arguments)
uint32_t in_len, // input byte count
uint8_t** out_ptr, // [out] pointer to plugin-allocated output
uint32_t* out_len // [out] output byte count
) -> int32_t; // status code
Rust type:
The plugin allocates the output buffer (typically via Box::into_raw) and
the host calls the descriptor's free_buffer to release it.
Arena¶
int32_t method(
const uint8_t* in_ptr, // serialized input (tuple-encoded arguments)
uint32_t in_len, // input byte count
uint8_t* arena_ptr, // host-provided arena base
uint32_t arena_cap, // capacity of the arena buffer
uint32_t* out_offset, // [out] offset into arena where output starts
uint32_t* out_len // [out] output byte count (or required size on TOO_SMALL)
) -> int32_t; // status code
Rust type:
The host provides a thread-local arena buffer of capacity arena_cap. The
plugin writes its output at some *out_offset within the arena and sets
*out_len to the byte count. If the arena is too small, the plugin returns
STATUS_BUFFER_TOO_SMALL, writes the required size to *out_len, and the
host grows the arena and retries exactly once. Arena plugins do not use
free_buffer — the descriptor's field is null, and the arena is reused for
subsequent calls on the same thread.
Argument Encoding¶
All method arguments are tuple-encoded at the FFI boundary. The input buffer contains the serialized form of a tuple containing all arguments:
| Arg count | Trait signature | Serialized input type | Example |
|---|---|---|---|
| 0 | fn status(&self) -> String |
() |
serialize(&()) |
| 1 | fn process(&self, input: String) -> String |
(String,) |
serialize(&("hello".to_string(),)) |
| 2 | fn add(&self, a: i64, b: i64) -> i64 |
(i64, i64) |
serialize(&(3i64, 7i64)) |
| N | fn foo(&self, a: A, b: B, c: C) -> R |
(A, B, C) |
serialize(&(a, b, c)) |
This encoding is uniform — there are no special cases. The #[plugin_impl]
macro generates the deserialization code automatically. Host-side callers using
call_method must pass the tuple-encoded input:
// Zero args
let result: String = handle.call_method(0, &()).unwrap();
// One arg
let result: String = handle.call_method(1, &("hello".to_string(),)).unwrap();
// Two args
let result: i64 = handle.call_method(2, &(3i64, 7i64)).unwrap();
Breaking change (v0.0.5): Prior to 0.0.5, single-argument methods used bare value encoding (not tuple-wrapped). All methods now use tuple encoding uniformly.
Free Buffer (PluginAllocated only)¶
Rust type:
Called by the host after reading the output buffer. Reconstructs a
Box<[u8]> from (ptr, len) and drops it. For Arena plugins the
descriptor's free_buffer field is null; the host reuses the arena buffer
directly and does not call any deallocation hook.
Status Codes¶
All i32. Returned by vtable function pointers.
| Code | Constant | Meaning |
|---|---|---|
0 |
STATUS_OK |
Success. Output buffer contains the serialized result. |
-1 |
STATUS_BUFFER_TOO_SMALL |
Output buffer too small (Arena only). out_len contains the required size; host grows arena and retries. |
-2 |
STATUS_SERIALIZATION_ERROR |
Serialization or deserialization failed at the FFI boundary. |
-3 |
STATUS_PLUGIN_ERROR |
Plugin returned an error. Output buffer contains a serialized PluginError. |
-4 |
STATUS_PANIC |
Panic caught via catch_unwind at the extern "C" boundary. |
Load Sequence¶
The host loads a plugin dylib through the following sequence:
- Architecture check -- Read binary header to verify format (ELF, Mach-O, PE) and architecture (x86_64, aarch64) match the host platform.
- dlopen -- Open the shared library via
libloading::Library::new(). - dlsym -- Look up the symbol
fidius_get_registry(signature:extern "C" fn() -> *const PluginRegistry). - Call registry function -- Invoke
fidius_get_registry()to obtain the registry pointer. - Validate magic -- Compare
registry.magicwithFIDIUS_MAGIC. Reject on mismatch. - Validate registry version -- Compare
registry.registry_versionwithREGISTRY_VERSION. Reject on mismatch. - Iterate descriptors -- For each of
registry.plugin_countdescriptors: - Validate ABI version -- Compare
descriptor.abi_versionwithABI_VERSION. Reject on mismatch. - Copy strings -- Read
interface_nameandplugin_nameasCStr, convert to ownedString. - Copy metadata -- Build
PluginInfofrom descriptor fields. - Interface validation (optional) -- If the host has expected values for
interface_hashorbuffer_strategy, compare each against the plugin's values. - Signature verification (optional) -- If
require_signatureis set, verify the.sigfile against trusted Ed25519 public keys.
Interface Hashing Algorithm¶
FNV-1a 64-bit, used to detect ABI drift at load time.
Constants¶
| Name | Value |
|---|---|
| FNV offset basis | 0xcbf29ce484222325 |
| FNV prime | 0x100000001b3 |
Both fnv1a and interface_hash are public API, defined in fidius_core::hash and re-exported from the fidius:: facade crate.
fnv1a(bytes: &[u8]) -> u64¶
hash = FNV_OFFSET_BASIS
for each byte in bytes:
hash = hash XOR byte
hash = hash * FNV_PRIME (wrapping multiplication)
return hash
This function is const fn and can be evaluated at compile time.
interface_hash(signatures: &[&str]) -> u64¶
- Copy the input slice and sort lexicographically (ensures order-independence).
- Join sorted signatures with
"\n"as separator. - Return
fnv1a(combined.as_bytes()).
Signature String Format¶
Each required method produces a canonical signature string:
- The
selfparameter is excluded. - Types are the
TokenStream::to_string()representation of thesyn::Type. - Optional methods are excluded from the interface hash.
Properties¶
- Deterministic: Same trait definition always produces the same hash.
- Order-independent: Method declaration order does not affect the hash (signatures are sorted).
- Case-sensitive:
Stringandstringproduce different hashes. - Optional-method-stable: Adding optional methods does not change the hash.
Registry Export Symbol¶
Each plugin cdylib exports a single entry point:
Generated by fidius_core::fidius_plugin_registry!(). The registry is built on first call from descriptors collected via inventory and cached in a OnceLock.
See Also¶
- #[plugin_interface] Reference -- macro that generates vtable and constants
- #[plugin_impl] Reference -- macro that generates shims and descriptors
- Host API Reference -- host-side loading API
- Errors Reference -- error types for load and call failures