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
-
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; }
-
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 }
-
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()) }
-
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
-
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); });
-
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
-
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)")?; }
-
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
-
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
-
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);
-
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());
-
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"); }); }