Call Rhai Functions from Rust
Rhai also allows working backwards from the other direction – i.e. calling a Rhai-scripted
function from Rust via Engine::call_fn.
┌─────────────┐
│ Rhai script │
└─────────────┘
import "process" as proc; // this is evaluated every time
fn hello(x, y) {
// hopefully 'my_var' is in scope when this is called
x.len + y + my_var
}
fn hello(x) {
// hopefully 'my_string' is in scope when this is called
x * my_string.len()
}
fn hello() {
// hopefully 'MY_CONST' is in scope when this is called
if MY_CONST {
proc::process_data(42); // can access imported module
}
}
┌──────┐
│ Rust │
└──────┘
// Compile the script to AST
let ast = engine.compile(script)?;
// Create a custom 'Scope'
let mut scope = Scope::new();
// A custom 'Scope' can also contain any variables/constants available to
// the functions
scope.push("my_var", 42_i64);
scope.push("my_string", "hello, world!");
scope.push_constant("MY_CONST", true);
// Evaluate a function defined in the script, passing arguments into the
// script as a tuple.
//
// Beware, arguments must be of the correct types because Rhai does not
// have built-in type conversions. If arguments of the wrong types are passed,
// the Engine will not find the function.
//
// Variables/constants pushed into the custom 'Scope'
// (i.e. 'my_var', 'my_string', 'MY_CONST') are visible to the function.
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ( "abc", 123_i64 ) )?;
// ^^^ ^^^^^^^^^^^^^^^^^^
// return type must be specified put arguments in a tuple
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ( 123_i64, ) )?;
// ^^^^^^^^^^^^ tuple of one
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", () )?;
// ^^ unit = tuple of zero
Functions with only one single parameter is easy to get wrong.
The proper Rust syntax is a tuple with one item:
( arg , )
Notice the comma (,) after the argument. Without it, the expression is a single value
(arg) which is the same as arg and not a tuple.
A syntax error with very confusing error message will be generated by the Rust compiler if the comma is omitted.
When using Engine::call_fn, the AST is always evaluated before the function is called.
This is usually desirable in order to import the necessary external modules that are needed by the function.
All new variables/constants introduced are, by default, not retained inside the Scope.
In other words, the Scope is rewound before each call.
If these default behaviors are not desirable, override them with Engine::call_fn_with_options.
FuncArgs Trait
Rhai implements FuncArgs for tuples, arrays and Vec<T>.
Engine::call_fn takes a parameter of any type that implements the FuncArgs trait,
which is used to parse a data type into individual argument values for the function call.
Custom types (e.g. structures) can also implement FuncArgs so they can be used for
calling Engine::call_fn.
use std::iter::once;
use rhai::FuncArgs;
// A struct containing function arguments
struct Options {
pub foo: bool,
pub bar: String,
pub baz: i64
}
impl FuncArgs for Options {
fn parse<C: Extend<Dynamic>>(self, container: &mut C) {
container.extend(once(self.foo.into()));
container.extend(once(self.bar.into()));
container.extend(once(self.baz.into()));
}
}
let options = Options { foo: true, bar: "world", baz: 42 };
// The type 'Options' can now be used as argument to 'call_fn'
// to call a function with three parameters: fn hello(foo, bar, baz)
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", options)?;
Implementing FuncArgs is almost never needed because Rhai works directly with
any custom type.
It is used only in niche cases where a custom type’s fields need to be split up to pass to functions.
Engine::call_fn_with_options
For more control, use Engine::call_fn_with_options, which takes a type CallFnOptions:
use rhai::{Engine, CallFnOptions};
let options = CallFnOptions::new()
.eval_ast(false) // do not evaluate the AST
.rewind_scope(false) // do not rewind the scope (i.e. keep new variables)
.bind_this_ptr(&mut state); // 'this' pointer
let result = engine.call_fn_with_options::<i64>(
options, // options
&mut scope, // scope to use
&ast, // AST containing the functions
"hello", // function entry-point
( "abc", 123_i64 ) // arguments
)?;
CallFnOptions allows control of the following:
| Field | Type | Default | Build method | Description |
|---|---|---|---|---|
eval_ast | bool | true | eval_ast | evaluate the AST before calling the target function (useful to run import statements) |
rewind_scope | bool | true | rewind_scope | rewind the custom Scope at the end of the function call so new local variables are removed |
this_ptr | Option<&mut Dynamic> | None | bind_this_ptr | bind the this pointer to a specific value |
in_all_namespaces | bool | false | in_all_namespaces | call function in all namespaces, not only scripted function within the AST |
tag | Option<Dynamic> | None | with_tag | set the custom state for this evaluation (accessed via NativeCallContext::tag) |
Skip evaluation of the AST
By default, the AST is evaluated before calling the target function.
This is necessary to make sure that necessary modules imported via import statements are available.
Setting eval_ast to false skips this evaluation.
Keep new variables/constants
By default, the Engine rewinds the custom Scope after each call to the initial size,
so any new variable/constant defined are cleared and will not spill into the custom Scope.
This prevents the Scope from being continuously polluted by new variables and is usually the
intuitively expected behavior.
Setting rewind_scope to false retains new variables/constants within the custom Scope.
This allows the function to easily pass values back to the caller by leaving them inside the
custom Scope.
┌─────────────┐
│ Rhai script │
└─────────────┘
fn initialize() {
let x = 42; // 'x' is retained
let y = x * 2; // 'y' is retained
// Use a new statements block to define temp variables
{
let temp = x + y; // 'temp' is NOT retained
foo = temp * temp; // 'foo' is visible in the scope
}
}
let foo = 123; // 'foo' is retained
// Use a new statements block to define temp variables
{
let bar = foo / 2; // 'bar' is NOT retained
foo = bar * bar;
}
┌──────┐
│ Rust │
└──────┘
let options = CallFnOptions::new().rewind_scope(false);
engine.call_fn_with_options(options, &mut scope, &ast, "initialize", ())?;
// At this point, 'scope' contains these variables: 'foo', 'x', 'y'
Bind the this pointer
CallFnOptions can also bind a value to the this pointer of a script-defined function.
It is possible, then, to call a function that uses this.
let ast = engine.compile("fn action(x) { this += x; }")?;
let mut value: Dynamic = 1_i64.into();
let options = CallFnOptions::new()
.eval_ast(false)
.rewind_scope(false)
.bind_this_ptr(&mut value);
engine.call_fn_with_options(options, &mut scope, &ast, "action", ( 41_i64, ))?;
assert_eq!(value.as_int()?, 42);
Just call any function
Sometimes it is useful to just make a function call without worrying whether the function is a
Rhai-scripted function in the AST.
Setting in_all_namespaces to true allows this.
let options = CallFnOptions::new().in_all_namespaces(true);
engine.call_fn_with_options(options, &mut scope, &ast, "calc", (1, 2, 3))?;
// The function 'calc' will be called with three arguments (1,2, 3) regardless of
// whether it is a scripted Rhai function, a native Rust function or a module function
// loaded into the 'Engine'.