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}