dyn-loader 0.2.0

Dynamic library loader with dyn-fat-pointer-bridge for loading trait objects from .so/.dylib plugins. IMPORTANT: strictly align the Rust compiler version across all libs and executables to guarantee ABI compatibility.
Documentation
//! # cdyn — COM-style C function-table loading (ABI-stable)
//!
//! Load Copy-sized vtable/descriptor structs from dynamic libraries using
//! plain C function tables — no Rust trait objects, no named-based access,
//! purely positional dispatch. This is the COM+-like emulated vtable system
//! (formerly the separate `cdyn-loader` crate).
//!
//! Unlike [`crate::dyn_mod`], this mode is ABI-stable across languages:
//! the C++ SDK (`cdyn-loader-sdks/cpp`) and Zig SDK (`cdyn-loader-sdks/zig`)
//! build plugins that match these layouts exactly.
//!
//! ## Usage
//!
//! ```ignore
//! #[repr(C)]
//! struct MyVtable {
//!     add: unsafe extern "C" fn(i32, i32) -> i32,
//!     name: unsafe extern "C" fn() -> *const std::ffi::c_char,
//! }
//!
//! let plugin = unsafe { VTablePlugin::<MyVtable>::load("libmy.so", b"my_get_vtable\0")? };
//! let n = unsafe { (plugin.vtable().add)(1, 2) };
//! ```

use std::ffi::c_char;
use std::path::Path;

use anyhow::Result;

use crate::DynLib;

// ---------------------------------------------------------------------------
// VTablePlugin — simpler vtable-based loading (Copy types only)
// ---------------------------------------------------------------------------

/// Load a Copy-sized vtable/descriptor struct from a dynamic library.
pub struct VTablePlugin<T: Copy> {
    _lib: DynLib,
    vtable: T,
}

impl<T: Copy> VTablePlugin<T> {
    /// Load a vtable struct from a dynamic library.
    ///
    /// # Safety
    ///
    /// - The target file must be a valid dynamic library.
    /// - The symbol must refer to a static vtable-compatible value of type `T`.
    pub unsafe fn load(path: &Path, symbol: &[u8]) -> Result<Self> {
        let lib = unsafe { DynLib::load(path) }?;
        unsafe { Self::from_lib(lib, symbol) }
    }

    /// # Safety
    ///
    /// The symbol must refer to a static vtable-compatible value of type `T`.
    pub unsafe fn from_lib(lib: DynLib, symbol: &[u8]) -> Result<Self> {
        let getter: unsafe extern "C" fn() -> *const T = unsafe { lib.symbol(symbol) }?;
        let vtable = unsafe { getter() };
        let vtable = unsafe { vtable.as_ref() }
            .copied()
            .ok_or_else(|| anyhow::anyhow!("vtable getter returned null"))?;
        Ok(Self { _lib: lib, vtable })
    }

    pub fn vtable(&self) -> &T {
        &self.vtable
    }
}

// ---------------------------------------------------------------------------
// C++ math module VTable ABI — matches cpp/math_module/include/math_vtable.h
// ---------------------------------------------------------------------------

/// Opaque handle for a C++ MathSession
pub type MathSession = *mut std::ffi::c_void;

/// Descriptor for a generated math function (matches C++ GeneratedFunction)
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct GeneratedFunction {
    pub name: *const c_char,
    pub signature: *const c_char,
    pub arg_count: u32,
    pub id: u32,
}

/// Vtable struct matching C++ MathModuleVtable layout exactly
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct MathModuleVtable {
    // Session lifecycle
    pub create_session: unsafe extern "C" fn() -> MathSession,
    pub destroy_session: unsafe extern "C" fn(MathSession),

    // Math operations (lazy — only "generated" if called)
    pub add_i32: unsafe extern "C" fn(MathSession, i32, i32) -> i32,
    pub add_i64: unsafe extern "C" fn(MathSession, i64, i64) -> i64,
    pub add_f64: unsafe extern "C" fn(MathSession, f64, f64) -> f64,
    pub mul_i32: unsafe extern "C" fn(MathSession, i32, i32) -> i32,
    pub mul_f64: unsafe extern "C" fn(MathSession, f64, f64) -> f64,
    pub pi: unsafe extern "C" fn(MathSession) -> f64,
    pub tau: unsafe extern "C" fn(MathSession) -> f64,

    // Code-generation introspection
    pub generated_count: unsafe extern "C" fn(MathSession) -> u32,
    pub generated_at: unsafe extern "C" fn(MathSession, u32) -> GeneratedFunction,

    // Module info
    pub module_name: unsafe extern "C" fn() -> *const c_char,
    pub module_version: unsafe extern "C" fn() -> u32,
}

// SAFETY: MathModuleVtable contains only function pointers and is safe to Send/Sync
unsafe impl Send for MathModuleVtable {}
unsafe impl Sync for MathModuleVtable {}

#[cfg(test)]
mod cpp_math_tests {
    use super::*;
    use std::ffi::CStr;
    use std::path::PathBuf;

    fn math_module_path() -> PathBuf {
        // Relative from crate root (rust/crates/dyn-loader) to cpp build output
        let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        p.push("../../cpp/build/math_module");
        p.push("libmath_module.so");
        p
    }

    #[test]
    fn load_cpp_math_module_via_vtable() {
        let path = math_module_path();
        if !path.exists() {
            eprintln!("SKIP: {} not found — build cpp/ first", path.display());
            return;
        }

        let plugin =
            unsafe { VTablePlugin::<MathModuleVtable>::load(&path, b"math_module_get_vtable\0") }
                .expect("failed to load math_module");
        let vt = plugin.vtable();

        // Module info
        unsafe {
            let name = CStr::from_ptr((vt.module_name)());
            assert_eq!(name.to_str().unwrap(), "core-ast-math");
            assert_eq!((vt.module_version)(), 1);
        }

        // Create session
        let session = unsafe { (vt.create_session)() };
        assert!(!session.is_null());

        // No functions generated yet
        assert_eq!(unsafe { (vt.generated_count)(session) }, 0);

        // Call add_i32 — marks it as "used"
        let result = unsafe { (vt.add_i32)(session, 10, 20) };
        assert_eq!(result, 30);
        assert_eq!(unsafe { (vt.generated_count)(session) }, 1);

        // Call mul_f64 — marks it as "used"
        let result = unsafe { (vt.mul_f64)(session, 3.0, 7.0) };
        assert!((result - 21.0).abs() < 1e-10);
        assert_eq!(unsafe { (vt.generated_count)(session) }, 2);

        // Call pi
        let pi_val = unsafe { (vt.pi)(session) };
        assert!((pi_val - std::f64::consts::PI).abs() < 1e-10);
        assert_eq!(unsafe { (vt.generated_count)(session) }, 3);

        // Introspect generated functions
        let func0 = unsafe { (vt.generated_at)(session, 0) };
        let func0_name = unsafe { CStr::from_ptr(func0.name) }.to_str().unwrap();
        assert_eq!(func0_name, "add_i32");

        let func1 = unsafe { (vt.generated_at)(session, 1) };
        let func1_name = unsafe { CStr::from_ptr(func1.name) }.to_str().unwrap();
        assert_eq!(func1_name, "mul_f64");

        let func2 = unsafe { (vt.generated_at)(session, 2) };
        let func2_name = unsafe { CStr::from_ptr(func2.name) }.to_str().unwrap();
        assert_eq!(func2_name, "pi");

        // Call add_i32 again — should NOT add duplicate
        let _ = unsafe { (vt.add_i32)(session, 1, 2) };
        assert_eq!(unsafe { (vt.generated_count)(session) }, 3);

        // Destroy session
        unsafe { (vt.destroy_session)(session) };
    }
}