use crate::BenchSpec;
use libc::c_char;
use std::cell::RefCell;
use std::ffi::CString;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr;
use std::slice;
#[repr(C)]
#[derive(Debug)]
pub struct MobenchBuf {
pub ptr: *mut u8,
pub len: usize,
pub cap: usize,
}
impl MobenchBuf {
fn clear(&mut self) {
self.ptr = ptr::null_mut();
self.len = 0;
self.cap = 0;
}
}
impl Default for MobenchBuf {
fn default() -> Self {
Self {
ptr: ptr::null_mut(),
len: 0,
cap: 0,
}
}
}
thread_local! {
static LAST_ERROR: RefCell<CString> = RefCell::new(CString::default());
}
pub unsafe fn mobench_run_benchmark_json_impl(
spec_ptr: *const u8,
spec_len: usize,
out: *mut MobenchBuf,
) -> i32 {
let result = catch_unwind(AssertUnwindSafe(|| {
if out.is_null() {
return Err("output buffer pointer must not be null".to_string());
}
unsafe { (*out).clear() };
if spec_len > 0 && spec_ptr.is_null() {
return Err("spec pointer must not be null when spec length is non-zero".to_string());
}
let spec_bytes = if spec_len == 0 {
&[]
} else {
unsafe { slice::from_raw_parts(spec_ptr, spec_len) }
};
let spec: BenchSpec = serde_json::from_slice(spec_bytes)
.map_err(|error| format!("failed to parse BenchSpec JSON: {error}"))?;
let report = crate::run_benchmark(spec).map_err(|error| error.to_string())?;
let mut bytes = serde_json::to_vec(&report)
.map_err(|error| format!("failed to serialize BenchReport JSON: {error}"))?;
let buf = MobenchBuf {
ptr: bytes.as_mut_ptr(),
len: bytes.len(),
cap: bytes.capacity(),
};
std::mem::forget(bytes);
unsafe { *out = buf };
Ok(())
}));
match result {
Ok(Ok(())) => {
clear_last_error();
0
}
Ok(Err(error)) => {
set_last_error(error);
1
}
Err(_) => {
set_last_error("benchmark panicked across native C ABI boundary");
2
}
}
}
pub unsafe fn mobench_free_buf_impl(buf: *mut MobenchBuf) {
if buf.is_null() {
return;
}
let buf_ref = unsafe { &mut *buf };
if !buf_ref.ptr.is_null() {
let ptr = buf_ref.ptr;
let len = buf_ref.len;
let cap = buf_ref.cap;
buf_ref.clear();
unsafe {
drop(Vec::from_raw_parts(ptr, len, cap));
}
} else {
buf_ref.clear();
}
}
pub fn mobench_last_error_message_impl() -> *const c_char {
LAST_ERROR.with(|message| message.borrow().as_ptr())
}
fn clear_last_error() {
LAST_ERROR.with(|message| *message.borrow_mut() = CString::default());
}
fn set_last_error(message: impl AsRef<str>) {
let sanitized = message.as_ref().replace('\0', "\\0");
let c_string = CString::new(sanitized).unwrap_or_default();
LAST_ERROR.with(|last_error| *last_error.borrow_mut() = c_string);
}
#[macro_export]
macro_rules! export_native_c_abi {
() => {
#[unsafe(no_mangle)]
pub unsafe extern "C" fn mobench_run_benchmark_json(
spec_ptr: *const u8,
spec_len: usize,
out: *mut $crate::MobenchBuf,
) -> i32 {
unsafe {
$crate::native_c_abi::mobench_run_benchmark_json_impl(spec_ptr, spec_len, out)
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn mobench_free_buf(buf: *mut $crate::MobenchBuf) {
unsafe { $crate::native_c_abi::mobench_free_buf_impl(buf) }
}
#[unsafe(no_mangle)]
pub extern "C" fn mobench_last_error_message() -> *const ::std::os::raw::c_char {
$crate::native_c_abi::mobench_last_error_message_impl()
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BenchFunction, TimingError};
use std::ffi::CStr;
fn native_abi_test_runner(spec: crate::BenchSpec) -> Result<crate::RunnerReport, TimingError> {
Ok(crate::RunnerReport {
spec,
samples: vec![crate::BenchSample {
duration_ns: 42,
cpu_time_ms: None,
peak_memory_kb: None,
process_peak_memory_kb: None,
}],
phases: Vec::new(),
timeline: Vec::new(),
})
}
inventory::submit! {
BenchFunction {
name: "native_abi_test_benchmark",
runner: native_abi_test_runner,
}
}
#[test]
fn runs_valid_spec_and_returns_report_json() {
let spec = br#"{"name":"native_abi_test_benchmark","iterations":1,"warmup":0}"#;
let mut out = MobenchBuf::default();
let status =
unsafe { mobench_run_benchmark_json_impl(spec.as_ptr(), spec.len(), &mut out) };
assert_eq!(status, 0);
assert!(!out.ptr.is_null());
assert!(out.len > 0);
let report_bytes = unsafe { slice::from_raw_parts(out.ptr, out.len) };
let report: crate::RunnerReport = serde_json::from_slice(report_bytes).unwrap();
assert_eq!(report.spec.name, "native_abi_test_benchmark");
assert_eq!(report.samples[0].duration_ns, 42);
unsafe { mobench_free_buf_impl(&mut out) };
assert!(out.ptr.is_null());
assert_eq!(out.len, 0);
assert_eq!(out.cap, 0);
}
#[test]
fn invalid_json_returns_error_without_output() {
let spec = b"not json";
let mut out = MobenchBuf::default();
let status =
unsafe { mobench_run_benchmark_json_impl(spec.as_ptr(), spec.len(), &mut out) };
assert_ne!(status, 0);
assert!(out.ptr.is_null());
let error = unsafe { CStr::from_ptr(mobench_last_error_message_impl()) }
.to_string_lossy()
.into_owned();
assert!(error.contains("failed to parse BenchSpec JSON"));
}
#[test]
fn unknown_benchmark_returns_error_without_output() {
let spec = br#"{"name":"definitely_missing","iterations":1,"warmup":0}"#;
let mut out = MobenchBuf::default();
let status =
unsafe { mobench_run_benchmark_json_impl(spec.as_ptr(), spec.len(), &mut out) };
assert_ne!(status, 0);
assert!(out.ptr.is_null());
let error = unsafe { CStr::from_ptr(mobench_last_error_message_impl()) }
.to_string_lossy()
.into_owned();
assert!(error.contains("unknown benchmark function"));
}
#[test]
fn free_null_and_empty_buffers_are_safe() {
unsafe { mobench_free_buf_impl(ptr::null_mut()) };
let mut out = MobenchBuf::default();
unsafe { mobench_free_buf_impl(&mut out) };
assert!(out.ptr.is_null());
assert_eq!(out.len, 0);
assert_eq!(out.cap, 0);
}
}