Skip to main content

dyn_loader/
lib.rs

1//! # dyn-loader
2//!
3//! Dynamic library loader with dyn-fat-pointer-bridge for loading trait objects
4//! from `.so`/`.dylib` plugin files.
5//!
6//! Architecture (adapted from my-agent's plugin-loader + safe-arc + rust-plugin-loader):
7//!
8//! - **DynLib**: Wraps `libloading::Library` with `Arc` for shared ownership.
9//! - **AbiDynFatPtr**: ABI-stable representation of a Rust fat pointer
10//!   (data ptr + vtable ptr), `#[repr(C)]` for C ABI compatibility.
11//! - **AbiStableDynRef**: Fat pointer + retain/release function pointers,
12//!   enabling safe cross-boundary Arc-like reference counting.
13//! - **DynPlugin<T>**: Loaded plugin holding a `SafeArcDyn<T>` that can be
14//!   dereferenced to `&T` to call trait methods on the loaded object.
15//!
16//! ## Usage
17//!
18//! ```ignore
19//! // In the plugin .so:
20//! #[no_mangle]
21//! pub extern "C" fn core_ast_transform_entry() -> AbiStableDynRef {
22//!     SafeArcDyn::from_arc(Arc::new(MyTransform) as Arc<dyn Transform>).into_abi()
23//! }
24//!
25//! // In the host:
26//! let plugin = DynPlugin::<dyn Transform>::load("libmy_transform.so", b"core_ast_transform_entry\0")?;
27//! let transform: &dyn Transform = plugin.as_ref();
28//! ```
29
30use std::ffi::{c_char, c_void};
31use std::marker::PhantomData;
32use std::path::Path;
33use std::sync::Arc;
34
35use anyhow::{Context, Result};
36use libloading::Library;
37
38// ---------------------------------------------------------------------------
39// DynLib — Arc-shared dynamic library
40// ---------------------------------------------------------------------------
41
42#[derive(Clone)]
43pub struct DynLib {
44    library: Arc<Library>,
45    path: std::path::PathBuf,
46}
47
48impl DynLib {
49    /// Load a dynamic library from the given path.
50    ///
51    /// # Safety
52    ///
53    /// The target file must be a valid dynamic library for the current process.
54    pub unsafe fn load(path: &Path) -> Result<Self> {
55        let library = unsafe { Library::new(path) }
56            .with_context(|| format!("failed to load dynamic library: {}", path.display()))?;
57        Ok(Self {
58            library: Arc::new(library),
59            path: path.to_path_buf(),
60        })
61    }
62    pub fn path(&self) -> &Path {
63        &self.path
64    }
65
66    /// Load a symbol from the library.
67    ///
68    /// # Safety
69    ///
70    /// The caller must ensure `T` matches the actual exported symbol type.
71    pub unsafe fn symbol<T: Copy>(&self, name: &[u8]) -> Result<T> {
72        let sym = unsafe { self.library.get::<T>(name) }.with_context(|| {
73            format!(
74                "symbol '{}' not found in {}",
75                display_symbol(name),
76                self.path.display()
77            )
78        })?;
79        Ok(*sym)
80    }
81
82    /// Try to load a symbol; returns `None` if not found.
83    ///
84    /// # Safety
85    ///
86    /// The caller must ensure `T` matches the actual exported symbol type.
87    pub unsafe fn try_symbol<T: Copy>(&self, name: &[u8]) -> Option<T> {
88        unsafe { self.library.get::<T>(name) }.ok().map(|s| *s)
89    }
90}
91
92// ---------------------------------------------------------------------------
93// AbiDynFatPtr — ABI-stable fat pointer
94// ---------------------------------------------------------------------------
95
96/// ABI-stable representation of a Rust dyn trait fat pointer.
97/// Two words: data pointer + vtable pointer.
98#[repr(C)]
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct AbiDynFatPtr {
101    pub data: *const c_void,
102    pub vtable: *const c_void,
103}
104
105impl AbiDynFatPtr {
106    pub const fn null() -> Self {
107        Self {
108            data: std::ptr::null(),
109            vtable: std::ptr::null(),
110        }
111    }
112
113    pub fn is_null(self) -> bool {
114        self.data.is_null() || self.vtable.is_null()
115    }
116}
117
118// SAFETY: AbiDynFatPtr is just two raw pointers; the actual thread-safety
119// is governed by the enclosing SafeArcDyn<T> where T: Send + Sync.
120unsafe impl Send for AbiDynFatPtr {}
121unsafe impl Sync for AbiDynFatPtr {}
122
123// ---------------------------------------------------------------------------
124// AbiStableDynRef — fat pointer + retain/release for Arc-like semantics
125// ---------------------------------------------------------------------------
126
127pub type RetainFn = unsafe extern "C" fn(AbiDynFatPtr);
128pub type ReleaseFn = unsafe extern "C" fn(AbiDynFatPtr);
129
130/// ABI-stable reference to a dyn trait object, with retain/release for
131/// safe reference counting across dynamic library boundaries.
132#[repr(C)]
133#[derive(Debug, Clone, Copy)]
134pub struct AbiStableDynRef {
135    pub object: AbiDynFatPtr,
136    pub retain: RetainFn,
137    pub release: ReleaseFn,
138}
139
140impl AbiStableDynRef {
141    pub const fn null() -> Self {
142        Self {
143            object: AbiDynFatPtr::null(),
144            retain: retain_noop,
145            release: release_noop,
146        }
147    }
148
149    pub fn is_null(self) -> bool {
150        self.object.is_null()
151    }
152}
153
154// SAFETY: The retain/release functions manage the Arc ref-count;
155// thread-safety follows T: Send + Sync.
156unsafe impl Send for AbiStableDynRef {}
157unsafe impl Sync for AbiStableDynRef {}
158
159unsafe extern "C" fn retain_noop(_: AbiDynFatPtr) {}
160unsafe extern "C" fn release_noop(_: AbiDynFatPtr) {}
161
162// ---------------------------------------------------------------------------
163// Pack / unpack fat pointers
164// ---------------------------------------------------------------------------
165
166/// Pack a `*const T` (where T: ?Sized) into an ABI-stable fat pointer.
167///
168/// # Safety
169///
170/// `ptr` must be a valid fat pointer (e.g., `*const dyn Trait`).
171pub unsafe fn pack_fat_ptr<T: ?Sized>(ptr: *const T) -> AbiDynFatPtr {
172    // Fat pointers are exactly 2 words: data + metadata (vtable for dyn Trait)
173    unsafe { std::mem::transmute_copy(&ptr) }
174}
175
176/// Unpack an ABI-stable fat pointer back to `*const T`.
177///
178/// # Safety
179///
180/// `ptr` must have been created from a compatible `*const T`.
181pub unsafe fn unpack_fat_ptr<T: ?Sized>(ptr: AbiDynFatPtr) -> *const T {
182    unsafe { std::mem::transmute_copy(&ptr) }
183}
184
185// ---------------------------------------------------------------------------
186// Arc retain/release for dyn trait objects
187// ---------------------------------------------------------------------------
188
189unsafe extern "C" fn retain_arc<T: ?Sized>(ptr: AbiDynFatPtr) {
190    let raw: *const T = unsafe { unpack_fat_ptr(ptr) };
191    unsafe { Arc::increment_strong_count(raw) };
192}
193
194unsafe extern "C" fn release_arc<T: ?Sized>(ptr: AbiDynFatPtr) {
195    let raw: *const T = unsafe { unpack_fat_ptr(ptr) };
196    unsafe { drop(Arc::from_raw(raw)) };
197}
198
199// ---------------------------------------------------------------------------
200// SafeArcDyn — safe wrapper around AbiStableDynRef
201// ---------------------------------------------------------------------------
202
203/// A safe, cloneable, droppable reference to a dyn trait object loaded
204/// from a dynamic library. Uses Arc-like retain/release for memory safety.
205#[repr(transparent)]
206pub struct SafeArcDyn<T: ?Sized> {
207    raw: AbiStableDynRef,
208    _marker: PhantomData<*const T>,
209}
210
211impl<T: ?Sized> SafeArcDyn<T> {
212    /// Create from an `Arc<T>`. The Arc's reference count is managed
213    /// via the retain/release function pointers.
214    pub fn from_arc(value: Arc<T>) -> Self {
215        let raw = Arc::into_raw(value);
216        Self {
217            raw: AbiStableDynRef {
218                object: unsafe { pack_fat_ptr(raw) },
219                retain: retain_arc::<T>,
220                release: release_arc::<T>,
221            },
222            _marker: PhantomData,
223        }
224    }
225
226    /// Get the ABI-stable representation (for exporting from a plugin).
227    pub fn into_abi(self) -> AbiStableDynRef {
228        let raw = self.raw;
229        std::mem::forget(self); // Don't drop — caller takes ownership
230        raw
231    }
232
233    /// Reconstruct from an ABI-stable representation (for loading in host).
234    ///
235    /// # Safety
236    ///
237    /// `raw` must have been created by `SafeArcDyn::<T>` or compatible code.
238    pub unsafe fn from_abi(raw: AbiStableDynRef) -> Self {
239        Self {
240            raw,
241            _marker: PhantomData,
242        }
243    }
244
245    /// Get a reference to the trait object.
246    ///
247    /// # Safety
248    ///
249    /// The stored fat pointer must be valid for the lifetime of this reference.
250    pub unsafe fn trait_ref(&self) -> &T {
251        unsafe { &*unpack_fat_ptr::<T>(self.raw.object) }
252    }
253}
254
255// SAFETY: SafeArcDyn<T> is Arc-like; safe to Send/Sync when T is.
256unsafe impl<T: ?Sized + Send> Send for SafeArcDyn<T> {}
257unsafe impl<T: ?Sized + Sync> Sync for SafeArcDyn<T> {}
258
259impl<T: ?Sized> Clone for SafeArcDyn<T> {
260    fn clone(&self) -> Self {
261        unsafe { (self.raw.retain)(self.raw.object) };
262        Self {
263            raw: self.raw,
264            _marker: PhantomData,
265        }
266    }
267}
268
269impl<T: ?Sized> Drop for SafeArcDyn<T> {
270    fn drop(&mut self) {
271        unsafe { (self.raw.release)(self.raw.object) };
272    }
273}
274
275// ---------------------------------------------------------------------------
276// DynPlugin — loaded plugin with trait object access
277// ---------------------------------------------------------------------------
278
279/// A loaded dynamic library plugin that exposes a trait object via
280/// the dyn-fat-pointer-bridge pattern.
281pub struct DynPlugin<T: ?Sized> {
282    _lib: DynLib,
283    plugin: SafeArcDyn<T>,
284}
285
286/// Type of the entry point function that plugins must export.
287pub type PluginEntryPoint = unsafe extern "C" fn() -> AbiStableDynRef;
288
289impl<T: ?Sized> DynPlugin<T> {
290    /// Load a plugin from a dynamic library file.
291    ///
292    /// The library must export a function with the given symbol name
293    /// that returns an `AbiStableDynRef` created via `SafeArcDyn::into_abi()`.
294    ///
295    /// # Safety
296    ///
297    /// - The target file must be a valid dynamic library for the current process.
298    /// - The entry point must return a valid `AbiStableDynRef` for trait `T`.
299    pub unsafe fn load(path: &Path, entry_symbol: &[u8]) -> Result<Self> {
300        let lib = unsafe { DynLib::load(path) }?;
301        unsafe { Self::from_lib(lib, entry_symbol) }
302    }
303    /// Load from an already-loaded `DynLib`.
304    ///
305    /// # Safety
306    ///
307    /// The entry point must return a valid `AbiStableDynRef` for trait `T`.
308    pub unsafe fn from_lib(lib: DynLib, entry_symbol: &[u8]) -> Result<Self> {
309        let entry: PluginEntryPoint = unsafe { lib.symbol(entry_symbol) }
310            .with_context(|| format!("entry point '{}' not found", display_symbol(entry_symbol)))?;
311        let abi_ref = unsafe { entry() };
312        if abi_ref.is_null() {
313            anyhow::bail!("entry point returned null AbiStableDynRef");
314        }
315        let plugin = unsafe { SafeArcDyn::<T>::from_abi(abi_ref) };
316        Ok(Self { _lib: lib, plugin })
317    }
318
319    /// Get a reference to the loaded trait object.
320    pub fn trait_ref(&self) -> &T {
321        unsafe { self.plugin.trait_ref() }
322    }
323
324    /// Get a cloned SafeArcDyn (for sharing across threads).
325    pub fn clone_handle(&self) -> SafeArcDyn<T> {
326        self.plugin.clone()
327    }
328}
329
330// SAFETY: DynPlugin<T> owns a DynLib (Arc<Library>) + SafeArcDyn<T>;
331// safe to Send/Sync when T is.
332unsafe impl<T: ?Sized + Send> Send for DynPlugin<T> {}
333unsafe impl<T: ?Sized + Sync> Sync for DynPlugin<T> {}
334
335// ---------------------------------------------------------------------------
336// VTablePlugin — simpler vtable-based loading (Copy types only)
337// ---------------------------------------------------------------------------
338
339/// Load a Copy-sized vtable/descriptor struct from a dynamic library.
340pub struct VTablePlugin<T: Copy> {
341    _lib: DynLib,
342    vtable: T,
343}
344
345impl<T: Copy> VTablePlugin<T> {
346    /// Load a vtable struct from a dynamic library.
347    ///
348    /// # Safety
349    ///
350    /// - The target file must be a valid dynamic library.
351    /// - The symbol must refer to a static vtable-compatible value of type `T`.
352    pub unsafe fn load(path: &Path, symbol: &[u8]) -> Result<Self> {
353        let lib = unsafe { DynLib::load(path) }?;
354        unsafe { Self::from_lib(lib, symbol) }
355    }
356
357    /// # Safety
358    ///
359    /// The symbol must refer to a static vtable-compatible value of type `T`.
360    pub unsafe fn from_lib(lib: DynLib, symbol: &[u8]) -> Result<Self> {
361        let getter: unsafe extern "C" fn() -> *const T = unsafe { lib.symbol(symbol) }?;
362        let vtable = unsafe { getter() };
363        let vtable = unsafe { vtable.as_ref() }
364            .copied()
365            .ok_or_else(|| anyhow::anyhow!("vtable getter returned null"))?;
366        Ok(Self { _lib: lib, vtable })
367    }
368
369    pub fn vtable(&self) -> &T {
370        &self.vtable
371    }
372}
373
374// ---------------------------------------------------------------------------
375// Helpers
376// ---------------------------------------------------------------------------
377
378fn display_symbol(symbol: &[u8]) -> String {
379    let end = symbol.iter().position(|&b| b == 0).unwrap_or(symbol.len());
380    String::from_utf8_lossy(&symbol[..end]).into_owned()
381}
382
383/// Quick check: does the file look like a plugin with the given entry symbol?
384///
385/// # Safety
386///
387/// Probes an arbitrary dynamic library for a specific exported symbol.
388pub unsafe fn looks_like_plugin(path: &Path, entry_symbol: &[u8]) -> bool {
389    match unsafe { DynLib::load(path) } {
390        Ok(lib) => unsafe { lib.try_symbol::<PluginEntryPoint>(entry_symbol) }.is_some(),
391        Err(_) => false,
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    trait Demo: Send + Sync {
400        fn value(&self) -> i32;
401    }
402
403    struct DemoValue(i32);
404
405    impl Demo for DemoValue {
406        fn value(&self) -> i32 {
407            self.0
408        }
409    }
410
411    #[test]
412    fn safe_arc_dyn_round_trip() {
413        let arc: Arc<dyn Demo> = Arc::new(DemoValue(42));
414        let wrapped = SafeArcDyn::from_arc(arc);
415        let abi = wrapped.into_abi();
416        let restored = unsafe { SafeArcDyn::<dyn Demo>::from_abi(abi) };
417        assert_eq!(unsafe { restored.trait_ref() }.value(), 42);
418    }
419
420    #[test]
421    fn safe_arc_dyn_clone() {
422        let arc: Arc<dyn Demo> = Arc::new(DemoValue(7));
423        let wrapped = SafeArcDyn::from_arc(arc);
424        let cloned = wrapped.clone();
425        assert_eq!(unsafe { wrapped.trait_ref() }.value(), 7);
426        assert_eq!(unsafe { cloned.trait_ref() }.value(), 7);
427        drop(wrapped);
428        assert_eq!(unsafe { cloned.trait_ref() }.value(), 7);
429    }
430}
431
432// ---------------------------------------------------------------------------
433// C++ math module VTable ABI — matches cpp/math_module/include/math_vtable.h
434// ---------------------------------------------------------------------------
435
436/// Opaque handle for a C++ MathSession
437pub type MathSession = *mut c_void;
438
439/// Descriptor for a generated math function (matches C++ GeneratedFunction)
440#[repr(C)]
441#[derive(Debug, Clone, Copy)]
442pub struct GeneratedFunction {
443    pub name: *const c_char,
444    pub signature: *const c_char,
445    pub arg_count: u32,
446    pub id: u32,
447}
448
449/// Vtable struct matching C++ MathModuleVtable layout exactly
450#[repr(C)]
451#[derive(Debug, Clone, Copy)]
452pub struct MathModuleVtable {
453    // Session lifecycle
454    pub create_session: unsafe extern "C" fn() -> MathSession,
455    pub destroy_session: unsafe extern "C" fn(MathSession),
456
457    // Math operations (lazy — only "generated" if called)
458    pub add_i32: unsafe extern "C" fn(MathSession, i32, i32) -> i32,
459    pub add_i64: unsafe extern "C" fn(MathSession, i64, i64) -> i64,
460    pub add_f64: unsafe extern "C" fn(MathSession, f64, f64) -> f64,
461    pub mul_i32: unsafe extern "C" fn(MathSession, i32, i32) -> i32,
462    pub mul_f64: unsafe extern "C" fn(MathSession, f64, f64) -> f64,
463    pub pi: unsafe extern "C" fn(MathSession) -> f64,
464    pub tau: unsafe extern "C" fn(MathSession) -> f64,
465
466    // Code-generation introspection
467    pub generated_count: unsafe extern "C" fn(MathSession) -> u32,
468    pub generated_at: unsafe extern "C" fn(MathSession, u32) -> GeneratedFunction,
469
470    // Module info
471    pub module_name: unsafe extern "C" fn() -> *const c_char,
472    pub module_version: unsafe extern "C" fn() -> u32,
473}
474
475// SAFETY: MathModuleVtable contains only function pointers and is safe to Send/Sync
476unsafe impl Send for MathModuleVtable {}
477unsafe impl Sync for MathModuleVtable {}
478
479#[cfg(test)]
480mod cpp_math_tests {
481    use super::*;
482    use std::ffi::CStr;
483    use std::path::PathBuf;
484
485    fn math_module_path() -> PathBuf {
486        // Relative from crate root (rust/crates/dyn-loader) to cpp build output
487        let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
488        p.push("../../cpp/build/math_module");
489        p.push("libmath_module.so");
490        p
491    }
492
493    #[test]
494    fn load_cpp_math_module_via_vtable() {
495        let path = math_module_path();
496        if !path.exists() {
497            eprintln!("SKIP: {} not found — build cpp/ first", path.display());
498            return;
499        }
500
501        let plugin =
502            unsafe { VTablePlugin::<MathModuleVtable>::load(&path, b"math_module_get_vtable\0") }
503                .expect("failed to load math_module");
504        let vt = plugin.vtable();
505
506        // Module info
507        unsafe {
508            let name = CStr::from_ptr((vt.module_name)());
509            assert_eq!(name.to_str().unwrap(), "core-ast-math");
510            assert_eq!((vt.module_version)(), 1);
511        }
512
513        // Create session
514        let session = unsafe { (vt.create_session)() };
515        assert!(!session.is_null());
516
517        // No functions generated yet
518        assert_eq!(unsafe { (vt.generated_count)(session) }, 0);
519
520        // Call add_i32 — marks it as "used"
521        let result = unsafe { (vt.add_i32)(session, 10, 20) };
522        assert_eq!(result, 30);
523        assert_eq!(unsafe { (vt.generated_count)(session) }, 1);
524
525        // Call mul_f64 — marks it as "used"
526        let result = unsafe { (vt.mul_f64)(session, 3.0, 7.0) };
527        assert!((result - 21.0).abs() < 1e-10);
528        assert_eq!(unsafe { (vt.generated_count)(session) }, 2);
529
530        // Call pi
531        let pi_val = unsafe { (vt.pi)(session) };
532        assert!((pi_val - std::f64::consts::PI).abs() < 1e-10);
533        assert_eq!(unsafe { (vt.generated_count)(session) }, 3);
534
535        // Introspect generated functions
536        let func0 = unsafe { (vt.generated_at)(session, 0) };
537        let func0_name = unsafe { CStr::from_ptr(func0.name) }.to_str().unwrap();
538        assert_eq!(func0_name, "add_i32");
539
540        let func1 = unsafe { (vt.generated_at)(session, 1) };
541        let func1_name = unsafe { CStr::from_ptr(func1.name) }.to_str().unwrap();
542        assert_eq!(func1_name, "mul_f64");
543
544        let func2 = unsafe { (vt.generated_at)(session, 2) };
545        let func2_name = unsafe { CStr::from_ptr(func2.name) }.to_str().unwrap();
546        assert_eq!(func2_name, "pi");
547
548        // Call add_i32 again — should NOT add duplicate
549        let _ = unsafe { (vt.add_i32)(session, 1, 2) };
550        assert_eq!(unsafe { (vt.generated_count)(session) }, 3);
551
552        // Destroy session
553        unsafe { (vt.destroy_session)(session) };
554    }
555}