Skip to main content

dyn_loader/
cdyn.rs

1//! # cdyn — COM-style C function-table loading (ABI-stable)
2//!
3//! Load Copy-sized vtable/descriptor structs from dynamic libraries using
4//! plain C function tables — no Rust trait objects, no named-based access,
5//! purely positional dispatch. This is the COM+-like emulated vtable system
6//! (formerly the separate `cdyn-loader` crate).
7//!
8//! Unlike [`crate::dyn_mod`], this mode is ABI-stable across languages:
9//! the C++ SDK (`cdyn-loader-sdks/cpp`) and Zig SDK (`cdyn-loader-sdks/zig`)
10//! build plugins that match these layouts exactly.
11//!
12//! ## Usage
13//!
14//! ```ignore
15//! #[repr(C)]
16//! struct MyVtable {
17//!     add: unsafe extern "C" fn(i32, i32) -> i32,
18//!     name: unsafe extern "C" fn() -> *const std::ffi::c_char,
19//! }
20//!
21//! let plugin = unsafe { VTablePlugin::<MyVtable>::load("libmy.so", b"my_get_vtable\0")? };
22//! let n = unsafe { (plugin.vtable().add)(1, 2) };
23//! ```
24
25use std::ffi::c_char;
26use std::marker::PhantomData;
27use std::path::Path;
28
29use anyhow::{Context, Result};
30
31use crate::DynLib;
32use crate::dyn_mod::{AbiStableDynRef, PluginEntryPoint};
33
34// ---------------------------------------------------------------------------
35// VTablePlugin — simpler vtable-based loading (Copy types only)
36// ---------------------------------------------------------------------------
37
38/// Load a Copy-sized vtable/descriptor struct from a dynamic library.
39pub struct VTablePlugin<T: Copy> {
40    _lib: DynLib,
41    vtable: T,
42}
43
44impl<T: Copy> VTablePlugin<T> {
45    /// Load a vtable struct from a dynamic library.
46    ///
47    /// # Safety
48    ///
49    /// - The target file must be a valid dynamic library.
50    /// - The symbol must refer to a static vtable-compatible value of type `T`.
51    pub unsafe fn load(path: &Path, symbol: &[u8]) -> Result<Self> {
52        let lib = unsafe { DynLib::load(path) }?;
53        unsafe { Self::from_lib(lib, symbol) }
54    }
55
56    /// # Safety
57    ///
58    /// The symbol must refer to a static vtable-compatible value of type `T`.
59    pub unsafe fn from_lib(lib: DynLib, symbol: &[u8]) -> Result<Self> {
60        let getter: unsafe extern "C" fn() -> *const T = unsafe { lib.symbol(symbol) }?;
61        let vtable = unsafe { getter() };
62        let vtable = unsafe { vtable.as_ref() }
63            .copied()
64            .ok_or_else(|| anyhow::anyhow!("vtable getter returned null"))?;
65        Ok(Self { _lib: lib, vtable })
66    }
67
68    pub fn vtable(&self) -> &T {
69        &self.vtable
70    }
71}
72
73// ---------------------------------------------------------------------------
74// C++ math module VTable ABI — matches cpp/math_module/include/math_vtable.h
75// ---------------------------------------------------------------------------
76
77/// Opaque handle for a C++ MathSession
78pub type MathSession = *mut std::ffi::c_void;
79
80/// Descriptor for a generated math function (matches C++ GeneratedFunction)
81#[repr(C)]
82#[derive(Debug, Clone, Copy)]
83pub struct GeneratedFunction {
84    pub name: *const c_char,
85    pub signature: *const c_char,
86    pub arg_count: u32,
87    pub id: u32,
88}
89
90/// Vtable struct matching C++ MathModuleVtable layout exactly
91#[repr(C)]
92#[derive(Debug, Clone, Copy)]
93pub struct MathModuleVtable {
94    // Session lifecycle
95    pub create_session: unsafe extern "C" fn() -> MathSession,
96    pub destroy_session: unsafe extern "C" fn(MathSession),
97
98    // Math operations (lazy — only "generated" if called)
99    pub add_i32: unsafe extern "C" fn(MathSession, i32, i32) -> i32,
100    pub add_i64: unsafe extern "C" fn(MathSession, i64, i64) -> i64,
101    pub add_f64: unsafe extern "C" fn(MathSession, f64, f64) -> f64,
102    pub mul_i32: unsafe extern "C" fn(MathSession, i32, i32) -> i32,
103    pub mul_f64: unsafe extern "C" fn(MathSession, f64, f64) -> f64,
104    pub pi: unsafe extern "C" fn(MathSession) -> f64,
105    pub tau: unsafe extern "C" fn(MathSession) -> f64,
106
107    // Code-generation introspection
108    pub generated_count: unsafe extern "C" fn(MathSession) -> u32,
109    pub generated_at: unsafe extern "C" fn(MathSession, u32) -> GeneratedFunction,
110
111    // Module info
112    pub module_name: unsafe extern "C" fn() -> *const c_char,
113    pub module_version: unsafe extern "C" fn() -> u32,
114}
115
116// SAFETY: MathModuleVtable contains only function pointers and is safe to Send/Sync
117unsafe impl Send for MathModuleVtable {}
118unsafe impl Sync for MathModuleVtable {}
119
120#[cfg(test)]
121mod cdyn_handle_tests {
122    use super::*;
123    use std::sync::atomic::{AtomicU32, Ordering};
124
125    // A minimal "foreign-style" plugin simulated in-process:
126    // static instance + atomic ref-count + vtable of thunks — exactly the
127    // pattern the C++ CdynExposed / Zig CdynPlugin generate.
128    static INSTANCE: u64 = 0xdead_beef;
129    static REFCOUNT: AtomicU32 = AtomicU32::new(0);
130
131    #[repr(C)]
132    #[derive(Clone, Copy)]
133    struct TestVtable {
134        get_value: unsafe extern "C" fn(ctx: *mut std::ffi::c_void) -> u64,
135    }
136
137    unsafe extern "C" fn test_get_value(ctx: *mut std::ffi::c_void) -> u64 {
138        // simulate thunk: ctx -> instance
139        let _ = ctx;
140        INSTANCE
141    }
142
143    unsafe extern "C" fn test_retain(_: AbiStableDynRef__FatPtr) {
144        REFCOUNT.fetch_add(1, Ordering::SeqCst);
145    }
146
147    unsafe extern "C" fn test_release(_: AbiStableDynRef__FatPtr) {
148        let prev = REFCOUNT.fetch_sub(1, Ordering::SeqCst);
149        assert!(prev > 0, "release called more times than retain");
150    }
151
152    // alias so the fns match the RetainFn/ReleaseFn signatures
153    type AbiStableDynRef__FatPtr = crate::dyn_mod::AbiDynFatPtr;
154
155    static TEST_VTABLE: TestVtable = TestVtable { get_value: test_get_value };
156
157    #[test]
158    fn cdyn_handle_retain_release_roundtrip() {
159        REFCOUNT.store(1, Ordering::SeqCst); // plugin starts with 1 ref
160
161        let raw = AbiStableDynRef {
162            object: crate::dyn_mod::AbiDynFatPtr {
163                data: &INSTANCE as *const u64 as *const std::ffi::c_void,
164                vtable: &TEST_VTABLE as *const TestVtable as *const std::ffi::c_void,
165            },
166            retain: test_retain,
167            release: test_release,
168        };
169
170        // from_raw (unowned lib)
171        let h1 = unsafe { CdynHandle::<TestVtable>::from_raw(raw) };
172        assert_eq!(REFCOUNT.load(Ordering::SeqCst), 1);
173
174        // clone → retain
175        let h2 = h1.clone();
176        assert_eq!(REFCOUNT.load(Ordering::SeqCst), 2);
177
178        // vtable call through the handle
179        unsafe {
180            let vt = h1.vtable();
181            assert_eq!((vt.get_value)(h1.ctx()), INSTANCE);
182        }
183        // same vtable pointer via h2
184        assert_eq!(h1.as_raw().object.vtable, h2.as_raw().object.vtable);
185
186        // drop → release
187        drop(h2);
188        assert_eq!(REFCOUNT.load(Ordering::SeqCst), 1);
189        drop(h1);
190        assert_eq!(REFCOUNT.load(Ordering::SeqCst), 0);
191    }
192
193    #[test]
194    fn cdyn_handle_into_raw_skips_release() {
195        REFCOUNT.store(1, Ordering::SeqCst);
196        let raw = AbiStableDynRef {
197            object: crate::dyn_mod::AbiDynFatPtr {
198                data: &INSTANCE as *const u64 as *const std::ffi::c_void,
199                vtable: &TEST_VTABLE as *const TestVtable as *const std::ffi::c_void,
200            },
201            retain: test_retain,
202            release: test_release,
203        };
204        let h = unsafe { CdynHandle::<TestVtable>::from_raw(raw) };
205        let raw2 = h.into_raw(); // must NOT call release
206        drop(raw2); // plain Copy struct, no Drop
207        assert_eq!(REFCOUNT.load(Ordering::SeqCst), 1); // unchanged
208        // give the ref back to the "plugin" to balance counts
209        REFCOUNT.fetch_sub(1, Ordering::SeqCst);
210    }
211}
212
213// ---------------------------------------------------------------------------
214// CdynHandle — cross-language ref-counted smart handle
215// ---------------------------------------------------------------------------
216
217/// Ref-counted handle to a **foreign** plugin object exposed through a
218/// `*_get_dyn` style entry point returning an [`AbiStableDynRef`].
219///
220/// This is the Rust-side counterpart of the C++ SDK's `CdynExposed` /
221/// `CdynPlugin` and the Zig SDK's `CdynPlugin(PluginType)`:
222///
223/// - [`clone`](Clone::clone) → calls the plugin's `retain`
224/// - [`drop`](Drop) → calls the plugin's `release`
225/// - [`ctx`](Self::ctx) → the instance pointer (first arg of every vtable method)
226/// - [`vtable`](Self::vtable) → `&T` reconstructed from the packed vtable pointer
227///
228/// `T` is the C-layout vtable struct (`#[repr(C)]`, `Copy`) — e.g.
229/// [`MathModuleVtable`] or your own. The vtable is **not** copied; it is
230/// referenced through the pointer packed inside the dyn ref (it points into
231/// the plugin's static storage, valid while the library stays loaded).
232///
233/// # Example
234///
235/// ```ignore
236/// // C++ plugin built with: CDYN_EXPOSE_CLASS(MathPlugin, MathModuleVtable, math)
237/// // exports: math_get_vtable() and math_get_dyn()
238/// let handle = unsafe { CdynHandle::<MathModuleVtable>::load(&path, b"math_get_dyn\0")? };
239/// let vt = unsafe { handle.vtable() };
240/// let session = unsafe { (vt.create_session)() };
241/// let sum = unsafe { (vt.add_i32)(session, 1, 2) };
242/// unsafe { (vt.destroy_session)(session) };
243/// // handle drop → plugin release()
244/// ```
245pub struct CdynHandle<T: Copy> {
246    _lib: DynLib,
247    raw: AbiStableDynRef,
248    _marker: PhantomData<T>,
249}
250
251impl<T: Copy> CdynHandle<T> {
252    /// Load a foreign plugin object via its dyn entry point.
253    ///
254    /// # Safety
255    ///
256    /// - The target file must be a valid dynamic library.
257    /// - The entry must return a valid `AbiStableDynRef` whose vtable pointer
258    ///   refers to a `T`-layout vtable.
259    pub unsafe fn load(path: &Path, dyn_entry: &[u8]) -> Result<Self> {
260        let lib = unsafe { DynLib::load(path) }?;
261        unsafe { Self::from_lib(lib, dyn_entry) }
262    }
263
264    /// Load from an already-loaded library.
265    ///
266    /// # Safety
267    ///
268    /// The entry must return a valid `AbiStableDynRef` whose vtable pointer
269    /// refers to a `T`-layout vtable.
270    pub unsafe fn from_lib(lib: DynLib, dyn_entry: &[u8]) -> Result<Self> {
271        let entry: PluginDynEntryPoint =
272            unsafe { lib.symbol(dyn_entry) }.with_context(|| {
273                format!("dyn entry '{}' not found", crate::helpers::display_symbol(dyn_entry))
274            })?;
275        let raw = unsafe { entry() };
276        if raw.is_null() {
277            anyhow::bail!("dyn entry returned null AbiStableDynRef");
278        }
279        Ok(Self {
280            _lib: lib,
281            raw,
282            _marker: PhantomData,
283        })
284    }
285
286    /// Reconstruct from a raw [`AbiStableDynRef`] obtained elsewhere.
287    ///
288    /// The handle does not own the originating library; the caller must keep
289    /// it loaded (e.g. hold another [`DynPlugin`](crate::DynPlugin) or
290    /// [`DynLib`](crate::DynLib)) for as long as this handle lives.
291    ///
292    /// # Safety
293    ///
294    /// `raw` must be a live ref produced by a compatible plugin.
295    pub unsafe fn from_raw(raw: AbiStableDynRef) -> Self {
296        Self {
297            _lib: DynLib::unowned(),
298            raw,
299            _marker: PhantomData,
300        }
301    }
302
303    /// Instance context pointer — pass as the first argument of vtable methods.
304    pub fn ctx(&self) -> *mut std::ffi::c_void {
305        self.raw.object.data as *mut std::ffi::c_void
306    }
307
308    /// The vtable, reconstructed from the pointer packed inside the dyn ref.
309    ///
310    /// # Safety
311    ///
312    /// `T` must be the exact vtable type the plugin used when building the ref.
313    pub unsafe fn vtable(&self) -> &T {
314        unsafe { &*(self.raw.object.vtable as *const T) }
315    }
316
317    /// Raw ABI ref (for passing back across the boundary).
318    pub fn as_raw(&self) -> &AbiStableDynRef {
319        &self.raw
320    }
321
322    /// Consume without calling `release` (ownership handed back to the plugin).
323    pub fn into_raw(self) -> AbiStableDynRef {
324        let mut this = std::mem::ManuallyDrop::new(self);
325        // Detach the library handle so Drop won't run for it either.
326        let lib = unsafe { std::ptr::read(&this._lib) };
327        std::mem::forget(lib);
328        this.raw
329    }
330}
331
332/// Type of the dyn entry point that foreign plugins export
333/// (C++ `CDYN_EXPORT AbiStableDynRef name_get_dyn()`, Zig `declareDynEntry`).
334pub type PluginDynEntryPoint = PluginEntryPoint;
335
336// SAFETY: CdynHandle manages the plugin's own ref-count via retain/release;
337// thread-safety follows the plugin's guarantees (same policy as SafeArcDyn).
338unsafe impl<T: Copy + Send> Send for CdynHandle<T> {}
339unsafe impl<T: Copy + Sync> Sync for CdynHandle<T> {}
340
341impl<T: Copy> Clone for CdynHandle<T> {
342    fn clone(&self) -> Self {
343        unsafe { (self.raw.retain)(self.raw.object) };
344        Self {
345            _lib: self._lib.clone(),
346            raw: self.raw,
347            _marker: PhantomData,
348        }
349    }
350}
351
352impl<T: Copy> Drop for CdynHandle<T> {
353    fn drop(&mut self) {
354        unsafe { (self.raw.release)(self.raw.object) };
355    }
356}
357
358#[cfg(test)]
359mod cpp_math_tests {
360    use super::*;
361    use std::ffi::CStr;
362    use std::path::PathBuf;
363
364    fn math_module_path() -> PathBuf {
365        // Relative from crate root (rust/crates/dyn-loader) to cpp build output
366        let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
367        p.push("../../cpp/build/math_module");
368        p.push("libmath_module.so");
369        p
370    }
371
372    #[test]
373    fn load_cpp_math_module_via_vtable() {
374        let path = math_module_path();
375        if !path.exists() {
376            eprintln!("SKIP: {} not found — build cpp/ first", path.display());
377            return;
378        }
379
380        let plugin =
381            unsafe { VTablePlugin::<MathModuleVtable>::load(&path, b"math_module_get_vtable\0") }
382                .expect("failed to load math_module");
383        let vt = plugin.vtable();
384
385        // Module info
386        unsafe {
387            let name = CStr::from_ptr((vt.module_name)());
388            assert_eq!(name.to_str().unwrap(), "core-ast-math");
389            assert_eq!((vt.module_version)(), 1);
390        }
391
392        // Create session
393        let session = unsafe { (vt.create_session)() };
394        assert!(!session.is_null());
395
396        // No functions generated yet
397        assert_eq!(unsafe { (vt.generated_count)(session) }, 0);
398
399        // Call add_i32 — marks it as "used"
400        let result = unsafe { (vt.add_i32)(session, 10, 20) };
401        assert_eq!(result, 30);
402        assert_eq!(unsafe { (vt.generated_count)(session) }, 1);
403
404        // Call mul_f64 — marks it as "used"
405        let result = unsafe { (vt.mul_f64)(session, 3.0, 7.0) };
406        assert!((result - 21.0).abs() < 1e-10);
407        assert_eq!(unsafe { (vt.generated_count)(session) }, 2);
408
409        // Call pi
410        let pi_val = unsafe { (vt.pi)(session) };
411        assert!((pi_val - std::f64::consts::PI).abs() < 1e-10);
412        assert_eq!(unsafe { (vt.generated_count)(session) }, 3);
413
414        // Introspect generated functions
415        let func0 = unsafe { (vt.generated_at)(session, 0) };
416        let func0_name = unsafe { CStr::from_ptr(func0.name) }.to_str().unwrap();
417        assert_eq!(func0_name, "add_i32");
418
419        let func1 = unsafe { (vt.generated_at)(session, 1) };
420        let func1_name = unsafe { CStr::from_ptr(func1.name) }.to_str().unwrap();
421        assert_eq!(func1_name, "mul_f64");
422
423        let func2 = unsafe { (vt.generated_at)(session, 2) };
424        let func2_name = unsafe { CStr::from_ptr(func2.name) }.to_str().unwrap();
425        assert_eq!(func2_name, "pi");
426
427        // Call add_i32 again — should NOT add duplicate
428        let _ = unsafe { (vt.add_i32)(session, 1, 2) };
429        assert_eq!(unsafe { (vt.generated_count)(session) }, 3);
430
431        // Destroy session
432        unsafe { (vt.destroy_session)(session) };
433    }
434}