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//! ## Cross-module memory ownership model
13//!
14//! The core of every cross-module boundary is **who manages memory**. Every
15//! pointer crossing the boundary must be paired with a deallocation function
16//! that lives in the module which allocated the memory (allocators are not
17//! guaranteed to match across module/CRT boundaries). This crate provides
18//! three ownership tiers, all following that rule:
19//!
20//! | Tier | Type | Ownership | Free function |
21//! |---|---|---|---|
22//! | Stateless | [`VTablePlugin<T>`] | none (static vtable) | — |
23//! | Instance (multi-owner) | [`CdynHandle<T>`] | ref-counted via `retain`/`release` fn ptrs in `AbiStableDynRef` | provided by the plugin |
24//! | Data (single-owner) | [`CdynBox`] / [`CdynBoxHandle`] | move-only, exactly one owner | `free` fn ptr in the box, provided by the allocating module |
25//!
26//! Cross-module safety rules enforced by construction:
27//!
28//! 1. **Allocator symmetry** — `free`/`release` always execute inside the
29//!    module that allocated (the function pointer belongs to that module's
30//!    code, e.g. `CdynBox::from_vec` pairs with a Rust-plugin allocator).
31//! 2. **Library lifetime** — [`CdynBoxHandle`] and [`CdynHandle<T>`] own a
32//!    [`DynLib`](crate::DynLib) (Arc-shared), so the library cannot be
33//!    unloaded while a handle (and thus a free/release fn ptr) still exists.
34//!
35//! ## Usage
36//!
37//! ```ignore
38//! #[repr(C)]
39//! struct MyVtable {
40//!     add: unsafe extern "C" fn(i32, i32) -> i32,
41//!     name: unsafe extern "C" fn() -> *const std::ffi::c_char,
42//! }
43//!
44//! let plugin = unsafe { VTablePlugin::<MyVtable>::load("libmy.so", b"my_get_vtable\0")? };
45//! let n = unsafe { (plugin.vtable().add)(1, 2) };
46//! ```
47
48use std::ffi::c_char;
49use std::marker::PhantomData;
50use std::path::Path;
51
52use anyhow::{Context, Result};
53
54use crate::DynLib;
55use crate::dyn_mod::{AbiStableDynRef, PluginEntryPoint};
56
57// ---------------------------------------------------------------------------
58// VTablePlugin — simpler vtable-based loading (Copy types only)
59// ---------------------------------------------------------------------------
60
61/// Load a Copy-sized vtable/descriptor struct from a dynamic library.
62pub struct VTablePlugin<T: Copy> {
63    _lib: DynLib,
64    vtable: T,
65}
66
67impl<T: Copy> VTablePlugin<T> {
68    /// Load a vtable struct from a dynamic library.
69    ///
70    /// # Safety
71    ///
72    /// - The target file must be a valid dynamic library.
73    /// - The symbol must refer to a static vtable-compatible value of type `T`.
74    pub unsafe fn load(path: &Path, symbol: &[u8]) -> Result<Self> {
75        let lib = unsafe { DynLib::load(path) }?;
76        unsafe { Self::from_lib(lib, symbol) }
77    }
78
79    /// # Safety
80    ///
81    /// The symbol must refer to a static vtable-compatible value of type `T`.
82    pub unsafe fn from_lib(lib: DynLib, symbol: &[u8]) -> Result<Self> {
83        let getter: unsafe extern "C" fn() -> *const T = unsafe { lib.symbol(symbol) }?;
84        let vtable = unsafe { getter() };
85        let vtable = unsafe { vtable.as_ref() }
86            .copied()
87            .ok_or_else(|| anyhow::anyhow!("vtable getter returned null"))?;
88        Ok(Self { _lib: lib, vtable })
89    }
90
91    pub fn vtable(&self) -> &T {
92        &self.vtable
93    }
94}
95
96// ---------------------------------------------------------------------------
97// C++ math module VTable ABI — matches cpp/math_module/include/math_vtable.h
98// ---------------------------------------------------------------------------
99
100/// Opaque handle for a C++ MathSession
101pub type MathSession = *mut std::ffi::c_void;
102
103/// Descriptor for a generated math function (matches C++ GeneratedFunction)
104#[repr(C)]
105#[derive(Debug, Clone, Copy)]
106pub struct GeneratedFunction {
107    pub name: *const c_char,
108    pub signature: *const c_char,
109    pub arg_count: u32,
110    pub id: u32,
111}
112
113/// Vtable struct matching C++ MathModuleVtable layout exactly
114#[repr(C)]
115#[derive(Debug, Clone, Copy)]
116pub struct MathModuleVtable {
117    // Session lifecycle
118    pub create_session: unsafe extern "C" fn() -> MathSession,
119    pub destroy_session: unsafe extern "C" fn(MathSession),
120
121    // Math operations (lazy — only "generated" if called)
122    pub add_i32: unsafe extern "C" fn(MathSession, i32, i32) -> i32,
123    pub add_i64: unsafe extern "C" fn(MathSession, i64, i64) -> i64,
124    pub add_f64: unsafe extern "C" fn(MathSession, f64, f64) -> f64,
125    pub mul_i32: unsafe extern "C" fn(MathSession, i32, i32) -> i32,
126    pub mul_f64: unsafe extern "C" fn(MathSession, f64, f64) -> f64,
127    pub pi: unsafe extern "C" fn(MathSession) -> f64,
128    pub tau: unsafe extern "C" fn(MathSession) -> f64,
129
130    // Code-generation introspection
131    pub generated_count: unsafe extern "C" fn(MathSession) -> u32,
132    pub generated_at: unsafe extern "C" fn(MathSession, u32) -> GeneratedFunction,
133
134    // Module info
135    pub module_name: unsafe extern "C" fn() -> *const c_char,
136    pub module_version: unsafe extern "C" fn() -> u32,
137}
138
139// SAFETY: MathModuleVtable contains only function pointers and is safe to Send/Sync
140unsafe impl Send for MathModuleVtable {}
141unsafe impl Sync for MathModuleVtable {}
142
143// ---------------------------------------------------------------------------
144// CdynBox — cross-module data box (single owner + free fn ptr)
145// ---------------------------------------------------------------------------
146
147/// A cross-module data box: memory allocated and freed by the **same** module.
148///
149/// This is the cdyn mode's raw-data smart pointer, complementing the
150/// ref-counted instance handle [`CdynHandle<T>`]:
151///
152/// - Instance objects (with vtables) → ref-counted, use `AbiStableDynRef`'s
153///   `retain`/`release` via [`CdynHandle<T>`].
154/// - Raw data buffers (payloads, serialized blobs, arrays) → single-owner,
155///   use this struct's `free` function pointer.
156///
157/// Both share the same principle: the deallocator lives in the module that
158/// allocated the memory, so allocator mismatches across module/CRT boundaries
159/// (notably on Windows) can never corrupt the heap.
160///
161/// The box is **move-only**: there is exactly one owner, and dropping it
162/// (or explicitly calling `free`) hands the memory back to its origin module.
163/// Ref-counted sharing of data should be modeled as an instance instead.
164///
165/// ## C/C++/Zig side
166///
167/// Any language can produce a box: allocate, fill, and export
168/// `struct { void* data; size_t len; void (*free)(void*, size_t); }`
169/// where `free` calls the module's own allocator (C++: `operator delete[]`
170/// / `std::free`, Zig: `allocator.free`). The layout is `#[repr(C)]` /
171/// plain C struct.
172#[repr(C)]
173#[derive(Debug, Clone, Copy)]
174pub struct CdynBox {
175    /// Pointer to the data. Allocated by the producing module.
176    pub data: *mut std::ffi::c_void,
177    /// Length in **bytes**.
178    pub len: usize,
179    /// Frees `data`. **Must** be a function from the module that allocated it.
180    pub free: unsafe extern "C" fn(data: *mut std::ffi::c_void, len: usize),
181}
182
183impl CdynBox {
184    /// A null box (no data, free is a no-op).
185    pub const fn null() -> Self {
186        Self {
187            data: std::ptr::null_mut(),
188            len: 0,
189            free: cdyn_box_free_noop,
190        }
191    }
192
193    pub fn is_null(&self) -> bool {
194        self.data.is_null()
195    }
196
197    /// **Plugin side (Rust)** — wrap an owned `Vec<u8>` into a box.
198    ///
199    /// The buffer is exact-fit (`into_boxed_slice`), so `len == capacity` and
200    /// the paired [`cdyn_box_free_rust`] can reconstruct and free it. The
201    /// returned `free` pointer executes in the plugin's code — the module
202    /// that owns the allocation.
203    pub fn from_vec(v: Vec<u8>) -> Self {
204        let boxed: Box<[u8]> = v.into_boxed_slice();
205        let len = boxed.len();
206        let data = Box::into_raw(boxed) as *mut std::ffi::c_void;
207        Self {
208            data,
209            len,
210            free: cdyn_box_free_rust,
211        }
212    }
213
214    /// View the contents as a byte slice.
215    ///
216    /// # Safety
217    ///
218    /// `data` must point to `len` readable bytes for the lifetime of `&self`.
219    pub unsafe fn as_slice(&self) -> &[u8] {
220        if self.data.is_null() {
221            &[]
222        } else {
223            unsafe { std::slice::from_raw_parts(self.data as *const u8, self.len) }
224        }
225    }
226
227    /// Hand ownership back to the caller (no `free` on drop afterwards).
228    pub fn into_raw(self) -> (Self, bool) {
229        let consumed = !self.is_null();
230        (self, consumed)
231    }
232}
233
234// SAFETY: CdynBox is a raw pointer + length + fn pointer; thread-safety is
235// the plugin's declared guarantee (same policy as AbiStableDynRef).
236unsafe impl Send for CdynBox {}
237unsafe impl Sync for CdynBox {}
238
239unsafe extern "C" fn cdyn_box_free_noop(_: *mut std::ffi::c_void, _: usize) {}
240
241/// **Rust plugin** free function paired with [`CdynBox::from_vec`].
242///
243/// Executes in the plugin module; reconstructs the exact-fit boxed slice and
244/// drops it with the plugin's own allocator.
245pub unsafe extern "C" fn cdyn_box_free_rust(
246    data: *mut std::ffi::c_void,
247    len: usize,
248) {
249    if data.is_null() {
250        return;
251    }
252    let slice_ptr = std::slice::from_raw_parts_mut(data as *mut u8, len) as *mut [u8];
253    drop(unsafe { Box::from_raw(slice_ptr) });
254}
255
256/// Host-side owning handle over a [`CdynBox`] received from a foreign module.
257///
258/// Drop → calls the box's `free` (the **producer module's** deallocator).
259/// The handle optionally owns the originating [`DynLib`](crate::DynLib) so the
260/// library stays loaded while the `free` function pointer is live — the
261/// cross-module-safety guarantee.
262pub struct CdynBoxHandle {
263    _lib: Option<DynLib>,
264    inner: Option<CdynBox>,
265}
266
267impl CdynBoxHandle {
268    /// Adopt a box received across the boundary, keeping `lib` loaded.
269    pub fn from_box(box_: CdynBox, lib: DynLib) -> Self {
270        Self {
271            _lib: Some(lib),
272            inner: Some(box_),
273        }
274    }
275
276    /// Adopt a box whose originating library is kept alive by other means.
277    ///
278    /// # Safety
279    ///
280    /// The caller must guarantee the producer library outlives this handle.
281    pub unsafe fn from_box_unowned(box_: CdynBox) -> Self {
282        Self {
283            _lib: Some(DynLib::unowned()),
284            inner: Some(box_),
285        }
286    }
287
288    /// Load from a dynamic library entry point returning a `CdynBox`.
289    ///
290    /// # Safety
291    ///
292    /// The entry must return a valid `CdynBox` whose `free` belongs to that
293    /// library.
294    pub unsafe fn load(path: &Path, symbol: &[u8]) -> Result<Self> {
295        let lib = unsafe { DynLib::load(path) }?;
296        unsafe { Self::from_lib(lib, symbol) }
297    }
298
299    /// # Safety
300    ///
301    /// The entry must return a valid `CdynBox` whose `free` belongs to `lib`.
302    pub unsafe fn from_lib(lib: DynLib, symbol: &[u8]) -> Result<Self> {
303        let getter: unsafe extern "C" fn() -> CdynBox = unsafe { lib.symbol(symbol) }?;
304        let box_ = unsafe { getter() };
305        if box_.is_null() {
306            anyhow::bail!("box entry returned null CdynBox");
307        }
308        Ok(Self::from_box(box_, lib))
309    }
310
311    /// View the contents.
312    pub fn as_slice(&self) -> &[u8] {
313        match &self.inner {
314            Some(b) => unsafe { b.as_slice() },
315            None => &[],
316        }
317    }
318
319    pub fn len(&self) -> usize {
320        self.inner.as_ref().map_or(0, |b| b.len)
321    }
322
323    pub fn is_empty(&self) -> bool {
324        self.len() == 0
325    }
326
327    /// Release ownership of the raw box without freeing
328    /// (the caller becomes responsible for calling `free`).
329    pub fn into_raw(mut self) -> CdynBox {
330        self.inner.take().unwrap_or_else(CdynBox::null)
331    }
332}
333
334impl std::ops::Deref for CdynBoxHandle {
335    type Target = [u8];
336    fn deref(&self) -> &[u8] {
337        self.as_slice()
338    }
339}
340
341impl Drop for CdynBoxHandle {
342    fn drop(&mut self) {
343        if let Some(box_) = self.inner.take() {
344            if !box_.is_null() {
345                // Executes in the producer module — never our allocator.
346                unsafe { (box_.free)(box_.data, box_.len) };
347            }
348        }
349    }
350}
351
352// SAFETY: the box's memory and free fn are governed by the producer library,
353// which the handle keeps loaded (or the caller promised to).
354unsafe impl Send for CdynBoxHandle {}
355unsafe impl Sync for CdynBoxHandle {}
356
357#[cfg(test)]
358mod cdyn_handle_tests {
359    use super::*;
360    use std::sync::atomic::{AtomicU32, Ordering};
361
362    // A minimal "foreign-style" plugin simulated in-process:
363    // static instance + atomic ref-count + vtable of thunks — exactly the
364    // pattern the C++ CdynExposed / Zig CdynPlugin generate.
365    static INSTANCE: u64 = 0xdead_beef;
366    static REFCOUNT: AtomicU32 = AtomicU32::new(0);
367
368    #[repr(C)]
369    #[derive(Clone, Copy)]
370    struct TestVtable {
371        get_value: unsafe extern "C" fn(ctx: *mut std::ffi::c_void) -> u64,
372    }
373
374    unsafe extern "C" fn test_get_value(ctx: *mut std::ffi::c_void) -> u64 {
375        // simulate thunk: ctx -> instance
376        let _ = ctx;
377        INSTANCE
378    }
379
380    unsafe extern "C" fn test_retain(_: AbiStableDynRef__FatPtr) {
381        REFCOUNT.fetch_add(1, Ordering::SeqCst);
382    }
383
384    unsafe extern "C" fn test_release(_: AbiStableDynRef__FatPtr) {
385        let prev = REFCOUNT.fetch_sub(1, Ordering::SeqCst);
386        assert!(prev > 0, "release called more times than retain");
387    }
388
389    // alias so the fns match the RetainFn/ReleaseFn signatures
390    type AbiStableDynRef__FatPtr = crate::dyn_mod::AbiDynFatPtr;
391
392    static TEST_VTABLE: TestVtable = TestVtable { get_value: test_get_value };
393
394    #[test]
395    fn cdyn_handle_retain_release_roundtrip() {
396        REFCOUNT.store(1, Ordering::SeqCst); // plugin starts with 1 ref
397
398        let raw = AbiStableDynRef {
399            object: crate::dyn_mod::AbiDynFatPtr {
400                data: &INSTANCE as *const u64 as *const std::ffi::c_void,
401                vtable: &TEST_VTABLE as *const TestVtable as *const std::ffi::c_void,
402            },
403            retain: test_retain,
404            release: test_release,
405        };
406
407        // from_raw (unowned lib)
408        let h1 = unsafe { CdynHandle::<TestVtable>::from_raw(raw) };
409        assert_eq!(REFCOUNT.load(Ordering::SeqCst), 1);
410
411        // clone → retain
412        let h2 = h1.clone();
413        assert_eq!(REFCOUNT.load(Ordering::SeqCst), 2);
414
415        // vtable call through the handle
416        unsafe {
417            let vt = h1.vtable();
418            assert_eq!((vt.get_value)(h1.ctx()), INSTANCE);
419        }
420        // same vtable pointer via h2
421        assert_eq!(h1.as_raw().object.vtable, h2.as_raw().object.vtable);
422
423        // drop → release
424        drop(h2);
425        assert_eq!(REFCOUNT.load(Ordering::SeqCst), 1);
426        drop(h1);
427        assert_eq!(REFCOUNT.load(Ordering::SeqCst), 0);
428    }
429
430    #[test]
431    fn cdyn_handle_into_raw_skips_release() {
432        REFCOUNT.store(1, Ordering::SeqCst);
433        let raw = AbiStableDynRef {
434            object: crate::dyn_mod::AbiDynFatPtr {
435                data: &INSTANCE as *const u64 as *const std::ffi::c_void,
436                vtable: &TEST_VTABLE as *const TestVtable as *const std::ffi::c_void,
437            },
438            retain: test_retain,
439            release: test_release,
440        };
441        let h = unsafe { CdynHandle::<TestVtable>::from_raw(raw) };
442        let raw2 = h.into_raw(); // must NOT call release
443        drop(raw2); // plain Copy struct, no Drop
444        assert_eq!(REFCOUNT.load(Ordering::SeqCst), 1); // unchanged
445        // give the ref back to the "plugin" to balance counts
446        REFCOUNT.fetch_sub(1, Ordering::SeqCst);
447    }
448
449    // ---- CdynBox: single-owner data with producer-side free fn ----
450
451    static BOX_FREED: AtomicU32 = AtomicU32::new(0);
452
453    unsafe extern "C" fn counting_free(data: *mut std::ffi::c_void, len: usize) {
454        BOX_FREED.fetch_add(1, Ordering::SeqCst);
455        // still actually free (exact-fit boxed slice, same as from_vec)
456        if !data.is_null() {
457            let slice_ptr = std::slice::from_raw_parts_mut(data as *mut u8, len) as *mut [u8];
458            drop(unsafe { Box::from_raw(slice_ptr) });
459        }
460    }
461
462    #[test]
463    fn cdyn_box_drop_calls_producer_free() {
464        BOX_FREED.store(0, Ordering::SeqCst);
465        let payload: Box<[u8]> = vec![1u8, 2, 3, 4].into_boxed_slice();
466        let len = payload.len();
467        let data = Box::into_raw(payload) as *mut std::ffi::c_void;
468        let box_ = CdynBox {
469            data,
470            len,
471            free: counting_free,
472        };
473
474        let handle = unsafe { CdynBoxHandle::from_box_unowned(box_) };
475        assert_eq!(handle.as_slice(), &[1, 2, 3, 4]);
476        assert_eq!(handle.len(), 4);
477        assert_eq!(BOX_FREED.load(Ordering::SeqCst), 0);
478
479        drop(handle); // → producer's free
480        assert_eq!(BOX_FREED.load(Ordering::SeqCst), 1);
481    }
482
483    #[test]
484    fn cdyn_box_from_vec_roundtrip_and_free() {
485        // Plugin side: from_vec pairs with cdyn_box_free_rust (same module).
486        let box_ = CdynBox::from_vec(b"hello cross-module".to_vec());
487        assert_eq!(box_.len, 18);
488        let handle = unsafe { CdynBoxHandle::from_box_unowned(box_) };
489        assert_eq!(&*handle, b"hello cross-module");
490        drop(handle); // cdyn_box_free_rust runs — no leak, no allocator mismatch
491
492        // into_raw transfers ownership: free must NOT run on drop
493        BOX_FREED.store(0, Ordering::SeqCst);
494        let box_ = CdynBox {
495            data: Box::into_raw(vec![9u8; 4].into_boxed_slice()) as *mut std::ffi::c_void,
496            len: 4,
497            free: counting_free,
498        };
499        let handle = unsafe { CdynBoxHandle::from_box_unowned(box_) };
500        let raw = handle.into_raw();
501        assert_eq!(BOX_FREED.load(Ordering::SeqCst), 0);
502        // caller now frees manually
503        unsafe { (raw.free)(raw.data, raw.len) };
504        assert_eq!(BOX_FREED.load(Ordering::SeqCst), 1);
505    }
506
507    #[test]
508    fn cdyn_box_null_is_safe() {
509        let handle = unsafe { CdynBoxHandle::from_box_unowned(CdynBox::null()) };
510        assert!(handle.is_empty());
511        assert!(handle.as_slice().is_empty());
512        drop(handle); // free is noop, no panic
513    }
514}
515
516// ---------------------------------------------------------------------------
517// CdynHandle — cross-language ref-counted smart handle
518// ---------------------------------------------------------------------------
519
520/// Ref-counted handle to a **foreign** plugin object exposed through a
521/// `*_get_dyn` style entry point returning an [`AbiStableDynRef`].
522///
523/// This is the Rust-side counterpart of the C++ SDK's `CdynExposed` /
524/// `CdynPlugin` and the Zig SDK's `CdynPlugin(PluginType)`:
525///
526/// - [`clone`](Clone::clone) → calls the plugin's `retain`
527/// - [`drop`](Drop) → calls the plugin's `release`
528/// - [`ctx`](Self::ctx) → the instance pointer (first arg of every vtable method)
529/// - [`vtable`](Self::vtable) → `&T` reconstructed from the packed vtable pointer
530///
531/// `T` is the C-layout vtable struct (`#[repr(C)]`, `Copy`) — e.g.
532/// [`MathModuleVtable`] or your own. The vtable is **not** copied; it is
533/// referenced through the pointer packed inside the dyn ref (it points into
534/// the plugin's static storage, valid while the library stays loaded).
535///
536/// # Example
537///
538/// ```ignore
539/// // C++ plugin built with: CDYN_EXPOSE_CLASS(MathPlugin, MathModuleVtable, math)
540/// // exports: math_get_vtable() and math_get_dyn()
541/// let handle = unsafe { CdynHandle::<MathModuleVtable>::load(&path, b"math_get_dyn\0")? };
542/// let vt = unsafe { handle.vtable() };
543/// let session = unsafe { (vt.create_session)() };
544/// let sum = unsafe { (vt.add_i32)(session, 1, 2) };
545/// unsafe { (vt.destroy_session)(session) };
546/// // handle drop → plugin release()
547/// ```
548pub struct CdynHandle<T: Copy> {
549    _lib: DynLib,
550    raw: AbiStableDynRef,
551    _marker: PhantomData<T>,
552}
553
554impl<T: Copy> CdynHandle<T> {
555    /// Load a foreign plugin object via its dyn entry point.
556    ///
557    /// # Safety
558    ///
559    /// - The target file must be a valid dynamic library.
560    /// - The entry must return a valid `AbiStableDynRef` whose vtable pointer
561    ///   refers to a `T`-layout vtable.
562    pub unsafe fn load(path: &Path, dyn_entry: &[u8]) -> Result<Self> {
563        let lib = unsafe { DynLib::load(path) }?;
564        unsafe { Self::from_lib(lib, dyn_entry) }
565    }
566
567    /// Load from an already-loaded library.
568    ///
569    /// # Safety
570    ///
571    /// The entry must return a valid `AbiStableDynRef` whose vtable pointer
572    /// refers to a `T`-layout vtable.
573    pub unsafe fn from_lib(lib: DynLib, dyn_entry: &[u8]) -> Result<Self> {
574        let entry: PluginDynEntryPoint =
575            unsafe { lib.symbol(dyn_entry) }.with_context(|| {
576                format!("dyn entry '{}' not found", crate::helpers::display_symbol(dyn_entry))
577            })?;
578        let raw = unsafe { entry() };
579        if raw.is_null() {
580            anyhow::bail!("dyn entry returned null AbiStableDynRef");
581        }
582        Ok(Self {
583            _lib: lib,
584            raw,
585            _marker: PhantomData,
586        })
587    }
588
589    /// Reconstruct from a raw [`AbiStableDynRef`] obtained elsewhere.
590    ///
591    /// The handle does not own the originating library; the caller must keep
592    /// it loaded (e.g. hold another [`DynPlugin`](crate::DynPlugin) or
593    /// [`DynLib`](crate::DynLib)) for as long as this handle lives.
594    ///
595    /// # Safety
596    ///
597    /// `raw` must be a live ref produced by a compatible plugin.
598    pub unsafe fn from_raw(raw: AbiStableDynRef) -> Self {
599        Self {
600            _lib: DynLib::unowned(),
601            raw,
602            _marker: PhantomData,
603        }
604    }
605
606    /// Instance context pointer — pass as the first argument of vtable methods.
607    pub fn ctx(&self) -> *mut std::ffi::c_void {
608        self.raw.object.data as *mut std::ffi::c_void
609    }
610
611    /// The vtable, reconstructed from the pointer packed inside the dyn ref.
612    ///
613    /// # Safety
614    ///
615    /// `T` must be the exact vtable type the plugin used when building the ref.
616    pub unsafe fn vtable(&self) -> &T {
617        unsafe { &*(self.raw.object.vtable as *const T) }
618    }
619
620    /// Raw ABI ref (for passing back across the boundary).
621    pub fn as_raw(&self) -> &AbiStableDynRef {
622        &self.raw
623    }
624
625    /// Consume without calling `release` (ownership handed back to the plugin).
626    pub fn into_raw(self) -> AbiStableDynRef {
627        let mut this = std::mem::ManuallyDrop::new(self);
628        // Detach the library handle so Drop won't run for it either.
629        let lib = unsafe { std::ptr::read(&this._lib) };
630        std::mem::forget(lib);
631        this.raw
632    }
633}
634
635/// Type of the dyn entry point that foreign plugins export
636/// (C++ `CDYN_EXPORT AbiStableDynRef name_get_dyn()`, Zig `declareDynEntry`).
637pub type PluginDynEntryPoint = PluginEntryPoint;
638
639// SAFETY: CdynHandle manages the plugin's own ref-count via retain/release;
640// thread-safety follows the plugin's guarantees (same policy as SafeArcDyn).
641unsafe impl<T: Copy + Send> Send for CdynHandle<T> {}
642unsafe impl<T: Copy + Sync> Sync for CdynHandle<T> {}
643
644impl<T: Copy> Clone for CdynHandle<T> {
645    fn clone(&self) -> Self {
646        unsafe { (self.raw.retain)(self.raw.object) };
647        Self {
648            _lib: self._lib.clone(),
649            raw: self.raw,
650            _marker: PhantomData,
651        }
652    }
653}
654
655impl<T: Copy> Drop for CdynHandle<T> {
656    fn drop(&mut self) {
657        unsafe { (self.raw.release)(self.raw.object) };
658    }
659}
660
661#[cfg(test)]
662mod cpp_math_tests {
663    use super::*;
664    use std::ffi::CStr;
665    use std::path::PathBuf;
666
667    fn math_module_path() -> PathBuf {
668        // Relative from crate root (rust/crates/dyn-loader) to cpp build output
669        let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
670        p.push("../../cpp/build/math_module");
671        p.push("libmath_module.so");
672        p
673    }
674
675    #[test]
676    fn load_cpp_math_module_via_vtable() {
677        let path = math_module_path();
678        if !path.exists() {
679            eprintln!("SKIP: {} not found — build cpp/ first", path.display());
680            return;
681        }
682
683        let plugin =
684            unsafe { VTablePlugin::<MathModuleVtable>::load(&path, b"math_module_get_vtable\0") }
685                .expect("failed to load math_module");
686        let vt = plugin.vtable();
687
688        // Module info
689        unsafe {
690            let name = CStr::from_ptr((vt.module_name)());
691            assert_eq!(name.to_str().unwrap(), "core-ast-math");
692            assert_eq!((vt.module_version)(), 1);
693        }
694
695        // Create session
696        let session = unsafe { (vt.create_session)() };
697        assert!(!session.is_null());
698
699        // No functions generated yet
700        assert_eq!(unsafe { (vt.generated_count)(session) }, 0);
701
702        // Call add_i32 — marks it as "used"
703        let result = unsafe { (vt.add_i32)(session, 10, 20) };
704        assert_eq!(result, 30);
705        assert_eq!(unsafe { (vt.generated_count)(session) }, 1);
706
707        // Call mul_f64 — marks it as "used"
708        let result = unsafe { (vt.mul_f64)(session, 3.0, 7.0) };
709        assert!((result - 21.0).abs() < 1e-10);
710        assert_eq!(unsafe { (vt.generated_count)(session) }, 2);
711
712        // Call pi
713        let pi_val = unsafe { (vt.pi)(session) };
714        assert!((pi_val - std::f64::consts::PI).abs() < 1e-10);
715        assert_eq!(unsafe { (vt.generated_count)(session) }, 3);
716
717        // Introspect generated functions
718        let func0 = unsafe { (vt.generated_at)(session, 0) };
719        let func0_name = unsafe { CStr::from_ptr(func0.name) }.to_str().unwrap();
720        assert_eq!(func0_name, "add_i32");
721
722        let func1 = unsafe { (vt.generated_at)(session, 1) };
723        let func1_name = unsafe { CStr::from_ptr(func1.name) }.to_str().unwrap();
724        assert_eq!(func1_name, "mul_f64");
725
726        let func2 = unsafe { (vt.generated_at)(session, 2) };
727        let func2_name = unsafe { CStr::from_ptr(func2.name) }.to_str().unwrap();
728        assert_eq!(func2_name, "pi");
729
730        // Call add_i32 again — should NOT add duplicate
731        let _ = unsafe { (vt.add_i32)(session, 1, 2) };
732        assert_eq!(unsafe { (vt.generated_count)(session) }, 3);
733
734        // Destroy session
735        unsafe { (vt.destroy_session)(session) };
736    }
737}