Skip to main content

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