9.2 KiB
Call Rhai Functions from Rust
{{#include ../links.md}}
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_:
```rust
( 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][`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`][traits] for tuples, arrays and `Vec<T>`.
Engine::call_fn
takes a parameter of any type that implements the [FuncArgs
][traits] 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
][traits] 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][custom type] 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> ][Dynamic ] |
None |
bind_this_ptr |
bind the this pointer to a specific value |
tag |
[Option<Dynamic> ][Dynamic ] |
None |
with_tag |
set the custom state for this evaluation (accessed via [NativeCallContext::tag ][NativeCallContext ]) |
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
].
If the [`Scope`] is not rewound, beware that all [variables]/[constants] defined at top level of the
[function] or in the script body will _persist_ inside the custom [`Scope`].
If any of them are temporary and not intended to be retained, define them inside a statements block
(see example below).
┌─────────────┐
│ 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
`Engine::call_fn` cannot call functions in _method-call_ style.
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);