dyn-loader 0.3.0

Dynamic library loader with dyn-fat-pointer-bridge for loading trait objects from .so/.dylib plugins. IMPORTANT: strictly align the Rust compiler version across all libs and executables to guarantee ABI compatibility.
Documentation

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)

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

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)

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)

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:

Mode Module ABI stability Cross-compiler-version safe? Cross-language (C++/Zig) safe?
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:

Toolchain pair (host ↔ plugin) dyn_mod (fat-pointer bridge) cdyn (C function table)
nightly-2025-05-06stable 1.98.1 (cross) ✅ Verified ✅ Verified
nightly-2025-05-061.95.0 (cross) ✅ Verified ✅ Verified
nightly-2025-05-061.92.0 (cross) ✅ Verified ✅ Verified
stable 1.98.11.95.0 (cross) ✅ Verified ✅ Verified
stable 1.98.11.92.0 (cross) ✅ Verified ✅ Verified
1.95.01.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