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_engine/rhaibook/engine/call-fn.md
2025-04-03 09:18:05 +02:00

9.2 KiB
Raw Blame History

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);