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 libc::c_char;
11use std::cell::RefCell;
12use std::ffi::CString;
13use std::panic::{AssertUnwindSafe, catch_unwind};
14use std::ptr;
15use std::slice;
16
17/// Owned byte buffer returned by the native mobench C ABI.
18///
19/// The buffer layout intentionally mirrors common Rust-to-C ownership patterns:
20/// Rust allocates the bytes, transfers ownership by filling this struct, and
21/// the caller returns ownership exactly once with `mobench_free_buf`.
22#[repr(C)]
23#[derive(Debug)]
24pub struct MobenchBuf {
25    /// Pointer to the first byte of the allocation.
26    pub ptr: *mut u8,
27    /// Number of initialized bytes.
28    pub len: usize,
29    /// Allocation capacity needed to reconstruct and free the buffer.
30    pub cap: usize,
31}
32
33impl MobenchBuf {
34    fn clear(&mut self) {
35        self.ptr = ptr::null_mut();
36        self.len = 0;
37        self.cap = 0;
38    }
39}
40
41impl Default for MobenchBuf {
42    fn default() -> Self {
43        Self {
44            ptr: ptr::null_mut(),
45            len: 0,
46            cap: 0,
47        }
48    }
49}
50
51thread_local! {
52    static LAST_ERROR: RefCell<CString> = RefCell::new(CString::default());
53}
54
55/// Runs a registered benchmark from JSON and writes the JSON report to `out`.
56///
57/// # Safety
58///
59/// `spec_ptr` must either be non-null and valid for reads of `spec_len` bytes,
60/// or `spec_len` must be zero. `out` must be non-null and valid for writes of
61/// one [`MobenchBuf`]. When this returns `0`, the caller owns `out` and must
62/// release it exactly once with [`mobench_free_buf_impl`].
63pub unsafe fn mobench_run_benchmark_json_impl(
64    spec_ptr: *const u8,
65    spec_len: usize,
66    out: *mut MobenchBuf,
67) -> i32 {
68    let result = catch_unwind(AssertUnwindSafe(|| {
69        if out.is_null() {
70            return Err("output buffer pointer must not be null".to_string());
71        }
72
73        // Leave `out` in a known empty state even when parsing or execution
74        // fails, so native callers can avoid conditional cleanup paths.
75        unsafe { (*out).clear() };
76
77        if spec_len > 0 && spec_ptr.is_null() {
78            return Err("spec pointer must not be null when spec length is non-zero".to_string());
79        }
80
81        let spec_bytes = if spec_len == 0 {
82            &[]
83        } else {
84            unsafe { slice::from_raw_parts(spec_ptr, spec_len) }
85        };
86        let spec: BenchSpec = serde_json::from_slice(spec_bytes)
87            .map_err(|error| format!("failed to parse BenchSpec JSON: {error}"))?;
88        let report = crate::run_benchmark(spec).map_err(|error| error.to_string())?;
89        let mut bytes = serde_json::to_vec(&report)
90            .map_err(|error| format!("failed to serialize BenchReport JSON: {error}"))?;
91
92        let buf = MobenchBuf {
93            ptr: bytes.as_mut_ptr(),
94            len: bytes.len(),
95            cap: bytes.capacity(),
96        };
97        std::mem::forget(bytes);
98        unsafe { *out = buf };
99        Ok(())
100    }));
101
102    match result {
103        Ok(Ok(())) => {
104            clear_last_error();
105            0
106        }
107        Ok(Err(error)) => {
108            set_last_error(error);
109            1
110        }
111        Err(_) => {
112            set_last_error("benchmark panicked across native C ABI boundary");
113            2
114        }
115    }
116}
117
118/// Frees a buffer returned by [`mobench_run_benchmark_json_impl`].
119///
120/// # Safety
121///
122/// `buf` may be null. If non-null and `buf.ptr` is non-null, the struct must
123/// contain a buffer previously returned by this module that has not already
124/// been freed. The struct is zeroed before this function returns.
125pub unsafe fn mobench_free_buf_impl(buf: *mut MobenchBuf) {
126    if buf.is_null() {
127        return;
128    }
129
130    let buf_ref = unsafe { &mut *buf };
131    if !buf_ref.ptr.is_null() {
132        let ptr = buf_ref.ptr;
133        let len = buf_ref.len;
134        let cap = buf_ref.cap;
135        buf_ref.clear();
136        unsafe {
137            drop(Vec::from_raw_parts(ptr, len, cap));
138        }
139    } else {
140        buf_ref.clear();
141    }
142}
143
144/// Returns the most recent native ABI error message for this thread.
145pub fn mobench_last_error_message_impl() -> *const c_char {
146    LAST_ERROR.with(|message| message.borrow().as_ptr())
147}
148
149fn clear_last_error() {
150    LAST_ERROR.with(|message| *message.borrow_mut() = CString::default());
151}
152
153fn set_last_error(message: impl AsRef<str>) {
154    let sanitized = message.as_ref().replace('\0', "\\0");
155    let c_string = CString::new(sanitized).unwrap_or_default();
156    LAST_ERROR.with(|last_error| *last_error.borrow_mut() = c_string);
157}
158
159/// Exports the stable mobench native JSON C ABI symbols from a benchmark crate.
160///
161/// Add this once in the root of a benchmark cdylib/staticlib crate that uses
162/// the mobench registry:
163///
164/// ```ignore
165/// mobench_sdk::export_native_c_abi!();
166/// ```
167#[macro_export]
168macro_rules! export_native_c_abi {
169    () => {
170        /// Runs a mobench benchmark from a JSON `BenchSpec` payload.
171        ///
172        /// # Safety
173        ///
174        /// `spec_ptr` must be valid for `spec_len` bytes when `spec_len` is
175        /// non-zero, and `out` must be valid for one writable
176        /// [`mobench_sdk::MobenchBuf`].
177        #[unsafe(no_mangle)]
178        pub unsafe extern "C" fn mobench_run_benchmark_json(
179            spec_ptr: *const u8,
180            spec_len: usize,
181            out: *mut $crate::MobenchBuf,
182        ) -> i32 {
183            unsafe {
184                $crate::native_c_abi::mobench_run_benchmark_json_impl(spec_ptr, spec_len, out)
185            }
186        }
187
188        /// Frees a `MobenchBuf` returned by `mobench_run_benchmark_json`.
189        ///
190        /// # Safety
191        ///
192        /// `buf` may be null. If non-null and non-empty, it must contain a
193        /// buffer returned by `mobench_run_benchmark_json` that has not already
194        /// been freed.
195        #[unsafe(no_mangle)]
196        pub unsafe extern "C" fn mobench_free_buf(buf: *mut $crate::MobenchBuf) {
197            unsafe { $crate::native_c_abi::mobench_free_buf_impl(buf) }
198        }
199
200        /// Returns the last native mobench C ABI error message for this thread.
201        #[unsafe(no_mangle)]
202        pub extern "C" fn mobench_last_error_message() -> *const ::std::os::raw::c_char {
203            $crate::native_c_abi::mobench_last_error_message_impl()
204        }
205    };
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::{BenchFunction, TimingError};
212    use std::ffi::CStr;
213
214    fn native_abi_test_runner(spec: crate::BenchSpec) -> Result<crate::RunnerReport, TimingError> {
215        Ok(crate::RunnerReport {
216            spec,
217            samples: vec![crate::BenchSample {
218                duration_ns: 42,
219                cpu_time_ms: None,
220                peak_memory_kb: None,
221                process_peak_memory_kb: None,
222            }],
223            phases: Vec::new(),
224            timeline: Vec::new(),
225        })
226    }
227
228    inventory::submit! {
229        BenchFunction {
230            name: "native_abi_test_benchmark",
231            runner: native_abi_test_runner,
232        }
233    }
234
235    #[test]
236    fn runs_valid_spec_and_returns_report_json() {
237        let spec = br#"{"name":"native_abi_test_benchmark","iterations":1,"warmup":0}"#;
238        let mut out = MobenchBuf::default();
239
240        let status =
241            unsafe { mobench_run_benchmark_json_impl(spec.as_ptr(), spec.len(), &mut out) };
242
243        assert_eq!(status, 0);
244        assert!(!out.ptr.is_null());
245        assert!(out.len > 0);
246
247        let report_bytes = unsafe { slice::from_raw_parts(out.ptr, out.len) };
248        let report: crate::RunnerReport = serde_json::from_slice(report_bytes).unwrap();
249        assert_eq!(report.spec.name, "native_abi_test_benchmark");
250        assert_eq!(report.samples[0].duration_ns, 42);
251
252        unsafe { mobench_free_buf_impl(&mut out) };
253        assert!(out.ptr.is_null());
254        assert_eq!(out.len, 0);
255        assert_eq!(out.cap, 0);
256    }
257
258    #[test]
259    fn invalid_json_returns_error_without_output() {
260        let spec = b"not json";
261        let mut out = MobenchBuf::default();
262
263        let status =
264            unsafe { mobench_run_benchmark_json_impl(spec.as_ptr(), spec.len(), &mut out) };
265
266        assert_ne!(status, 0);
267        assert!(out.ptr.is_null());
268        let error = unsafe { CStr::from_ptr(mobench_last_error_message_impl()) }
269            .to_string_lossy()
270            .into_owned();
271        assert!(error.contains("failed to parse BenchSpec JSON"));
272    }
273
274    #[test]
275    fn unknown_benchmark_returns_error_without_output() {
276        let spec = br#"{"name":"definitely_missing","iterations":1,"warmup":0}"#;
277        let mut out = MobenchBuf::default();
278
279        let status =
280            unsafe { mobench_run_benchmark_json_impl(spec.as_ptr(), spec.len(), &mut out) };
281
282        assert_ne!(status, 0);
283        assert!(out.ptr.is_null());
284        let error = unsafe { CStr::from_ptr(mobench_last_error_message_impl()) }
285            .to_string_lossy()
286            .into_owned();
287        assert!(error.contains("unknown benchmark function"));
288    }
289
290    #[test]
291    fn free_null_and_empty_buffers_are_safe() {
292        unsafe { mobench_free_buf_impl(ptr::null_mut()) };
293
294        let mut out = MobenchBuf::default();
295        unsafe { mobench_free_buf_impl(&mut out) };
296
297        assert!(out.ptr.is_null());
298        assert_eq!(out.len, 0);
299        assert_eq!(out.cap, 0);
300    }
301}