995 lines
26 KiB
Markdown
995 lines
26 KiB
Markdown
# Best Practices for Wrapping Rust Functions with Rhai
|
|
|
|
This document provides comprehensive guidance on how to effectively wrap Rust functions with different standard arguments, pass structs, and handle various return types including errors when using the Rhai scripting language.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Introduction](#introduction)
|
|
2. [Basic Function Registration](#basic-function-registration)
|
|
3. [Working with Different Argument Types](#working-with-different-argument-types)
|
|
4. [Passing and Working with Structs](#passing-and-working-with-structs)
|
|
5. [Error Handling](#error-handling)
|
|
6. [Returning Different Types](#returning-different-types)
|
|
7. [Native Function Handling](#native-function-handling)
|
|
8. [Advanced Patterns](#advanced-patterns)
|
|
9. [Complete Examples](#complete-examples)
|
|
|
|
## Introduction
|
|
|
|
Rhai is an embedded scripting language for Rust that allows you to expose Rust functions to scripts and vice versa. This document focuses on the best practices for wrapping Rust functions so they can be called from Rhai scripts, with special attention to handling different argument types, structs, and error conditions.
|
|
|
|
## Basic Function Registration
|
|
|
|
### Simple Function Registration
|
|
|
|
The most basic way to register a Rust function with Rhai is using the `register_fn` method:
|
|
|
|
```rust
|
|
fn add(x: i64, y: i64) -> i64 {
|
|
x + y
|
|
}
|
|
|
|
fn main() -> Result<(), Box<EvalAltResult>> {
|
|
let mut engine = Engine::new();
|
|
|
|
// Register the function with Rhai
|
|
engine.register_fn("add", add);
|
|
|
|
// Now the function can be called from Rhai scripts
|
|
let result = engine.eval::<i64>("add(40, 2)")?;
|
|
|
|
println!("Result: {}", result); // prints 42
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Function Naming Conventions
|
|
|
|
When registering functions, follow these naming conventions:
|
|
|
|
1. Use snake_case for function names to maintain consistency with Rhai's style
|
|
2. Choose descriptive names that clearly indicate the function's purpose
|
|
3. For functions that operate on specific types, consider prefixing with the type name (e.g., `string_length`)
|
|
|
|
## Working with Different Argument Types
|
|
|
|
### Primitive Types
|
|
|
|
Rhai supports the following primitive types that can be directly used as function arguments:
|
|
|
|
- `i64` (integer)
|
|
- `f64` (float)
|
|
- `bool` (boolean)
|
|
- `String` or `&str` (string)
|
|
- `char` (character)
|
|
- `()` (unit type)
|
|
|
|
Example:
|
|
|
|
```rust
|
|
fn calculate(num: i64, factor: f64, enabled: bool) -> f64 {
|
|
if enabled {
|
|
num as f64 * factor
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
engine.register_fn("calculate", calculate);
|
|
```
|
|
|
|
### Arrays and Collections
|
|
|
|
For array arguments:
|
|
|
|
```rust
|
|
fn sum_array(arr: Array) -> i64 {
|
|
arr.iter()
|
|
.filter_map(|v| v.as_int().ok())
|
|
.sum()
|
|
}
|
|
|
|
engine.register_fn("sum_array", sum_array);
|
|
```
|
|
|
|
### Optional Arguments and Function Overloading
|
|
|
|
Rhai supports function overloading, which allows you to register multiple functions with the same name but different parameter types or counts:
|
|
|
|
```rust
|
|
fn greet(name: &str) -> String {
|
|
format!("Hello, {}!", name)
|
|
}
|
|
|
|
fn greet_with_title(title: &str, name: &str) -> String {
|
|
format!("Hello, {} {}!", title, name)
|
|
}
|
|
|
|
engine.register_fn("greet", greet);
|
|
engine.register_fn("greet", greet_with_title);
|
|
|
|
// In Rhai:
|
|
// greet("World") -> "Hello, World!"
|
|
// greet("Mr.", "Smith") -> "Hello, Mr. Smith!"
|
|
```
|
|
|
|
## Passing and Working with Structs
|
|
|
|
### Registering Custom Types
|
|
|
|
To use Rust structs in Rhai, you need to register them:
|
|
|
|
#### Method 1: Using the CustomType Trait (Recommended)
|
|
|
|
```rust
|
|
#[derive(Debug, Clone, CustomType)]
|
|
#[rhai_type(extra = Self::build_extra)]
|
|
struct TestStruct {
|
|
x: i64,
|
|
}
|
|
|
|
impl TestStruct {
|
|
pub fn new() -> Self {
|
|
Self { x: 1 }
|
|
}
|
|
|
|
pub fn update(&mut self) {
|
|
self.x += 1000;
|
|
}
|
|
|
|
pub fn calculate(&mut self, data: i64) -> i64 {
|
|
self.x * data
|
|
}
|
|
|
|
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
|
builder
|
|
.with_name("TestStruct")
|
|
.with_fn("new_ts", Self::new)
|
|
.with_fn("update", Self::update)
|
|
.with_fn("calc", Self::calculate);
|
|
}
|
|
}
|
|
|
|
// In your main function:
|
|
let mut engine = Engine::new();
|
|
engine.build_type::<TestStruct>();
|
|
```
|
|
|
|
#### Method 2: Manual Registration
|
|
|
|
```rust
|
|
#[derive(Debug, Clone)]
|
|
struct TestStruct {
|
|
x: i64,
|
|
}
|
|
|
|
impl TestStruct {
|
|
pub fn new() -> Self {
|
|
Self { x: 1 }
|
|
}
|
|
|
|
pub fn update(&mut self) {
|
|
self.x += 1000;
|
|
}
|
|
}
|
|
|
|
let mut engine = Engine::new();
|
|
|
|
engine
|
|
.register_type_with_name::<TestStruct>("TestStruct")
|
|
.register_fn("new_ts", TestStruct::new)
|
|
.register_fn("update", TestStruct::update);
|
|
```
|
|
|
|
### Accessing Struct Fields
|
|
|
|
By default, Rhai can access public fields of registered structs:
|
|
|
|
```rust
|
|
// In Rhai script:
|
|
let x = new_ts();
|
|
x.x = 42; // Direct field access
|
|
```
|
|
|
|
### Passing Structs as Arguments
|
|
|
|
When passing structs as arguments to functions, ensure they implement the `Clone` trait:
|
|
|
|
```rust
|
|
fn process_struct(test: TestStruct) -> i64 {
|
|
test.x * 2
|
|
}
|
|
|
|
engine.register_fn("process_struct", process_struct);
|
|
```
|
|
|
|
### Returning Structs from Functions
|
|
|
|
You can return custom structs from functions:
|
|
|
|
```rust
|
|
fn create_struct(value: i64) -> TestStruct {
|
|
TestStruct { x: value }
|
|
}
|
|
|
|
engine.register_fn("create_struct", create_struct);
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
Error handling is a critical aspect of integrating Rust functions with Rhai. Proper error handling ensures that script execution fails gracefully with meaningful error messages.
|
|
|
|
### Basic Error Handling
|
|
|
|
The most basic way to handle errors is to return a `Result` type:
|
|
|
|
```rust
|
|
fn divide(a: i64, b: i64) -> Result<i64, Box<EvalAltResult>> {
|
|
if b == 0 {
|
|
// Return an error if division by zero
|
|
Err("Division by zero".into())
|
|
} else {
|
|
Ok(a / b)
|
|
}
|
|
}
|
|
|
|
engine.register_fn("divide", divide);
|
|
```
|
|
|
|
### EvalAltResult Types
|
|
|
|
Rhai provides several error types through the `EvalAltResult` enum:
|
|
|
|
```rust
|
|
use rhai::EvalAltResult;
|
|
use rhai::Position;
|
|
|
|
fn my_function() -> Result<i64, Box<EvalAltResult>> {
|
|
// Different error types
|
|
|
|
// Runtime error - general purpose error
|
|
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
|
"Something went wrong".into(),
|
|
Position::NONE
|
|
)));
|
|
|
|
// Type error - when a type mismatch occurs
|
|
return Err(Box::new(EvalAltResult::ErrorMismatchOutputType(
|
|
"expected i64, got string".into(),
|
|
Position::NONE,
|
|
"i64".into()
|
|
)));
|
|
|
|
// Function not found error
|
|
return Err(Box::new(EvalAltResult::ErrorFunctionNotFound(
|
|
"function_name".into(),
|
|
Position::NONE
|
|
)));
|
|
}
|
|
```
|
|
|
|
### Custom Error Types
|
|
|
|
For more structured error handling, you can create custom error types:
|
|
|
|
```rust
|
|
use thiserror::Error;
|
|
use rhai::{EvalAltResult, Position};
|
|
|
|
#[derive(Error, Debug)]
|
|
enum MyError {
|
|
#[error("Invalid input: {0}")]
|
|
InvalidInput(String),
|
|
|
|
#[error("Calculation error: {0}")]
|
|
CalculationError(String),
|
|
|
|
#[error("Database error: {0}")]
|
|
DatabaseError(String),
|
|
}
|
|
|
|
// Convert your custom error to EvalAltResult
|
|
fn process_data(input: i64) -> Result<i64, Box<EvalAltResult>> {
|
|
// Your logic here that might return a custom error
|
|
let result = validate_input(input)
|
|
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
|
format!("Validation failed: {}", e),
|
|
Position::NONE
|
|
)))?;
|
|
|
|
let processed = calculate(result)
|
|
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
|
format!("Calculation failed: {}", e),
|
|
Position::NONE
|
|
)))?;
|
|
|
|
if processed < 0 {
|
|
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
|
"Negative result not allowed".into(),
|
|
Position::NONE
|
|
)));
|
|
}
|
|
|
|
Ok(processed)
|
|
}
|
|
|
|
// Helper functions that return our custom error type
|
|
fn validate_input(input: i64) -> Result<i64, MyError> {
|
|
if input <= 0 {
|
|
return Err(MyError::InvalidInput("Input must be positive".into()));
|
|
}
|
|
Ok(input)
|
|
}
|
|
|
|
fn calculate(value: i64) -> Result<i64, MyError> {
|
|
if value > 1000 {
|
|
return Err(MyError::CalculationError("Value too large".into()));
|
|
}
|
|
Ok(value * 2)
|
|
}
|
|
```
|
|
|
|
### Error Propagation
|
|
|
|
When calling Rhai functions from Rust, errors are propagated through the `?` operator:
|
|
|
|
```rust
|
|
let result = engine.eval::<i64>("divide(10, 0)")?; // This will propagate the error
|
|
```
|
|
|
|
### Error Context and Position Information
|
|
|
|
For better debugging, include position information in your errors:
|
|
|
|
```rust
|
|
fn parse_config(config: &str) -> Result<Map, Box<EvalAltResult>> {
|
|
// Get the call position from the context
|
|
let pos = Position::NONE; // In a real function, you'd get this from NativeCallContext
|
|
|
|
match serde_json::from_str::<serde_json::Value>(config) {
|
|
Ok(json) => {
|
|
// Convert JSON to Rhai Map
|
|
let mut map = Map::new();
|
|
// ... conversion logic ...
|
|
Ok(map)
|
|
},
|
|
Err(e) => {
|
|
Err(Box::new(EvalAltResult::ErrorRuntime(
|
|
format!("Failed to parse config: {}", e),
|
|
pos
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Best Practices for Error Handling
|
|
|
|
1. **Be Specific**: Provide clear, specific error messages that help script writers understand what went wrong
|
|
2. **Include Context**: When possible, include relevant context in error messages (e.g., variable values, expected types)
|
|
3. **Consistent Error Types**: Use consistent error types for similar issues
|
|
4. **Validate Early**: Validate inputs at the beginning of functions to fail fast
|
|
5. **Document Error Conditions**: Document possible error conditions for functions exposed to Rhai
|
|
|
|
|
|
## Returning Different Types
|
|
|
|
Properly handling return types is crucial for creating a seamless integration between Rust and Rhai. This section covers various approaches to returning different types of data from Rust functions to Rhai scripts.
|
|
|
|
### Simple Return Types
|
|
|
|
For simple return types, specify the type when registering the function:
|
|
|
|
```rust
|
|
fn get_number() -> i64 { 42 }
|
|
fn get_string() -> String { "hello".to_string() }
|
|
fn get_boolean() -> bool { true }
|
|
fn get_float() -> f64 { 3.14159 }
|
|
fn get_char() -> char { 'A' }
|
|
fn get_unit() -> () { () }
|
|
|
|
engine.register_fn("get_number", get_number);
|
|
engine.register_fn("get_string", get_string);
|
|
engine.register_fn("get_boolean", get_boolean);
|
|
engine.register_fn("get_float", get_float);
|
|
engine.register_fn("get_char", get_char);
|
|
engine.register_fn("get_unit", get_unit);
|
|
```
|
|
|
|
### Dynamic Return Types
|
|
|
|
WE SHOULD TRY NOT TO DO THIS
|
|
|
|
For functions that may return different types based on conditions, use the `Dynamic` type:
|
|
|
|
```rust
|
|
fn get_value(which: i64) -> Dynamic {
|
|
match which {
|
|
0 => Dynamic::from(42),
|
|
1 => Dynamic::from("hello"),
|
|
2 => Dynamic::from(true),
|
|
3 => Dynamic::from(3.14159),
|
|
4 => {
|
|
let mut array = Array::new();
|
|
array.push(Dynamic::from(1));
|
|
array.push(Dynamic::from(2));
|
|
Dynamic::from_array(array)
|
|
},
|
|
5 => {
|
|
let mut map = Map::new();
|
|
map.insert("key".into(), "value".into());
|
|
Dynamic::from_map(map)
|
|
},
|
|
_ => Dynamic::UNIT,
|
|
}
|
|
}
|
|
|
|
engine.register_fn("get_value", get_value);
|
|
```
|
|
|
|
### Returning Collections
|
|
|
|
Rhai supports various collection types:
|
|
|
|
```rust
|
|
// Returning an array
|
|
fn get_array() -> Array {
|
|
let mut array = Array::new();
|
|
array.push(Dynamic::from(1));
|
|
array.push(Dynamic::from("hello"));
|
|
array.push(Dynamic::from(true));
|
|
array
|
|
}
|
|
|
|
// Returning a map
|
|
fn get_map() -> Map {
|
|
let mut map = Map::new();
|
|
map.insert("number".into(), 42.into());
|
|
map.insert("string".into(), "hello".into());
|
|
map.insert("boolean".into(), true.into());
|
|
map
|
|
}
|
|
|
|
// Returning a typed Vec (will be converted to Rhai Array)
|
|
fn get_numbers() -> Vec<i64> {
|
|
vec![1, 2, 3, 4, 5]
|
|
}
|
|
|
|
// Returning a HashMap (will be converted to Rhai Map)
|
|
fn get_config() -> HashMap<String, String> {
|
|
let mut map = HashMap::new();
|
|
map.insert("host".to_string(), "localhost".to_string());
|
|
map.insert("port".to_string(), "8080".to_string());
|
|
map
|
|
}
|
|
|
|
engine.register_fn("get_array", get_array);
|
|
engine.register_fn("get_map", get_map);
|
|
engine.register_fn("get_numbers", get_numbers);
|
|
engine.register_fn("get_config", get_config);
|
|
```
|
|
|
|
### Returning Custom Structs
|
|
|
|
For returning custom structs, ensure they implement the `Clone` trait:
|
|
|
|
```rust
|
|
#[derive(Debug, Clone)]
|
|
struct TestStruct {
|
|
x: i64,
|
|
name: String,
|
|
active: bool,
|
|
}
|
|
|
|
fn create_struct(value: i64, name: &str, active: bool) -> TestStruct {
|
|
TestStruct {
|
|
x: value,
|
|
name: name.to_string(),
|
|
active
|
|
}
|
|
}
|
|
|
|
fn get_struct_array() -> Vec<TestStruct> {
|
|
vec![
|
|
TestStruct { x: 1, name: "one".to_string(), active: true },
|
|
TestStruct { x: 2, name: "two".to_string(), active: false },
|
|
]
|
|
}
|
|
|
|
engine.register_type_with_name::<TestStruct>("TestStruct")
|
|
.register_fn("create_struct", create_struct)
|
|
.register_fn("get_struct_array", get_struct_array);
|
|
```
|
|
|
|
### Returning Results and Options
|
|
|
|
For functions that might fail or return optional values:
|
|
|
|
```rust
|
|
// Returning a Result
|
|
fn divide(a: i64, b: i64) -> Result<i64, Box<EvalAltResult>> {
|
|
if b == 0 {
|
|
Err("Division by zero".into())
|
|
} else {
|
|
Ok(a / b)
|
|
}
|
|
}
|
|
|
|
// Returning an Option (converted to Dynamic)
|
|
fn find_item(id: i64) -> Dynamic {
|
|
let item = lookup_item(id);
|
|
|
|
match item {
|
|
Some(value) => value.into(),
|
|
None => Dynamic::UNIT, // Rhai has no null, so use () for None
|
|
}
|
|
}
|
|
|
|
// Helper function returning Option
|
|
fn lookup_item(id: i64) -> Option<TestStruct> {
|
|
match id {
|
|
1 => Some(TestStruct { x: 1, name: "one".to_string(), active: true }),
|
|
2 => Some(TestStruct { x: 2, name: "two".to_string(), active: false }),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
engine.register_fn("divide", divide);
|
|
engine.register_fn("find_item", find_item);
|
|
```
|
|
|
|
### Serialization and Deserialization
|
|
|
|
When working with JSON or other serialized formats:
|
|
|
|
```rust
|
|
use serde_json::{Value as JsonValue, json};
|
|
|
|
// Return JSON data as a Rhai Map
|
|
fn get_json_data() -> Result<Map, Box<EvalAltResult>> {
|
|
// Simulate fetching JSON data
|
|
let json_data = json!({
|
|
"name": "John Doe",
|
|
"age": 30,
|
|
"address": {
|
|
"street": "123 Main St",
|
|
"city": "Anytown"
|
|
},
|
|
"phones": ["+1-555-1234", "+1-555-5678"]
|
|
});
|
|
|
|
// Convert JSON to Rhai Map
|
|
json_to_rhai_value(json_data)
|
|
.and_then(|v| v.try_cast::<Map>().map_err(|_| "Expected a map".into()))
|
|
}
|
|
|
|
// Helper function to convert JSON Value to Rhai Dynamic
|
|
fn json_to_rhai_value(json: JsonValue) -> Result<Dynamic, Box<EvalAltResult>> {
|
|
match json {
|
|
JsonValue::Null => Ok(Dynamic::UNIT),
|
|
JsonValue::Bool(b) => Ok(b.into()),
|
|
JsonValue::Number(n) => {
|
|
if n.is_i64() {
|
|
Ok(n.as_i64().unwrap().into())
|
|
} else {
|
|
Ok(n.as_f64().unwrap().into())
|
|
}
|
|
},
|
|
JsonValue::String(s) => Ok(s.into()),
|
|
JsonValue::Array(arr) => {
|
|
let mut rhai_array = Array::new();
|
|
for item in arr {
|
|
rhai_array.push(json_to_rhai_value(item)?);
|
|
}
|
|
Ok(Dynamic::from_array(rhai_array))
|
|
},
|
|
JsonValue::Object(obj) => {
|
|
let mut rhai_map = Map::new();
|
|
for (k, v) in obj {
|
|
rhai_map.insert(k.into(), json_to_rhai_value(v)?);
|
|
}
|
|
Ok(Dynamic::from_map(rhai_map))
|
|
}
|
|
}
|
|
}
|
|
|
|
engine.register_fn("get_json_data", get_json_data);
|
|
```
|
|
|
|
### Working with Dynamic Type System
|
|
|
|
Understanding how to work with Rhai's Dynamic type system is essential:
|
|
|
|
```rust
|
|
// Function that examines a Dynamic value and returns information about it
|
|
fn inspect_value(value: Dynamic) -> Map {
|
|
let mut info = Map::new();
|
|
|
|
// Store the type name
|
|
info.insert("type".into(), value.type_name().into());
|
|
|
|
// Store specific type information
|
|
if value.is_int() {
|
|
info.insert("category".into(), "number".into());
|
|
info.insert("value".into(), value.clone());
|
|
} else if value.is_float() {
|
|
info.insert("category".into(), "number".into());
|
|
info.insert("value".into(), value.clone());
|
|
} else if value.is_string() {
|
|
info.insert("category".into(), "string".into());
|
|
info.insert("length".into(), value.clone_cast::<String>().len().into());
|
|
info.insert("value".into(), value.clone());
|
|
} else if value.is_array() {
|
|
info.insert("category".into(), "array".into());
|
|
info.insert("length".into(), value.clone_cast::<Array>().len().into());
|
|
} else if value.is_map() {
|
|
info.insert("category".into(), "map".into());
|
|
info.insert("keys".into(), value.clone_cast::<Map>().keys().len().into());
|
|
} else if value.is_bool() {
|
|
info.insert("category".into(), "boolean".into());
|
|
info.insert("value".into(), value.clone());
|
|
} else {
|
|
info.insert("category".into(), "other".into());
|
|
}
|
|
|
|
info
|
|
}
|
|
|
|
engine.register_fn("inspect", inspect_value);
|
|
```
|
|
|
|
## Native Function Handling
|
|
|
|
When working with native Rust functions in Rhai, there are several important considerations for handling different argument types, especially when dealing with complex data structures and error cases.
|
|
|
|
### Native Function Signature
|
|
|
|
Native Rust functions registered with Rhai can have one of two signatures:
|
|
|
|
1. **Standard Function Signature**: Functions with typed parameters
|
|
```rust
|
|
fn my_function(param1: Type1, param2: Type2, ...) -> ReturnType { ... }
|
|
```
|
|
|
|
2. **Dynamic Function Signature**: Functions that handle raw Dynamic values
|
|
```rust
|
|
fn my_dynamic_function(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> { ... }
|
|
```
|
|
|
|
### Working with Raw Dynamic Arguments
|
|
|
|
The dynamic function signature gives you more control but requires manual type checking and conversion:
|
|
|
|
```rust
|
|
fn process_dynamic_args(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
|
// Check number of arguments
|
|
if args.len() != 2 {
|
|
return Err("Expected exactly 2 arguments".into());
|
|
}
|
|
|
|
// Extract and convert the first argument to an integer
|
|
let arg1 = args[0].as_int().map_err(|_| "First argument must be an integer".into())?;
|
|
|
|
// Extract and convert the second argument to a string
|
|
let arg2 = args[1].as_str().map_err(|_| "Second argument must be a string".into())?;
|
|
|
|
// Process the arguments
|
|
let result = format!("{}: {}", arg2, arg1);
|
|
|
|
// Return the result as a Dynamic value
|
|
Ok(result.into())
|
|
}
|
|
|
|
// Register the function
|
|
engine.register_fn("process", process_dynamic_args);
|
|
```
|
|
|
|
### Handling Complex Struct Arguments
|
|
|
|
When working with complex struct arguments, you have several options:
|
|
|
|
#### Option 1: Use typed parameters (recommended for simple cases)
|
|
|
|
```rust
|
|
#[derive(Clone)]
|
|
struct ComplexData {
|
|
id: i64,
|
|
values: Vec<f64>,
|
|
}
|
|
|
|
fn process_complex(data: &mut ComplexData, factor: f64) -> f64 {
|
|
let sum: f64 = data.values.iter().sum();
|
|
data.values.push(sum * factor);
|
|
sum * factor
|
|
}
|
|
|
|
engine.register_fn("process_complex", process_complex);
|
|
```
|
|
|
|
#### Option 2: Use Dynamic parameters for more flexibility
|
|
|
|
```rust
|
|
fn process_complex_dynamic(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
|
// Check arguments
|
|
if args.len() != 2 {
|
|
return Err("Expected exactly 2 arguments".into());
|
|
}
|
|
|
|
// Get mutable reference to the complex data
|
|
let data = args[0].write_lock::<ComplexData>()
|
|
.ok_or_else(|| "First argument must be ComplexData".into())?;
|
|
|
|
// Get the factor
|
|
let factor = args[1].as_float().map_err(|_| "Second argument must be a number".into())?;
|
|
|
|
// Process the data
|
|
let sum: f64 = data.values.iter().sum();
|
|
data.values.push(sum * factor);
|
|
|
|
Ok((sum * factor).into())
|
|
}
|
|
```
|
|
|
|
### Handling Variable Arguments
|
|
|
|
For functions that accept a variable number of arguments:
|
|
|
|
```rust
|
|
fn sum_all(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
|
let mut total: i64 = 0;
|
|
|
|
for arg in args.iter() {
|
|
total += arg.as_int().map_err(|_| "All arguments must be integers".into())?;
|
|
}
|
|
|
|
Ok(total.into())
|
|
}
|
|
|
|
engine.register_fn("sum_all", sum_all);
|
|
|
|
// In Rhai:
|
|
// sum_all(1, 2, 3, 4, 5) -> 15
|
|
// sum_all(10, 20) -> 30
|
|
```
|
|
|
|
### Handling Optional Arguments
|
|
|
|
For functions with optional arguments, use function overloading:
|
|
|
|
```rust
|
|
fn create_person(name: &str) -> Person {
|
|
Person { name: name.to_string(), age: 30 } // Default age
|
|
}
|
|
|
|
fn create_person_with_age(name: &str, age: i64) -> Person {
|
|
Person { name: name.to_string(), age }
|
|
}
|
|
|
|
engine.register_fn("create_person", create_person);
|
|
engine.register_fn("create_person", create_person_with_age);
|
|
|
|
// In Rhai:
|
|
// create_person("John") -> Person with name "John" and age 30
|
|
// create_person("John", 25) -> Person with name "John" and age 25
|
|
```
|
|
|
|
### Handling Default Arguments
|
|
|
|
Rhai doesn't directly support default arguments, but you can simulate them:
|
|
|
|
```rust
|
|
fn configure(options: &mut Map) -> Result<(), Box<EvalAltResult>> {
|
|
// Check if certain options exist, if not, set defaults
|
|
if !options.contains_key("timeout") {
|
|
options.insert("timeout".into(), 30_i64.into());
|
|
}
|
|
|
|
if !options.contains_key("retry") {
|
|
options.insert("retry".into(), true.into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
engine.register_fn("configure", configure);
|
|
|
|
// In Rhai:
|
|
// let options = #{};
|
|
// configure(options);
|
|
// print(options.timeout); // Prints 30
|
|
```
|
|
|
|
### Handling Mutable and Immutable References
|
|
|
|
Rhai supports both mutable and immutable references:
|
|
|
|
```rust
|
|
// Function taking an immutable reference
|
|
fn get_name(person: &Person) -> String {
|
|
person.name.clone()
|
|
}
|
|
|
|
// Function taking a mutable reference
|
|
fn increment_age(person: &mut Person) {
|
|
person.age += 1;
|
|
}
|
|
|
|
engine.register_fn("get_name", get_name);
|
|
engine.register_fn("increment_age", increment_age);
|
|
```
|
|
|
|
### Converting Between Rust and Rhai Types
|
|
|
|
When you need to convert between Rust and Rhai types:
|
|
|
|
```rust
|
|
// Convert a Rust HashMap to a Rhai Map
|
|
fn create_config() -> Map {
|
|
let mut rust_map = HashMap::new();
|
|
rust_map.insert("server".to_string(), "localhost".to_string());
|
|
rust_map.insert("port".to_string(), "8080".to_string());
|
|
|
|
// Convert to Rhai Map
|
|
let mut rhai_map = Map::new();
|
|
for (k, v) in rust_map {
|
|
rhai_map.insert(k.into(), v.into());
|
|
}
|
|
|
|
rhai_map
|
|
}
|
|
|
|
// Convert a Rhai Array to a Rust Vec
|
|
fn process_array(arr: Array) -> Result<i64, Box<EvalAltResult>> {
|
|
// Convert to Rust Vec<i64>
|
|
let rust_vec: Result<Vec<i64>, _> = arr.iter()
|
|
.map(|v| v.as_int().map_err(|_| "Array must contain only integers".into()))
|
|
.collect();
|
|
|
|
let numbers = rust_vec?;
|
|
Ok(numbers.iter().sum())
|
|
}
|
|
```
|
|
|
|
## Complete Examples
|
|
|
|
### Example 1: Basic Function Registration and Struct Handling
|
|
|
|
```rust
|
|
use rhai::{Engine, EvalAltResult, RegisterFn};
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Person {
|
|
name: String,
|
|
age: i64,
|
|
}
|
|
|
|
impl Person {
|
|
fn new(name: &str, age: i64) -> Self {
|
|
Self {
|
|
name: name.to_string(),
|
|
age,
|
|
}
|
|
}
|
|
|
|
fn greet(&self) -> String {
|
|
format!("Hello, my name is {} and I am {} years old.", self.name, self.age)
|
|
}
|
|
|
|
fn have_birthday(&mut self) {
|
|
self.age += 1;
|
|
}
|
|
}
|
|
|
|
fn is_adult(person: &Person) -> bool {
|
|
person.age >= 18
|
|
}
|
|
|
|
fn main() -> Result<(), Box<EvalAltResult>> {
|
|
let mut engine = Engine::new();
|
|
|
|
// Register the Person type
|
|
engine
|
|
.register_type_with_name::<Person>("Person")
|
|
.register_fn("new_person", Person::new)
|
|
.register_fn("greet", Person::greet)
|
|
.register_fn("have_birthday", Person::have_birthday)
|
|
.register_fn("is_adult", is_adult);
|
|
|
|
// Run a script that uses the Person type
|
|
let result = engine.eval::<String>(r#"
|
|
let p = new_person("John", 17);
|
|
let greeting = p.greet();
|
|
|
|
if !is_adult(p) {
|
|
p.have_birthday();
|
|
}
|
|
|
|
greeting + " Now I am " + p.age.to_string() + " years old."
|
|
"#)?;
|
|
|
|
println!("{}", result);
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Example 2: Error Handling and Complex Return Types
|
|
|
|
```rust
|
|
use rhai::{Engine, EvalAltResult, Map, Dynamic};
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Product {
|
|
id: i64,
|
|
name: String,
|
|
price: f64,
|
|
}
|
|
|
|
fn get_product(id: i64) -> Result<Product, Box<EvalAltResult>> {
|
|
match id {
|
|
1 => Ok(Product { id: 1, name: "Laptop".to_string(), price: 999.99 }),
|
|
2 => Ok(Product { id: 2, name: "Phone".to_string(), price: 499.99 }),
|
|
_ => Err("Product not found".into())
|
|
}
|
|
}
|
|
|
|
fn calculate_total(products: Array) -> Result<f64, Box<EvalAltResult>> {
|
|
let mut total = 0.0;
|
|
|
|
for product_dynamic in products.iter() {
|
|
let product = product_dynamic.clone().try_cast::<Product>()
|
|
.map_err(|_| "Invalid product in array".into())?;
|
|
|
|
total += product.price;
|
|
}
|
|
|
|
Ok(total)
|
|
}
|
|
|
|
fn get_product_map() -> Map {
|
|
let mut map = Map::new();
|
|
|
|
map.insert("laptop".into(),
|
|
Dynamic::from(Product { id: 1, name: "Laptop".to_string(), price: 999.99 }));
|
|
map.insert("phone".into(),
|
|
Dynamic::from(Product { id: 2, name: "Phone".to_string(), price: 499.99 }));
|
|
|
|
map
|
|
}
|
|
|
|
fn main() -> Result<(), Box<EvalAltResult>> {
|
|
let mut engine = Engine::new();
|
|
|
|
engine
|
|
.register_type_with_name::<Product>("Product")
|
|
.register_fn("get_product", get_product)
|
|
.register_fn("calculate_total", calculate_total)
|
|
.register_fn("get_product_map", get_product_map);
|
|
|
|
let result = engine.eval::<f64>(r#"
|
|
let products = [];
|
|
|
|
// Try to get products
|
|
try {
|
|
products.push(get_product(1));
|
|
products.push(get_product(2));
|
|
products.push(get_product(3)); // This will throw an error
|
|
} catch(err) {
|
|
print(`Error: ${err}`);
|
|
}
|
|
|
|
// Get products from map
|
|
let product_map = get_product_map();
|
|
products.push(product_map.laptop);
|
|
|
|
calculate_total(products)
|
|
"#)?;
|
|
|
|
println!("Total: ${:.2}", result);
|
|
|
|
Ok(())
|
|
}
|
|
```
|