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}