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>whereCustomStructimplementsFromRhaiMap(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
Dynamictype 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
Pointthat derivesToRhaiMapandFromRhaiMap: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::Arrayelements are converted usingfrom_rhai_map; outgoingVecelements are converted usingto_rhai_mapbefore 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::Dynamicorrhai::Map) and return value conversion (torhai::Dynamicorrhai::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.