use std::ffi::c_char;
use std::marker::PhantomData;
use std::path::Path;
use anyhow::{Context, Result};
use crate::DynLib;
use crate::native::{AbiStableDynRef, ModuleDynEntryPoint as _ModuleDynEntry};
pub struct AbiTable<T: Copy> {
_lib: DynLib,
vtable: T,
}
impl<T: Copy> AbiTable<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
}
}
pub type MathSession = *mut std::ffi::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 {}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct AbiBox {
pub data: *mut std::ffi::c_void,
pub len: usize,
pub free: unsafe extern "C" fn(data: *mut std::ffi::c_void, len: usize),
}
impl AbiBox {
pub const fn null() -> Self {
Self {
data: std::ptr::null_mut(),
len: 0,
free: abi_box_free_noop,
}
}
pub fn is_null(&self) -> bool {
self.data.is_null()
}
pub fn from_vec(v: Vec<u8>) -> Self {
let boxed: Box<[u8]> = v.into_boxed_slice();
let len = boxed.len();
let data = Box::into_raw(boxed) as *mut std::ffi::c_void;
Self {
data,
len,
free: abi_box_free_rust,
}
}
pub unsafe fn as_slice(&self) -> &[u8] {
if self.data.is_null() {
&[]
} else {
unsafe { std::slice::from_raw_parts(self.data as *const u8, self.len) }
}
}
pub fn into_raw(self) -> (Self, bool) {
let consumed = !self.is_null();
(self, consumed)
}
}
unsafe impl Send for AbiBox {}
unsafe impl Sync for AbiBox {}
unsafe extern "C" fn abi_box_free_noop(_: *mut std::ffi::c_void, _: usize) {}
pub unsafe extern "C" fn abi_box_free_rust(
data: *mut std::ffi::c_void,
len: usize,
) {
if data.is_null() {
return;
}
let slice_ptr = std::slice::from_raw_parts_mut(data as *mut u8, len) as *mut [u8];
drop(unsafe { Box::from_raw(slice_ptr) });
}
pub struct AbiBoxHandle {
_lib: Option<DynLib>,
inner: Option<AbiBox>,
}
impl AbiBoxHandle {
pub fn from_box(box_: AbiBox, lib: DynLib) -> Self {
Self {
_lib: Some(lib),
inner: Some(box_),
}
}
pub unsafe fn from_box_unowned(box_: AbiBox) -> Self {
Self {
_lib: Some(DynLib::unowned()),
inner: Some(box_),
}
}
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() -> AbiBox = unsafe { lib.symbol(symbol) }?;
let box_ = unsafe { getter() };
if box_.is_null() {
anyhow::bail!("box entry returned null AbiBox");
}
Ok(Self::from_box(box_, lib))
}
pub fn as_slice(&self) -> &[u8] {
match &self.inner {
Some(b) => unsafe { b.as_slice() },
None => &[],
}
}
pub fn len(&self) -> usize {
self.inner.as_ref().map_or(0, |b| b.len)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn into_raw(mut self) -> AbiBox {
self.inner.take().unwrap_or_else(AbiBox::null)
}
}
impl std::ops::Deref for AbiBoxHandle {
type Target = [u8];
fn deref(&self) -> &[u8] {
self.as_slice()
}
}
impl Drop for AbiBoxHandle {
fn drop(&mut self) {
if let Some(box_) = self.inner.take() {
if !box_.is_null() {
unsafe { (box_.free)(box_.data, box_.len) };
}
}
}
}
unsafe impl Send for AbiBoxHandle {}
unsafe impl Sync for AbiBoxHandle {}
#[cfg(test)]
mod cdyn_handle_tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
static INSTANCE: u64 = 0xdead_beef;
static REFCOUNT: AtomicU32 = AtomicU32::new(0);
#[repr(C)]
#[derive(Clone, Copy)]
struct TestVtable {
get_value: unsafe extern "C" fn(ctx: *mut std::ffi::c_void) -> u64,
}
unsafe extern "C" fn test_get_value(ctx: *mut std::ffi::c_void) -> u64 {
let _ = ctx;
INSTANCE
}
unsafe extern "C" fn test_retain(_: AbiStableDynRef__FatPtr) {
REFCOUNT.fetch_add(1, Ordering::SeqCst);
}
unsafe extern "C" fn test_release(_: AbiStableDynRef__FatPtr) {
let prev = REFCOUNT.fetch_sub(1, Ordering::SeqCst);
assert!(prev > 0, "release called more times than retain");
}
type AbiStableDynRef__FatPtr = crate::native::AbiDynFatPtr;
static TEST_VTABLE: TestVtable = TestVtable { get_value: test_get_value };
#[test]
fn cdyn_handle_retain_release_roundtrip() {
REFCOUNT.store(1, Ordering::SeqCst);
let raw = AbiStableDynRef {
object: crate::native::AbiDynFatPtr {
data: &INSTANCE as *const u64 as *const std::ffi::c_void,
vtable: &TEST_VTABLE as *const TestVtable as *const std::ffi::c_void,
},
retain: test_retain,
release: test_release,
};
let h1 = unsafe { AbiRef::<TestVtable>::from_raw(raw) };
assert_eq!(REFCOUNT.load(Ordering::SeqCst), 1);
let h2 = h1.clone();
assert_eq!(REFCOUNT.load(Ordering::SeqCst), 2);
unsafe {
let vt = h1.vtable();
assert_eq!((vt.get_value)(h1.ctx()), INSTANCE);
}
assert_eq!(h1.as_raw().object.vtable, h2.as_raw().object.vtable);
drop(h2);
assert_eq!(REFCOUNT.load(Ordering::SeqCst), 1);
drop(h1);
assert_eq!(REFCOUNT.load(Ordering::SeqCst), 0);
}
#[test]
fn cdyn_handle_into_raw_skips_release() {
REFCOUNT.store(1, Ordering::SeqCst);
let raw = AbiStableDynRef {
object: crate::native::AbiDynFatPtr {
data: &INSTANCE as *const u64 as *const std::ffi::c_void,
vtable: &TEST_VTABLE as *const TestVtable as *const std::ffi::c_void,
},
retain: test_retain,
release: test_release,
};
let h = unsafe { AbiRef::<TestVtable>::from_raw(raw) };
let raw2 = h.into_raw(); drop(raw2); assert_eq!(REFCOUNT.load(Ordering::SeqCst), 1); REFCOUNT.fetch_sub(1, Ordering::SeqCst);
}
static BOX_FREED: AtomicU32 = AtomicU32::new(0);
unsafe extern "C" fn counting_free(data: *mut std::ffi::c_void, len: usize) {
BOX_FREED.fetch_add(1, Ordering::SeqCst);
if !data.is_null() {
let slice_ptr = std::slice::from_raw_parts_mut(data as *mut u8, len) as *mut [u8];
drop(unsafe { Box::from_raw(slice_ptr) });
}
}
#[test]
fn cdyn_box_drop_calls_producer_free() {
BOX_FREED.store(0, Ordering::SeqCst);
let payload: Box<[u8]> = vec![1u8, 2, 3, 4].into_boxed_slice();
let len = payload.len();
let data = Box::into_raw(payload) as *mut std::ffi::c_void;
let box_ = AbiBox {
data,
len,
free: counting_free,
};
let handle = unsafe { AbiBoxHandle::from_box_unowned(box_) };
assert_eq!(handle.as_slice(), &[1, 2, 3, 4]);
assert_eq!(handle.len(), 4);
assert_eq!(BOX_FREED.load(Ordering::SeqCst), 0);
drop(handle); assert_eq!(BOX_FREED.load(Ordering::SeqCst), 1);
}
#[test]
fn cdyn_box_from_vec_roundtrip_and_free() {
let box_ = AbiBox::from_vec(b"hello cross-module".to_vec());
assert_eq!(box_.len, 18);
let handle = unsafe { AbiBoxHandle::from_box_unowned(box_) };
assert_eq!(&*handle, b"hello cross-module");
drop(handle);
BOX_FREED.store(0, Ordering::SeqCst);
let box_ = AbiBox {
data: Box::into_raw(vec![9u8; 4].into_boxed_slice()) as *mut std::ffi::c_void,
len: 4,
free: counting_free,
};
let handle = unsafe { AbiBoxHandle::from_box_unowned(box_) };
let raw = handle.into_raw();
assert_eq!(BOX_FREED.load(Ordering::SeqCst), 0);
unsafe { (raw.free)(raw.data, raw.len) };
assert_eq!(BOX_FREED.load(Ordering::SeqCst), 1);
}
#[test]
fn cdyn_box_null_is_safe() {
let handle = unsafe { AbiBoxHandle::from_box_unowned(AbiBox::null()) };
assert!(handle.is_empty());
assert!(handle.as_slice().is_empty());
drop(handle); }
}
pub struct AbiRef<T: Copy> {
_lib: DynLib,
raw: AbiStableDynRef,
_marker: PhantomData<T>,
}
impl<T: Copy> AbiRef<T> {
pub unsafe fn load(path: &Path, dyn_entry: &[u8]) -> Result<Self> {
let lib = unsafe { DynLib::load(path) }?;
unsafe { Self::from_lib(lib, dyn_entry) }
}
pub unsafe fn from_lib(lib: DynLib, dyn_entry: &[u8]) -> Result<Self> {
let entry: _ModuleDynEntry =
unsafe { lib.symbol(dyn_entry) }.with_context(|| {
format!("dyn entry '{}' not found", crate::helpers::display_symbol(dyn_entry))
})?;
let raw = unsafe { entry() };
if raw.is_null() {
anyhow::bail!("dyn entry returned null AbiStableDynRef");
}
Ok(Self {
_lib: lib,
raw,
_marker: PhantomData,
})
}
pub unsafe fn from_raw(raw: AbiStableDynRef) -> Self {
Self {
_lib: DynLib::unowned(),
raw,
_marker: PhantomData,
}
}
pub fn ctx(&self) -> *mut std::ffi::c_void {
self.raw.object.data as *mut std::ffi::c_void
}
pub unsafe fn vtable(&self) -> &T {
unsafe { &*(self.raw.object.vtable as *const T) }
}
pub fn as_raw(&self) -> &AbiStableDynRef {
&self.raw
}
pub fn into_raw(self) -> AbiStableDynRef {
let mut this = std::mem::ManuallyDrop::new(self);
let lib = unsafe { std::ptr::read(&this._lib) };
std::mem::forget(lib);
this.raw
}
}
pub use crate::native::ModuleDynEntryPoint;
unsafe impl<T: Copy + Send> Send for AbiRef<T> {}
unsafe impl<T: Copy + Sync> Sync for AbiRef<T> {}
impl<T: Copy> Clone for AbiRef<T> {
fn clone(&self) -> Self {
unsafe { (self.raw.retain)(self.raw.object) };
Self {
_lib: self._lib.clone(),
raw: self.raw,
_marker: PhantomData,
}
}
}
impl<T: Copy> Drop for AbiRef<T> {
fn drop(&mut self) {
unsafe { (self.raw.release)(self.raw.object) };
}
}
#[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 { AbiTable::<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) };
}
}