Skip to main content

dyn_loader/
dyn.rs

1//! # dyn — Rust fat-pointer bridge for trait objects
2//!
3//! Load trait objects from `.so`/`.dylib` plugins using Rust's native
4//! fat-pointer representation (data pointer + vtable pointer), wrapped with
5//! Arc-like retain/release for cross-boundary memory safety.
6//!
7//! - **AbiDynFatPtr**: ABI-stable representation of a Rust fat pointer,
8//!   `#[repr(C)]` for C ABI compatibility.
9//! - **AbiStableDynRef**: fat pointer + retain/release function pointers.
10//! - **SafeArcDyn<T>**: safe, cloneable handle over an `AbiStableDynRef`.
11//! - **DynPlugin<T>**: loaded plugin dereferencing to `&T`.
12//!
13//! ## Usage
14//!
15//! ```ignore
16//! // In the plugin .so:
17//! #[no_mangle]
18//! pub extern "C" fn core_ast_transform_entry() -> AbiStableDynRef {
19//!     SafeArcDyn::from_arc(Arc::new(MyTransform) as Arc<dyn Transform>).into_abi()
20//! }
21//!
22//! // In the host:
23//! use dyn_loader::dyn_mod::{DynPlugin, AbiStableDynRef, SafeArcDyn};
24//! let plugin = DynPlugin::<dyn Transform>::load("libmy_transform.so", b"core_ast_transform_entry\0")?;
25//! let transform: &dyn Transform = plugin.trait_ref();
26//! ```
27
28use std::ffi::c_void;
29use std::marker::PhantomData;
30use std::path::Path;
31use std::sync::Arc;
32
33use anyhow::{Context, Result};
34
35use crate::DynLib;
36use crate::helpers::display_symbol;
37
38// ---------------------------------------------------------------------------
39// AbiDynFatPtr — ABI-stable fat pointer
40// ---------------------------------------------------------------------------
41
42/// ABI-stable representation of a Rust dyn trait fat pointer.
43/// Two words: data pointer + vtable pointer.
44#[repr(C)]
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct AbiDynFatPtr {
47    pub data: *const c_void,
48    pub vtable: *const c_void,
49}
50
51impl AbiDynFatPtr {
52    pub const fn null() -> Self {
53        Self {
54            data: std::ptr::null(),
55            vtable: std::ptr::null(),
56        }
57    }
58
59    pub fn is_null(self) -> bool {
60        self.data.is_null() || self.vtable.is_null()
61    }
62}
63
64// SAFETY: AbiDynFatPtr is just two raw pointers; the actual thread-safety
65// is governed by the enclosing SafeArcDyn<T> where T: Send + Sync.
66unsafe impl Send for AbiDynFatPtr {}
67unsafe impl Sync for AbiDynFatPtr {}
68
69// ---------------------------------------------------------------------------
70// AbiStableDynRef — fat pointer + retain/release for Arc-like semantics
71// ---------------------------------------------------------------------------
72
73pub type RetainFn = unsafe extern "C" fn(AbiDynFatPtr);
74pub type ReleaseFn = unsafe extern "C" fn(AbiDynFatPtr);
75
76/// ABI-stable reference to a dyn trait object, with retain/release for
77/// safe reference counting across dynamic library boundaries.
78#[repr(C)]
79#[derive(Debug, Clone, Copy)]
80pub struct AbiStableDynRef {
81    pub object: AbiDynFatPtr,
82    pub retain: RetainFn,
83    pub release: ReleaseFn,
84}
85
86impl AbiStableDynRef {
87    pub const fn null() -> Self {
88        Self {
89            object: AbiDynFatPtr::null(),
90            retain: retain_noop,
91            release: release_noop,
92        }
93    }
94
95    pub fn is_null(self) -> bool {
96        self.object.is_null()
97    }
98}
99
100// SAFETY: The retain/release functions manage the Arc ref-count;
101// thread-safety follows T: Send + Sync.
102unsafe impl Send for AbiStableDynRef {}
103unsafe impl Sync for AbiStableDynRef {}
104
105unsafe extern "C" fn retain_noop(_: AbiDynFatPtr) {}
106unsafe extern "C" fn release_noop(_: AbiDynFatPtr) {}
107
108// ---------------------------------------------------------------------------
109// Pack / unpack fat pointers
110// ---------------------------------------------------------------------------
111
112/// Pack a `*const T` (where T: ?Sized) into an ABI-stable fat pointer.
113///
114/// # Safety
115///
116/// `ptr` must be a valid fat pointer (e.g., `*const dyn Trait`).
117pub unsafe fn pack_fat_ptr<T: ?Sized>(ptr: *const T) -> AbiDynFatPtr {
118    // Fat pointers are exactly 2 words: data + metadata (vtable for dyn Trait)
119    unsafe { std::mem::transmute_copy(&ptr) }
120}
121
122/// Unpack an ABI-stable fat pointer back to `*const T`.
123///
124/// # Safety
125///
126/// `ptr` must have been created from a compatible `*const T`.
127pub unsafe fn unpack_fat_ptr<T: ?Sized>(ptr: AbiDynFatPtr) -> *const T {
128    unsafe { std::mem::transmute_copy(&ptr) }
129}
130
131// ---------------------------------------------------------------------------
132// Arc retain/release for dyn trait objects
133// ---------------------------------------------------------------------------
134
135unsafe extern "C" fn retain_arc<T: ?Sized>(ptr: AbiDynFatPtr) {
136    let raw: *const T = unsafe { unpack_fat_ptr(ptr) };
137    unsafe { Arc::increment_strong_count(raw) };
138}
139
140unsafe extern "C" fn release_arc<T: ?Sized>(ptr: AbiDynFatPtr) {
141    let raw: *const T = unsafe { unpack_fat_ptr(ptr) };
142    unsafe { drop(Arc::from_raw(raw)) };
143}
144
145// ---------------------------------------------------------------------------
146// SafeArcDyn — safe wrapper around AbiStableDynRef
147// ---------------------------------------------------------------------------
148
149/// A safe, cloneable, droppable reference to a dyn trait object loaded
150/// from a dynamic library. Uses Arc-like retain/release for memory safety.
151#[repr(transparent)]
152pub struct SafeArcDyn<T: ?Sized> {
153    raw: AbiStableDynRef,
154    _marker: PhantomData<*const T>,
155}
156
157impl<T: ?Sized> SafeArcDyn<T> {
158    /// Create from an `Arc<T>`. The Arc's reference count is managed
159    /// via the retain/release function pointers.
160    pub fn from_arc(value: Arc<T>) -> Self {
161        let raw = Arc::into_raw(value);
162        Self {
163            raw: AbiStableDynRef {
164                object: unsafe { pack_fat_ptr(raw) },
165                retain: retain_arc::<T>,
166                release: release_arc::<T>,
167            },
168            _marker: PhantomData,
169        }
170    }
171
172    /// Get the ABI-stable representation (for exporting from a plugin).
173    pub fn into_abi(self) -> AbiStableDynRef {
174        let raw = self.raw;
175        std::mem::forget(self); // Don't drop — caller takes ownership
176        raw
177    }
178
179    /// Reconstruct from an ABI-stable representation (for loading in host).
180    ///
181    /// # Safety
182    ///
183    /// `raw` must have been created by `SafeArcDyn::<T>` or compatible code.
184    pub unsafe fn from_abi(raw: AbiStableDynRef) -> Self {
185        Self {
186            raw,
187            _marker: PhantomData,
188        }
189    }
190
191    /// Get a reference to the trait object.
192    ///
193    /// # Safety
194    ///
195    /// The stored fat pointer must be valid for the lifetime of this reference.
196    pub unsafe fn trait_ref(&self) -> &T {
197        unsafe { &*unpack_fat_ptr::<T>(self.raw.object) }
198    }
199}
200
201// SAFETY: SafeArcDyn<T> is Arc-like; safe to Send/Sync when T is.
202unsafe impl<T: ?Sized + Send> Send for SafeArcDyn<T> {}
203unsafe impl<T: ?Sized + Sync> Sync for SafeArcDyn<T> {}
204
205impl<T: ?Sized> Clone for SafeArcDyn<T> {
206    fn clone(&self) -> Self {
207        unsafe { (self.raw.retain)(self.raw.object) };
208        Self {
209            raw: self.raw,
210            _marker: PhantomData,
211        }
212    }
213}
214
215impl<T: ?Sized> Drop for SafeArcDyn<T> {
216    fn drop(&mut self) {
217        unsafe { (self.raw.release)(self.raw.object) };
218    }
219}
220
221// ---------------------------------------------------------------------------
222// DynPlugin — loaded plugin with trait object access
223// ---------------------------------------------------------------------------
224
225/// A loaded dynamic library plugin that exposes a trait object via
226/// the dyn-fat-pointer-bridge pattern.
227pub struct DynPlugin<T: ?Sized> {
228    _lib: DynLib,
229    plugin: SafeArcDyn<T>,
230}
231
232/// Type of the entry point function that plugins must export.
233pub type PluginEntryPoint = unsafe extern "C" fn() -> AbiStableDynRef;
234
235impl<T: ?Sized> DynPlugin<T> {
236    /// Load a plugin from a dynamic library file.
237    ///
238    /// The library must export a function with the given symbol name
239    /// that returns an `AbiStableDynRef` created via `SafeArcDyn::into_abi()`.
240    ///
241    /// # Safety
242    ///
243    /// - The target file must be a valid dynamic library for the current process.
244    /// - The entry point must return a valid `AbiStableDynRef` for trait `T`.
245    pub unsafe fn load(path: &Path, entry_symbol: &[u8]) -> Result<Self> {
246        let lib = unsafe { DynLib::load(path) }?;
247        unsafe { Self::from_lib(lib, entry_symbol) }
248    }
249    /// Load from an already-loaded `DynLib`.
250    ///
251    /// # Safety
252    ///
253    /// The entry point must return a valid `AbiStableDynRef` for trait `T`.
254    pub unsafe fn from_lib(lib: DynLib, entry_symbol: &[u8]) -> Result<Self> {
255        let entry: PluginEntryPoint = unsafe { lib.symbol(entry_symbol) }
256            .with_context(|| format!("entry point '{}' not found", display_symbol(entry_symbol)))?;
257        let abi_ref = unsafe { entry() };
258        if abi_ref.is_null() {
259            anyhow::bail!("entry point returned null AbiStableDynRef");
260        }
261        let plugin = unsafe { SafeArcDyn::<T>::from_abi(abi_ref) };
262        Ok(Self { _lib: lib, plugin })
263    }
264
265    /// Get a reference to the loaded trait object.
266    pub fn trait_ref(&self) -> &T {
267        unsafe { self.plugin.trait_ref() }
268    }
269
270    /// Get a cloned SafeArcDyn (for sharing across threads).
271    pub fn clone_handle(&self) -> SafeArcDyn<T> {
272        self.plugin.clone()
273    }
274}
275
276// SAFETY: DynPlugin<T> owns a DynLib (Arc<Library>) + SafeArcDyn<T>;
277// safe to Send/Sync when T is.
278unsafe impl<T: ?Sized + Send> Send for DynPlugin<T> {}
279unsafe impl<T: ?Sized + Sync> Sync for DynPlugin<T> {}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    trait Demo: Send + Sync {
286        fn value(&self) -> i32;
287    }
288
289    struct DemoValue(i32);
290
291    impl Demo for DemoValue {
292        fn value(&self) -> i32 {
293            self.0
294        }
295    }
296
297    #[test]
298    fn safe_arc_dyn_round_trip() {
299        let arc: Arc<dyn Demo> = Arc::new(DemoValue(42));
300        let wrapped = SafeArcDyn::from_arc(arc);
301        let abi = wrapped.into_abi();
302        let restored = unsafe { SafeArcDyn::<dyn Demo>::from_abi(abi) };
303        assert_eq!(unsafe { restored.trait_ref() }.value(), 42);
304    }
305
306    #[test]
307    fn safe_arc_dyn_clone() {
308        let arc: Arc<dyn Demo> = Arc::new(DemoValue(7));
309        let wrapped = SafeArcDyn::from_arc(arc);
310        let cloned = wrapped.clone();
311        assert_eq!(unsafe { wrapped.trait_ref() }.value(), 7);
312        assert_eq!(unsafe { cloned.trait_ref() }.value(), 7);
313        drop(wrapped);
314        assert_eq!(unsafe { cloned.trait_ref() }.value(), 7);
315    }
316}