use std::ffi::{c_char, c_void};
use std::marker::PhantomData;
use std::path::Path;
use std::sync::Arc;
use anyhow::{Context, Result};
use libloading::Library;
#[derive(Clone)]
pub struct DynLib {
library: Arc<Library>,
path: std::path::PathBuf,
}
impl DynLib {
pub unsafe fn load(path: &Path) -> Result<Self> {
let library = unsafe { Library::new(path) }
.with_context(|| format!("failed to load dynamic library: {}", path.display()))?;
Ok(Self {
library: Arc::new(library),
path: path.to_path_buf(),
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub unsafe fn symbol<T: Copy>(&self, name: &[u8]) -> Result<T> {
let sym = unsafe { self.library.get::<T>(name) }.with_context(|| {
format!(
"symbol '{}' not found in {}",
display_symbol(name),
self.path.display()
)
})?;
Ok(*sym)
}
pub unsafe fn try_symbol<T: Copy>(&self, name: &[u8]) -> Option<T> {
unsafe { self.library.get::<T>(name) }.ok().map(|s| *s)
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AbiDynFatPtr {
pub data: *const c_void,
pub vtable: *const c_void,
}
impl AbiDynFatPtr {
pub const fn null() -> Self {
Self {
data: std::ptr::null(),
vtable: std::ptr::null(),
}
}
pub fn is_null(self) -> bool {
self.data.is_null() || self.vtable.is_null()
}
}
unsafe impl Send for AbiDynFatPtr {}
unsafe impl Sync for AbiDynFatPtr {}
pub type RetainFn = unsafe extern "C" fn(AbiDynFatPtr);
pub type ReleaseFn = unsafe extern "C" fn(AbiDynFatPtr);
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct AbiStableDynRef {
pub object: AbiDynFatPtr,
pub retain: RetainFn,
pub release: ReleaseFn,
}
impl AbiStableDynRef {
pub const fn null() -> Self {
Self {
object: AbiDynFatPtr::null(),
retain: retain_noop,
release: release_noop,
}
}
pub fn is_null(self) -> bool {
self.object.is_null()
}
}
unsafe impl Send for AbiStableDynRef {}
unsafe impl Sync for AbiStableDynRef {}
unsafe extern "C" fn retain_noop(_: AbiDynFatPtr) {}
unsafe extern "C" fn release_noop(_: AbiDynFatPtr) {}
pub unsafe fn pack_fat_ptr<T: ?Sized>(ptr: *const T) -> AbiDynFatPtr {
unsafe { std::mem::transmute_copy(&ptr) }
}
pub unsafe fn unpack_fat_ptr<T: ?Sized>(ptr: AbiDynFatPtr) -> *const T {
unsafe { std::mem::transmute_copy(&ptr) }
}
unsafe extern "C" fn retain_arc<T: ?Sized>(ptr: AbiDynFatPtr) {
let raw: *const T = unsafe { unpack_fat_ptr(ptr) };
unsafe { Arc::increment_strong_count(raw) };
}
unsafe extern "C" fn release_arc<T: ?Sized>(ptr: AbiDynFatPtr) {
let raw: *const T = unsafe { unpack_fat_ptr(ptr) };
unsafe { drop(Arc::from_raw(raw)) };
}
#[repr(transparent)]
pub struct SafeArcDyn<T: ?Sized> {
raw: AbiStableDynRef,
_marker: PhantomData<*const T>,
}
impl<T: ?Sized> SafeArcDyn<T> {
pub fn from_arc(value: Arc<T>) -> Self {
let raw = Arc::into_raw(value);
Self {
raw: AbiStableDynRef {
object: unsafe { pack_fat_ptr(raw) },
retain: retain_arc::<T>,
release: release_arc::<T>,
},
_marker: PhantomData,
}
}
pub fn into_abi(self) -> AbiStableDynRef {
let raw = self.raw;
std::mem::forget(self); raw
}
pub unsafe fn from_abi(raw: AbiStableDynRef) -> Self {
Self {
raw,
_marker: PhantomData,
}
}
pub unsafe fn trait_ref(&self) -> &T {
unsafe { &*unpack_fat_ptr::<T>(self.raw.object) }
}
}
unsafe impl<T: ?Sized + Send> Send for SafeArcDyn<T> {}
unsafe impl<T: ?Sized + Sync> Sync for SafeArcDyn<T> {}
impl<T: ?Sized> Clone for SafeArcDyn<T> {
fn clone(&self) -> Self {
unsafe { (self.raw.retain)(self.raw.object) };
Self {
raw: self.raw,
_marker: PhantomData,
}
}
}
impl<T: ?Sized> Drop for SafeArcDyn<T> {
fn drop(&mut self) {
unsafe { (self.raw.release)(self.raw.object) };
}
}
pub struct DynPlugin<T: ?Sized> {
_lib: DynLib,
plugin: SafeArcDyn<T>,
}
pub type PluginEntryPoint = unsafe extern "C" fn() -> AbiStableDynRef;
impl<T: ?Sized> DynPlugin<T> {
pub unsafe fn load(path: &Path, entry_symbol: &[u8]) -> Result<Self> {
let lib = unsafe { DynLib::load(path) }?;
unsafe { Self::from_lib(lib, entry_symbol) }
}
pub unsafe fn from_lib(lib: DynLib, entry_symbol: &[u8]) -> Result<Self> {
let entry: PluginEntryPoint = unsafe { lib.symbol(entry_symbol) }
.with_context(|| format!("entry point '{}' not found", display_symbol(entry_symbol)))?;
let abi_ref = unsafe { entry() };
if abi_ref.is_null() {
anyhow::bail!("entry point returned null AbiStableDynRef");
}
let plugin = unsafe { SafeArcDyn::<T>::from_abi(abi_ref) };
Ok(Self { _lib: lib, plugin })
}
pub fn trait_ref(&self) -> &T {
unsafe { self.plugin.trait_ref() }
}
pub fn clone_handle(&self) -> SafeArcDyn<T> {
self.plugin.clone()
}
}
unsafe impl<T: ?Sized + Send> Send for DynPlugin<T> {}
unsafe impl<T: ?Sized + Sync> Sync for DynPlugin<T> {}
pub struct VTablePlugin<T: Copy> {
_lib: DynLib,
vtable: T,
}
impl<T: Copy> VTablePlugin<T> {
pub unsafe fn load(path: &Path, symbol: &[u8]) -> Result<Self> {
let lib = unsafe { DynLib::load(path) }?;
unsafe { Self::from_lib(lib, symbol) }
}
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
}
}
fn display_symbol(symbol: &[u8]) -> String {
let end = symbol.iter().position(|&b| b == 0).unwrap_or(symbol.len());
String::from_utf8_lossy(&symbol[..end]).into_owned()
}
pub unsafe fn looks_like_plugin(path: &Path, entry_symbol: &[u8]) -> bool {
match unsafe { DynLib::load(path) } {
Ok(lib) => unsafe { lib.try_symbol::<PluginEntryPoint>(entry_symbol) }.is_some(),
Err(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
trait Demo: Send + Sync {
fn value(&self) -> i32;
}
struct DemoValue(i32);
impl Demo for DemoValue {
fn value(&self) -> i32 {
self.0
}
}
#[test]
fn safe_arc_dyn_round_trip() {
let arc: Arc<dyn Demo> = Arc::new(DemoValue(42));
let wrapped = SafeArcDyn::from_arc(arc);
let abi = wrapped.into_abi();
let restored = unsafe { SafeArcDyn::<dyn Demo>::from_abi(abi) };
assert_eq!(unsafe { restored.trait_ref() }.value(), 42);
}
#[test]
fn safe_arc_dyn_clone() {
let arc: Arc<dyn Demo> = Arc::new(DemoValue(7));
let wrapped = SafeArcDyn::from_arc(arc);
let cloned = wrapped.clone();
assert_eq!(unsafe { wrapped.trait_ref() }.value(), 7);
assert_eq!(unsafe { cloned.trait_ref() }.value(), 7);
drop(wrapped);
assert_eq!(unsafe { cloned.trait_ref() }.value(), 7);
}
}
pub type MathSession = *mut c_void;
#[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,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct MathModuleVtable {
pub create_session: unsafe extern "C" fn() -> MathSession,
pub destroy_session: unsafe extern "C" fn(MathSession),
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,
pub generated_count: unsafe extern "C" fn(MathSession) -> u32,
pub generated_at: unsafe extern "C" fn(MathSession, u32) -> GeneratedFunction,
pub module_name: unsafe extern "C" fn() -> *const c_char,
pub module_version: unsafe extern "C" fn() -> u32,
}
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 {
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();
unsafe {
let name = CStr::from_ptr((vt.module_name)());
assert_eq!(name.to_str().unwrap(), "core-ast-math");
assert_eq!((vt.module_version)(), 1);
}
let session = unsafe { (vt.create_session)() };
assert!(!session.is_null());
assert_eq!(unsafe { (vt.generated_count)(session) }, 0);
let result = unsafe { (vt.add_i32)(session, 10, 20) };
assert_eq!(result, 30);
assert_eq!(unsafe { (vt.generated_count)(session) }, 1);
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);
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);
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");
let _ = unsafe { (vt.add_i32)(session, 1, 2) };
assert_eq!(unsafe { (vt.generated_count)(session) }, 3);
unsafe { (vt.destroy_session)(session) };
}
}