mobench_sdk/
native_c_abi.rs1use crate::BenchSpec;
10use core::ffi::c_char;
11use std::any::Any;
12use std::cell::RefCell;
13use std::ffi::CString;
14use std::panic::{AssertUnwindSafe, catch_unwind};
15use std::ptr;
16use std::slice;
17
18#[repr(C)]
24#[derive(Debug)]
25pub struct MobenchBuf {
26 pub ptr: *mut u8,
28 pub len: usize,
30 pub cap: usize,
32}
33
34impl MobenchBuf {
35 fn clear(&mut self) {
36 self.ptr = ptr::null_mut();
37 self.len = 0;
38 self.cap = 0;
39 }
40}
41
42impl Default for MobenchBuf {
43 fn default() -> Self {
44 Self {
45 ptr: ptr::null_mut(),
46 len: 0,
47 cap: 0,
48 }
49 }
50}
51
52thread_local! {
53 static LAST_ERROR: RefCell<CString> = RefCell::new(CString::default());
54}
55
56pub unsafe fn mobench_run_benchmark_json_impl(
65 spec_ptr: *const u8,
66 spec_len: usize,
67 out: *mut MobenchBuf,
68) -> i32 {
69 let result = catch_unwind(AssertUnwindSafe(|| {
70 crate::metrics::clear();
71
72 if out.is_null() {
73 return Err("output buffer pointer must not be null".to_string());
74 }
75
76 unsafe { (*out).clear() };
79
80 if spec_len > 0 && spec_ptr.is_null() {
81 return Err("spec pointer must not be null when spec length is non-zero".to_string());
82 }
83
84 let spec_bytes = if spec_len == 0 {
85 &[]
86 } else {
87 unsafe { slice::from_raw_parts(spec_ptr, spec_len) }
88 };
89 let spec: BenchSpec = serde_json::from_slice(spec_bytes)
90 .map_err(|error| format!("failed to parse BenchSpec JSON: {error}"))?;
91 let report = crate::run_benchmark(spec).map_err(|error| error.to_string())?;
92 let mut report_value = serde_json::to_value(&report)
93 .map_err(|error| format!("failed to serialize BenchReport JSON: {error}"))?;
94 let custom_metrics = crate::metrics::take();
95 if !custom_metrics.is_empty() {
96 let report_object = report_value
97 .as_object_mut()
98 .ok_or_else(|| "serialized benchmark report must be a JSON object".to_string())?;
99 report_object.insert(
100 "custom_metrics".to_string(),
101 serde_json::to_value(custom_metrics)
102 .map_err(|error| format!("failed to serialize custom metrics: {error}"))?,
103 );
104 }
105 let mut bytes = serde_json::to_vec(&report_value)
106 .map_err(|error| format!("failed to serialize BenchReport JSON: {error}"))?;
107
108 let buf = MobenchBuf {
109 ptr: bytes.as_mut_ptr(),
110 len: bytes.len(),
111 cap: bytes.capacity(),
112 };
113 std::mem::forget(bytes);
114 unsafe { *out = buf };
115 Ok(())
116 }));
117
118 match result {
119 Ok(Ok(())) => {
120 clear_last_error();
121 0
122 }
123 Ok(Err(error)) => {
124 set_last_error(error);
125 1
126 }
127 Err(payload) => {
128 set_last_error(format!(
129 "benchmark panicked across native C ABI boundary: {}",
130 panic_payload_message(payload.as_ref())
131 ));
132 2
133 }
134 }
135}
136
137fn panic_payload_message(payload: &(dyn Any + Send)) -> &str {
138 payload
139 .downcast_ref::<&str>()
140 .copied()
141 .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
142 .unwrap_or("non-string panic payload")
143}
144
145pub unsafe fn mobench_free_buf_impl(buf: *mut MobenchBuf) {
153 if buf.is_null() {
154 return;
155 }
156
157 let buf_ref = unsafe { &mut *buf };
158 if !buf_ref.ptr.is_null() {
159 let ptr = buf_ref.ptr;
160 let len = buf_ref.len;
161 let cap = buf_ref.cap;
162 buf_ref.clear();
163 unsafe {
164 drop(Vec::from_raw_parts(ptr, len, cap));
165 }
166 } else {
167 buf_ref.clear();
168 }
169}
170
171pub fn mobench_last_error_message_impl() -> *const c_char {
173 LAST_ERROR.with(|message| message.borrow().as_ptr())
174}
175
176fn clear_last_error() {
177 LAST_ERROR.with(|message| *message.borrow_mut() = CString::default());
178}
179
180fn set_last_error(message: impl AsRef<str>) {
181 let sanitized = message.as_ref().replace('\0', "\\0");
182 let c_string = CString::new(sanitized).unwrap_or_default();
183 LAST_ERROR.with(|last_error| *last_error.borrow_mut() = c_string);
184}
185
186#[macro_export]
195macro_rules! export_native_c_abi {
196 () => {
197 #[unsafe(no_mangle)]
205 pub unsafe extern "C" fn mobench_run_benchmark_json(
206 spec_ptr: *const u8,
207 spec_len: usize,
208 out: *mut $crate::MobenchBuf,
209 ) -> i32 {
210 unsafe {
211 $crate::native_c_abi::mobench_run_benchmark_json_impl(spec_ptr, spec_len, out)
212 }
213 }
214
215 #[unsafe(no_mangle)]
223 pub unsafe extern "C" fn mobench_free_buf(buf: *mut $crate::MobenchBuf) {
224 unsafe { $crate::native_c_abi::mobench_free_buf_impl(buf) }
225 }
226
227 #[unsafe(no_mangle)]
229 pub extern "C" fn mobench_last_error_message() -> *const ::std::os::raw::c_char {
230 $crate::native_c_abi::mobench_last_error_message_impl()
231 }
232 };
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::{BenchFunction, TimingError};
239 use std::ffi::CStr;
240
241 fn native_abi_test_runner(spec: crate::BenchSpec) -> Result<crate::RunnerReport, TimingError> {
242 crate::record_run_u64("payload_size_bytes", 4096);
243 crate::record_sample_u64("proof_size_bytes", 192);
244 Ok(crate::RunnerReport {
245 spec,
246 samples: vec![crate::BenchSample {
247 duration_ns: 42,
248 cpu_time_ms: None,
249 peak_memory_kb: None,
250 process_peak_memory_kb: None,
251 }],
252 phases: Vec::new(),
253 timeline: Vec::new(),
254 })
255 }
256
257 fn native_abi_panicking_runner(
258 _spec: crate::BenchSpec,
259 ) -> Result<crate::RunnerReport, TimingError> {
260 panic!("diagnostic panic payload")
261 }
262
263 inventory::submit! {
264 BenchFunction {
265 name: "native_abi_test_benchmark",
266 runner: native_abi_test_runner,
267 }
268 }
269
270 inventory::submit! {
271 BenchFunction {
272 name: "native_abi_panicking_benchmark",
273 runner: native_abi_panicking_runner,
274 }
275 }
276
277 #[test]
278 fn runs_valid_spec_and_returns_report_json() {
279 let spec = br#"{"name":"native_abi_test_benchmark","iterations":1,"warmup":0}"#;
280 let mut out = MobenchBuf::default();
281
282 let status =
283 unsafe { mobench_run_benchmark_json_impl(spec.as_ptr(), spec.len(), &mut out) };
284
285 assert_eq!(status, 0);
286 assert!(!out.ptr.is_null());
287 assert!(out.len > 0);
288
289 let report_bytes = unsafe { slice::from_raw_parts(out.ptr, out.len) };
290 let report: serde_json::Value = serde_json::from_slice(report_bytes).unwrap();
291 assert_eq!(
292 report["spec"]["name"],
293 serde_json::json!("native_abi_test_benchmark")
294 );
295 assert_eq!(report["samples"][0]["duration_ns"], serde_json::json!(42));
296 assert_eq!(
297 report["custom_metrics"]["sample_u64"]["proof_size_bytes"],
298 serde_json::json!([192])
299 );
300 assert_eq!(
301 report["custom_metrics"]["run_u64"]["payload_size_bytes"],
302 serde_json::json!(4096)
303 );
304
305 unsafe { mobench_free_buf_impl(&mut out) };
306 assert!(out.ptr.is_null());
307 assert_eq!(out.len, 0);
308 assert_eq!(out.cap, 0);
309 }
310
311 #[test]
312 fn invalid_json_returns_error_without_output() {
313 let spec = b"not json";
314 let mut out = MobenchBuf::default();
315
316 let status =
317 unsafe { mobench_run_benchmark_json_impl(spec.as_ptr(), spec.len(), &mut out) };
318
319 assert_ne!(status, 0);
320 assert!(out.ptr.is_null());
321 let error = unsafe { CStr::from_ptr(mobench_last_error_message_impl()) }
322 .to_string_lossy()
323 .into_owned();
324 assert!(error.contains("failed to parse BenchSpec JSON"));
325 }
326
327 #[test]
328 fn unknown_benchmark_returns_error_without_output() {
329 let spec = br#"{"name":"definitely_missing","iterations":1,"warmup":0}"#;
330 let mut out = MobenchBuf::default();
331
332 let status =
333 unsafe { mobench_run_benchmark_json_impl(spec.as_ptr(), spec.len(), &mut out) };
334
335 assert_ne!(status, 0);
336 assert!(out.ptr.is_null());
337 let error = unsafe { CStr::from_ptr(mobench_last_error_message_impl()) }
338 .to_string_lossy()
339 .into_owned();
340 assert!(error.contains("unknown benchmark function"));
341 }
342
343 #[test]
344 fn panic_error_includes_the_string_payload() {
345 let spec = br#"{"name":"native_abi_panicking_benchmark","iterations":1,"warmup":0}"#;
346 let mut out = MobenchBuf::default();
347
348 let status =
349 unsafe { mobench_run_benchmark_json_impl(spec.as_ptr(), spec.len(), &mut out) };
350
351 assert_eq!(status, 2);
352 assert!(out.ptr.is_null());
353 let error = unsafe { CStr::from_ptr(mobench_last_error_message_impl()) }
354 .to_string_lossy()
355 .into_owned();
356 assert_eq!(
357 error,
358 "benchmark panicked across native C ABI boundary: diagnostic panic payload"
359 );
360 }
361
362 #[test]
363 fn free_null_and_empty_buffers_are_safe() {
364 unsafe { mobench_free_buf_impl(ptr::null_mut()) };
365
366 let mut out = MobenchBuf::default();
367 unsafe { mobench_free_buf_impl(&mut out) };
368
369 assert!(out.ptr.is_null());
370 assert_eq!(out.len, 0);
371 assert_eq!(out.cap, 0);
372 }
373}