Skip to main content

mobench_sdk/
native_c_abi.rs

1//! Native JSON C ABI for benchmark runners.
2//!
3//! This module provides the implementation behind the stable native backend
4//! contract. Benchmark crates can export the C symbols with
5//! [`crate::export_native_c_abi!`], then generated Android/iOS apps can pass a
6//! serialized [`crate::BenchSpec`] JSON payload and receive a serialized
7//! [`crate::RunnerReport`] JSON payload without using UniFFI-generated bindings.
8
9use 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/// Owned byte buffer returned by the native mobench C ABI.
19///
20/// The buffer layout intentionally mirrors common Rust-to-C ownership patterns:
21/// Rust allocates the bytes, transfers ownership by filling this struct, and
22/// the caller returns ownership exactly once with `mobench_free_buf`.
23#[repr(C)]
24#[derive(Debug)]
25pub struct MobenchBuf {
26    /// Pointer to the first byte of the allocation.
27    pub ptr: *mut u8,
28    /// Number of initialized bytes.
29    pub len: usize,
30    /// Allocation capacity needed to reconstruct and free the buffer.
31    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
56/// Runs a registered benchmark from JSON and writes the JSON report to `out`.
57///
58/// # Safety
59///
60/// `spec_ptr` must either be non-null and valid for reads of `spec_len` bytes,
61/// or `spec_len` must be zero. `out` must be non-null and valid for writes of
62/// one [`MobenchBuf`]. When this returns `0`, the caller owns `out` and must
63/// release it exactly once with [`mobench_free_buf_impl`].
64pub 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        // Leave `out` in a known empty state even when parsing or execution
77        // fails, so native callers can avoid conditional cleanup paths.
78        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
145/// Frees a buffer returned by [`mobench_run_benchmark_json_impl`].
146///
147/// # Safety
148///
149/// `buf` may be null. If non-null and `buf.ptr` is non-null, the struct must
150/// contain a buffer previously returned by this module that has not already
151/// been freed. The struct is zeroed before this function returns.
152pub 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
171/// Returns the most recent native ABI error message for this thread.
172pub 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/// Exports the stable mobench native JSON C ABI symbols from a benchmark crate.
187///
188/// Add this once in the root of a benchmark cdylib/staticlib crate that uses
189/// the mobench registry:
190///
191/// ```ignore
192/// mobench_sdk::export_native_c_abi!();
193/// ```
194#[macro_export]
195macro_rules! export_native_c_abi {
196    () => {
197        /// Runs a mobench benchmark from a JSON `BenchSpec` payload.
198        ///
199        /// # Safety
200        ///
201        /// `spec_ptr` must be valid for `spec_len` bytes when `spec_len` is
202        /// non-zero, and `out` must be valid for one writable
203        /// [`mobench_sdk::MobenchBuf`].
204        #[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        /// Frees a `MobenchBuf` returned by `mobench_run_benchmark_json`.
216        ///
217        /// # Safety
218        ///
219        /// `buf` may be null. If non-null and non-empty, it must contain a
220        /// buffer returned by `mobench_run_benchmark_json` that has not already
221        /// been freed.
222        #[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        /// Returns the last native mobench C ABI error message for this thread.
228        #[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}