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    // Note: We configure rustls to use 'ring' instead of 'aws-lc-rs' (default in rustls 0.23+)
84    // because aws-lc-rs doesn't compile for Android NDK targets.
85    let cargo_toml = format!(
86        r#"[package]
87name = "{}"
88version = "0.1.0"
89edition = "2021"
90
91[lib]
92crate-type = ["cdylib", "staticlib", "rlib"]
93
94[dependencies]
95mobench-sdk = {{ path = ".." }}
96uniffi = "0.28"
97{} = {{ path = ".." }}
98
99[features]
100default = []
101
102[build-dependencies]
103uniffi = {{ version = "0.28", features = ["build"] }}
104
105# Binary for generating UniFFI bindings (used by mobench build)
106[[bin]]
107name = "uniffi-bindgen"
108path = "src/bin/uniffi-bindgen.rs"
109
110# IMPORTANT: If your project uses rustls (directly or transitively), you must configure
111# it to use the 'ring' crypto backend instead of 'aws-lc-rs' (the default in rustls 0.23+).
112# aws-lc-rs doesn't compile for Android NDK targets due to C compilation issues.
113#
114# Add this to your root Cargo.toml:
115# [workspace.dependencies]
116# rustls = {{ version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }}
117#
118# Then in each crate that uses rustls:
119# [dependencies]
120# rustls = {{ workspace = true }}
121"#,
122        crate_name, project_name
123    );
124
125    fs::write(crate_dir.join("Cargo.toml"), cargo_toml)?;
126
127    // Generate src/lib.rs
128    let lib_rs_template = r#"//! Mobile FFI bindings for benchmarks
129//!
130//! This crate provides the FFI boundary between Rust benchmarks and mobile
131//! platforms (Android/iOS). It uses UniFFI to generate type-safe bindings.
132
133use uniffi;
134
135// Ensure the user crate is linked so benchmark registrations are pulled in.
136extern crate {{USER_CRATE}} as _bench_user_crate;
137
138// Re-export mobench-sdk types with UniFFI annotations
139#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
140pub struct BenchSpec {
141    pub name: String,
142    pub iterations: u32,
143    pub warmup: u32,
144}
145
146#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
147pub struct BenchSample {
148    pub duration_ns: u64,
149}
150
151#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, uniffi::Record)]
152pub struct BenchReport {
153    pub spec: BenchSpec,
154    pub samples: Vec<BenchSample>,
155}
156
157#[derive(Debug, thiserror::Error, uniffi::Error)]
158#[uniffi(flat_error)]
159pub enum BenchError {
160    #[error("iterations must be greater than zero")]
161    InvalidIterations,
162
163    #[error("unknown benchmark function: {name}")]
164    UnknownFunction { name: String },
165
166    #[error("benchmark execution failed: {reason}")]
167    ExecutionFailed { reason: String },
168}
169
170// Convert from mobench-sdk types
171impl From<mobench_sdk::BenchSpec> for BenchSpec {
172    fn from(spec: mobench_sdk::BenchSpec) -> Self {
173        Self {
174            name: spec.name,
175            iterations: spec.iterations,
176            warmup: spec.warmup,
177        }
178    }
179}
180
181impl From<BenchSpec> for mobench_sdk::BenchSpec {
182    fn from(spec: BenchSpec) -> Self {
183        Self {
184            name: spec.name,
185            iterations: spec.iterations,
186            warmup: spec.warmup,
187        }
188    }
189}
190
191impl From<mobench_sdk::BenchSample> for BenchSample {
192    fn from(sample: mobench_sdk::BenchSample) -> Self {
193        Self {
194            duration_ns: sample.duration_ns,
195        }
196    }
197}
198
199impl From<mobench_sdk::RunnerReport> for BenchReport {
200    fn from(report: mobench_sdk::RunnerReport) -> Self {
201        Self {
202            spec: report.spec.into(),
203            samples: report.samples.into_iter().map(Into::into).collect(),
204        }
205    }
206}
207
208impl From<mobench_sdk::BenchError> for BenchError {
209    fn from(err: mobench_sdk::BenchError) -> Self {
210        match err {
211            mobench_sdk::BenchError::Runner(runner_err) => {
212                BenchError::ExecutionFailed {
213                    reason: runner_err.to_string(),
214                }
215            }
216            mobench_sdk::BenchError::UnknownFunction(name) => {
217                BenchError::UnknownFunction { name }
218            }
219            _ => BenchError::ExecutionFailed {
220                reason: err.to_string(),
221            },
222        }
223    }
224}
225
226/// Runs a benchmark by name with the given specification
227///
228/// This is the main FFI entry point called from mobile platforms.
229#[uniffi::export]
230pub fn run_benchmark(spec: BenchSpec) -> Result<BenchReport, BenchError> {
231    let sdk_spec: mobench_sdk::BenchSpec = spec.into();
232    let report = mobench_sdk::run_benchmark(sdk_spec)?;
233    Ok(report.into())
234}
235
236// Generate UniFFI scaffolding
237uniffi::setup_scaffolding!();
238"#;
239
240    let lib_rs = render_template(
241        lib_rs_template,
242        &[TemplateVar {
243            name: "USER_CRATE",
244            value: project_name.replace('-', "_"),
245        }],
246    );
247    fs::write(crate_dir.join("src/lib.rs"), lib_rs)?;
248
249    // Generate build.rs
250    let build_rs = r#"fn main() {
251    uniffi::generate_scaffolding("src/lib.rs").unwrap();
252}
253"#;
254
255    fs::write(crate_dir.join("build.rs"), build_rs)?;
256
257    // Generate uniffi-bindgen binary (used by mobench build)
258    let bin_dir = crate_dir.join("src/bin");
259    fs::create_dir_all(&bin_dir)?;
260    let uniffi_bindgen_rs = r#"fn main() {
261    uniffi::uniffi_bindgen_main()
262}
263"#;
264    fs::write(bin_dir.join("uniffi-bindgen.rs"), uniffi_bindgen_rs)?;
265
266    Ok(())
267}
268
269/// Generates Android project structure from templates
270///
271/// This function can be called standalone to generate just the Android
272/// project scaffolding, useful for auto-generation during build.
273///
274/// # Arguments
275///
276/// * `output_dir` - Directory to write the `android/` project into
277/// * `project_slug` - Project name (e.g., "bench-mobile" -> "bench_mobile")
278pub fn generate_android_project(output_dir: &Path, project_slug: &str) -> Result<(), BenchError> {
279    let target_dir = output_dir.join("android");
280    let vars = vec![
281        TemplateVar {
282            name: "PACKAGE_NAME",
283            value: format!("dev.world.{}", project_slug),
284        },
285        TemplateVar {
286            name: "UNIFFI_NAMESPACE",
287            value: project_slug.replace('-', "_"),
288        },
289        TemplateVar {
290            name: "LIBRARY_NAME",
291            value: project_slug.replace('-', "_"),
292        },
293        TemplateVar {
294            name: "DEFAULT_FUNCTION",
295            value: "example_fibonacci".to_string(),
296        },
297    ];
298    render_dir(&ANDROID_TEMPLATES, &target_dir, &vars)?;
299    Ok(())
300}
301
302/// Generates iOS project structure from templates
303///
304/// This function can be called standalone to generate just the iOS
305/// project scaffolding, useful for auto-generation during build.
306///
307/// # Arguments
308///
309/// * `output_dir` - Directory to write the `ios/` project into
310/// * `project_slug` - Project name (e.g., "bench-mobile" -> "bench_mobile")
311/// * `project_pascal` - PascalCase version of project name (e.g., "BenchMobile")
312/// * `bundle_prefix` - iOS bundle ID prefix (e.g., "dev.world.bench")
313pub fn generate_ios_project(
314    output_dir: &Path,
315    project_slug: &str,
316    project_pascal: &str,
317    bundle_prefix: &str,
318) -> Result<(), BenchError> {
319    let target_dir = output_dir.join("ios");
320    let vars = vec![
321        TemplateVar {
322            name: "DEFAULT_FUNCTION",
323            value: "example_fibonacci".to_string(),
324        },
325        TemplateVar {
326            name: "PROJECT_NAME_PASCAL",
327            value: project_pascal.to_string(),
328        },
329        TemplateVar {
330            name: "BUNDLE_ID_PREFIX",
331            value: bundle_prefix.to_string(),
332        },
333        TemplateVar {
334            name: "BUNDLE_ID",
335            value: format!("{}.{}", bundle_prefix, project_slug),
336        },
337        TemplateVar {
338            name: "LIBRARY_NAME",
339            value: project_slug.replace('-', "_"),
340        },
341    ];
342    render_dir(&IOS_TEMPLATES, &target_dir, &vars)?;
343    Ok(())
344}
345
346/// Generates bench-config.toml configuration file
347fn generate_config_file(output_dir: &Path, config: &InitConfig) -> Result<(), BenchError> {
348    let config_target = match config.target {
349        Target::Ios => "ios",
350        Target::Android | Target::Both => "android",
351    };
352    let config_content = format!(
353        r#"# mobench configuration
354# This file controls how benchmarks are executed on devices.
355
356target = "{}"
357function = "example_fibonacci"
358iterations = 100
359warmup = 10
360device_matrix = "device-matrix.yaml"
361device_tags = ["default"]
362
363[browserstack]
364app_automate_username = "${{BROWSERSTACK_USERNAME}}"
365app_automate_access_key = "${{BROWSERSTACK_ACCESS_KEY}}"
366project = "{}-benchmarks"
367
368[ios_xcuitest]
369app = "target/ios/BenchRunner.ipa"
370test_suite = "target/ios/BenchRunnerUITests.zip"
371"#,
372        config_target, config.project_name
373    );
374
375    fs::write(output_dir.join("bench-config.toml"), config_content)?;
376
377    Ok(())
378}
379
380/// Generates example benchmark functions
381fn generate_example_benchmarks(output_dir: &Path) -> Result<(), BenchError> {
382    let examples_dir = output_dir.join("benches");
383    fs::create_dir_all(&examples_dir)?;
384
385    let example_content = r#"//! Example benchmarks
386//!
387//! This file demonstrates how to write benchmarks with mobench-sdk.
388
389use mobench_sdk::benchmark;
390
391/// Simple benchmark example
392#[benchmark]
393fn example_fibonacci() {
394    let result = fibonacci(30);
395    std::hint::black_box(result);
396}
397
398/// Another example with a loop
399#[benchmark]
400fn example_sum() {
401    let mut sum = 0u64;
402    for i in 0..10000 {
403        sum = sum.wrapping_add(i);
404    }
405    std::hint::black_box(sum);
406}
407
408// Helper function (not benchmarked)
409fn fibonacci(n: u32) -> u64 {
410    match n {
411        0 => 0,
412        1 => 1,
413        _ => {
414            let mut a = 0u64;
415            let mut b = 1u64;
416            for _ in 2..=n {
417                let next = a.wrapping_add(b);
418                a = b;
419                b = next;
420            }
421            b
422        }
423    }
424}
425"#;
426
427    fs::write(examples_dir.join("example.rs"), example_content)?;
428
429    Ok(())
430}
431
432fn render_dir(
433    dir: &Dir,
434    out_root: &Path,
435    vars: &[TemplateVar],
436) -> Result<(), BenchError> {
437    for entry in dir.entries() {
438        match entry {
439            DirEntry::Dir(sub) => {
440                // Skip cache directories
441                if sub.path().components().any(|c| c.as_os_str() == ".gradle") {
442                    continue;
443                }
444                render_dir(sub, out_root, vars)?;
445            }
446            DirEntry::File(file) => {
447                if file.path().components().any(|c| c.as_os_str() == ".gradle") {
448                    continue;
449                }
450                // file.path() returns the full relative path from the embedded dir root
451                let mut relative = file.path().to_path_buf();
452                let mut contents = file.contents().to_vec();
453                if let Some(ext) = relative.extension()
454                    && ext == "template"
455                {
456                    relative.set_extension("");
457                    let rendered = render_template(
458                        std::str::from_utf8(&contents).map_err(|e| {
459                            BenchError::Build(format!(
460                                "invalid UTF-8 in template {:?}: {}",
461                                file.path(),
462                                e
463                            ))
464                        })?,
465                        vars,
466                    );
467                    contents = rendered.into_bytes();
468                }
469                let out_path = out_root.join(relative);
470                if let Some(parent) = out_path.parent() {
471                    fs::create_dir_all(parent)?;
472                }
473                fs::write(&out_path, contents)?;
474            }
475        }
476    }
477    Ok(())
478}
479
480fn render_template(input: &str, vars: &[TemplateVar]) -> String {
481    let mut output = input.to_string();
482    for var in vars {
483        output = output.replace(&format!("{{{{{}}}}}", var.name), &var.value);
484    }
485    output
486}
487
488fn sanitize_package_name(name: &str) -> String {
489    name.chars()
490        .map(|c| {
491            if c.is_ascii_alphanumeric() {
492                c.to_ascii_lowercase()
493            } else {
494                '-'
495            }
496        })
497        .collect::<String>()
498        .trim_matches('-')
499        .replace("--", "-")
500}
501
502/// Converts a string to PascalCase
503pub fn to_pascal_case(input: &str) -> String {
504    input
505        .split(|c: char| !c.is_ascii_alphanumeric())
506        .filter(|s| !s.is_empty())
507        .map(|s| {
508            let mut chars = s.chars();
509            let first = chars.next().unwrap().to_ascii_uppercase();
510            let rest: String = chars.map(|c| c.to_ascii_lowercase()).collect();
511            format!("{}{}", first, rest)
512        })
513        .collect::<String>()
514}
515
516/// Checks if the Android project scaffolding exists at the given output directory
517///
518/// Returns true if the `android/build.gradle` or `android/build.gradle.kts` file exists.
519pub fn android_project_exists(output_dir: &Path) -> bool {
520    let android_dir = output_dir.join("android");
521    android_dir.join("build.gradle").exists() || android_dir.join("build.gradle.kts").exists()
522}
523
524/// Checks if the iOS project scaffolding exists at the given output directory
525///
526/// Returns true if the `ios/BenchRunner/project.yml` file exists.
527pub fn ios_project_exists(output_dir: &Path) -> bool {
528    output_dir.join("ios/BenchRunner/project.yml").exists()
529}
530
531/// Auto-generates Android project scaffolding from a crate name
532///
533/// This is a convenience function that derives template variables from the
534/// crate name and generates the Android project structure.
535///
536/// # Arguments
537///
538/// * `output_dir` - Directory to write the `android/` project into
539/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
540pub fn ensure_android_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
541    if android_project_exists(output_dir) {
542        return Ok(());
543    }
544
545    println!("Android project not found, generating scaffolding...");
546    let project_slug = crate_name.replace('-', "_");
547    generate_android_project(output_dir, &project_slug)?;
548    println!("  Generated Android project at {:?}", output_dir.join("android"));
549    Ok(())
550}
551
552/// Auto-generates iOS project scaffolding from a crate name
553///
554/// This is a convenience function that derives template variables from the
555/// crate name and generates the iOS project structure.
556///
557/// # Arguments
558///
559/// * `output_dir` - Directory to write the `ios/` project into
560/// * `crate_name` - Name of the benchmark crate (e.g., "bench-mobile")
561pub fn ensure_ios_project(output_dir: &Path, crate_name: &str) -> Result<(), BenchError> {
562    if ios_project_exists(output_dir) {
563        return Ok(());
564    }
565
566    println!("iOS project not found, generating scaffolding...");
567    // Use fixed "BenchRunner" for project/scheme name to match template directory structure
568    let project_pascal = "BenchRunner";
569    // Derive library name and bundle prefix from crate name
570    let library_name = crate_name.replace('-', "_");
571    let bundle_prefix = format!("dev.world.{}", library_name.replace('_', "-"));
572    generate_ios_project(output_dir, &library_name, project_pascal, &bundle_prefix)?;
573    println!("  Generated iOS project at {:?}", output_dir.join("ios"));
574    Ok(())
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use std::env;
581
582    #[test]
583    fn test_generate_bench_mobile_crate() {
584        let temp_dir = env::temp_dir().join("mobench-sdk-test");
585        fs::create_dir_all(&temp_dir).unwrap();
586
587        let result = generate_bench_mobile_crate(&temp_dir, "test_project");
588        assert!(result.is_ok());
589
590        // Verify files were created
591        assert!(temp_dir.join("bench-mobile/Cargo.toml").exists());
592        assert!(temp_dir.join("bench-mobile/src/lib.rs").exists());
593        assert!(temp_dir.join("bench-mobile/build.rs").exists());
594
595        // Cleanup
596        fs::remove_dir_all(&temp_dir).ok();
597    }
598}