7.4 KiB
Rhai Wrapper Crate
This crate provides utilities to simplify the process of wrapping Rust functions and types for use with the Rhai scripting engine. Its primary component is the wrap_for_rhai!
macro, which generates the necessary boilerplate to make Rust functions callable from Rhai scripts, including type conversions for arguments and return values.
This crate works in conjunction with the rhai_macros_derive
crate, which provides ToRhaiMap
and FromRhaiMap
derive macros for custom Rust structs.
Features
wrap_for_rhai!
macro: Simplifies registering Rust functions with Rhai.- Support for various function signatures:
- Primitive types (
INT
,FLOAT
,bool
,String
). Option<T>
for optional return values.Vec<PrimitiveType>
(e.g.,Vec<INT>
,Vec<String>
).Vec<CustomStruct>
whereCustomStruct
implementsFromRhaiMap
(for arguments) andToRhaiMap
(for elements in return values).- Custom structs as direct arguments (if they implement
FromRhaiMap
). - Custom structs as direct return values (if they implement
ToRhaiMap
).
- Primitive types (
- Automatic conversion between Rhai's
Dynamic
type and Rust types.
Dependencies
Ensure you have rhai
and rhai_macros_derive
(if using custom structs) in your Cargo.toml
:
[dependencies]
rhai = "<version>" # e.g., "1.16.0"
rhai_macros_derive = { path = "../rhai_macros_derive" } # If in the same workspace
# This crate (rhai_wrapper) would also be a local path dependency
# rhai_wrapper = { path = "../rhai_wrapper" }
wrap_for_rhai!
Macro
The wrap_for_rhai!
macro is the core of this crate. It takes your Rust function and its type signature (in a specific format) and generates a closure that Rhai can register.
Basic Usage
use rhai_wrapper::wrap_for_rhai;
use rhai::{Engine, INT, FLOAT};
// Functions to be wrapped
fn add(a: INT, b: INT) -> INT { a + b }
fn greet(name: String) -> String { format!("Hello, {}!", name) }
fn get_pi() -> FLOAT { 3.14159 }
fn main() {
let mut engine = Engine::new();
// Registering functions using the macro
engine.register_fn("add_rhai", wrap_for_rhai!(add, INT, INT -> INT));
engine.register_fn("greet_rhai", wrap_for_rhai!(greet, String -> String));
engine.register_fn("get_pi_rhai", wrap_for_rhai!(get_pi, -> FLOAT));
let result: INT = engine.eval("add_rhai(10, 32)").unwrap();
assert_eq!(result, 42);
let message: String = engine.eval(r#"greet_rhai("Rhai")"#).unwrap();
assert_eq!(message, "Hello, Rhai!");
}
Supported Signatures & Examples
The macro uses a pattern-matching style to handle different function signatures.
-
Primitives:
(INT, INT -> INT)
,(String -> String)
,(FLOAT, bool -> String)
fn my_func(a: INT, b: String) -> bool { /* ... */ false } // engine.register_fn("my_func_rhai", wrap_for_rhai!(my_func, INT, String -> bool));
-
No Arguments:
(-> INT)
fn get_answer() -> INT { 42 } // engine.register_fn("get_answer_rhai", wrap_for_rhai!(get_answer, -> INT));
-
Option<T>
Return Type:(INT -> Option<String>)
fn maybe_get_name(id: INT) -> Option<String> { if id == 1 { Some("Alice".to_string()) } else { None } } // engine.register_fn("maybe_get_name_rhai", wrap_for_rhai!(maybe_get_name, INT -> Option<String>));
Rhai will receive
()
(unit/nothing) if the Rust function returnsNone
. -
Vec<Primitive>
Argument:(Vec<INT> -> INT)
,(Vec<String> -> String)
fn sum_numbers(numbers: Vec<INT>) -> INT { numbers.iter().sum() } // engine.register_fn("sum_rhai", wrap_for_rhai!(sum_numbers, Vec<INT> -> INT)); // Rhai script: sum_rhai([1, 2, 3]) -> 6
-
Custom Structs (with
rhai_macros_derive
)Assume you have a struct
Point
that derivesToRhaiMap
andFromRhaiMap
:use rhai_macros_derive::{ToRhaiMap, FromRhaiMap}; use rhai::CustomType; #[derive(CustomType, ToRhaiMap, FromRhaiMap, Clone, Debug, PartialEq)] struct Point { x: INT, y: INT }
-
Custom Struct Argument:
(Point -> String)
fn print_point(p: Point) -> String { format!("Point(x={}, y={})", p.x, p.y) } // engine.build_type::<Point>(); // engine.register_fn("print_point_rhai", wrap_for_rhai!(print_point, Point -> String)); // Rhai script: print_point_rhai(#{x:1, y:2})
-
Custom Struct Return:
(INT, INT -> Point)
fn make_point(x: INT, y: INT) -> Point { Point { x, y } } // engine.build_type::<Point>(); // engine.register_fn("make_point_rhai", wrap_for_rhai!(make_point, INT, INT -> Point)); // Rhai script: let p = make_point_rhai(3,4); p.x == 3
-
Vec<CustomStruct>
Argument:(Vec<Point> -> INT)
fn sum_point_coords(points: Vec<Point>) -> INT { points.iter().map(|p| p.x + p.y).sum() } // engine.build_type::<Point>(); // engine.register_fn("sum_points_rhai", wrap_for_rhai!(sum_point_coords, Vec<Point> -> INT)); // Rhai script: sum_points_rhai([#{x:1,y:2}, #{x:3,y:4}])
-
Vec<CustomStruct>
Return:(INT -> Vec<Point>)
fn generate_points(count: INT) -> Vec<Point> { (0..count).map(|i| Point { x: i, y: i*2 }).collect() } // engine.build_type::<Point>(); // engine.register_fn("gen_points_rhai", wrap_for_rhai!(generate_points, INT -> Vec<Point>)); // Rhai script: let arr = gen_points_rhai(2); arr[0].x == 0
-
Vec<CustomStruct>
Argument andVec<Primitive>
Return:(Vec<Point> -> Vec<INT>)
fn get_x_coords(points: Vec<Point>) -> Vec<INT> { points.iter().map(|p| p.x).collect() } // engine.build_type::<Point>(); // engine.register_fn("get_xs_rhai", wrap_for_rhai!(get_x_coords, Vec<Point> -> Vec<INT>));
-
How Custom Structs are Handled
- When a custom struct is an argument (
MyStructType
): The macro expects the Rhai script to pass an object map. This map is then passed toMyStructType::from_rhai_map(map)
(provided by#[derive(FromRhaiMap)]
) to convert it into a Rust struct instance. - When a custom struct is a return value: The Rust function returns an instance of
MyStructType
. The macro callsinstance.to_rhai_map()
(provided by#[derive(ToRhaiMap)]
) to convert it into arhai::Map
, which Rhai receives as an object map. - For
Vec<CustomStruct>
: Similar logic applies element-wise. Incomingrhai::Array
elements are converted usingfrom_rhai_map
; outgoingVec
elements are converted usingto_rhai_map
before being collected into arhai::Array
.
Adding New Macro Arms
The wrap_for_rhai!
macro is defined with several arms, each matching a specific function signature pattern. If you need to support a new, common signature:
- Open
rhai_wrapper/src/lib.rs
. - Add a new macro arm following the existing patterns.
- Pay attention to argument conversion (from
rhai::Dynamic
orrhai::Map
) and return value conversion (torhai::Dynamic
orrhai::Map
). - For custom types, rely on
YourType::from_rhai_map(...)
andyour_instance.to_rhai_map()
. - Consider the order of macro arms if a more specific arm needs to match before a more general one.
This crate aims to reduce boilerplate and make Rhai integration smoother for common Rust patterns.