sal/aiprompts/rhaiwrapping_best_practices.md
2025-04-04 15:05:48 +02:00

6.2 KiB

Best Practices and Optimization

When wrapping Rust functions for use with Rhai, following these best practices will help you create efficient, maintainable, and robust code.

Performance Considerations

  1. Minimize Cloning: Rhai often requires cloning data, but you can minimize this overhead:

    // Prefer immutable references when possible
    fn process_data(data: &MyStruct) -> i64 {
        // Work with data without cloning
        data.value * 2
    }
    
    // Use mutable references for in-place modifications
    fn update_data(data: &mut MyStruct) {
        data.value += 1;
    }
    
  2. Avoid Excessive Type Conversions: Converting between Rhai's Dynamic type and Rust types has overhead:

    // Inefficient - multiple conversions
    fn process_inefficient(ctx: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
        let value = args[0].as_int()?;
        let result = value * 2;
        Ok(Dynamic::from(result))
    }
    
    // More efficient - use typed parameters when possible
    fn process_efficient(value: i64) -> i64 {
        value * 2
    }
    
  3. Batch Operations: For operations on collections, batch processing is more efficient:

    // Process an entire array at once rather than element by element
    fn sum_array(arr: Array) -> Result<i64, Box<EvalAltResult>> {
        arr.iter()
           .map(|v| v.as_int())
           .collect::<Result<Vec<i64>, _>>()
           .map(|nums| nums.iter().sum())
           .map_err(|_| "Array must contain only integers".into())
    }
    
  4. Compile Scripts Once: Reuse compiled ASTs for scripts that are executed multiple times:

    // Compile once
    let ast = engine.compile(script)?;
    
    // Execute multiple times with different parameters
    for i in 0..10 {
        let result = engine.eval_ast::<i64>(&ast)?;
        println!("Result {}: {}", i, result);
    }
    

Thread Safety

  1. Use Sync Mode When Needed: If you need thread safety, use the sync feature:

    // In Cargo.toml
    // rhai = { version = "1.x", features = ["sync"] }
    
    // This creates a thread-safe engine
    let engine = Engine::new();
    
    // Now you can safely share the engine between threads
    std::thread::spawn(move || {
        let result = engine.eval::<i64>("40 + 2")?;
        println!("Result: {}", result);
    });
    
  2. Clone the Engine for Multiple Threads: When not using sync, clone the engine for each thread:

    let engine = Engine::new();
    
    let handles: Vec<_> = (0..5).map(|i| {
        let engine_clone = engine.clone();
        std::thread::spawn(move || {
            let result = engine_clone.eval::<i64>(&format!("{} + 2", i * 10))?;
            println!("Thread {}: {}", i, result);
        })
    }).collect();
    
    for handle in handles {
        handle.join().unwrap();
    }
    

Memory Management

  1. Control Scope Size: Be mindful of the size of your scopes:

    // Create a new scope for each operation to avoid memory buildup
    for item in items {
        let mut scope = Scope::new();
        scope.push("item", item);
        engine.eval_with_scope::<()>(&mut scope, "process(item)")?;
    }
    
  2. Limit Script Complexity: Use engine options to limit script complexity:

    let mut engine = Engine::new();
    
    // Set limits to prevent scripts from consuming too many resources
    engine.set_max_expr_depths(64, 64)     // Max expression/statement depth
          .set_max_function_expr_depth(64) // Max function depth
          .set_max_array_size(10000)       // Max array size
          .set_max_map_size(10000)         // Max map size
          .set_max_string_size(10000)      // Max string size
          .set_max_call_levels(64);        // Max call stack depth
    
  3. Use Shared Values Carefully: Shared values (via closures) have reference-counting overhead:

    // Avoid unnecessary capturing in closures when possible
    engine.register_fn("process", |x: i64| x * 2);
    
    // Instead of capturing large data structures
    let large_data = vec![1, 2, 3, /* ... thousands of items ... */];
    engine.register_fn("process_data", move |idx: i64| {
        if idx >= 0 && (idx as usize) < large_data.len() {
            large_data[idx as usize]
        } else {
            0
        }
    });
    
    // Consider registering a lookup function instead
    let large_data = std::sync::Arc::new(vec![1, 2, 3, /* ... thousands of items ... */]);
    let data_ref = large_data.clone();
    engine.register_fn("lookup", move |idx: i64| {
        if idx >= 0 && (idx as usize) < data_ref.len() {
            data_ref[idx as usize]
        } else {
            0
        }
    });
    

API Design

  1. Consistent Naming: Use consistent naming conventions:

    // Good: Consistent naming pattern
    engine.register_fn("create_user", create_user)
          .register_fn("update_user", update_user)
          .register_fn("delete_user", delete_user);
    
    // Bad: Inconsistent naming
    engine.register_fn("create_user", create_user)
          .register_fn("user_update", update_user)
          .register_fn("remove", delete_user);
    
  2. Logical Function Grouping: Group related functions together:

    // Register all string-related functions together
    engine.register_fn("str_length", |s: &str| s.len() as i64)
          .register_fn("str_uppercase", |s: &str| s.to_uppercase())
          .register_fn("str_lowercase", |s: &str| s.to_lowercase());
    
    // Register all math-related functions together
    engine.register_fn("math_sin", |x: f64| x.sin())
          .register_fn("math_cos", |x: f64| x.cos())
          .register_fn("math_tan", |x: f64| x.tan());
    
  3. Comprehensive Documentation: Document your API thoroughly:

    // Add documentation for script writers
    let mut engine = Engine::new();
    
    #[cfg(feature = "metadata")]
    {
        // Add function documentation
        engine.register_fn("calculate_tax", calculate_tax)
              .register_fn_metadata("calculate_tax", |metadata| {
                  metadata.set_doc_comment("Calculates tax based on income and rate.\n\nParameters:\n- income: Annual income\n- rate: Tax rate (0.0-1.0)\n\nReturns: Calculated tax amount");
              });
    }