mobench_sdk/uniffi_types.rs
1//! UniFFI integration helpers for generating mobile bindings.
2//!
3//! This module provides utilities for integrating mobench-sdk types with UniFFI
4//! for generating Kotlin/Swift bindings. Since UniFFI requires scaffolding to be
5//! set up in the consuming crate, this module provides conversion traits and
6//! ready-to-use type definitions that can be easily adapted.
7//!
8//! ## Quick Start
9//!
10//! To use mobench-sdk with UniFFI in your crate:
11//!
12//! 1. Add uniffi to your dependencies:
13//!
14//! ```toml
15//! [dependencies]
16//! mobench-sdk = "0.1.37"
17//! uniffi = { version = "0.28", features = ["cli"] }
18//!
19//! [build-dependencies]
20//! uniffi = { version = "0.28", features = ["build"] }
21//! ```
22//!
23//! 2. Define your FFI types with UniFFI annotations:
24//!
25//! ```ignore
26//! use uniffi;
27//!
28//! // Set up UniFFI scaffolding
29//! uniffi::setup_scaffolding!();
30//!
31//! #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
32//! pub struct BenchSpec {
33//! pub name: String,
34//! pub iterations: u32,
35//! pub warmup: u32,
36//! }
37//!
38//! #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
39//! pub struct BenchSample {
40//! pub duration_ns: u64,
41//! pub cpu_time_ms: Option<u64>,
42//! pub peak_memory_kb: Option<u64>,
43//! pub process_peak_memory_kb: Option<u64>,
44//! }
45//!
46//! #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
47//! pub struct BenchReport {
48//! pub spec: BenchSpec,
49//! pub samples: Vec<BenchSample>,
50//! }
51//!
52//! #[derive(Debug, thiserror::Error, uniffi::Error)]
53//! #[uniffi(flat_error)]
54//! pub enum BenchError {
55//! #[error("iterations must be greater than zero")]
56//! InvalidIterations,
57//! #[error("unknown benchmark function: {name}")]
58//! UnknownFunction { name: String },
59//! #[error("benchmark execution failed: {reason}")]
60//! ExecutionFailed { reason: String },
61//! }
62//! ```
63//!
64//! 3. Implement conversions using the traits from this module:
65//!
66//! ```ignore
67//! use mobench_sdk::uniffi_types::{FromSdkSpec, FromSdkSample, FromSdkReport, FromSdkError};
68//!
69//! impl FromSdkSpec for BenchSpec {
70//! fn from_sdk(spec: mobench_sdk::BenchSpec) -> Self {
71//! Self {
72//! name: spec.name,
73//! iterations: spec.iterations,
74//! warmup: spec.warmup,
75//! }
76//! }
77//!
78//! fn to_sdk(&self) -> mobench_sdk::BenchSpec {
79//! mobench_sdk::BenchSpec {
80//! name: self.name.clone(),
81//! iterations: self.iterations,
82//! warmup: self.warmup,
83//! }
84//! }
85//! }
86//!
87//! // ... implement other traits similarly
88//! ```
89//!
90//! 4. Export your benchmark function:
91//!
92//! ```ignore
93//! #[uniffi::export]
94//! pub fn run_benchmark(spec: BenchSpec) -> Result<BenchReport, BenchError> {
95//! let sdk_spec = spec.to_sdk();
96//! let sdk_report = mobench_sdk::run_benchmark(sdk_spec)?;
97//! Ok(BenchReport::from_sdk_report(sdk_report))
98//! }
99//! ```
100//!
101//! ## Complete Example
102//!
103//! See the `examples/ffi-benchmark` directory for a complete working example.
104
105use serde::{Deserialize, Serialize};
106
107/// Trait for converting from SDK's BenchSpec type.
108///
109/// Implement this trait on your UniFFI-annotated BenchSpec type.
110pub trait FromSdkSpec: Sized {
111 /// Convert from the SDK's BenchSpec type.
112 fn from_sdk(spec: crate::BenchSpec) -> Self;
113
114 /// Convert to the SDK's BenchSpec type.
115 fn to_sdk(&self) -> crate::BenchSpec;
116}
117
118/// Trait for converting from SDK's BenchSample type.
119///
120/// Implement this trait on your UniFFI-annotated BenchSample type.
121pub trait FromSdkSample: Sized {
122 /// Convert from the SDK's BenchSample type.
123 fn from_sdk(sample: crate::BenchSample) -> Self;
124
125 /// Convert to the SDK's BenchSample type.
126 fn to_sdk(&self) -> crate::BenchSample;
127}
128
129/// Trait for converting from SDK's RunnerReport type.
130///
131/// Implement this trait on your UniFFI-annotated BenchReport type.
132pub trait FromSdkReport<Spec: FromSdkSpec, Sample: FromSdkSample>: Sized {
133 /// Convert from the SDK's RunnerReport type.
134 fn from_sdk_report(report: crate::RunnerReport) -> Self;
135}
136
137/// Trait for converting from SDK's BenchError type.
138///
139/// Implement this trait on your UniFFI-annotated error type.
140pub trait FromSdkError: Sized {
141 /// Convert from the SDK's BenchError type.
142 fn from_sdk(err: crate::types::BenchError) -> Self;
143}
144
145/// Pre-defined BenchSpec structure matching SDK's BenchSpec.
146///
147/// This struct can be used as a template for your own UniFFI-annotated type.
148/// Copy this definition and add the `#[derive(uniffi::Record)]` attribute.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct BenchSpecTemplate {
151 /// Name of the benchmark function to run.
152 pub name: String,
153 /// Number of measurement iterations.
154 pub iterations: u32,
155 /// Number of warmup iterations before measurement.
156 pub warmup: u32,
157}
158
159impl From<crate::BenchSpec> for BenchSpecTemplate {
160 fn from(spec: crate::BenchSpec) -> Self {
161 Self {
162 name: spec.name,
163 iterations: spec.iterations,
164 warmup: spec.warmup,
165 }
166 }
167}
168
169impl From<BenchSpecTemplate> for crate::BenchSpec {
170 fn from(spec: BenchSpecTemplate) -> Self {
171 Self {
172 name: spec.name,
173 iterations: spec.iterations,
174 warmup: spec.warmup,
175 }
176 }
177}
178
179/// Pre-defined BenchSample structure matching SDK's BenchSample.
180///
181/// This struct can be used as a template for your own UniFFI-annotated type.
182/// Copy this definition and add the `#[derive(uniffi::Record)]` attribute.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct BenchSampleTemplate {
185 /// Duration of the iteration in nanoseconds.
186 pub duration_ns: u64,
187 /// CPU time consumed by the measured iteration in milliseconds.
188 pub cpu_time_ms: Option<u64>,
189 /// Peak memory growth during the measured iteration in kilobytes.
190 ///
191 /// This is the legacy wire field for baseline-adjusted growth, not
192 /// absolute process or device peak memory.
193 pub peak_memory_kb: Option<u64>,
194 /// Peak resident memory of the benchmark process during the measured iteration.
195 pub process_peak_memory_kb: Option<u64>,
196}
197
198impl From<crate::BenchSample> for BenchSampleTemplate {
199 fn from(sample: crate::BenchSample) -> Self {
200 Self {
201 duration_ns: sample.duration_ns,
202 cpu_time_ms: sample.cpu_time_ms,
203 peak_memory_kb: sample.peak_memory_kb,
204 process_peak_memory_kb: sample.process_peak_memory_kb,
205 }
206 }
207}
208
209impl From<BenchSampleTemplate> for crate::BenchSample {
210 fn from(sample: BenchSampleTemplate) -> Self {
211 Self {
212 duration_ns: sample.duration_ns,
213 cpu_time_ms: sample.cpu_time_ms,
214 peak_memory_kb: sample.peak_memory_kb,
215 process_peak_memory_kb: sample.process_peak_memory_kb,
216 }
217 }
218}
219
220/// Pre-defined BenchReport structure matching SDK's RunnerReport.
221///
222/// This struct can be used as a template for your own UniFFI-annotated type.
223/// Copy this definition and add the `#[derive(uniffi::Record)]` attribute.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct BenchReportTemplate {
226 /// The specification used for this benchmark run.
227 pub spec: BenchSpecTemplate,
228 /// All collected timing samples.
229 pub samples: Vec<BenchSampleTemplate>,
230 /// Optional semantic phase timings captured during measured iterations.
231 pub phases: Vec<crate::SemanticPhase>,
232 /// Exact harness timeline spans in execution order.
233 pub timeline: Vec<crate::HarnessTimelineSpan>,
234}
235
236impl From<crate::RunnerReport> for BenchReportTemplate {
237 fn from(report: crate::RunnerReport) -> Self {
238 Self {
239 spec: report.spec.into(),
240 samples: report.samples.into_iter().map(Into::into).collect(),
241 phases: report.phases,
242 timeline: report.timeline,
243 }
244 }
245}
246
247/// Error variant enum for UniFFI integration.
248///
249/// This enum provides the standard error variants. Copy this and add
250/// `#[derive(uniffi::Error)]` and `#[uniffi(flat_error)]` attributes.
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub enum BenchErrorVariant {
253 /// The iteration count was zero.
254 InvalidIterations,
255 /// The requested benchmark function was not found.
256 UnknownFunction { name: String },
257 /// An error occurred during benchmark execution.
258 ExecutionFailed { reason: String },
259 /// Configuration error.
260 ConfigError { message: String },
261 /// I/O error.
262 IoError { message: String },
263}
264
265impl From<crate::types::BenchError> for BenchErrorVariant {
266 fn from(err: crate::types::BenchError) -> Self {
267 match err {
268 crate::types::BenchError::Runner(runner_err) => match runner_err {
269 crate::timing::TimingError::NoIterations { .. } => {
270 BenchErrorVariant::InvalidIterations
271 }
272 crate::timing::TimingError::Execution(msg) => {
273 BenchErrorVariant::ExecutionFailed { reason: msg }
274 }
275 },
276 crate::types::BenchError::UnknownFunction(name, _available) => {
277 BenchErrorVariant::UnknownFunction { name }
278 }
279 crate::types::BenchError::Execution(msg) => {
280 BenchErrorVariant::ExecutionFailed { reason: msg }
281 }
282 crate::types::BenchError::Io(e) => BenchErrorVariant::IoError {
283 message: e.to_string(),
284 },
285 crate::types::BenchError::Serialization(e) => BenchErrorVariant::ConfigError {
286 message: e.to_string(),
287 },
288 crate::types::BenchError::Config(msg) => {
289 BenchErrorVariant::ConfigError { message: msg }
290 }
291 crate::types::BenchError::Build(msg) => BenchErrorVariant::ExecutionFailed {
292 reason: format!("build error: {}", msg),
293 },
294 }
295 }
296}
297
298impl From<crate::timing::TimingError> for BenchErrorVariant {
299 fn from(err: crate::timing::TimingError) -> Self {
300 match err {
301 crate::timing::TimingError::NoIterations { .. } => BenchErrorVariant::InvalidIterations,
302 crate::timing::TimingError::Execution(msg) => {
303 BenchErrorVariant::ExecutionFailed { reason: msg }
304 }
305 }
306 }
307}
308
309/// Helper function to run a benchmark and convert result to template types.
310///
311/// This is useful for implementing your own `run_benchmark` FFI function:
312///
313/// ```ignore
314/// #[uniffi::export]
315/// pub fn run_benchmark(spec: BenchSpec) -> Result<BenchReport, BenchError> {
316/// let sdk_spec: mobench_sdk::BenchSpec = spec.into();
317/// let template_result = mobench_sdk::uniffi_types::run_benchmark_template(sdk_spec);
318/// match template_result {
319/// Ok(report) => Ok(BenchReport::from(report)),
320/// Err(err) => Err(BenchError::from(err)),
321/// }
322/// }
323/// ```
324#[cfg(feature = "registry")]
325pub fn run_benchmark_template(
326 spec: crate::BenchSpec,
327) -> Result<BenchReportTemplate, BenchErrorVariant> {
328 crate::run_benchmark(spec)
329 .map(Into::into)
330 .map_err(Into::into)
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn test_bench_spec_template_conversion() {
339 let sdk_spec = crate::BenchSpec {
340 name: "test".to_string(),
341 iterations: 100,
342 warmup: 10,
343 };
344
345 let template: BenchSpecTemplate = sdk_spec.clone().into();
346 assert_eq!(template.name, "test");
347 assert_eq!(template.iterations, 100);
348 assert_eq!(template.warmup, 10);
349
350 let back: crate::BenchSpec = template.into();
351 assert_eq!(back.name, sdk_spec.name);
352 assert_eq!(back.iterations, sdk_spec.iterations);
353 assert_eq!(back.warmup, sdk_spec.warmup);
354 }
355
356 #[test]
357 fn test_bench_sample_template_conversion() {
358 let sdk_sample = crate::BenchSample {
359 duration_ns: 12345,
360 cpu_time_ms: Some(12),
361 peak_memory_kb: Some(48),
362 process_peak_memory_kb: Some(1024),
363 };
364 let template: BenchSampleTemplate = sdk_sample.into();
365 assert_eq!(template.duration_ns, 12345);
366 assert_eq!(template.cpu_time_ms, Some(12));
367 assert_eq!(template.peak_memory_kb, Some(48));
368 assert_eq!(template.process_peak_memory_kb, Some(1024));
369 }
370
371 #[test]
372 fn test_bench_error_variant_conversion() {
373 let err = crate::types::BenchError::UnknownFunction(
374 "test_func".to_string(),
375 vec!["available_func".to_string()],
376 );
377 let variant: BenchErrorVariant = err.into();
378 match variant {
379 BenchErrorVariant::UnknownFunction { name } => assert_eq!(name, "test_func"),
380 _ => panic!("Expected UnknownFunction variant"),
381 }
382 }
383}