# dyn-loader
Dynamic library loader with **two plugin-loading modes**:
1. **`dyn` mode** — Rust fat-pointer bridge: load Rust trait objects from
`.so`/`.dylib` plugin files, with Arc-like retain/release.
2. **`cdyn` mode** — COM-style C function tables (`VTablePlugin<T>`): plain
`#[repr(C)]` function-pointer structs, ABI-stable, usable from C/C++/Zig.
Core types:
- **`DynLib`**: wraps `libloading::Library` with `Arc` for shared ownership.
- **`AbiDynFatPtr`**: ABI-stable representation of a Rust fat pointer
(data ptr + vtable ptr), `#[repr(C)]`.
- **`AbiStableDynRef`**: fat pointer + retain/release function pointers,
enabling safe cross-boundary Arc-like reference counting.
- **`SafeArcDyn<T>`**: safe, cloneable handle over an `AbiStableDynRef`.
- **`DynPlugin<T>`**: loaded plugin holding a `SafeArcDyn<T>` that dereferences
to `&T` for calling trait methods on the loaded object.
- **`VTablePlugin<T>`**: loaded COM-style function table (cdyn mode).
## ⚠️ IMPORTANT — ABI compatibility / compiler version alignment
> **You must strictly align the Rust compiler version.** Every library and
> executable that exchanges dyn fat pointers across the boundary **must be
> built with the exact same Rust compiler version** to guarantee a stable
> ABI. Rust makes no ABI stability guarantees between compiler releases
> (vtable layout, metadata encoding, etc. may change). Mixing compiler
> versions between the host and plugins is **undefined behavior**.
>
> Pin one toolchain (e.g. via `rust-toolchain.toml`) and rebuild **all**
> crates, libs and executables together with that single version.
## Usage
### Mode 1: `dyn` — Rust fat-pointer bridge
**Plugin side (the `.so`/`.dylib`)**
```rust,ignore
use dyn_loader::{AbiStableDynRef, SafeArcDyn};
use std::sync::Arc;
#[no_mangle]
pub extern "C" fn core_ast_transform_entry() -> AbiStableDynRef {
SafeArcDyn::from_arc(Arc::new(MyTransform) as Arc<dyn Transform>).into_abi()
}
```
**Host side**
```rust,ignore
use dyn_loader::DynPlugin;
let plugin = DynPlugin::<dyn Transform>::load(
"libmy_transform.so",
b"core_ast_transform_entry\0",
)?;
let transform: &dyn Transform = plugin.trait_ref();
```
### Mode 2: `cdyn` — COM-style C function table (cross-language)
The plugin exports a function returning a `*const` to a plain
`#[repr(C)]` struct of function pointers. No trait objects involved —
callable from C, C++, Zig, anything with a C FFI.
**Plugin side (Rust)**
```rust,ignore
use dyn_loader::VTablePlugin;
#[repr(C)]
pub struct MyVtable {
pub add: unsafe extern "C" fn(i32, i32) -> i32,
pub name: unsafe extern "C" fn() -> *const std::os::raw::c_char,
}
unsafe extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
unsafe extern "C" fn name() -> *const std::os::raw::c_char {
c"my_plugin".as_ptr()
}
#[no_mangle]
pub static MY_PLUGIN_VTABLE: MyVtable = MyVtable { add, name };
#[no_mangle]
pub extern "C" fn my_plugin_get_vtable() -> *const MyVtable {
&MY_PLUGIN_VTABLE
}
```
**Host side (Rust)**
```rust,ignore
let plugin = unsafe {
VTablePlugin::<MyVtable>::load("libmy_plugin.so", b"my_plugin_get_vtable\0")?
};
let vtable = plugin.vtable();
let sum = unsafe { (vtable.add)(2, 3) }; // 5
```
The same vtable layout can be produced from any language with a C FFI
(C, C++, Zig, ...): just export a function returning `*const MyVtable`
to a `#[repr(C)]`-equivalent struct.
## Toolchain / ABI compatibility matrix
The two loading modes have different ABI stability guarantees:
| Rust fat-pointer bridge | `dyn_mod` | ❌ Unstable — depends on Rust vtable layout | **No** — host & plugin must use the *exact same* compiler version | ❌ No (Rust trait objects only) |
| COM-style C function table | `cdyn` (`VTablePlugin<T>`) | ✅ Stable — plain `#[repr(C)]` struct of function pointers | ✅ Yes, to a large extent (layout is fixed by `#[repr(C)]`) | ✅ Yes — any language with a C FFI |
### Verified compiler versions
Cross-version interoperability was verified with an actual host/plugin matrix
test (plugin compiled to a `.so` with toolchain A, loaded by a host binary
compiled with toolchain B). **All 16 combinations passed** (both modes) as of
2026-09:
| `nightly-2025-05-06` ↔ `stable 1.98.1` (cross) | ✅ Verified | ✅ Verified |
| `nightly-2025-05-06` ↔ `1.95.0` (cross) | ✅ Verified | ✅ Verified |
| `nightly-2025-05-06` ↔ `1.92.0` (cross) | ✅ Verified | ✅ Verified |
| `stable 1.98.1` ↔ `1.95.0` (cross) | ✅ Verified | ✅ Verified |
| `stable 1.98.1` ↔ `1.92.0` (cross) | ✅ Verified | ✅ Verified |
| `1.95.0` ↔ `1.92.0` (cross) | ✅ Verified | ✅ Verified |
| Same-version pairs (all 4 toolchains) | ✅ Verified | ✅ Verified |
| **Other / future versions** | ❌**Undefined behavior — do not rely on it** | ⚠️ Usually works, not guaranteed |
Notes:
- The cross-version `dyn_mod` results above are an **observation, not a
guarantee**: vtable layout happened to be identical across these four
toolchains (spanning nightly-2025-05-06 through stable-1.98.1). Rust
officially makes no ABI stability promise between compiler releases — a
future release may break it silently. Always re-run the matrix test when
adopting a new toolchain, and prefer exact version alignment in production.
- `cdyn` mode is layout-stable by construction (`#[repr(C)]` struct of
function pointers), but both sides must still compile the *same* `T`
definition (same field order, same pointer widths).
- The `AbiDynFatPtr` / `AbiStableDynRef` structs are `#[repr(C)]` and
layout-stable across versions; what is *not* guaranteed stable is the
**vtable contents** behind a `dyn Trait` pointer, which is why `dyn_mod`
requires version alignment.
## Safety
Loading dynamic libraries and unpacking raw fat pointers is inherently unsafe.
See the `# Safety` sections on each API. The retain/release function pointers
give you Arc-like reference counting across the library boundary, but the
caller is still responsible for ABI compatibility (see the warning above).
## License
MIT