Skip to main content

dyn_loader/
cdyn.rs

1//! # cdyn — COM-style C function-table loading (ABI-stable)
2//!
3//! Load Copy-sized vtable/descriptor structs from dynamic libraries using
4//! plain C function tables — no Rust trait objects, no named-based access,
5//! purely positional dispatch. This is the COM+-like emulated vtable system
6//! (formerly the separate `cdyn-loader` crate).
7//!
8//! Unlike [`crate::dyn_mod`], this mode is ABI-stable across languages:
9//! the C++ SDK (`cdyn-loader-sdks/cpp`) and Zig SDK (`cdyn-loader-sdks/zig`)
10//! build plugins that match these layouts exactly.
11//!
12//! ## Usage
13//!
14//! ```ignore
15//! #[repr(C)]
16//! struct MyVtable {
17//!     add: unsafe extern "C" fn(i32, i32) -> i32,
18//!     name: unsafe extern "C" fn() -> *const std::ffi::c_char,
19//! }
20//!
21//! let plugin = unsafe { VTablePlugin::<MyVtable>::load("libmy.so", b"my_get_vtable\0")? };
22//! let n = unsafe { (plugin.vtable().add)(1, 2) };
23//! ```
24
25use std::ffi::c_char;
26use std::path::Path;
27
28use anyhow::Result;
29
30use crate::DynLib;
31
32// ---------------------------------------------------------------------------
33// VTablePlugin — simpler vtable-based loading (Copy types only)
34// ---------------------------------------------------------------------------
35
36/// Load a Copy-sized vtable/descriptor struct from a dynamic library.
37pub struct VTablePlugin<T: Copy> {
38    _lib: DynLib,
39    vtable: T,
40}
41
42impl<T: Copy> VTablePlugin<T> {
43    /// Load a vtable struct from a dynamic library.
44    ///
45    /// # Safety
46    ///
47    /// - The target file must be a valid dynamic library.
48    /// - The symbol must refer to a static vtable-compatible value of type `T`.
49    pub unsafe fn load(path: &Path, symbol: &[u8]) -> Result<Self> {
50        let lib = unsafe { DynLib::load(path) }?;
51        unsafe { Self::from_lib(lib, symbol) }
52    }
53
54    /// # Safety
55    ///
56    /// The symbol must refer to a static vtable-compatible value of type `T`.
57    pub unsafe fn from_lib(lib: DynLib, symbol: &[u8]) -> Result<Self> {
58        let getter: unsafe extern "C" fn() -> *const T = unsafe { lib.symbol(symbol) }?;
59        let vtable = unsafe { getter() };
60        let vtable = unsafe { vtable.as_ref() }
61            .copied()
62            .ok_or_else(|| anyhow::anyhow!("vtable getter returned null"))?;
63        Ok(Self { _lib: lib, vtable })
64    }
65
66    pub fn vtable(&self) -> &T {
67        &self.vtable
68    }
69}
70
71// ---------------------------------------------------------------------------
72// C++ math module VTable ABI — matches cpp/math_module/include/math_vtable.h
73// ---------------------------------------------------------------------------
74
75/// Opaque handle for a C++ MathSession
76pub type MathSession = *mut std::ffi::c_void;
77
78/// Descriptor for a generated math function (matches C++ GeneratedFunction)
79#[repr(C)]
80#[derive(Debug, Clone, Copy)]
81pub struct GeneratedFunction {
82    pub name: *const c_char,
83    pub signature: *const c_char,
84    pub arg_count: u32,
85    pub id: u32,
86}
87
88/// Vtable struct matching C++ MathModuleVtable layout exactly
89#[repr(C)]
90#[derive(Debug, Clone, Copy)]
91pub struct MathModuleVtable {
92    // Session lifecycle
93    pub create_session: unsafe extern "C" fn() -> MathSession,
94    pub destroy_session: unsafe extern "C" fn(MathSession),
95
96    // Math operations (lazy — only "generated" if called)
97    pub add_i32: unsafe extern "C" fn(MathSession, i32, i32) -> i32,
98    pub add_i64: unsafe extern "C" fn(MathSession, i64, i64) -> i64,
99    pub add_f64: unsafe extern "C" fn(MathSession, f64, f64) -> f64,
100    pub mul_i32: unsafe extern "C" fn(MathSession, i32, i32) -> i32,
101    pub mul_f64: unsafe extern "C" fn(MathSession, f64, f64) -> f64,
102    pub pi: unsafe extern "C" fn(MathSession) -> f64,
103    pub tau: unsafe extern "C" fn(MathSession) -> f64,
104
105    // Code-generation introspection
106    pub generated_count: unsafe extern "C" fn(MathSession) -> u32,
107    pub generated_at: unsafe extern "C" fn(MathSession, u32) -> GeneratedFunction,
108
109    // Module info
110    pub module_name: unsafe extern "C" fn() -> *const c_char,
111    pub module_version: unsafe extern "C" fn() -> u32,
112}
113
114// SAFETY: MathModuleVtable contains only function pointers and is safe to Send/Sync
115unsafe impl Send for MathModuleVtable {}
116unsafe impl Sync for MathModuleVtable {}
117
118#[cfg(test)]
119mod cpp_math_tests {
120    use super::*;
121    use std::ffi::CStr;
122    use std::path::PathBuf;
123
124    fn math_module_path() -> PathBuf {
125        // Relative from crate root (rust/crates/dyn-loader) to cpp build output
126        let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
127        p.push("../../cpp/build/math_module");
128        p.push("libmath_module.so");
129        p
130    }
131
132    #[test]
133    fn load_cpp_math_module_via_vtable() {
134        let path = math_module_path();
135        if !path.exists() {
136            eprintln!("SKIP: {} not found — build cpp/ first", path.display());
137            return;
138        }
139
140        let plugin =
141            unsafe { VTablePlugin::<MathModuleVtable>::load(&path, b"math_module_get_vtable\0") }
142                .expect("failed to load math_module");
143        let vt = plugin.vtable();
144
145        // Module info
146        unsafe {
147            let name = CStr::from_ptr((vt.module_name)());
148            assert_eq!(name.to_str().unwrap(), "core-ast-math");
149            assert_eq!((vt.module_version)(), 1);
150        }
151
152        // Create session
153        let session = unsafe { (vt.create_session)() };
154        assert!(!session.is_null());
155
156        // No functions generated yet
157        assert_eq!(unsafe { (vt.generated_count)(session) }, 0);
158
159        // Call add_i32 — marks it as "used"
160        let result = unsafe { (vt.add_i32)(session, 10, 20) };
161        assert_eq!(result, 30);
162        assert_eq!(unsafe { (vt.generated_count)(session) }, 1);
163
164        // Call mul_f64 — marks it as "used"
165        let result = unsafe { (vt.mul_f64)(session, 3.0, 7.0) };
166        assert!((result - 21.0).abs() < 1e-10);
167        assert_eq!(unsafe { (vt.generated_count)(session) }, 2);
168
169        // Call pi
170        let pi_val = unsafe { (vt.pi)(session) };
171        assert!((pi_val - std::f64::consts::PI).abs() < 1e-10);
172        assert_eq!(unsafe { (vt.generated_count)(session) }, 3);
173
174        // Introspect generated functions
175        let func0 = unsafe { (vt.generated_at)(session, 0) };
176        let func0_name = unsafe { CStr::from_ptr(func0.name) }.to_str().unwrap();
177        assert_eq!(func0_name, "add_i32");
178
179        let func1 = unsafe { (vt.generated_at)(session, 1) };
180        let func1_name = unsafe { CStr::from_ptr(func1.name) }.to_str().unwrap();
181        assert_eq!(func1_name, "mul_f64");
182
183        let func2 = unsafe { (vt.generated_at)(session, 2) };
184        let func2_name = unsafe { CStr::from_ptr(func2.name) }.to_str().unwrap();
185        assert_eq!(func2_name, "pi");
186
187        // Call add_i32 again — should NOT add duplicate
188        let _ = unsafe { (vt.add_i32)(session, 1, 2) };
189        assert_eq!(unsafe { (vt.generated_count)(session) }, 3);
190
191        // Destroy session
192        unsafe { (vt.destroy_session)(session) };
193    }
194}