This commit is contained in:
despiegk 2025-04-04 14:58:51 +02:00
parent 2221f3f88c
commit c7a7201cfd
3 changed files with 1310 additions and 9 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,134 @@
### Error Handling in Dynamic Functions
When working with the dynamic function signature, error handling is slightly different:
```rust
fn dynamic_function(ctx: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
// Get the position information from the context
let pos = ctx.position();
// Validate arguments
if args.len() < 2 {
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Expected at least 2 arguments, got {}", args.len()),
pos
)));
}
// Try to convert arguments with proper error handling
let arg1 = match args[0].as_int() {
Ok(val) => val,
Err(_) => return Err(Box::new(EvalAltResult::ErrorMismatchOutputType(
"Expected first argument to be an integer".into(),
pos,
"i64".into()
)))
};
// Process with error handling
if arg1 <= 0 {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"First argument must be positive".into(),
pos
)));
}
// Return success
Ok(Dynamic::from(arg1 * 2))
}
```
## Advanced Patterns
### Working with Function Pointers
You can create function pointers that bind to Rust functions:
```rust
fn my_awesome_fn(ctx: NativeCallContext, args: &mut[&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
// Check number of arguments
if args.len() != 2 {
return Err("one argument is required, plus the object".into());
}
// Get call arguments
let x = args[1].try_cast::<i64>().map_err(|_| "argument must be an integer".into())?;
// Get mutable reference to the object map, which is passed as the first argument
let map = &mut *args[0].as_map_mut().map_err(|_| "object must be a map".into())?;
// Do something awesome here ...
let result = x * 2;
Ok(result.into())
}
// Register a function to create a pre-defined object
engine.register_fn("create_awesome_object", || {
// Use an object map as base
let mut map = Map::new();
// Create a function pointer that binds to 'my_awesome_fn'
let fp = FnPtr::from_fn("awesome", my_awesome_fn)?;
// ^ name of method
// ^ native function
// Store the function pointer in the object map
map.insert("awesome".into(), fp.into());
Ok(Dynamic::from_map(map))
});
```
### Creating Rust Closures from Rhai Functions
You can encapsulate a Rhai script as a Rust closure:
```rust
use rhai::{Engine, Func};
let engine = Engine::new();
let script = "fn calc(x, y) { x + y.len < 42 }";
// Create a Rust closure from a Rhai function
let func = Func::<(i64, &str), bool>::create_from_script(
engine, // the 'Engine' is consumed into the closure
script, // the script
"calc" // the entry-point function name
)?;
// Call the closure
let result = func(123, "hello")?;
// Pass it as a callback to another function
schedule_callback(func);
```
### Calling Rhai Functions from Rust
You can call Rhai functions from Rust:
```rust
// Compile the script to AST
let ast = engine.compile(script)?;
// Create a custom 'Scope'
let mut scope = Scope::new();
// Add variables to the scope
scope.push("my_var", 42_i64);
scope.push("my_string", "hello, world!");
scope.push_constant("MY_CONST", true);
// Call a function defined in the script
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ("abc", 123_i64))?;
// For a function with one parameter, use a tuple with a trailing comma
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", (123_i64,))?;
// For a function with no parameters
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ())?;
```

View File

@ -0,0 +1,187 @@
## 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:
```rust
// 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:
```rust
// 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:
```rust
// 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:
```rust
// 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:
```rust
// 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:
```rust
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:
```rust
// 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:
```rust
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:
```rust
// 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:
```rust
// 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:
```rust
// 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:
```rust
// 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");
});
}
```