This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
rhaj/rhai_wrapper
2025-05-13 02:00:35 +03:00
..
examples more efforts to automate rhai bindings 2025-05-13 02:00:35 +03:00
src more efforts to automate rhai bindings 2025-05-13 02:00:35 +03:00
tests more efforts to automate rhai bindings 2025-05-13 02:00:35 +03:00
Cargo.lock more efforts to automate rhai bindings 2025-05-13 02:00:35 +03:00
Cargo.toml more efforts to automate rhai bindings 2025-05-13 02:00:35 +03:00
README.md create macros for generating rhai wrappers and add tests 2025-05-12 02:31:45 +03:00

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> where CustomStruct implements FromRhaiMap (for arguments) and ToRhaiMap (for elements in return values).
    • Custom structs as direct arguments (if they implement FromRhaiMap).
    • Custom structs as direct return values (if they implement ToRhaiMap).
  • 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.

  1. 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));
    
  2. No Arguments: (-> INT)

    fn get_answer() -> INT { 42 }
    // engine.register_fn("get_answer_rhai", wrap_for_rhai!(get_answer, -> INT));
    
  3. 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 returns None.

  4. 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
    
  5. Custom Structs (with rhai_macros_derive)

    Assume you have a struct Point that derives ToRhaiMap and FromRhaiMap:

    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 and Vec<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 to MyStructType::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 calls instance.to_rhai_map() (provided by #[derive(ToRhaiMap)]) to convert it into a rhai::Map, which Rhai receives as an object map.
  • For Vec<CustomStruct>: Similar logic applies element-wise. Incoming rhai::Array elements are converted using from_rhai_map; outgoing Vec elements are converted using to_rhai_map before being collected into a rhai::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:

  1. Open rhai_wrapper/src/lib.rs.
  2. Add a new macro arm following the existing patterns.
  3. Pay attention to argument conversion (from rhai::Dynamic or rhai::Map) and return value conversion (to rhai::Dynamic or rhai::Map).
  4. For custom types, rely on YourType::from_rhai_map(...) and your_instance.to_rhai_map().
  5. 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.