mobench_sdk/
codegen.rs

1//! Code generation and template management
2//!
3//! This module provides functionality for generating mobile app projects from
4//! embedded templates. It handles template parameterization and file generation.
5
6use crate::types::{BenchError, InitConfig, Target};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10use include_dir::{Dir, DirEntry, include_dir};
11
12const ANDROID_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/android");
13const IOS_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/ios");
14
15/// Template variable that can be replaced in template files
16#[derive(Debug, Clone)]
17pub struct TemplateVar {
18    pub name: &'static str,
19    pub value: String,
20}
21
22/// Generates a new mobile benchmark project from templates
23///
24/// Creates the necessary directory structure and files for benchmarking on
25/// mobile platforms. This includes:
26/// - A `bench-mobile/` crate for FFI bindings
27/// - Platform-specific app projects (Android and/or iOS)
28/// - Configuration files
29///
30/// # Arguments
31///
32/// * `config` - Configuration for project initialization
33///
34/// # Returns
35///
36/// * `Ok(PathBuf)` - Path to the generated project root
37/// * `Err(BenchError)` - If generation fails
38pub fn generate_project(config: &InitConfig) -> Result<PathBuf, BenchError> {
39    let output_dir = &config.output_dir;
40    let project_slug = sanitize_package_name(&config.project_name);
41    let project_pascal = to_pascal_case(&project_slug);
42    let bundle_prefix = format!("dev.world.{}", project_slug);
43
44    // Create base directories
45    fs::create_dir_all(output_dir)?;
46
47    // Generate bench-mobile FFI wrapper crate
48    generate_bench_mobile_crate(output_dir, &project_slug)?;
49
50    // Generate platform-specific projects
51    match config.target {
52        Target::Android => {
53            generate_android_project(output_dir, &project_slug)?;
54        }
55        Target::Ios => {
56            generate_ios_project(output_dir, &project_slug, &project_pascal, &bundle_prefix)?;
57        }
58        Target::Both => {
59            generate_android_project(output_dir, &project_slug)?;
60            generate_ios_project(output_dir, &project_slug, &project_pascal, &bundle_prefix)?;
61        }
62    }
63
64    // Generate config file
65    generate_config_file(output_dir, config)?;
66
67    // Generate examples if requested
68    if config.generate_examples {
69        generate_example_benchmarks(output_dir)?;
70    }
71
72    Ok(output_dir.clone())
73}
74
75/// Generates the bench-mobile FFI wrapper crate
76fn generate_bench_mobile_crate(output_dir: &Path, project_name: &str) -> Result<(), BenchError> {
77    let crate_dir = output_dir.join("bench-mobile");
78    fs::create_dir_all(crate_dir.join("src"))?;
79
80    let crate_name = format!("{}-bench-mobile", project_name);
81
82    // Generate Cargo.toml
83    let cargo_toml = format!(
84        r#"[package]
85name = "{}"
86version = "0.1.0"
87edition = "2021"
88
89[lib]
90crate-type = ["cdylib", "staticlib", "rlib"]
91
92[dependencies]
93bench-sdk = {{ path = ".." }}
94uniffi = "0.28"
95{} = {{ path = ".." }}
96
97[features]
98default = []
99
100[build-dependencies]
101uniffi = {{ version = "0.28", features = ["build"] }}
102"#,
103        crate_name, project_name
104    );
105
106    fs::write(crate_dir.join("Cargo.toml"), cargo_toml)?;
107
108    // Generate src/lib.rs
109    let lib_rs_template = r#"//! Mobile FFI bindings for benchmarks
110//!
111//! This crate provides the FFI boundary between Rust benchmarks and mobile
112//! platforms (Android/iOS). It uses UniFFI to generate type-safe bindings.
113
114use uniffi;
115
116// Ensure the user crate is linked so benchmark registrations are pulled in.
117extern crate {{USER_CRATE}} as _bench_user_crate;
118
119// Re-export bench-sdk types with UniFFI annotations
120#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
121pub struct BenchSpec {
122    pub name: String,
123    pub iterations: u32,
124    pub warmup: u32,
125}
126
127#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
128pub struct BenchSample {
129    pub duration_ns: u64,
130}
131
132#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
133pub struct BenchReport {
134    pub spec: BenchSpec,
135    pub samples: Vec<BenchSample>,
136}
137
138#[derive(Debug, thiserror::Error, uniffi::Error)]
139#[uniffi(flat_error)]
140pub enum BenchError {
141    #[error("iterations must be greater than zero")]
142    InvalidIterations,
143
144    #[error("unknown benchmark function: {name}")]
145    UnknownFunction { name: String },
146
147    #[error("benchmark execution failed: {reason}")]
148    ExecutionFailed { reason: String },
149}
150
151// Convert from bench-sdk types
152impl From<mobench_sdk::BenchSpec> for BenchSpec {
153    fn from(spec: mobench_sdk::BenchSpec) -> Self {
154        Self {
155            name: spec.name,
156            iterations: spec.iterations,
157            warmup: spec.warmup,
158        }
159    }
160}
161
162impl From<BenchSpec> for mobench_sdk::BenchSpec {
163    fn from(spec: BenchSpec) -> Self {
164        Self {
165            name: spec.name,
166            iterations: spec.iterations,
167            warmup: spec.warmup,
168        }
169    }
170}
171
172impl From<mobench_sdk::BenchSample> for BenchSample {
173    fn from(sample: mobench_sdk::BenchSample) -> Self {
174        Self {
175            duration_ns: sample.duration_ns,
176        }
177    }
178}
179
180impl From<mobench_sdk::RunnerReport> for BenchReport {
181    fn from(report: mobench_sdk::RunnerReport) -> Self {
182        Self {
183            spec: report.spec.into(),
184            samples: report.samples.into_iter().map(Into::into).collect(),
185        }
186    }
187}
188
189impl From<mobench_sdk::BenchError> for BenchError {
190    fn from(err: mobench_sdk::BenchError) -> Self {
191        match err {
192            mobench_sdk::BenchError::Runner(runner_err) => {
193                BenchError::ExecutionFailed {
194                    reason: runner_err.to_string(),
195                }
196            }
197            mobench_sdk::BenchError::UnknownFunction(name) => {
198                BenchError::UnknownFunction { name }
199            }
200            _ => BenchError::ExecutionFailed {
201                reason: err.to_string(),
202            },
203        }
204    }
205}
206
207/// Runs a benchmark by name with the given specification
208///
209/// This is the main FFI entry point called from mobile platforms.
210#[uniffi::export]
211pub fn run_benchmark(spec: BenchSpec) -> Result<BenchReport, BenchError> {
212    let sdk_spec: mobench_sdk::BenchSpec = spec.into();
213    let report = mobench_sdk::run_benchmark(sdk_spec)?;
214    Ok(report.into())
215}
216
217// Generate UniFFI scaffolding
218uniffi::setup_scaffolding!();
219"#;
220
221    let lib_rs = render_template(
222        lib_rs_template,
223        &[TemplateVar {
224            name: "USER_CRATE",
225            value: project_name.replace('-', "_"),
226        }],
227    );
228    fs::write(crate_dir.join("src/lib.rs"), lib_rs)?;
229
230    // Generate build.rs
231    let build_rs = r#"fn main() {
232    uniffi::generate_scaffolding("src/lib.rs").unwrap();
233}
234"#;
235
236    fs::write(crate_dir.join("build.rs"), build_rs)?;
237
238    Ok(())
239}
240
241/// Generates Android project structure
242fn generate_android_project(output_dir: &Path, project_slug: &str) -> Result<(), BenchError> {
243    let target_dir = output_dir.join("android");
244    let vars = vec![
245        TemplateVar {
246            name: "PACKAGE_NAME",
247            value: format!("dev.world.{}", project_slug),
248        },
249        TemplateVar {
250            name: "UNIFFI_NAMESPACE",
251            value: project_slug.replace('-', "_"),
252        },
253        TemplateVar {
254            name: "LIBRARY_NAME",
255            value: project_slug.replace('-', "_"),
256        },
257        TemplateVar {
258            name: "DEFAULT_FUNCTION",
259            value: "example_fibonacci".to_string(),
260        },
261    ];
262    render_dir(&ANDROID_TEMPLATES, Path::new(""), &target_dir, &vars)?;
263    Ok(())
264}
265
266/// Generates iOS project structure
267fn generate_ios_project(
268    output_dir: &Path,
269    project_slug: &str,
270    project_pascal: &str,
271    bundle_prefix: &str,
272) -> Result<(), BenchError> {
273    let target_dir = output_dir.join("ios");
274    let vars = vec![
275        TemplateVar {
276            name: "DEFAULT_FUNCTION",
277            value: "example_fibonacci".to_string(),
278        },
279        TemplateVar {
280            name: "PROJECT_NAME_PASCAL",
281            value: project_pascal.to_string(),
282        },
283        TemplateVar {
284            name: "BUNDLE_ID_PREFIX",
285            value: bundle_prefix.to_string(),
286        },
287        TemplateVar {
288            name: "BUNDLE_ID",
289            value: format!("{}.{}", bundle_prefix, project_slug),
290        },
291        TemplateVar {
292            name: "LIBRARY_NAME",
293            value: project_slug.replace('-', "_"),
294        },
295    ];
296    render_dir(&IOS_TEMPLATES, Path::new(""), &target_dir, &vars)?;
297    Ok(())
298}
299
300/// Generates bench-sdk.toml configuration file
301fn generate_config_file(output_dir: &Path, config: &InitConfig) -> Result<(), BenchError> {
302    let config_content = format!(
303        r#"# Bench SDK Configuration
304# This file controls how benchmarks are built and executed
305
306[project]
307name = "{}"
308target = "{}"
309
310[build]
311profile = "debug"  # or "release"
312
313# BrowserStack configuration (optional)
314# Uncomment and fill in your credentials to use BrowserStack
315# [browserstack]
316# username = "${{BROWSERSTACK_USERNAME}}"
317# access_key = "${{BROWSERSTACK_ACCESS_KEY}}"
318# project = "{}-benchmarks"
319
320# Device matrix (optional)
321# Uncomment to specify devices for BrowserStack runs
322# [[devices]]
323# name = "Pixel 7"
324# os = "android"
325# os_version = "13.0"
326# tags = ["default"]
327
328# [[devices]]
329# name = "iPhone 14"
330# os = "ios"
331# os_version = "16"
332# tags = ["default"]
333"#,
334        config.project_name,
335        config.target.as_str(),
336        config.project_name
337    );
338
339    fs::write(output_dir.join("bench-sdk.toml"), config_content)?;
340
341    Ok(())
342}
343
344/// Generates example benchmark functions
345fn generate_example_benchmarks(output_dir: &Path) -> Result<(), BenchError> {
346    let examples_dir = output_dir.join("benches");
347    fs::create_dir_all(&examples_dir)?;
348
349    let example_content = r#"//! Example benchmarks
350//!
351//! This file demonstrates how to write benchmarks with bench-sdk.
352
353use mobench_sdk::benchmark;
354
355/// Simple benchmark example
356#[benchmark]
357fn example_fibonacci() {
358    let result = fibonacci(30);
359    std::hint::black_box(result);
360}
361
362/// Another example with a loop
363#[benchmark]
364fn example_sum() {
365    let mut sum = 0u64;
366    for i in 0..10000 {
367        sum = sum.wrapping_add(i);
368    }
369    std::hint::black_box(sum);
370}
371
372// Helper function (not benchmarked)
373fn fibonacci(n: u32) -> u64 {
374    match n {
375        0 => 0,
376        1 => 1,
377        _ => {
378            let mut a = 0u64;
379            let mut b = 1u64;
380            for _ in 2..=n {
381                let next = a.wrapping_add(b);
382                a = b;
383                b = next;
384            }
385            b
386        }
387    }
388}
389"#;
390
391    fs::write(examples_dir.join("example.rs"), example_content)?;
392
393    Ok(())
394}
395
396fn render_dir(
397    dir: &Dir,
398    prefix: &Path,
399    out_root: &Path,
400    vars: &[TemplateVar],
401) -> Result<(), BenchError> {
402    for entry in dir.entries() {
403        match entry {
404            DirEntry::Dir(sub) => {
405                // Skip cache directories
406                if sub.path().components().any(|c| c.as_os_str() == ".gradle") {
407                    continue;
408                }
409                let next_prefix = prefix.join(sub.path());
410                render_dir(sub, &next_prefix, out_root, vars)?;
411            }
412            DirEntry::File(file) => {
413                if file.path().components().any(|c| c.as_os_str() == ".gradle") {
414                    continue;
415                }
416                let mut relative = prefix.join(file.path());
417                let mut contents = file.contents().to_vec();
418                if let Some(ext) = relative.extension()
419                    && ext == "template"
420                {
421                    relative.set_extension("");
422                    let rendered = render_template(
423                        std::str::from_utf8(&contents).map_err(|e| {
424                            BenchError::Build(format!(
425                                "invalid UTF-8 in template {:?}: {}",
426                                file.path(),
427                                e
428                            ))
429                        })?,
430                        vars,
431                    );
432                    contents = rendered.into_bytes();
433                }
434                let out_path = out_root.join(relative);
435                if let Some(parent) = out_path.parent() {
436                    fs::create_dir_all(parent)?;
437                }
438                fs::write(&out_path, contents)?;
439            }
440        }
441    }
442    Ok(())
443}
444
445fn render_template(input: &str, vars: &[TemplateVar]) -> String {
446    let mut output = input.to_string();
447    for var in vars {
448        output = output.replace(&format!("{{{{{}}}}}", var.name), &var.value);
449    }
450    output
451}
452
453fn sanitize_package_name(name: &str) -> String {
454    name.chars()
455        .map(|c| {
456            if c.is_ascii_alphanumeric() {
457                c.to_ascii_lowercase()
458            } else {
459                '-'
460            }
461        })
462        .collect::<String>()
463        .trim_matches('-')
464        .replace("--", "-")
465}
466
467fn to_pascal_case(input: &str) -> String {
468    input
469        .split(|c: char| !c.is_ascii_alphanumeric())
470        .filter(|s| !s.is_empty())
471        .map(|s| {
472            let mut chars = s.chars();
473            let first = chars.next().unwrap().to_ascii_uppercase();
474            let rest: String = chars.map(|c| c.to_ascii_lowercase()).collect();
475            format!("{}{}", first, rest)
476        })
477        .collect::<String>()
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use std::env;
484
485    #[test]
486    fn test_generate_bench_mobile_crate() {
487        let temp_dir = env::temp_dir().join("bench-sdk-test");
488        fs::create_dir_all(&temp_dir).unwrap();
489
490        let result = generate_bench_mobile_crate(&temp_dir, "test_project");
491        assert!(result.is_ok());
492
493        // Verify files were created
494        assert!(temp_dir.join("bench-mobile/Cargo.toml").exists());
495        assert!(temp_dir.join("bench-mobile/src/lib.rs").exists());
496        assert!(temp_dir.join("bench-mobile/build.rs").exists());
497
498        // Cleanup
499        fs::remove_dir_all(&temp_dir).ok();
500    }
501}