Skip to main content

dyn_loader/
abi.rs

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