The Rhai Book

Rhai Logo

Rhai is an embedded scripting language and evaluation engine for Rust that gives a safe and easy way to add scripting to any application.

Versions

This Book is for version 1.8.0 of Rhai.

Introduction to Rhai

Trivia

Etymology of the name “Rhai”

In the beginning there was ChaiScript, which is an embedded scripting language for C++. Originally it was intended to be a scripting language similar to JavaScript.

With java being a kind of hot beverage, the new language was named after another hot beverage – Chai, which is the word for “tea” in many world languages and, in particular, a popular kind of spicy milk tea consumed in India.

Later, when the novel implementation technique behind ChaiScript was ported from C++ to Rust, logically the C was changed to an R to make it “RhaiScript”, or just “Rhai”.

– Rhai author Johnathan Turner

The rhai.rs domain

@yrashk sponsored the domain rhai.rs.

Features of Rhai

Easy

Fast

  • Fairly efficient evaluation (1 million iterations in 0.3 sec on a single-core, 2.3 GHz Linux VM).

  • Compile once to AST for repeated evaluations.

  • Scripts are optimized (useful for template-based machine-generated scripts).

Dynamic

Safe

  • Relatively little unsafe code (yes there are some for performance reasons).

  • Sand-boxed – the scripting Engine, if declared immutable, cannot mutate the containing environment unless explicitly permitted.

  • Passes Miri.

Rugged

Flexible

Supported Targets and Builds

Rhai supports all CPU and O/S targets supported by Rust, including:

Minimum Rust Version

The minimum version of Rust required to compile Rhai is 1.57.

What Rhai Isn’t

Rhai’s purpose is to provide a dynamic layer over Rust code, in the same spirit of zero cost abstractions. It doesn’t attempt to be a new language. For example:

  • No classes. Well, Rust doesn’t either. On the other hand…

  • No traits… so it is also not Rust. Do your Rusty stuff in Rust.

  • No structures/records/tuples – define your types in Rust instead; Rhai can seamlessly work with any Rust type that implements Clone.

    There is, however, a built-in object map type which is adequate for most uses. It is possible to simulate object-oriented programming (OOP) by storing function pointers or closures in object map properties, turning them into methods.

  • No first-class functions – Code your functions in Rust instead, and register them with Rhai.

    There is, however, support for simple function pointers to allow runtime dispatch by function name.

  • No garbage collection – this should be expected, so…

  • No first-class closures – do your closure magic in Rust instead: turn a Rhai scripted function into a Rust closure.

    There is, however, support for simulated closures via currying a function pointer with captured shared variables.

  • No byte-codes/JIT – Rhai has an optimized AST-walking interpreter which is fast enough for most casual usage scenarios. Essential AST data structures are packed and kept together to maximize cache friendliness.

    Functions are dispatched based on pre-calculated hashes and accessing variables are mostly through pre-calculated offsets to the variables file (a Scope), so it is seldom necessary to look something up by text name.

    In addition, Rhai’s design deliberately avoids maintaining a scope chain so function scopes do not pay any speed penalty. This particular design also allows variables data to be kept together in a contiguous block, avoiding allocations and fragmentation while being cache-friendly. In a typical script evaluation run, no data is shared and nothing is locked.

    Still, the purpose of Rhai is not to be super fast, but to make it as easy and versatile as possible to integrate with native Rust applications. What you lose from running an AST walker, you gain back from increased flexibility.

  • No formal language grammar – Rhai uses a hand-coded lexer, a hand-coded top-down recursive-descent parser for statements, and a hand-coded Pratt parser for expressions.

    This lack of formalism allows the tokenizer and parser themselves to be exposed as services in order to support advanced features such as disabling keywords and operators, dynamically changing tokens during parsing, adding custom operators, defining custom syntax and filtering variables definition.

Do not write the next 4D VR game in Rhai

Due to this intended usage, Rhai deliberately keeps the language simple and small by omitting advanced language features such as classes, inheritance, interfaces, generics, first-class functions/closures, pattern matching, concurrency, byte-codes VM, JIT etc. Focus is on flexibility and ease of use instead of raw speed.

Avoid the temptation to write full-fledge application logic entirely in Rhai – that use case is best fulfilled by more complete languages such as JavaScript or Lua.

Tip: Use Rhai as a thin dynamic wrapper layer over Rust code

In actual practice, it is usually best to expose a Rust API into Rhai for scripts to call.

All the core functionalities should be written in Rust, with Rhai being the dynamic control layer.

This is similar to some dynamic languages where most of the core functionalities reside in a C/C++ standard library.

Another similar scenario is a web front-end driving back-end services written in a systems language. In this case, JavaScript takes the role of Rhai while the back-end language, well… it can actually also be Rust. Except that Rhai integrates with Rust much more tightly, removing the need for interfaces such as XHR calls and payload encoding such as JSON.

Licensing

Rhai is licensed under either of the following, at your choice:

Notice

Unless explicitly stated otherwise, any contribution intentionally submitted for inclusion in this crate, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.

Related Resources

Online resources

External tools

Syntax highlighting

  • VS Code Extension – Support .rhai script files syntax highlighting for Visual Studio Code

  • Sublime Text 3 Plugin – Support .rhai script files syntax highlighting for Sublime Text 3

  • For other syntax highlighting purposes, e.g. vim, highlight.js, both Rust or JavaScript can be used successfully.

    Use rust when there is no string interpolation. This way, closures and functions (via the fn keyword) are styled properly. Elements not highlighted include:

    Use js (JavaScript) when there is strings interpolation. Elements not highlighted include:

    • functions definition (via the fn keyword)
    • closures (via the Rust-like |...| { ... } syntax)
    • built-in functions such as Fn, call, type_of, is_shared, is_def_var, is_def_fn

Other cool projects

Getting Started

Online Playground

See also

For more details, see the section here.

Rhai provides an online playground to try out its language and engine features without having to install anything.

The playground provides a syntax-highlighting script editor with example snippets.

Scripts can be evaluated directly from the editor.

Online Playground

Install the Rhai Crate

In order to use Rhai in a project, the Rhai crate must first be made a dependency.

Use specific version

The easiest way is to install the Rhai crate from crates.io, starting by looking up the latest version and adding this line under dependencies in the project’s Cargo.toml:

[dependencies]
rhai = "1.8.0"    # assuming 1.8.0 is the latest version

Use latest release version

Automatically use the latest released crate version on crates.io:

[dependencies]
rhai = "*"

Use latest development version

Crate versions are released on crates.io infrequently, so to track the latest features, enhancements and bug fixes, pull directly from GitHub:

[dependencies]
rhai = { git = "https://github.com/rhaiscript/rhai" }

Optional Features

By default, Rhai includes all the standard functionalities in a small, tight package.

Features are not additive

Most Rhai features are not strictly additive, i.e. they do not only add optional functionalities.

In fact, most features are subtractive, i.e. they opt-out of unneeded functionalities. Notice that this deviates from Rust norm where features are additive.

Excluding functionalities result in smaller, faster builds as well as more control over what scripts can (or cannot) do.

There is a reason for this design, because the lack of a language feature by itself is a feature (that’s deep…).

See here for more details.

Features that Enable Special Functionalities

FeatureAdditive?Description
syncnorestricts all values types to those that are Send + Sync; under this feature, all Rhai types, including Engine, Scope and AST, are all Send + Sync
decimalnoenables the Decimal number type
unicode-xid-identnoallows Unicode Standard Annex #31 as identifiers
serdeyesenables serialization/deserialization via serde (pulls in the serde crate)
metadatayesenables exporting functions metadata; implies serde and additionally pulls in serde_json
internalsyesexposes internal data structures (e.g. AST nodes);

Safety Warnings
  • allowing access to internal types may enable external attack vectors
  • internal types and functions are volatile and may change from version to version
debuggingyesenables the debugging interface; implies internals

Features that Disable Certain Language Features

FeatureAdditive?Description
no_floatnodisables floating-point numbers and math
no_indexnodisables arrays and indexing features
no_objectnodisables support for custom types and object maps
no_functionnodisables script-defined functions; implies no_closure
no_modulenodisables loading external modules
no_closurenodisables capturing external variables in anonymous functions to simulate closures

Features that Disable Certain Engine Features

FeatureAdditive?Description
uncheckednodisables arithmetic checking (such as over-flows and division by zero), call stack depth limit, operations count limit, modules loading limit and data size limit.
Beware that a bad script may panic the entire system!
no_optimizenodisables script optimization
no_positionnodisables position tracking during parsing

Features that Configure the Engine

FeatureAdditive?Description
f32_floatnosets the system floating-point type (FLOAT) to f32 instead of f64; no effect under no_float
only_i32nosets the system integer type (INT) to i32 and disable all other integer types
only_i64nosets the system integer type (INT) to i64 and disable all other integer types

Features for no-std Builds

The following features are provided exclusively for no-std targets. Do not use them when not compiling for no-std.

FeatureAdditive?Description
no_stdnobuilds for no-std; notice that additional dependencies will be pulled in to replace std features

Features for WebAssembly (WASM) Builds

The following features are provided exclusively for WASM targets. Do not use them for non-WASM targets.

FeatureAdditive?Description
wasm-bindgennouses wasm-bindgen to compile for WASM
stdwebnouses stdweb to compile for WASM

Features for Building Bin Tools

The feature bin-features include all the features necessary for building the bin tools.

By default, it includes: decimal, metadata, serde, debugging and rustyline.

Example

The Cargo.toml configuration below:

[dependencies]
rhai = { version = "1.8.0", features = [ "sync", "unchecked", "only_i32", "no_float", "no_module", "no_function" ] }

turns on these six features:

FeatureDescription
synceverything is Send + Sync
uncheckeddisable all safety checks (should not be used with untrusted user scripts)
only_i32use only 32-bit signed integers and no others
no_floatno floating point numbers
no_moduleno loading external modules
no_functionno defining functions

The resulting scripting engine supports only the i32 integer numeral type (and no others like u32, i16 or i64), no floating-point, is Send + Sync (so it can be safely used across threads), and does not support defining functions nor loading external modules.

This configuration is perfect for an expression parser in a 32-bit embedded system without floating-point hardware.

Packaged Utilities

A number of Rhai-driven tools can be found in the src/bin directory:

Tip: Domain-specific tools

It is possible to turn these tools into Domain-Specific Tools.

ToolRequired feature(s)Description
rhai-runruns Rhai script files
rhai-replrustylinea simple REPL tool
rhai-dbgdebuggingthe Rhai Debugger

Tip: bin-features

Some bin tools require certain features and will not be built by default without those features set.

For convenience, a feature named bin-features is available which is a combination of the following:

FeatureDescription
decimalsupport for decimal numbers
metadataaccess functions metadata
serdeexport functions metadata to JSON
debuggingrequired by rhai-dbg
rustylinerequired by rhai-repl

Install Tools

To install all these tools (with full features), use the following command:

cargo install --path . --bins  --features bin-features

or specifically:

cargo install --path . --bin sample_app_to_run  --features bin-features

Run a Tool from Cargo

Tools can also be run with the following cargo command:

cargo run --features bin-features --bin sample_app_to_run

Tools List

rhai-repl – The Rhai REPL Tool

rhai-repl is a particularly useful tool – it allows one to interactively try out Rhai’s language features in a standard REPL (Read-Eval-Print Loop).

Filenames passed to it as command line arguments are run and loaded as Rhai scripts before the REPL starts.

Test functions

The following test functions are pre-registered, via Engine::register_fn, into rhai-repl. They are intended for testing purposes.

FunctionDescription
test(x: i64, y: i64)returns a string with both numbers
test(x: &mut i64, y: i64, z: &str)displays the parameters and add y to x

Example

The following command first runs three scripts – init1.rhai, init2.rhai and init3.rhai – loading the functions defined in each script into the global namespace.

Then it enters an REPL, which can call the above functions freely.

rhai-repl init1.rhai init2.rhai init3.rhai

rhai-run – The Rhai Runner

Use rhai-run to run Rhai scripts.

Filenames passed to it as command line arguments are run in sequence as Rhai scripts.

Example

The following command runs the scripts script1.rhai, script2.rhai and script3.rhai in order.

rhai-run script1.rhai script2.rhai script3.rhai

rhai-dbg – The Rhai Debugger

Use rhai-dbg to debug a Rhai script.

Filename passed to it will be loaded as a Rhai script for debugging.

Example

The following command debugs the script my_script.rhai.

rhai-dbg my_script.rhai

Using the Engine

Rhai’s interpreter resides in the Engine type under the master rhai namespace.

This section shows how to set up, configure and use this scripting engine.

Your First Script in Rhai

Run a Script

To get going with Rhai is as simple as creating an instance of the scripting engine rhai::Engine via Engine::new, then calling Engine::run.

use rhai::{Engine, EvalAltResult};

pub fn main() -> Result<(), Box<EvalAltResult>>
//                          ^^^^^^^^^^^^^^^^^^
//                          Rhai API error type
{
    // Create an 'Engine'
    let engine = Engine::new();

    // Your first Rhai Script
    let script = "print(40 + 2);";

    // Run the script - prints "42"
    engine.run(script)?;

    // Done!
    Ok(())
}

Get a Return Value

To return a value from the script, use Engine::eval instead.

use rhai::{Engine, EvalAltResult};

pub fn main() -> Result<(), Box<EvalAltResult>>
{
    let engine = Engine::new();

    let result = engine.eval::<i64>("40 + 2")?;
    //                      ^^^^^^^ required: cast the result to a type

    println!("Answer: {}", result);             // prints 42

    Ok(())
}

Use Script Files

Script file extension

Rhai script files are customarily named with extension .rhai.

Or evaluate a script file directly with Engine::run_file or Engine::eval_file.

Loading and running script files is not available for no_std or WASM builds.

let result = engine.eval_file::<i64>("hello_world.rhai".into())?;
//                                   ^^^^^^^^^^^^^^^^^^^^^^^^^
//                                   a 'PathBuf' is needed

// Running a script file also works in a similar manner
engine.run_file("hello_world.rhai".into())?;

Tip: Unix shebangs

On Unix-like systems, the shebang (#!) is used at the very beginning of a script file to mark a script with an interpreter (for Rhai this would be rhai-run).

If a script file starts with #!, the entire first line is skipped by Engine::compile_file and Engine::eval_file. Because of this, Rhai scripts with shebangs at the beginning need no special processing.

This behavior is also present for non-Unix (e.g. Windows) environments so scripts are portable.

#!/home/to/me/bin/rhai-run

// This is a Rhai script

let answer = 42;
print(`The answer is: ${answer}`);

Specify the Return Type

Tip: Dynamic

Use Dynamic if you’re uncertain of the return type.

The type parameter for Engine::eval is used to specify the type of the return value, which must match the actual type or an error is returned. Rhai is very strict here.

There are two ways to specify the return type: turbofish notation, or type inference.

Turbofish

let result = engine.eval::<i64>("40 + 2")?;     // return type is i64

result.is::<i64>() == true;

let result = engine.eval::<Dynamic>("boo()")?;  // use 'Dynamic' if you're not sure what type it'll be!

let result = engine.eval::<String>("40 + 2")?;  // returns an error because the actual return type is i64, not String

Type inference

let result: i64 = engine.eval("40 + 2")?;       // return type is inferred to be i64

result.is::<i64>() == true;

let result: Dynamic = engine.eval("boo()")?;    // use 'Dynamic' if you're not sure what type it'll be!

let result: String = engine.eval("40 + 2")?;    // returns an error because the actual return type is i64, not String

Compile a Script (to AST)

To repeatedly evaluate a script, compile it first with Engine::compile into an AST (Abstract Syntax Tree) form.

Engine::eval_ast_XXX and Engine::run_ast_XXX evaluate a pre-compiled AST.

// Compile to an AST and store it for later evaluations
let ast = engine.compile("40 + 2")?;

for _ in 0..42 {
    let result: i64 = engine.eval_ast(&ast)?;

    println!("Answer #{}: {}", i, result);      // prints 42
}

Tip: Compile script file

Compiling script files is also supported via Engine::compile_file (not available for no_std or WASM builds).

let ast = engine.compile_file("hello_world.rhai".into())?;

AST manipulation API

Advanced users who may want to manipulate an AST, especially the functions contained within, should see the section on Manage AST’s for more details.

Raw Engine

Engine::new creates a scripting Engine with common functionalities (e.g. printing to stdout via print or debug).

In many controlled embedded environments, however, these may not be needed and unnecessarily occupy application code storage space.

Built-in operators

Even with a raw Engine, some operators are built-in and always available.

See Built-in Operators for a full list.

Use Engine::new_raw to create a raw Engine, in which only a minimal set of built-in basic arithmetic and logical operators are supported.

To add more functionalities to a raw Engine, load packages into it.

Since packages can be shared, this is an extremely efficient way to create multiple instances of the same Engine with the same set of functions.

Engine::newEngine::new_raw
Built-in operatorsyesyes
Package loadedStandardPackagenone
Module resolverFileModuleResolvernone
on_printyesnone
on_debugyesnone

Engine::new is equivalent to…

use rhai::module_resolvers::FileModuleResolver;
use rhai::packages::StandardPackage;

// Create a raw scripting Engine
let mut engine = Engine::new_raw();

// Use the file-based module resolver
engine.set_module_resolver(FileModuleResolver::new());

// Default print/debug implementations
engine.on_print(|text| println!("{}", text));

engine.on_debug(|text, source, pos| {
    if let Some(source) = source {
        println!("{} @ {:?} | {}", source, pos, text);
    } else if pos.is_none() {
        println!("{}", text);
    } else {
        println!("{:?} | {}", pos, text);
    }
});

// Register the Standard Package
let package = StandardPackage::new().as_shared_module();

engine.register_global_module(package);

Built-in Operators

The following operators are built-in, meaning that they are always available, even when using a raw Engine.

All built-in operators are binary, and are supported for both operands of the same type.

OperatorsAssignment operatorsSupported types
(see standard types)
+,+=
-, *, /, %, **,-=, *=, /=, %=, **=
<<, >><<=, >>=
  • INT
&, |, ^&=, |=, ^=
  • INT (bit-wise)
  • bool (non-short-circuiting)
&&, ||
  • bool (short-circuits)
==, !=
>, >=, <, <=
in

Tip

FLOAT and Decimal also inter-operate with INT, while strings inter-operate with characters for certain operators (e.g. +).

Scope – Maintaining State

By default, Rhai treats each Engine invocation as a fresh one, persisting only the functions that have been registered but no global state.

This gives each evaluation a clean starting slate.

In order to continue using the same global state from one invocation to the next, such a state (a Scope) must be manually created and passed in.

All Scope variables and constants have values that are Dynamic, meaning they can store values of any type.

Under sync, however, only types that are Send + Sync are supported, and the entire Scope itself will also be Send + Sync. This is extremely useful in multi-threaded applications.

Shadowing

A newly-added variable or constant shadows previous ones of the same name.

In other words, all versions are kept for variables and constants, but only the latest ones can be accessed via get_value<T>, get_mut<T> and set_value<T>.

Essentially, a Scope is always searched in reverse order.

Tip: The lifetime parameter

The Scope has a lifetime parameter, in the vast majority of cases it can be omitted and automatically inferred to be 'static.

Currently, that lifetime parameter is not used. It is there to maintain backwards compatibility as well as for possible future expansion when references can also be put into the Scope.

The lifetime parameter is not guaranteed to remain unused for future versions.

In order to put a Scope into a struct, use Scope<'static>.

Scope API

MethodDescription
new instance methodcreate a new empty Scope
lennumber of variables/constants currently within the Scope
rewindrewind (i.e. reset) the Scope to a particular number of variables/constants
clearremove all variables/constants from the Scope, making it empty
is_emptyis the Scope empty?
is_constantis the particular variable/constant in the Scope a constant?
push, push_constantadd a new variable/constant into the Scope with a specified value
push_dynamic, push_constant_dynamicadd a new variable/constant into the Scope with a Dynamic value
set_or_push<T>set the value of a variable within the Scope if it exists and is not constant; add a new variable into the Scope otherwise
containsdoes the particular variable or constant exist in the Scope?
get_value<T>get the value of a variable/constant within the Scope
set_value<T>set the value of a variable within the Scope, panics if it is constant
getget a reference to the value of a variable/constant within the Scope
get_mutget a reference to the value of a variable within the Scope, None if it is constant
set_aliasexported a variable/constant within the Scope under a particular name
iter, iter_raw, IntoIterator::into_iterget an iterator to the variables/constants within the Scope
Extend::extendadd variables/constants to the Scope

Scope public API

For details on the Scope API, refer to the documentation online.

Example

In the following example, a Scope is created with a few initialized variables, then it is threaded through multiple evaluations.

use rhai::{Engine, Scope, EvalAltResult};

let engine = Engine::new();

// First create the state
let mut scope = Scope::new();

// Then push (i.e. add) some initialized variables into the state.
// Remember the system number types in Rhai are i64 (i32 if 'only_i32')
// and f64 (f32 if 'f32_float').
// Better stick to them or it gets hard working with the script.
scope.push("y", 42_i64)
     .push("z", 999_i64)
     .push_constant("MY_NUMBER", 123_i64)       // constants can also be added
     .set_value("s", "hello, world!");          // 'set_value' adds a new variable when one doesn't exist

// First invocation
engine.run_with_scope(&mut scope, 
"
    let x = 4 + 5 - y + z + MY_NUMBER + s.len;
    y = 1;
")?;

// Second invocation using the same state.
// Notice that the new variable 'x', defined previously, is still here.
let result = engine.eval_with_scope::<i64>(&mut scope, "x + y")?;

println!("result: {}", result);                 // prints 1103

// Variable y is changed in the script - read it with 'get_value'
assert_eq!(scope.get_value::<i64>("y").expect("variable y should exist"), 1);

// We can modify scope variables directly with 'set_value'
scope.set_value("y", 42_i64);
assert_eq!(scope.get_value::<i64>("y").expect("variable y should exist"), 42);

Engine API Using Scope

Engine API methods that accept a Scope parameter all end with _with_scope, making that Scope (and everything inside it) available to the script:

Engine APINot available under
Engine::eval_with_scope
Engine::eval_ast_with_scope
Engine::eval_file_with_scopeno_std
Engine::eval_expression_with_scope
Engine::run_with_scope
Engine::run_ast_with_scope
Engine::run_file_with_scopeno_std
Engine::compile_file_with_scopeno_std
Engine::compile_expression_with_scope

Don’t forget to rewind

Variables or constants defined at the global level of a script persist inside the custom Scope even after the script ends.

let mut scope = Scope::new();

engine.run_with_scope(&mut scope, "let x = 42;")?;

// Variable 'x' stays inside the custom scope!
engine.run_with_scope(&mut scope, "print(x);")?;    //  prints 42

Due to variable shadowing, new variables/constants are simply added on top of existing ones (even when they already exist), so care must be taken that new variables/constants inside the custom Scope do not grow without bounds.

let mut scope = Scope::new();

// Don't do this - this creates 1 million variables named 'x'
//                 inside 'scope'!!!
for _ in 0..1_000_000 {
    engine.run_with_scope(&mut scope, "let x = 42;")?;
}

// The 'scope' contains a LOT of variables...
assert_eq!(scope.len(), 1_000_000);

// Variable 'x' stays inside the custom scope!
engine.run_with_scope(&mut scope, "print(x);")?;    //  prints 42

In order to remove variables or constants introduced by a script, use the rewind method.

// Run a million times
for _ in 0..1_000_000 {
    // Save the current size of the 'scope'
    let orig_scope_size = scope.len();

    engine.run_with_scope(&mut scope, "let x = 42;")?;

    // Rewind the 'scope' to the original size
    scope.rewind(orig_scope_size);
}

// The 'scope' is empty
assert_eq!(scope.len(), 0);

// Variable 'x' is no longer inside 'scope'!
engine.run_with_scope(&mut scope, "print(x);")?;    //  error: variable 'x' not found

Evaluate Expressions Only

Tip: Dynamic

Use Dynamic if you’re uncertain of the return type.

Very often, a use case does not require a full-blown scripting language, but only needs to evaluate expressions.

In these cases, use the Engine::compile_expression and Engine::eval_expression methods or their _with_scope variants.

let result: i64 = engine.eval_expression("2 + (10 + 10) * 2")?;

let result: Dynamic = engine.eval_expression("get_value(42)")?;

// Usually this is done together with a custom scope with variables...

let mut scope = Scope::new();

scope.push("x", 42_i64);
scope.push_constant("SCALE", 10_i64);

let result: i64 = engine.eval_expression_with_scope(&mut scope,
                        "(x + 1) * SCALE"
                  )?;

No statements allowed

When evaluating expressions, no full-blown statement (e.g. if, while, for, fn) – not even variable assignment – is supported and will be considered syntax errors.

This is true even for if expressions, switch expressions, statement expressions and anonymous functions/closures.

// The following are all syntax errors because the script
// is not a strict expression.

engine.eval_expression::<()>("x = 42")?;

let ast = engine.compile_expression("let x = 42")?;

let result = engine.eval_expression_with_scope::<i64>(&mut scope,
                    "if x { 42 } else { 123 }"
             )?;

let fp: FnPtr = engine.eval_expression("|x| x + 1")?;

Engine Configuration Options

A number of other configuration options are available from the Engine to fine-tune behavior and safeguards.

Compile-Time Language Features

MethodDescription
set_optimization_level
(not available under no_optimize)
sets the amount of script optimizations performed (see script optimization)
set_allow_if_expressionallows/disallows if-expressions
set_allow_switch_expressionallows/disallows switch expressions
set_allow_statement_expressionallows/disallows statement expressions
set_allow_anonymous_fn
(not available under no_function)
allows/disallows anonymous functions
set_allow_loopingallows/disallows looping (i.e. while, loop, do and for statements)
set_allow_shadowingallows/disallows shadowing of variables
set_strict_variablesenables/disables Strict Variables mode
disable_symboldisables a certain keyword or operator (see disable keywords and operators)

Beware that these options activate during compile-time only. If an AST is compiled on an Engine but then evaluated on a different Engine with different configuration, disallowed features contained inside the AST will still run as normal.

Runtime Behavior

MethodDescription
set_fail_on_invalid_map_property
(not available under no_object)
sets whether to raise errors (instead of returning ()) when invalid properties are accessed on object maps

Safety Limits

MethodNot available underDescription
set_max_expr_depthsuncheckedsets the maximum nesting levels of an expression/statement (see maximum statement depth)
set_max_call_levelsuncheckedsets the maximum number of function call levels (default 50) to avoid infinite recursion (see maximum call stack depth)
set_max_operationsuncheckedsets the maximum number of operations that a script is allowed to consume (see maximum number of operations)
set_max_modulesuncheckedsets the maximum number of modules that a script is allowed to load (see maximum number of modules)
set_max_string_sizeuncheckedsets the maximum length (in UTF-8 bytes) for strings (see maximum length of strings)
set_max_array_sizeunchecked, no_indexsets the maximum size for arrays (see maximum size of arrays)
set_max_map_sizeunchecked, no_objectsets the maximum number of properties for object maps (see maximum size of object maps)

Examples

Rust

Example Scripts

Language Feature Scripts

There are also a number of examples scripts that showcase Rhai’s features, all in the scripts directory:

ScriptDescription
array.rhaiarrays example
assignment.rhaivariable declarations
comments.rhaijust regular comments
doc-comments.rhaidoc-comments example
for1.rhaifor loops
for2.rhaifor loops with array iterations
for3.rhaifor loops with closures
function_decl1.rhaia function without parameters
function_decl2.rhaia function with two parameters
function_decl3.rhaia function with many parameters
function_decl4.rhaia function acting as a method
if1.rhaiif example
if2.rhaiif-expression example
loop.rhaicount-down loop in Rhai, emulating a dowhile loop
module.rhaiimport a script file as a module
oop.rhaisimulate object-oriented programming (OOP) with closures
op1.rhaijust simple addition
op2.rhaisimple addition and multiplication
op3.rhaichange evaluation order with parenthesis
string.rhaistring operations, including interpolation
strings_map.rhaistring and object map operations
switch.rhaiswitch example
while.rhaiwhile loop

Benchmark Scripts

The following scripts are for benchmarking the speed of Rhai:

ScriptsDescription
speed_test.rhaia simple application to measure the speed of Rhai’s interpreter (1 million iterations)
primes.rhaiuse Sieve of Eratosthenes to find all primes smaller than a limit
fibonacci.rhaicalculate the n-th Fibonacci number using a really dumb algorithm
mat_mul.rhaimatrix multiplication test to measure the speed of multi-dimensional array access

Run Example Scripts

The rhai-run utility can be used to run Rhai scripts:

cargo run --bin rhai-run scripts/any_script.rhai

Special Builds

It is possible to mix-and-match various features of the Rhai crate to make specialized builds with specific characteristics and behaviors.

Performance Build

Some features are for performance. In order to squeeze out the maximum performance from Rhai, the following features should be considered:

FeatureDescriptionRationale
only_i32support only a single i32 integer typereduce data size
no_floatremove support for floating-point numbersreduce code size
f32_floatset floating-point numbers (if not disabled) to 32-bitreduce data size
no_closureremove support for variables sharingno need for data locking
uncheckeddisable all safety checksremove non-essential code
no_positiondisable position tracking during parsingremove non-essential code

When the above feature flags are used, performance may increase by around 15-20%.

Use Only One Integer Type

If only a single integer type is needed in scripts – most of the time this is the case – it is best to avoid registering lots of functions related to other integer types that will never be used. As a result, Engine creation will be faster because fewer functions need to be loaded.

The only_i32 and only_i64 features disable all integer types except i32 or i64 respectively.

Use Only 32-Bit Numbers

If only 32-bit integers are needed – again, most of the time this is the case – turn on only_i32. Under this feature, only i32 is supported as a built-in integer type and no others.

On 64-bit targets this may not gain much, but on certain 32-bit targets this improves performance due to 64-bit arithmetic requiring more CPU cycles to complete.

Minimize Size of Dynamic

Turning on f32_float (or no_float) and only_i32 on 32-bit targets makes the critical Dynamic data type only 8 bytes long for 32-bit targets.

Normally Dynamic needs to be up 12-16 bytes long in order to hold an i64 or f64.

A smaller Dynamic helps performance due to better cache efficiency.

Use ImmutableString

Internally, Rhai uses immutable strings instead of the Rust String type. This is mainly to avoid excessive cloning when passing function arguments.

Rhai’s internal string type is ImmutableString (basically Rc<SmartString> or Arc<SmartString> depending on the sync feature). It is cheap to clone, but expensive to modify (a new copy of the string must be made in order to change it).

Therefore, functions taking String parameters should use ImmutableString or &str (maps to ImmutableString) for the best performance with Rhai.

Disable Closure Capturing

Anonymous functions still work

Anonymous functions continue to work even under no_closure.

Only capturing of external shared variables is disabled.

Support for closures that capture shared variables adds material overhead to script evaluation.

This is because every data access must be checked whether it is a shared value and, if so, take a read lock before reading it.

As the vast majority of variables are not shared, needless to say this is a non-trivial performance overhead.

Use no_closure to disable support for closures to optimize the hot path because it no longer needs to take locks for shared data.

Unchecked Build

By default, Rhai provides a Don’t Panic guarantee and prevents malicious scripts from bringing down the host. Any panic can be considered a bug.

For maximum performance, however, these safety checks can be turned off via the unchecked feature.

Disable Position

For embedded scripts that are not expected to cause errors, the no_position feature can be used to disable position tracking during parsing.

No line number/character position information is kept for error reporting purposes.

This may result in a slightly fast build due to elimination of code related to position tracking.

Avoid Cloning

Rhai values are typically cloned when passed around, especially into function calls. Large data structures may incur material cloning overhead.

Some functions accept the first parameter as a mutable reference (i.e. &mut), for example methods for custom types, and may avoid potentially-costly cloning.

For example, the += (append) compound assignment takes a mutable reference to the variable while the corresponding + (add) assignment usually doesn’t. The difference in performance can be huge:

let x = create_some_very_big_and_expensive_type();

x = x + 1;
//  ^ 'x' is cloned here

// The above is equivalent to:
let temp_value = x.clone() + 1;
x = temp_value;

x += 1;             // <- 'x' is NOT cloned

Tip: Simple variable references are already optimized

Rhai’s script optimizer is usually smart enough to rewrite function calls into method-call style or compound assignment style to take advantage of this.

However, there are limits to its intelligence, and only simple variable references are optimized.

x = x + 1;          // <- this statement...

x += 1;             // ... is rewritten as this

x[y] = x[y] + 1;    // <- but this is not, so this is MUCH slower...

x[y] += 1;          // ... than this

some_func(x, 1);    // <- this statement...

x.some_func(1);     // ... is rewritten as this

some_func(x[y], 1); // <- but this is not, so 'x[y]` is cloned

Short Variable Names for 32-Bit Systems

On 32-bit systems, variable and constant names longer than 11 ASCII characters incur additional allocation overhead.

This is particularly true for local variables inside a hot loop, where they are created and destroyed in rapid succession.

Therefore, avoid long variable and constant names that are over this limit.

On 64-bit systems, this limit is raised to 23 ASCII characters, which is almost always adequate.

Minimal Build

Configuration

In order to compile a minimal build – i.e. a build optimized for size – perhaps for no-std embedded targets or for compiling to WASM, it is essential that the correct linker flags are used in Cargo.toml:

[profile.release]
lto = "fat"         # turn on Link-Time Optimizations
codegen-units = 1   # trade compile time with maximum optimization
opt-level = "z"     # optimize for size

Use i32 Only

For embedded systems that must optimize for code size, the architecture is commonly 32-bit. Use only_i32 to prune away large sections of code implementing functions for other numeric types (including i64).

If, for some reason, 64-bit long integers must be supported, use only_i64 instead of only_i32.

Opt-Out of Features

Opt out of as many features as possible, if they are not needed, to reduce code size because, remember, by default all code is compiled into the final binary since what a script requires cannot be predicted. If a language feature will never be needed, omitting it is a prudent strategy to optimize the build for size.

Removing the script optimizer (no_optimize) yields a sizable code saving, at the expense of a less efficient script.

Omitting arrays (no_index) yields the most code-size savings, followed by floating-point support (no_float), safety checks (unchecked) and finally object maps and custom types (no_object).

Where the usage scenario does not call for loading externally-defined modules, use no_module to save some bytes. Disable script-defined functions (no_function) and possibly closures (no_closure) when the features are not needed. Both of these have some code size savings but not much.

For embedded scripts that are not expected to cause errors, the no_position feature can be used to disable position tracking during parsing. No line number/character position information is kept for error reporting purposes. This may result in a slightly smaller build due to elimination of code related to position tracking.

Use a Raw Engine

Engine::new_raw creates a raw engine. A raw engine supports, out of the box, only a very restricted set of basic arithmetic and logical operators.

Selectively include other necessary functionalities by picking specific packages to minimize the footprint.

Packages are shared (even across threads via the sync feature), so they only have to be created once.

no-std Build

The feature no_std automatically converts the scripting engine into a no-std build.

Usually, a no-std build goes hand-in-hand with minimal builds because typical embedded hardware (the primary target for no-std) has limited storage.

Nightly required

Currently, no_std requires the nightly compiler due to the crates that it uses.

Implementation

Rhai allocates, so the first thing that must be included in any no-std project is an allocator crate, such as wee_alloc.

Then there is the need to set up proper error/panic handlers. The following example uses panic = "abort" and wee_alloc as the allocator.

// Set up for no-std.
#![no_std]

// The following no-std features are usually needed.
#![feature(alloc_error_handler, start, core_intrinsics, lang_items, link_cfg)]

// Set up the global allocator.
extern crate alloc;
extern crate wee_alloc;

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

// Rust needs a CRT runtime on Windows when compiled with MSVC.
#[cfg(all(windows, target_env = "msvc"))]
#[link(name = "msvcrt")]
#[link(name = "libcmt")]
extern "C" {}

// Set up panic and error handlers
#[alloc_error_handler]
fn err_handler(_: core::alloc::Layout) -> ! {
    core::intrinsics::abort();
}

#[panic_handler]
#[lang = "panic_impl"]
extern "C" fn rust_begin_panic(_: &core::panic::PanicInfo) -> ! {
    core::intrinsics::abort();
}

#[lang = "eh_personality"]
extern "C" fn eh_personality() {}

#[no_mangle]
extern "C" fn rust_eh_register_frames() {}

#[no_mangle]
extern "C" fn rust_eh_unregister_frames() {}

#[no_mangle]
extern "C" fn _Unwind_Resume() {}

#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
    // ... main program ...
}

Samples

Check out the no-std sample applications for different operating environments.

WebAssembly (WASM) Build

Why would you do that?

There is already a fast and powerful scripting language that integrates nicely with WASM – JavaScript.

Anyhow, do it because you can!

It is possible to use Rhai when compiling to WebAssembly (WASM).

This yields a scripting engine (and language) that can be run in a standard web browser, among other places.

Unavailable features

When building for WASM, certain features will not be available, such as the script file API’s and loading modules from external script files.

Sample

Check out the Online Playground project which is driven by a Rhai Engine compiled into WASM.

JavaScript Interop

Specify either of the wasm-bindgen or stdweb features when building for WASM. This selects the appropriate JavaScript interop layer to use.

It is still possible to compile for WASM without either wasm-bindgen or stdweb, but then the interop code must be explicitly provided.

Size

Also look into minimal builds to reduce generated WASM size.

As of this version, a typical, full-featured Rhai scripting engine compiles to a single WASM file less than 300KB gzipped.

When excluding features that are marginal in WASM environment, the gzipped payload can be further shrunk to 160KB.

The following standard packages, when excluded, provide the corresponding size savings:

PackageWASM size saving
CorePackage18 KB
BitFieldPackage1 KB
LogicPackage< 1 KB
BasicMathPackage1 KB
BasicArrayPackage15 KB
BasicBlobPackage7 KB
BasicMapPackage4 KB
MoreStringPackage34 KB

Speed

In benchmark tests, a WASM build runs scripts roughly 1.7-2.2x slower than a native optimized release build.

Common Features

Some Rhai functionalities are not necessary in a WASM environment, so the following features are typically used for a WASM build:

FeatureDescription
wasm-bindgen or stdwebuse wasm-bindgen or stdweb as the JavaScript interop layer, omit if using custom interop code
uncheckedwhen a WASM module panics, it doesn’t crash the entire web app; however this also disables maximum number of operations and progress tracking so a script can still run indefinitely – the web app must terminate it itself
only_i32WASM supports 32-bit and 64-bit integers, but most scripts will only need 32-bit
f32_floatWASM supports 32-bit single-precision and 64-bit double-precision floating-point numbers, but single-precision is usually fine for most uses
no_modulea WASM module cannot load modules from the file system, so usually this is not needed, but the savings are minimal; alternatively, a custom module resolver can be provided that loads other Rhai scripts

The following features are typically not used because they don’t make sense in a WASM build:

FeatureWhy unnecessary
syncWASM is single-threaded
no_stdstd lib works fine with WASM
metadataWASM usually doesn’t need access to Rhai functions metadata
internalsWASM usually doesn’t need to access Rhai internal data structures, unless you are walking the AST
debuggingunless debugging is needed

Extend Rhai with Rust

Most features and functionalities required by a Rhai script should actually be coded in Rust, which leverages the superior native run-time speed.

This section discusses how to extend Rhai with functionalities written in Rust.

Traits

A number of traits, under the rhai:: module namespace, provide additional functionalities.

TraitDescriptionMethods
Functrait for creating Rust closures from scriptcreate_from_ast, create_from_script
FuncArgstrait for parsing function call argumentsparse
ModuleResolvertrait implemented by module resolution servicesresolve, resolve_ast
plugin::PluginFunctiontrait implemented by plugin functionscall, is_method_call, is_variadic, clone_boxed, input_names, input_types, return_type

Register a Rust Function for Use in Rhai Scripts

Rhai’s scripting engine is very lightweight. It gets most of its abilities from functions.

To call these functions, they need to be registered via Engine::register_fn or Engine::register_result_fn (see fallible functions).

Tip: Function overloading

Functions registered with the Engine can be overloaded as long as the signature is unique, i.e. different functions can have the same name as long as their parameters are of different types or different numbers (i.e. arity).

New definitions overwrite previous definitions of the same name, same arity and same parameter types.

use rhai::{Dynamic, Engine, ImmutableString};

// Normal function that returns a standard type
// Remember to use 'ImmutableString' and not 'String'
fn add_len(x: i64, s: ImmutableString) -> i64 {
    x + s.len()
}
// Alternatively, '&str' maps directly to 'ImmutableString'
fn add_len_count(x: i64, s: &str, c: i64) -> i64 {
    x + s.len() * c
}
// Function that returns a 'Dynamic' value
fn get_any_value() -> Dynamic {
    42_i64.into()                       // standard types can use '.into()'
}

let mut engine = Engine::new();

engine.register_fn("add", add_len)
      .register_fn("add", add_len_count)
      .register_fn("add", get_any_value)
      .register_fn("inc", |x: i64| {    // closure is also OK!
          x + 1
      })
      .register_fn("log", |label: &str, x: i64| {
          println!("{} = {}", label, x);
      });

let result = engine.eval::<i64>(r#"add(40, "xx")"#)?;

println!("Answer: {}", result);         // prints 42

let result = engine.eval::<i64>(r#"add(40, "x", 2)"#)?;

println!("Answer: {}", result);         // prints 42

let result = engine.eval::<i64>("add()")?;

println!("Answer: {}", result);         // prints 42

let result = engine.eval::<i64>("inc(41)")?;

println!("Answer: {}", result);         // prints 42

engine.run(r#"log("value", 42)"#)?;     // prints "value = 42"

Tip: Use closures

It is common for short functions to be registered via a closure.

engine.register_fn("foo", |x: i64, y: bool| ...);

Tip: Create a Dynamic

To create a Dynamic value, use Dynamic::from.

Standard types in Rhai can also use .into().

use rhai::Dynamic;

let obj = TestStruct::new();

let x = Dynamic::from(obj);

// '.into()' works for standard types

let x = 42_i64.into();

let y = "hello!".into();

String Parameters in Rust Functions

Warning: Avoid String parameters

As much as possible, avoid using String parameters in functions.

Each String argument is cloned during every single call to that function – and the copy immediately thrown away right after the call.

Needless to say, it is extremely inefficient to use String parameters.

&str Maps to ImmutableString

Common mistake

A common mistake made by novice Rhai users is to register functions with String parameters.

Rust functions accepting parameters of String should use &str instead because it maps directly to ImmutableString which is the type that Rhai uses to represent strings internally.

The parameter type String involves always converting an ImmutableString into a String which mandates cloning it.

Using ImmutableString or &str is much more efficient.

fn get_len1(s: String) -> i64 {             // BAD!!! Very inefficient!!!
    s.len() as i64
}
fn get_len2(s: &str) -> i64 {               // this is better
    s.len() as i64
}
fn get_len3(s: ImmutableString) -> i64 {    // the above is equivalent to this
    s.len() as i64
}

engine.register_fn("len1", get_len1)
      .register_fn("len2", get_len2)
      .register_fn("len3", get_len3);

let len = engine.eval::<i64>("len1(x)")?;   // 'x' cloned, very inefficient!!!
let len = engine.eval::<i64>("len2(x)")?;   // 'x' is shared
let len = engine.eval::<i64>("len3(x)")?;   // 'x' is shared

&mut String does not work – use &mut ImmutableString instead

A function with the first parameter being &mut String does not match a string argument passed to it, which has type ImmutableString.

In fact, &mut String is treated as an opaque custom type.

fn bad(s: &mut String) { ... }              // '&mut String' will not match string values

fn good(s: &mut ImmutableString) { ... }

engine.register_fn("bad", bad)
      .register_fn("good", good);

engine.eval(r#"bad("hello")"#)?;            // <- error: function 'bad (string)' not found

engine.eval(r#"good("hello")"#)?;           // <- this one works

Register a Generic Rust Function

No monomorphization

Due to its dynamic nature, Rhai cannot monomorphize generic functions automatically.

Monomorphization of generic functions must be performed manually.

Rust generic functions can be used in Rhai, but separate instances for each concrete type must be registered separately.

This essentially overloads the function with different parameter types as Rhai does not natively support generics but Rhai does support function overloading.

The example below shows how to register multiple functions (or, in this case, multiple overloaded versions of the same function) under the same name.

use std::fmt::Display;

use rhai::Engine;

fn show_it<T: Display>(x: &mut T) {
    println!("put up a good show: {}!", x)
}

let mut engine = Engine::new();

engine.register_fn("print", show_it::<i64>)
      .register_fn("print", show_it::<bool>)
      .register_fn("print", show_it::<ImmutableString>);

Register a Fallible Rust Function

If a function is fallible (i.e. it returns a Result<_, _>), it can be registered with via Engine::register_result_fn.

Return type

The function must return Result<T, Box<EvalAltResult>> where T is any clonable type.

use rhai::{Engine, EvalAltResult, Position};

// Function that may fail - the error type must be 'Box<EvalAltResult>'
fn safe_divide(x: i64, y: i64) -> Result<i64, Box<EvalAltResult>> {
    if y == 0 {
        // Return an error if y is zero
        Err("Division by zero!".into())         // shortcut to create Box<EvalAltResult::ErrorRuntime>
    } else {
        Ok(x / y)
    }
}

let mut engine = Engine::new();

// Fallible functions that return Result values must use register_result_fn()
engine.register_result_fn("divide", safe_divide);

if let Err(error) = engine.eval::<i64>("divide(40, 0)") {
    println!("Error: {:?}", *error);         // prints ErrorRuntime("Division by zero detected!", (1, 1)")
}

Tip: Create a Box<EvalAltResult>

Box<EvalAltResult> implements From<&str> and From<String> etc. and the error text gets converted into Box<EvalAltResult::ErrorRuntime>.

The error values are Box-ed in order to reduce memory footprint of the error path, which should be hit rarely.

Dynamic Parameters in Rust Functions

It is possible for Rust functions to contain parameters of type Dynamic.

A Dynamic value can hold any clonable type.

Trivia

The push method of an array is implemented as follows (minus code for safety protection against over-sized arrays), allowing the function to be called with all item types.

// 'item: Dynamic' matches all data types
fn push(array: &mut Array, item: Dynamic) {
    array.push(item);
}

Precedence

Any parameter in a registered Rust function with a specific type has higher precedence over Dynamic, so it is important to understand which version of a function will be used.

Parameter matching starts from the left to the right. Candidate functions will be matched in order of parameter types.

Therefore, always leave Dynamic parameters (up to 16, see below) as far to the right as possible.

use rhai::{Engine, Dynamic};

// Different versions of the same function 'foo'
// will be matched in the following order.

fn foo1(x: i64, y: &str, z: bool) { }

fn foo2(x: i64, y: &str, z: Dynamic) { }

fn foo3(x: i64, y: Dynamic, z: bool) { }

fn foo4(x: i64, y: Dynamic, z: Dynamic) { }

fn foo5(x: Dynamic, y: &str, z: bool) { }

fn foo6(x: Dynamic, y: &str, z: Dynamic) { }

fn foo7(x: Dynamic, y: Dynamic, z: bool) { }

fn foo8(x: Dynamic, y: Dynamic, z: Dynamic) { }

let mut engine = Engine::new();

// Register all functions under the same name (order does not matter)

engine.register_fn("foo", foo5)
      .register_fn("foo", foo7)
      .register_fn("foo", foo2)
      .register_fn("foo", foo8)
      .register_fn("foo", foo1)
      .register_fn("foo", foo3)
      .register_fn("foo", foo6)
      .register_fn("foo", foo4);

Only the right-most 16 parameters can be Dynamic

The number of parameter permutations goes up exponentially, and therefore there is a realistic limit of 16 parameters allowed to be Dynamic, counting from the right-most side.

For example, Rhai will not find the following function – Oh! and those 16 parameters to the right certainly have nothing to do with it!

// The 'd' parameter counts 17th from the right!
fn weird(a: i64, d: Dynamic, x1: i64, x2: i64, x3: i64, x4: i64,
                             x5: i64, x6: i64, x7: i64, x8: i64,
                             x9: i64, x10: i64, x11: i64, x12: i64,
                             x13: i64, x14: i64, x15: i64, x16: i64) {

    // ... do something unspeakably evil with all those parameters ...
}

TL;DR

How is this implemented?

Hash lookup

Since functions in Rhai can be overloaded, Rhai uses a single hash number to quickly lookup the actual function, based on argument types.

For each function call, a hash is calculated from:

  1. the function’s namespace, if any,
  2. the function’s name,
  3. number of arguments (its arity),
  4. unique ID of the type of each argument, if any.

The correct function is then obtained via a simple hash lookup.

Limitations

This method is fast, but at the expense of flexibility (such as multiple argument types that must map to a single version). That is because each type has a different ID, and thus they calculate to different hash numbers.

This is the reason why generic functions must be expanded into concrete types.

The type ID of Dynamic is different from any other type, but it must match all types seamlessly. Needless to say, this creates a slight problem.

Trying combinations

If the combined hash calculated from the actual argument type ID’s is not found, then the Engine calculates hashes for different combinations of argument types and Dynamic, systematically replacing different arguments with Dynamic starting from the right-most parameter.

Thus, assuming a three-argument function call:

foo(42, "hello", true);

The following hashes will be calculated, in order. They will be all different.

OrderHash calculation method
1foo + 3 + i64 + string + bool
2foo + 3 + i64 + string + Dynamic
3foo + 3 + i64 + Dynamic + bool
4foo + 3 + i64 + Dynamic + Dynamic
5foo + 3 + Dynamic + string + bool
6foo + 3 + Dynamic + string + Dynamic
7foo + 3 + Dynamic + Dynamic + bool
8foo + 3 + Dynamic + Dynamic + Dynamic

Therefore, the version with all the correct parameter types will always be found first if it exists.

At soon as a hash is found, the process stops.

Otherwise, it goes on for up to 16 arguments, or at most 65,536 tries. That’s where the 16 parameters limit comes from.

What?! It calculates 65,536 hashes for each function call???!!!

Of course not. Don’t be silly.

Not every function has 16 parameters

Studies have repeatedly shown that most functions accept few parameters, with the mean between 2-3 parameters per function. Functions with more than 5 parameters are rare in normal code bases. If at all, they are usually closures that capture lots of external variables, bumping up the parameter count; but closures are always script-defined and thus all parameters are already Dynamic.

In fact, you have a bigger problem if you write such a function that you need to call regularly. It would be far more efficient to group those parameters into object maps.

Caching to the rescue

Function hashes are cached, so this process only happens once, and only up to the number of rounds for the correct function to be found.

If not, then yes, it will calculate up to 2n hashes where n is the number of arguments (up to 16). But again, this will only be done once for that particular combination of argument types.

But then… beware module functions

The functions resolution cache resides only in the global namespace. This is a limitation.

Therefore, calls to functions in an imported module (i.e. qualified with a namespace path) do not have the benefit of a cache.

Thus, up to 2n hashes are calculated during every function call. This is unlikely to cause a performance issue since most functions accept only a few parameters.

NativeCallContext

If the first parameter of a function is of type rhai::NativeCallContext, then it is treated specially by the Engine.

NativeCallContext is a type that encapsulates the current call context of a Rust function call and exposes the following.

MethodReturn typeDescription
engine()&Enginethe current Engine, with all configurations and settings.
This is sometimes useful for calling a script-defined function within the same evaluation context using Engine::call_fn, or calling a function pointer.
fn_name()&strname of the function called (useful when the same Rust function is mapped to multiple Rhai-callable function names)
source()Option<&str>reference to the current source, if any
position()Positionposition of the function call
call_level()usizethe current nesting level of function calls
tag()&Dynamicreference to the custom state that is persistent during the current run
iter_imports()impl Iterator<Item = (&str,&Module)>iterator of the current stack of modules imported via import statements, in reverse order (i.e. later modules come first)
global_runtime_state()&GlobalRuntimeStatereference to the current global runtime state (including the stack of modules imported via import statements); requires the internals feature
iter_namespaces()impl Iterator<Item =&Module>iterator of the namespaces (as modules) containing all script-defined functions, in reverse order (i.e. later modules come first)
namespaces()&[&Module]reference to the namespaces (as modules) containing all script-defined functions; requires the internals feature
call_fn()Result<T, Box<EvalAltResult>>call a function with the supplied arguments, casting the result into the required type
call_fn_raw()Result<Dynamic, Box<EvalAltResult>>call a function with the supplied arguments; this is an advanced method

Example – Implement Safety Checks

The native call context is useful for protecting a function from malicious scripts.

use rhai::{Array, NativeCallContext, EvalAltResult, Position};

// This function builds an array of arbitrary size, but is protected
// against attacks by first checking with the allowed limit set
// into the 'Engine'.
pub fn grow(context: NativeCallContext, size: i64) -> Result<Array, Box<EvalAltResult>>
{
    // Make sure the function does not generate a
    // data structure larger than the allowed limit
    // for the Engine!
    if size as usize > context.engine().max_array_size() {
        return Err(EvalAltResult::ErrorDataTooLarge(
            "Size to grow".to_string(),
            context.engine().max_array_size(),
            size as usize,
            context.position(),
        ).into());
    }

    let array = Array::new();

    for x in 0..size {
        array.push(x.into());
    }

    OK(array)
}

Example – Call a Function Within a Function

The native call context can be used to call a function within the current evaluation via call_fn.

use rhai::{Engine, NativeCallContext};

let mut engine = Engine::new();

// A function expecting a callback in form of a function pointer.
fn super_call(context: NativeCallContext, value: i64) -> Result<i64, Box<EvalAltResult>>
{
    // Use 'call_fn' to call a function within the current evaluation!
    context.call_fn("double", (value,))
    //                        ^^^^^^^^ arguments passed in tuple
}

engine.register_result_fn("super_call", super_call);

Example – Implement a Callback

The native call context can be used to call a function pointer or closure that has been passed as a parameter to the function (via FnPtr::call_with_context), thereby implementing a callback.

use rhai::{Dynamic, FnPtr, NativeCallContext, EvalAltResult};

pub fn greet(context: NativeCallContext, callback: FnPtr) -> Result<String, Box<EvalAltResult>>
{
    // Call the callback closure with the current evaluation context!
    let name = callback.call_within_context(&context, ())?;
    Ok(format!("hello, {}!", name))
}

Override a Built-in Function

Any similarly-named function defined in a script overrides any built-in or registered native Rust function of the same name and number of parameters.

// Override the built-in function 'to_float' when called as a method
fn to_float() {
    print(`Ha! Gotcha! ${this}`);
    42.0
}

let x = 123.to_float();

print(x);       // what happens?

Tip: Monkey patching Rhai

Most of Rhai’s built-in functionality resides in registered functions.

If you dislike any built-in function, simply provide your own implementation to override the built-in version.

The ability to modify the operating environment dynamically at runtime is called “monkey patching.” It is rarely recommended, but if you need it, you need it bad.

In other words, do it only when all else fails. Do not monkey patch Rhai simply because you can.

Search order for functions

Rhai searches for the correct implementation of a function in the following order:

  1. Rhai script-defined functions,

  2. Native Rust functions registered directly via the Engine::register_XXX API,

  3. Native Rust functions in packages that have been loaded via Engine::register_global_module,

  4. Native Rust or Rhai script-defined functions in imported modules that are exposed to the global namespace (e.g. via the #[rhai_fn(global)] attribute in a plugin module),

  5. Native Rust or Rhai script-defined functions in modules loaded via Engine::register_static_module that are exposed to the global namespace (e.g. via the #[rhai_fn(global)] attribute in a plugin module),

  6. Built-in functions.

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: i64 = engine.call_fn(&mut scope, &ast, "hello", ( "abc", 123_i64 ) )?;
//          ^^^                                             ^^^^^^^^^^^^^^^^^^
//          return type must be specified                   put arguments in a tuple

let result: i64 = engine.call_fn(&mut scope, &ast, "hello", ( 123_i64, ) )?;
//                                                          ^^^^^^^^^^^^ tuple of one

let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )?;
//                                                          ^^ unit = tuple of zero

Default behavior

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, use Engine::call_fn_raw.

FuncArgs Trait

Note

Rhai implements FuncArgs for tuples 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 arguments to 'call_fn'!
let result: i64 = engine.call_fn(&mut scope, &ast, "hello", options)?;

Low-Level API – Engine::call_fn_raw

For more control, construct all arguments as Dynamic values and use Engine::call_fn_raw, passing it anything that implements AsMut<[Dynamic]> (such as a simple array or a Vec<Dynamic>):

let result = engine.call_fn_raw(
                &mut scope,         // scope to use
                &ast,               // AST containing the functions
                false,              // false = do not evaluate the AST
                false,              // false = do not rewind the scope (i.e. keep new variables)
                "hello",            // function entry-point
                None,               // 'this' pointer, if any
                [ "abc".into(), 123_i64.into() ]    // arguments
             )?;

Engine::call_fn_raw extends control to the following:

  • Whether to skip evaluation of the AST before calling the target function
  • Whether to rewind the custom Scope at the end of the function call
  • Whether to bind the this pointer to a specific value

Skip evaluation of the AST

By default, the AST is evaluated before calling the target function. A parameter can be passed to skip 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 keeps the Scope from being continuously polluted by new variables and is usually the expected intuitive behavior.

A parameter can be passed to keep 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.

Warning: new variables persist in 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 │
└──────┘

engine.call_fn_raw(&mut scope, &ast, true, false, "initialize", None, [])?;
//                                   ^^^^ evaluate AST before call
//                                         ^^^^^ do not rewind scope

// At this point, 'scope' contains these variables: 'foo', 'x', 'y'

Bind the this pointer

Note

Engine::call_fn cannot call functions in method-call style.

Engine::call_fn_raw 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();

engine.call_fn_raw(
            &mut scope,
            &ast,
            false,
            false,
            "action",
            Some(&mut value),       // binding the 'this' pointer
            [ 41_i64.into() ]
       )?;

assert_eq!(value.as_int()?, 42);

Create a Rust Closure from a Rhai Function

Tip

Very useful as callback functions!

It is possible to further encapsulate a script in Rust such that it becomes a normal Rust closure.

Creating them is accomplished via the Func trait which contains create_from_script (as well as its companion method create_from_ast).

use rhai::{Engine, Func};       // use 'Func' for 'create_from_script'

let engine = Engine::new();     // create a new 'Engine' just for this

let script = "fn calc(x, y) { x + y.len < 42 }";

// Func takes two type parameters:
//   1) a tuple made up of the types of the script function's parameters
//   2) the return type of the script function
//
// 'func' will have type Box<dyn Fn(i64, &str) -> Result<bool, Box<EvalAltResult>>> and is callable!
let func = Func::<(i64, &str), bool>::create_from_script(
//                ^^^^^^^^^^^ function parameter types in tuple

                engine,         // the 'Engine' is consumed into the closure
                script,         // the script, notice number of parameters must match
                "calc"          // the entry-point function name
)?;

func(123, "hello")? == false;   // call the closure

schedule_callback(func);        // pass it as a callback to another function

// Although there is nothing you can't do by manually writing out the closure yourself...
let engine = Engine::new();
let ast = engine.compile(script)?;
schedule_callback(Box::new(move |x: i64, y: String| -> Result<bool, Box<EvalAltResult>> {
    engine.call_fn(&mut Scope::new(), &ast, "calc", (x, y))
}));

Operator Overloading

In Rhai, a lot of functionalities are actually implemented as functions, including basic operations such as arithmetic calculations.

For example, in the expression “a + b”, the + operator actually calls a function named “+”!

let x = a + b;

let x = +(a, b);        // <- the above is equivalent to this function call

Similarly, comparison operators including ==, != etc. are all implemented as functions, with the stark exception of &&, || and ??.

&&, || and ?? cannot be overloaded

Because they short-circuit, &&, || and ?? are handled specially and not via a function.

Overriding them has no effect at all.

Overload Operator via Rust Function

Operator functions cannot be defined in script because operators are usually not valid function names.

However, operator functions can be registered via Engine::register_fn.

When a custom operator function is registered with the same name as an operator, it overrides the built-in version.

use rhai::{Engine, EvalAltResult};

let mut engine = Engine::new();

fn strange_add(a: i64, b: i64) -> i64 { (a + b) * 42 }

engine.register_fn("+", strange_add);               // overload '+' operator for two integers!

let result: i64 = engine.eval("1 + 0");             // the overloading version is used

result == 42;

let result: f64 = engine.eval("1.0 + 0.0");         // '+' operator for two floats not overloaded

result == 1.0;

fn mixed_add(a: i64, b: bool) -> f64 { a + if b { 42 } else { 99 } }

engine.register_fn("+", mixed_add);                 // register '+' operator for an integer and a bool

let result: i64 = engine.eval("1 + true");          // <- normally an error...

result == 43;                                       //    ... but not now

Considerations

Use operator overloading for custom types only.

Be very careful when overriding built-in operators because users expect standard operators to behave in a consistent and predictable manner, and will be annoyed if an expression involving + turns into a subtraction, for example. You may think it is amusing, but users who need to get things done won’t.

Operator overloading also impacts script optimization when using OptimizationLevel::Full. See the section on script optimization for more details.

Working with Any Rust Type

Tip: Shared types

The only requirement of a type to work with Rhai is Clone.

Therefore, it is extremely easy to use Rhai with data types such as Rc<...>, Arc<...>, Rc<RefCell<...>>, Arc<Mutex<...>> etc.

Under sync

If the sync feature is used, a custom type must also be Send + Sync.

Rhai works seamlessly with any Rust type, as long as it implements Clone as this allows the Engine to pass by value.

A type that is not one of the standard types is termed a “custom type”.

Custom types can have the following:

Free Typing

Why “Custom”?

Rhai internally supports a number of standard data types (see this list).

Any type outside of the list is considered custom.

Rhai works seamlessly with any Rust type.

A custom type is stored in Rhai as a Rust trait object (specifically, a dyn rhai::Variant), with no restrictions other than being Clone (plus Send + Sync under the sync feature).

The type literally does not have any prerequisite other than being Clone.

It does not need to implement any other trait or use any custom #[derive].

This allows Rhai to be integrated into an existing Rust code base with as little plumbing as possible, usually silently and seamlessly.

External types that are not defined within the same crate (and thus cannot implement special Rhai traits or use special #[derive]) can also be used easily with Rhai.

Support for custom types can be turned off via the no_object feature.

Custom types are slower

Custom types run slower than built-in types due to an additional level of indirection, but for all other purposes there is no difference.

Register a Custom Type

The custom type needs to be registered using Engine::register_type or Engine::register_type_with_name.

use rhai::{Engine, EvalAltResult};

#[derive(Debug, Clone)]
struct TestStruct {
    field: i64
}

let mut engine = Engine::new();

// Register custom type with friendly  name
engine.register_type_with_name::<TestStruct>("TestStruct");

Tip: Working with enums

It is also possible to use Rust enums with Rhai.

See the pattern Working with Enums for more details.

type_of() a Custom Type

type_of() works fine with custom types and returns the name of the type.

If Engine::register_type_with_name is used to register the custom type with a special “pretty-print” name, type_of() will return that name instead.

engine.register_type::<TestStruct1>()
      .register_fn("new_ts1", TestStruct1::new)
      .register_type_with_name::<TestStruct2>("TestStruct")
      .register_fn("new_ts2", TestStruct2::new);

let ts1_type = engine.eval::<String>("let x = new_ts1(); x.type_of()")?;
let ts2_type = engine.eval::<String>("let x = new_ts2(); x.type_of()")?;

println!("{}", ts1_type);               // prints 'path::to::TestStruct'
println!("{}", ts1_type);               // prints 'TestStruct'

== Operator

Many standard functions (e.g. filtering, searching and sorting) expect a custom type to be comparable, meaning that the == operator must be registered for the custom type.

For example, in order to use the in operator with a custom type for an array, the == operator is used to check whether two values are the same.

// Assume 'TestStruct' implements `PartialEq`
engine.register_fn("==",
    |item1: &mut TestStruct, item2: TestStruct| item1 == &item2
);

// Then this works in Rhai:
let item = new_ts();        // construct a new 'TestStruct'
item in array;              // 'in' operator uses '=='

Methods

Methods of custom types are registered via Engine::register_fn.

use rhai::{Engine, EvalAltResult};

#[derive(Debug, Clone)]
struct TestStruct {
    field: i64
}

impl TestStruct {
    fn new() -> Self {
        Self { field: 1 }
    }

    fn update(&mut self, x: i64) {      // methods take &mut as first parameter
        self.field += x;
    }
}

let mut engine = Engine::new();

// Most Engine API's can be chained up.
engine.register_type_with_name::<TestStruct>("TestStruct")
      .register_fn("new_ts", TestStruct::new)
      .register_fn("update", TestStruct::update);

// Cast result back to custom type.
let result = engine.eval::<TestStruct>(
"
    let x = new_ts();                   // calls 'TestStruct::new'
    x.update(41);                       // calls 'TestStruct::update'
    x                                   // 'x' holds a 'TestStruct'
")?;

println!("result: {}", result.field);   // prints 42

First Parameter Must be &mut

Methods of custom types take a &mut first parameter to that type, so that invoking methods can always update it.

All other parameters in Rhai are passed by value (i.e. clones).

No support for references

Rhai does NOT support normal references (i.e. &T) as parameters. All references must be mutable (i.e. &mut T).

Custom Type Property Getters and Setters

Cannot override object maps

Property getters and setters are intended for custom types.

Any getter or setter function registered for object maps is simply ignored.

Get/set syntax on object maps is interpreted as access to properties.

A custom type can also expose properties by registering get and/or set functions.

Properties can be accessed in a Rust-like syntax:

object . property

object . property = value ;

The Elvis operator can be used to short-circuit processing if the object itself is ():

// returns () if object is ()
object ?. property

// no action if object is ()
object ?. property = value ;

Property getter and setter functions are called behind the scene. They each take a &mut reference to the first parameter.

Getters and setters are disabled under the no_object feature.

Engine APIFunction signature(s)
(T: Clone = custom type,
V: Clone = data type)
Can mutate T?
register_getFn(&mut T) -> Vyes, but not advised
register_setFn(&mut T, V)yes
register_get_setgetter: Fn(&mut T) -> V
setter: Fn(&mut T, V)
yes, but not advised in getter
register_get_resultFn(&mut T) -> Result<V, Box<EvalAltResult>>yes, but not advised
register_set_resultFn(&mut T, V) -> Result<(), Box<EvalAltResult>>yes

No support for references

Rhai does NOT support normal references (i.e. &T) as parameters. All references must be mutable (i.e. &mut T).

Getters must be pure

By convention, property getters are assumed to be pure, meaning that they are not supposed to mutate the custom type, although there is nothing that prevents this mutation in Rust.

Even though a property getter function also takes &mut as the first parameter, Rhai assumes that no data is changed when the function is called.

Examples

#[derive(Debug, Clone)]
struct TestStruct {
    field: String
}

impl TestStruct {
    // Remember &mut must be used even for getters.
    fn get_field(&mut self) -> String {
        // Property getters are assumed to be PURE, meaning they are
        // not supposed to mutate any data.
        self.field.clone()
    }

    fn set_field(&mut self, new_val: &str) {
        self.field = new_val.to_string();
    }

    fn new() -> Self {
        Self { field: "hello" }
    }
}

let mut engine = Engine::new();

engine.register_type::<TestStruct>()
      .register_get_set("xyz", TestStruct::get_field, TestStruct::set_field)
      .register_fn("new_ts", TestStruct::new);

let result = engine.eval::<String>(
r#"
    let a = new_ts();
    a.xyz = "42";
    a.xyz
"#)?;

println!("Answer: {}", result);                 // prints 42

Fallback to Indexer

Tip: Property bag

This feature makes it very easy for custom types to act as property bags (similar to an object map) which can add/remove properties at will.

If the getter/setter of a particular property is not defined, but an indexer is defined on the custom type with string index, then the corresponding indexer will be called with the name of the property as the index value.

In other words, indexers act as a fallback to property getters/setters.

a.foo           // if property getter for 'foo' doesn't exist...

a["foo"]        // an indexer (if any) is tried

Chaining Updates

It is possible to chain property accesses and/or indexing (via indexers) together to modify a particular property value at the end of the chain.

Rhai detects such modifications and updates the changed values all the way back up the chain.

In the end, the syntax works as expected by intuition, automatically and without special attention.

// Assume a deeply-nested object...
let root = get_new_container_object();

root.prop1.sub["hello"].list[0].value = 42;

// The above is equivalent to:

// First getting all the intermediate values...
let prop1_value = root.prop1;                   // via property getter
let sub_value = prop1_value.sub;                // via property getter
let sub_value_item = sub_value["hello"];        // via index getter
let list_value = sub_value_item.list;           // via property getter
let list_item = list_value[0];                  // via index getter

list_item.value = 42;       // modify property value deep down the chain

// Propagate the changes back up the chain...
list_value[0] = list_item;                      // via index setter
sub_value_item.list = list_value;               // via property setter
sub_value["hello"] = sub_value_item;            // via index setter
prop1_value.sub = sub_value;                    // via property setter
root.prop1 = prop1_value;                       // via property setter

// The below prints 42...
print(root.prop1.sub["hello"].list[0].value);

Custom Type Indexers

A custom type can also expose an indexer by registering an indexer function.

A custom type with an indexer function defined can use the bracket notation to get/set a property value at a particular index:

object [ index ]

object [ index ] = value ;

The Elvis notation is similar except that it returns () if the object itself is ().

// returns () if object is ()
object ?[ index ]

// no action if object is ()
object ?[ index ] = value ;

Like property getters/setters, indexers take a &mut reference to the first parameter.

They also take an additional parameter of any type that serves as the index within brackets.

Indexers are disabled when the no_index and no_object features are used together.

Engine APIFunction signature(s)
(T: Clone = custom type,
X: Clone = index type,
V: Clone = data type)
Can mutate T?
register_indexer_getFn(&mut T, X) -> Vyes, but not advised
register_indexer_setFn(&mut T, X, V)yes
register_indexer_get_setgetter: Fn(&mut T, X) -> V
setter: Fn(&mut T, X, V)
yes, but not advised in getter
register_indexer_get_result (fallible)Fn(&mut T, X) -> Result<V, Box<EvalAltResult>>yes, but not advised
register_indexer_set_result (fallible)Fn(&mut T, X, V) -> Result<(), Box<EvalAltResult>>yes

No support for references

Rhai does NOT support normal references (i.e. &T) as parameters. All references must be mutable (i.e. &mut T).

Getters must be pure

By convention, index getters are not supposed to mutate the custom type, although there is nothing that prevents this mutation.

Tip: EvalAltResult::ErrorIndexNotFound

For fallible indexers, it is customary to return EvalAltResult::ErrorIndexNotFound when called with an invalid index value.

Cannot Override Arrays, BLOB’s, Object Maps, Strings and Integers

Plugins

They can be defined in a plugin module, but will be ignored.

For efficiency reasons, indexers cannot be used to overload (i.e. override) built-in indexing operations for arrays, object maps, strings and integers (acting as bit-field operation).

The following types have built-in indexer implementations that are fast and efficient.

TypeIndex typeReturn typeDescription
ArrayINTDynamicaccess a particular element inside the array
BlobINTINTaccess a particular byte value inside the BLOB
MapImmutableString,
String, &str
Dynamicaccess a particular property inside the object map
ImmutableString,
String, &str
INTcharacteraccess a particular character inside the string
INTINTbooleanaccess a particular bit inside the integer number as a bit-field
INTrangeINTaccess a particular range of bits inside the integer number as a bit-field

Do not overload indexers for built-in standard types

In general, it is a bad idea to overload indexers for any of the standard types supported internally by Rhai, since built-in indexers may be added in future versions.

Examples

#[derive(Debug, Clone)]
struct TestStruct {
    fields: Vec<i64>
}

impl TestStruct {
    // Remember &mut must be used even for getters
    fn get_field(&mut self, index: String) -> i64 {
        self.fields[index.len()]
    }
    fn set_field(&mut self, index: String, value: i64) {
        self.fields[index.len()] = value
    }

    fn new() -> Self {
        Self { fields: vec![1, 2, 3, 4, 5] }
    }
}

let mut engine = Engine::new();

engine.register_type::<TestStruct>()
      .register_fn("new_ts", TestStruct::new)
      // Short-hand: .register_indexer_get_set(TestStruct::get_field, TestStruct::set_field);
      .register_indexer_get(TestStruct::get_field)
      .register_indexer_set(TestStruct::set_field);

let result = engine.eval::<i64>(
r#"
    let a = new_ts();
    a["xyz"] = 42;                  // these indexers use strings
    a["xyz"]                        // as the index type
"#)?;

println!("Answer: {}", result);     // prints 42

Convention for Negative Index

If the indexer takes a signed integer as an index (e.g. the standard INT type), care should be taken to handle negative values passed as the index.

It is a standard API convention for Rhai to assume that an index position counts backwards from the end if it is negative.

-1 as an index usually refers to the last item, -2 the second to last item, and so on.

Therefore, negative index values go from -1 (last item) to -length (first item).

A typical implementation for negative index values is:

// The following assumes:
//   'index' is 'INT', 'items: usize' is the number of elements
let actual_index = if index < 0 {
    index.checked_abs().map_or(0, |n| items - (n as usize).min(items))
} else {
    index as usize
};

The end of a data type can be interpreted creatively. For example, in an integer used as a bit-field, the start is the least-significant-bit (LSB) while the end is the most-significant-bit (MSB).

Convention for Range Index

Tip: Negative values

By convention, negative values are not interpreted specially in indexers for ranges.

It is very common for ranges to be used as indexer parameters via the types std::ops::Range<INT> (exclusive) and std::ops::RangeInclusive<INT> (inclusive).

One complication is that two versions of the same indexer must be defined to support exclusive and inclusive ranges respectively.

use std::ops::{Range, RangeInclusive};

let mut engine = Engine::new();

engine
    /// Version of indexer that accepts an exclusive range
    .register_indexer_get_set(
        |obj: &mut TestStruct, range: Range<i64>| -> bool { ... },
        |obj: &mut TestStruct, range: Range<i64>, value: bool| { ... },
    )
    /// Version of indexer that accepts an inclusive range
    .register_indexer_get_set(
        |obj: &mut TestStruct, range: RangeInclusive<i64>| -> bool { ... },
        |obj: &mut TestStruct, range: RangeInclusive<i64>, value: bool| { ... },
    );

engine.run(
"
    let obj = new_ts();

    let x = obj[0..12];             // use exclusive range

    obj[0..=11] = !x;               // use inclusive range
")?;

Indexer as Property Access Fallback

Tip: Property bag

Such an indexer allows easy creation of property bags (similar to object maps) which can dynamically add/remove properties.

An indexer taking a string index is a special case – it acts as a fallback to property getters/setters.

During a property access, if the appropriate property getter/setter is not defined, an indexer is called and passed the string name of the property.

This is also extremely useful as a short-hand for indexers, when the string keys conform to property name syntax.

// Assume 'obj' has an indexer defined with string parameters...

// Let's create a new key...
obj.hello_world = 42;

// The above is equivalent to this:
obj["hello_world"] = 42;

// You can write this...
let x = obj["hello_world"];

// but it is easier with this...
let x = obj.hello_world;

Caveat – reverse is NOT true

The reverse, however, is not true – when an indexer fails or doesn’t exist, the corresponding property getter/setter, if any, is not called.

type MyType = HashMap<String, i64>;

let mut engine = Engine::new();

// Define custom type, property getter and string indexers
engine.register_type::<MyType>()
      .register_fn("new_ts", || {
          let mut obj = MyType::new();
          obj.insert("foo".to_string(), 1);
          obj.insert("bar".to_string(), 42);
          obj.insert("baz".to_string(), 123);
          obj
      })
      // Property 'hello'
      .register_get("hello", |obj: &mut MyType| obj.len() as i64)
      // Index getter/setter
      .register_indexer_get_result(|obj: &mut MyType, prop: &str|
          obj.get(index).cloned().ok_or_else(|| "not found".into())
      ).register_indexer_set(|obj: &mut MyType, prop: &str, value: i64|
          obj.insert(prop.to_string(), value)
      );

engine.run("let ts = new_ts(); print(ts.foo);");
//                                   ^^^^^^
//                 Calls ts["foo"] - getter for 'foo' does not exist

engine.run("let ts = new_ts(); print(ts.bar);");
//                                   ^^^^^^
//                 Calls ts["bar"] - getter for 'bar' does not exist

engine.run("let ts = new_ts(); ts.baz = 999;");
//                             ^^^^^^^^^^^^
//                 Calls ts["baz"] = 999 - setter for 'baz' does not exist

engine.run(r#"let ts = new_ts(); print(ts["hello"]);"#);
//                                     ^^^^^^^^^^^
//                 Error: Property getter for 'hello' not a fallback for indexer

Call Method as Function

Method-Call Style vs. Function-Call Style

Method-call syntax

object . function ( parameter,, parameter)

Method-call style not supported under no_object

// Below is a syntax error under 'no_object'.
engine.run("let x = [42]; x.clear();")?;
                        // ^ cannot call method-style

Function-call syntax

function ( object, parameter,, parameter)

Equivalence

Note

This design is similar to Rust.

Internally, methods on a custom type is the same as a function taking a &mut first argument of the object’s type.

Therefore, methods and functions can be called interchangeably.

impl TestStruct {
    fn foo(&mut self) -> i64 {
        self.field
    }
}

engine.register_fn("foo", TestStruct::foo);

let result = engine.eval::<i64>(
"
    let x = new_ts();
    foo(x);                         // normal call to 'foo'
    x.foo()                         // 'foo' can also be called like a method on 'x'
")?;

println!("result: {}", result);     // prints 1

First &mut Parameter

The opposite direction also works — methods in a Rust custom type registered with the Engine can be called just like a regular function. In fact, like Rust, object methods are registered as regular functions in Rhai that take a first &mut parameter.

Unlike functions defined in script (for which all arguments are passed by value), native Rust functions may mutate the first &mut argument.

Sometimes, however, there are more subtle differences. Methods called in normal function-call style may end up not muting the object afterall — see the example below.

Custom types, properties, indexers and methods are disabled under the no_object feature.

let a = new_ts();   // constructor function
a.field = 500;      // property setter
a.update();         // method call, 'a' can be modified

update(a);          // <- this de-sugars to 'a.update()'
                    //    'a' can be modified and is not a copy

let array = [ a ];

update(array[0]);   // <- 'array[0]' is an expression returning a calculated value,
                    //    a transient (i.e. a copy), so this statement has no effect
                    //    except waste time cloning 'a'

array[0].update();  // <- call in method-call style will update 'a'

No support for references

Rhai does NOT support normal references (i.e. &T) as parameters. All references must be mutable (i.e. &mut T).

Number of Parameters in Methods

Native Rust methods registered with an Engine take one additional parameter more than an equivalent method coded in script, where the object is accessed via the this pointer instead.

The following table illustrates the differences:

Function typeNo. of parametersObject referenceFunction signature
Native RustN + 1first &mut T parameterFn(obj: &mut T, x: U, y: V)
Rhai scriptNthisFn(x: U, y: V)

&mut is Efficient, Except for &mut ImmutableString

Using a &mut first parameter is highly encouraged when using types that are expensive to clone, even when the intention is not to mutate that argument, because it avoids cloning that argument value.

Even when a function is never intended to be a method – for example an operator, it is still sometimes beneficial to make it method-like (i.e. with a first &mut parameter) if the first parameter is not modified.

For types that are expensive to clone (remember, all function calls are passed cloned copies of argument values), this may result in a significant performance boost.

For primary types that are cheap to clone (e.g. those that implement Copy), including ImmutableString, this is not necessary.

// This is a type that is very expensive to clone.
#[derive(Debug, Clone)]
struct VeryComplexType { ... }

// Calculate some value by adding 'VeryComplexType' with an integer number.
fn do_add(obj: &VeryComplexType, offset: i64) -> i64 {
    ...
}

engine.register_type::<VeryComplexType>()
      .register_fn("+", add_pure /* or  add_method*/);

// Very expensive to call, as the 'VeryComplexType' is cloned before each call.
fn add_pure(obj: VeryComplexType, offset: i64) -> i64 {
    do_add(obj, offset)
}

// Efficient to call, as only a reference to the 'VeryComplexType' is passed.
fn add_method(obj: &mut VeryComplexType, offset: i64) -> i64 {
    do_add(obj, offset)
}

Data Race Considerations

Data races

Data races are not possible in Rhai under the no_closure feature because no sharing ever occurs.

Because methods always take a mutable reference as the first argument, even it the value is never changed, care must be taken when using shared values with methods.

Usually data races are not possible in Rhai because, for each function call, there is ever only one value that is mutable – the first argument of a method. All other arguments are cloned.

It is possible, however, to create a data race with a shared value, when the same value is captured in a closure and then used again as the object of calling that closure!

let x = 20;

x.is_shared() == false;             // 'x' is not shared, so no data race is possible

let f = |a| this += x + a;          // 'x' is captured in this closure

x.is_shared() == true;              // now 'x' is shared

x.call(f, 2);                       // <- error: data race detected on 'x'

Custom Collection Types

Tip

Collections can also hold Dynamic values (e.g. like an array).

A collection type holds a… well… collection of items. It can be homogeneous (all items are the same type) or heterogeneous (items are of different types, use Dynamic to hold).

Because their only purpose for existence is to hold a number of items, collection types commonly register the following methods.

MethodDescription
len method and propertygets the total number of items in the collection
clearclears the collection
containschecks if a particular item exists in the collection
add, += operatoradds a particular item to the collection
remove, -= operatorremoves a particular item from the collection
merge or + operatormerges two collections, yielding a new collection with all items

Tip: Define type iterator

Collections are typically iterable.

It is customary to use Engine::register_iterator to allow iterating the collection if it implements IntoIterator.

Alternative, register a specific type iterator for the custom type.

Tip: Use a plugin module

A plugin module makes defining an entire API for a custom type a snap.

Example

type MyBag = HashSet<MyItem>;

engine
    .register_type_with_name::<MyBag>("MyBag")
    .register_iterator::<MyBag>()
    .register_fn("new_bag", || MyBag::new())
    .register_fn("len", |col: &mut MyBag| col.len() as i64)
    .register_get("len", |col: &mut MyBag| col.len() as i64)
    .register_fn("clear", |col: &mut MyBag| col.clear())
    .register_fn("contains", |col: &mut MyBag, item: i64| col.contains(&item))
    .register_fn("add", |col: &mut MyBag, item: MyItem| col.insert(item))
    .register_fn("+=", |col: &mut MyBag, item: MyItem| col.insert(item))
    .register_fn("remove", |col: &mut MyBag, item: MyItem| col.remove(&item))
    .register_fn("-=", |col: &mut MyBag, item: MyItem| col.remove(&item))
    .register_fn("+", |mut col1: MyBag, col2: MyBag| {
        col1.extend(col2.into_iter());
        col1
    });

Disable Custom Types

The no_object feature disables support for custom types including:

  • method-style function calls (e.g. obj.method()),

  • object maps and the Map type,

  • the register_get, register_get_result, register_set, register_set_result and register_get_set API’s for Engine

Printing for Custom Types

To use custom types for print and debug, or convert a custom type into a string, it is necessary that the following functions be registered (assuming the custom type is T: Display + Debug).

FunctionSignatureTypical implementationUsage
to_string|x: &mut T| -> Stringx.to_string()converts the custom type into a string
to_debug|x: &mut T| -> Stringformat!("{:?}", x)converts the custom type into a string in debug format

The following functions are implemented using to_string() or to_debug() by default, but can be overloaded with custom versions.

FunctionSignatureDefaultUsage
print|x: &mut T| -> Stringto_stringconverts the custom type into a string for the print statement
debug|x: &mut T| -> Stringto_debugconverts the custom type into a string for the debug statement
+ operator|s: &str, x: T| -> Stringto_stringconcatenates the custom type with another string
+ operator|x: &mut T, s: &str| -> Stringto_stringconcatenates another string with the custom type
+= operator|s: &mut ImmutableString, x: T|to_stringappends the custom type to an existing string

Modules

Rhai allows organizing functionalities (functions, both Rust-based and scripted, and variables) into independent modules.

A module has the type rhai::Module and holds a collection of functions, variables, type iterators and sub-modules.

It may contain entirely Rust functions, or it may encapsulate a Rhai script together with all the functions and variables defined by that script.

Other scripts then load this module and use the functions and variables exported.

Alternatively, modules can be registered directly into an Engine and made available to scripts, either globally or under individual static module namespaces.

Modules can be disabled via the no_module feature.

Usage Patterns

UsageAPILookupSub-modules?Variables?
Global moduleEngine:: register_global_modulesimple nameignoredyes
Static moduleEngine:: register_static_modulenamespace-qualified nameyesyes
Dynamic moduleimport statementnamespace-qualified nameyesyes

Create a Module in Rust

The Easy Way – Plugin

By far the simplest way to create a module is via a plugin module which converts a normal Rust module into a Rhai module via procedural macros.

The Hard Way – Module API

Manually creating a module is possible via the Module public API, which is volatile and may change from time to time.

Module public API

For the complete Module public API, refer to the documentation online.

Create a Module from an AST

Module::eval_ast_as_new

Encapsulated environment

Module::eval_ast_as_new encapsulates the entire AST into each function call, merging the module namespace with the global namespace.

Therefore, functions defined within the same module script can cross-call each other.

See also

See Export Variables, Functions and Sub-Modules from Script for details on how to prepare a Rhai script for this purpose as well as to control which functions/variables to export.

A module can be created from a single script (or pre-compiled AST) containing global variables, functions and sub-modules via Module::eval_ast_as_new.

When given an AST, it is first evaluated (usually to import modules and set up global constants used by functions), then the following items are exposed as members of the new module:

Examples

Don’t forget the export statement, otherwise there will be no variables exposed by the module other than non-private functions (unless that’s intentional).

use rhai::{Engine, Module};

let engine = Engine::new();

// Compile a script into an 'AST'
let ast = engine.compile(
r#"
    // Functions become module functions
    fn calc(x) {
        x + add_len(x, 1)       // functions within the same module
                                // can always cross-call each other!
    }
    fn add_len(x, y) {
        x + y.len
    }

    // Imported modules become sub-modules
    import "another module" as extra;

    // Variables defined at global level can become module variables
    const x = 123;
    let foo = 41;
    let hello;

    // Variable values become constant module variable values
    foo = calc(foo);
    hello = `hello, ${foo} worlds!`;

    // Finally, export the variables and modules
    export x as abc;            // aliased variable name
    export foo;
    export hello;
"#)?;

// Convert the 'AST' into a module, using the 'Engine' to evaluate it first
// A copy of the entire 'AST' is encapsulated into each function,
// allowing functions in the module script to cross-call each other.
let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;

// 'module' now contains:
//   - sub-module: 'extra'
//   - functions: 'calc', 'add_len'
//   - constants: 'abc' (renamed from 'x'), 'foo', 'hello'

Make a Module Available to Scripts

Use Case 1 – Make It Globally Available

Engine::register_global_module registers a shared module into the global namespace.

This is by far the easiest way to expose a module’s functionalities to Rhai.

Tip: No qualifiers

All functions, variables/constants and type iterators can be accessed without namespace qualifiers.

Warning

Sub-modules are ignored.

use rhai::{Engine, Module};

let mut module = Module::new();             // new module

// Use 'Module::set_native_fn' to add functions.
let hash = module.set_native_fn("inc", |x: i64| Ok(x + 1));

// Remember to update the parameter names/types and return type metadata
// when using the 'metadata' feature.
// 'Module::set_native_fn' by default does not set function metadata.
module.update_fn_metadata(hash, &["x: i64", "i64"]);

// Use 'Module::set_var' to add variables.
module.set_var("MYSTIC_NUMBER", 41_i64);

// Register the module into the global namespace of the Engine.
let mut engine = Engine::new();
engine.register_global_module(module.into());

// No need to import module...
engine.eval::<i64>("inc(MYSTIC_NUMBER)")? == 42;

Equivalent to Engine::register_XXX

Trivia

Engine::register_fn etc. are actually implemented by adding functions to an internal module!

Registering a module via Engine::register_global_module is essentially the same as calling Engine::register_fn (or any of the Engine::register_XXX API) individually on each top-level function within that module.

// The above is essentially the same as:
let mut engine = Engine::new();

engine.register_fn("inc", |x: i64| x + 1);

engine.eval::<i64>("inc(41)")? == 42;       // no need to import module

Use Case 2 – Make It a Static Namespace

Engine::register_static_module registers a module and under a specific module namespace.

use rhai::{Engine, Module};

let mut module = Module::new();             // new module

// Use 'Module::set_native_fn' to add functions.
let hash = module.set_native_fn("inc", |x: i64| Ok(x + 1));

// Remember to update the parameter names/types and return type metadata
// when using the 'metadata' feature.
// 'Module::set_native_fn' by default does not set function metadata.
module.update_fn_metadata(hash, &["x: i64", "i64"]);

// Use 'Module::set_var' to add variables.
module.set_var("MYSTIC_NUMBER", 41_i64);

// Register the module into the Engine as the static module namespace path
// 'services::calc'
let mut engine = Engine::new();
engine.register_static_module("services::calc", module.into());

// Refer to the 'services::calc' module...
engine.eval::<i64>("services::calc::inc(services::calc::MYSTIC_NUMBER)")? == 42;

Expose functions to the global namespace

Tip: Type iterators

Type iterators are special — they are always exposed to the global namespace.

The Module API can optionally expose functions to the global namespace by setting the namespace parameter to FnNamespace::Global.

This way, getters/setters and indexers for custom types can work as expected.

use rhai::{Engine, Module, FnNamespace};

let mut module = Module::new();             // new module

// Use 'Module::set_native_fn' to add functions.
let hash = module.set_native_fn("inc", |x: &mut i64| Ok(x + 1));

// Remember to update the parameter names/types and return type metadata
// when using the 'metadata' feature.
// 'Module::set_native_fn' by default does not set function metadata.
module.update_fn_metadata(hash, &["x: &mut i64", "i64"]);

// Expose method 'inc' to the global namespace (default is 'FnNamespace::Internal')
module.update_fn_namespace(hash, FnNamespace::Global);

// Use 'Module::set_var' to add variables.
module.set_var("MYSTIC_NUMBER", 41_i64);

// Register the module into the Engine as a static module namespace 'calc'
let mut engine = Engine::new();
engine.register_static_module("calc", module.into());

// 'inc' works when qualified by the namespace
engine.eval::<i64>("calc::inc(calc::MYSTIC_NUMBER)")? == 42;

// 'inc' also works without a namespace qualifier
// because it is exposed to the global namespace
engine.eval::<i64>("let x = calc::MYSTIC_NUMBER; x.inc()")? == 42;
engine.eval::<i64>("let x = calc::MYSTIC_NUMBER; inc(x)")? == 42;

Use Case 3 – Make It Dynamically Loadable

In order to dynamically load a custom module, there must be a module resolver which serves the module when loaded via import statements.

The easiest way is to use, for example, the StaticModuleResolver to hold such a custom module.

use rhai::{Engine, Scope, Module};
use rhai::module_resolvers::StaticModuleResolver;

let mut module = Module::new();             // new module
module.set_var("answer", 41_i64);           // variable 'answer' under module
module.set_native_fn("inc", |x: i64| {      // use 'Module::set_native_fn' to add functions
    Ok(x + 1)
});

// Create the module resolver
let mut resolver = StaticModuleResolver::new();

// Add the module into the module resolver under the name 'question'
// They module can then be accessed via: 'import "question" as q;'
resolver.insert("question", module);

// Set the module resolver into the 'Engine'
let mut engine = Engine::new();
engine.set_module_resolver(resolver);

// Use namespace-qualified variables
engine.eval::<i64>(
r#"
    import "question" as q;
    q::answer + 1
"#)? == 42;

// Call namespace-qualified functions
engine.eval::<i64>(
r#"
    import "question" as q;
    q::inc(q::answer)
"#)? == 42;

Module Resolvers

import

See the section on Importing Modules for more details.

When encountering an import statement, Rhai attempts to resolve the module based on the path string.

Module Resolvers are service types that implement the ModuleResolver trait.

Set into Engine

An Engine’s module resolver is set via a call to Engine::set_module_resolver:

use rhai::module_resolvers::{DummyModuleResolver, StaticModuleResolver};

// Create a module resolver
let resolver = StaticModuleResolver::new();

// Register functions into 'resolver'...

// Use the module resolver
engine.set_module_resolver(resolver);

// Effectively disable 'import' statements by setting module resolver to
// the 'DummyModuleResolver' which acts as... well... a dummy.
engine.set_module_resolver(DummyModuleResolver::new());

Built-in Module Resolvers

There are a number of standard resolvers built into Rhai, the default being the FileModuleResolver which simply loads a script file based on the path (with .rhai extension attached) and execute it to form a module.

Built-in module resolvers are grouped under the rhai::module_resolvers module namespace.

DummyResolversCollection (default for no-std)

This module resolver acts as a dummy and fails all module resolution calls.

FileModuleResolver (normal default)

The default module resolution service, not available for no_std or WASM builds. Loads a script file (based off the current directory or a specified one) with .rhai extension.

Function namespace

All functions in the global namespace, plus all those defined in the same module, are merged into a unified namespace.

All modules imported at global level via import statements become sub-modules, which are also available to functions defined within the same script file.

Base directory

Tip: Default

If the base directory is not set, then relative paths are based off the directory of the loading script.

This allows scripts to simply cross-load each other.

Relative paths are resolved relative to a root directory, which is usually the base directory.

The base directory can be set via FileModuleResolver::new_with_path or FileModuleResolver::set_base_path.

Custom Scope

Tip

This Scope can conveniently hold global constants etc.

The set_scope method adds an optional Scope which will be used to optimize module scripts.

Caching

Tip: Enable/disable caching

Use enable_cache to enable/disable the cache.

By default, modules are also cached so a script file is only evaluated once, even when repeatedly imported.

Unix Shebangs

On Unix-like systems, the shebang (#!) is used at the very beginning of a script file to mark a script with an interpreter (for Rhai this would be rhai-run).

If a script file starts with #!, the entire first line is skipped. Because of this, Rhai scripts with shebangs at the beginning need no special processing.

#!/home/to/me/bin/rhai-run

// This is a Rhai script

let answer = 42;
print(`The answer is: ${answer}`);

Example

┌────────────────┐
│ my_module.rhai │
└────────────────┘

// This function overrides any in the main script.
private fn inner_message() { "hello! from module!" }

fn greet() {
    print(inner_message());     // call function in module script
}

fn greet_main() {
    print(main_message());      // call function not in module script
}


┌───────────┐
│ main.rhai │
└───────────┘

// This function is overridden by the module script.
fn inner_message() { "hi! from main!" }

// This function is found by the module script.
fn main_message() { "main here!" }

import "my_module" as m;

m::greet();                     // prints "hello! from module!"

m::greet_main();                // prints "main here!"

Simulate virtual functions

When calling a namespace-qualified function defined within a module, other functions defined within the same module script override any similar-named functions (with the same number of parameters) defined in the global namespace. This is to ensure that a module acts as a self-contained unit and functions defined in the calling script do not override module code.

In some situations, however, it is actually beneficial to do it in reverse: have module code call functions defined in the calling script (i.e. in the global namespace) if they exist, and only call those defined in the module script if none are found.

One such situation is the need to provide a default implementation to a simulated virtual function:

┌────────────────┐
│ my_module.rhai │
└────────────────┘

// Do not do this (it will override the main script):
// fn message() { "hello! from module!" }

// This function acts as the default implementation.
private fn default_message() { "hello! from module!" }

// This function depends on a 'virtual' function 'message'
// which is not defined in the module script.
fn greet() {
    if is_def_fn("message", 0) {    // 'is_def_fn' detects if 'message' is defined.
        print(message());
    } else {
        print(default_message());
    }
}


┌───────────┐
│ main.rhai │
└───────────┘

// The main script defines 'message' which is needed by the module script.
fn message() { "hi! from main!" }

import "my_module" as m;

m::greet();                         // prints "hi! from main!"


┌────────────┐
│ main2.rhai │
└────────────┘

// The main script does not define 'message' which is needed by the module script.

import "my_module" as m;

m::greet();                         // prints "hello! from module!"

StaticModuleResolver

Tip: Typical usage

StaticModuleResolver is often used in no_std or embedded environments without a file system.

Loads modules that are statically added.

Functions are searched in the global namespace by default.

use rhai::{Module, module_resolvers::StaticModuleResolver};

let module: Module = create_a_module();

let mut resolver = StaticModuleResolver::new();
resolver.insert("my_module", module);

ModuleResolversCollection

A collection of module resolvers.

Modules are resolved from each resolver in sequential order.

This is useful when multiple types of modules are needed simultaneously.

Implement a Custom Module Resolver

For many applications in which Rhai is embedded, it is necessary to customize the way that modules are resolved. For instance, modules may need to be loaded from script texts stored in a database, not in the file system.

A module resolver must implement the ModuleResolver trait, which contains only one required function: resolve.

When Rhai prepares to load a module, ModuleResolver::resolve is called with the name of the module path (i.e. the path specified in the import statement).

Success

Upon success, it should return a shared module wrapped by Rc (or Arc under sync).

The module resolver should call Module::build_index on the target module before returning it.

  • This method flattens the entire module tree and indexes it for fast function name resolution.
  • If the module is already indexed, calling this method has no effect.

Failure

  • If the path does not resolve to a valid module, return EvalAltResult::ErrorModuleNotFound.

  • If the module failed to load, return EvalAltResult::ErrorInModule.

Example of a Custom Module Resolver

use rhai::{ModuleResolver, Module, Engine, EvalAltResult};

// Define a custom module resolver.
struct MyModuleResolver {}

// Implement the 'ModuleResolver' trait.
impl ModuleResolver for MyModuleResolver {
    // Only required function.
    fn resolve(
        &self,
        engine: &Engine,                        // reference to the current 'Engine'
        source_path: Option<&str>,              // path of the parent module
        path: &str,                             // the module path
        pos: Position,                          // position of the 'import' statement
    ) -> Result<Rc<Module>, Box<EvalAltResult>> {
        // Check module path.
        if is_valid_module_path(path) {
            // Load the custom module
            match load_secret_module(path) {
                Ok(my_module) => {
                    my_module.build_index();    // index it
                    Rc::new(my_module)          // make it shared
                },
                // Return 'EvalAltResult::ErrorInModule' upon loading error
                Err(err) => Err(EvalAltResult::ErrorInModule(path.into(), Box::new(err), pos).into())
            }
        } else {
            // Return 'EvalAltResult::ErrorModuleNotFound' if the path is invalid
            Err(EvalAltResult::ErrorModuleNotFound(path.into(), pos).into())
        }
    }
}

let mut engine = Engine::new();

// Set the custom module resolver into the 'Engine'.
engine.set_module_resolver(MyModuleResolver {});

engine.run(
r#"
    import "hello" as foo;  // this 'import' statement will call
                            // 'MyModuleResolver::resolve' with "hello" as 'path'
    foo:bar();
"#)?;

Advanced – ModuleResolver::resolve_ast

There is another function in the ModuleResolver trait, resolve_ast, which is a low-level API intended for advanced usage scenarios.

ModuleResolver::resolve_ast has a default implementation that simply returns None, which indicates that this API is not supported by the module resolver.

Any module resolver that serves modules based on Rhai scripts should implement ModuleResolver::resolve_ast. When called, the compiled AST of the script should be returned.

ModuleResolver::resolve_ast should not return an error if ModuleResolver::resolve will not. On the other hand, the same error should be returned if ModuleResolver::resolve will return one.

Compile to a Self-Contained AST

Tip

It does not matter where the import statement occurs — e.g. deep within statement blocks or within function bodies.

When a script imports external modules that may not be available later on, it is possible to eagerly pre-resolve these imports and embed them directly into a self-contained AST.

For instance, a system may periodically connect to a central source (e.g. a database) to load scripts and compile them to AST form. Afterwards, in order to conserve bandwidth (or due to other physical limitations), it is disconnected from the central source for self-contained operation.

Compile a script into a self-contained AST via Engine::compile_into_self_contained.

let mut engine = Engine::new();

// Compile script into self-contained AST using the current
// module resolver (default to `FileModuleResolver`) to pre-resolve
// 'import' statements.
let ast = engine.compile_into_self_contained(&mut scope, script)?;

// Make sure we can no longer resolve any module!
engine.set_module_resolver(DummyModuleResolver::new());

// The AST still evaluates fine, even with 'import' statements!
engine.run(&ast)?;

When such an AST is evaluated, import statements within are provided the pre-resolved modules without going through the normal module resolution process.

Only Static Paths

Engine::compile_into_self_contained only pre-resolves import statements in the script that are static, i.e. with a path that is a string literal.

// The following import is pre-resolved.
import "hello" as h;

if some_event() {
    // The following import is pre-resolved.
    import "hello" as h;
}

fn foo() {
    // The following import is pre-resolved.
    import "hello" as h;
}

// The following import is also pre-resolved because the expression
// is usually optimized into a single string during compilation.
import "he" + "llo" as h;

let module_name = "hello";

// The following import is NOT pre-resolved.
import module_name as h;

Plugins

Rhai contains a robust plugin system that greatly simplifies registration of custom functionality.

Instead of using the complicated Engine::register_XXX or Module API to register Rust functions, a plugin simplifies the work of creating and registering new functionality in an Engine.

Plugins are processed via a set of procedural macros under the rhai::plugin module. These allow registering Rust functions directly in the Engine, or adding Rust modules as packages.

There are two types of plugins:

  1. Plugin Functions

  2. Plugin Modules

Export a Rust Module to Rhai

Import Prelude

When using the plugins system, the entire rhai::plugin module must be imported as a prelude because code generated will need these imports.

use rhai::plugin::*;

#[export_module]

When applied to a Rust module, the #[export_module] attribute generates the necessary code and metadata to allow Rhai access to its public (i.e. marked pub) functions, constants, type aliases, and sub-modules.

This code is exactly what would need to be written by hand to achieve the same goal, and is custom fit to each exported item.

All pub functions become registered functions, constants become module constants, type aliases become custom types, and sub-modules become Rhai sub-modules.

Module elementExampleRhai module equivalent
pub constantpub const FOO: i64 = 42;constant
pub type aliaspub type Foo = Bar<i64>custom type
pub functionpub fn foo(...) { ... }function
pub sub-modulepub mod foo { ... }sub-module
use rhai::plugin::*;        // a "prelude" import for macros

// My custom type
pub struct TestStruct {
    pub value: i64
}

#[export_module]
mod my_module {
    // This type alias will register the friendly name 'ABC' for the
    // custom type 'TestStruct'.
    pub type ABC = TestStruct;

    // This constant will be registered as the constant variable 'MY_NUMBER'.
    // Ignored when registered as a global module.
    pub const MY_NUMBER: i64 = 42;

    // This function will be registered as 'greet'
    // but is only available with the 'greetings' feature.
    #[cfg(feature = "greetings")]
    pub fn greet(name: &str) -> String {
        format!("hello, {}!", name)
    }
    /// This function will be registered as 'get_num'.
    ///
    /// If this is a Rust doc-comment, then it is included in the metadata.
    pub fn get_num() -> i64 {
        mystic_number()
    }
    /// This function will be registered as 'create_abc'.
    pub fn create_abc(value: i64) -> ABC {
        ABC { value }
    }
    /// This function will be registered as the 'value' property of type 'ABC'.
    #[rhai_fn(get = "value")]
    pub fn get_value(ts: &mut ABC) -> i64 {
        ts.value
    }
    // This function will be registered as 'increment'.
    // It will also be exposed to the global namespace since 'global' is set.
    #[rhai_fn(global)]
    pub fn increment(ts: &mut ABC) {
        ts.value += 1;
    }
    // This function is not 'pub', so NOT registered.
    fn mystic_number() -> i64 {
        42
    }

    // Sub-modules are ignored when the module is registered globally.
    pub mod my_sub_module {
        // This function is ignored when registered globally.
        // Otherwise it is a valid registered function under a sub-module.
        pub fn get_info() -> String {
            "hello".to_string()
        }
    }

    // Sub-modules are commonly used to put feature gates on a group of
    // functions because feature gates cannot be put on function definitions.
    // This is currently a limitation of the plugin procedural macros.
    #[cfg(feature = "advanced_functions")]
    pub mod advanced {
        // This function is ignored when registered globally.
        // Otherwise it is a valid registered function under a sub-module
        // which only exists when the 'advanced_functions' feature is used.
        pub fn advanced_calc(input: i64) -> i64 {
            input * 2
        }
    }
}

Doc-comments

If the metadata feature is active, doc-comments (i.e. comments starting with /// or wrapped with /***/) on plugin functions are extracted into metadata.

It is always a good idea to put doc-comments onto plugin modules and plugin functions, as they can be used to auto-generate documentation later on.

Usage

The plugin module can be registered into an Engine as a normal module.

This is usually done via the exported_module! macro.

The macro combine_with_exported_module! can also be used to combine all the functions and variables into an existing module, flattening the namespace – i.e. all sub-modules are eliminated and their contents promoted to the top level. This is typical for developing custom packages.

Register with Engine::register_global_module

The simplest way to register the plugin module into an Engine is:

  1. use the exported_module! macro to turn it into a normal Rhai module,
  2. call Engine::register_global_module to register it
fn main() {
    let mut engine = Engine::new();

    // The macro call creates a Rhai module from the plugin module.
    let module = exported_module!(my_module);

    // A module can simply be registered into the global namespace.
    engine.register_global_module(module.into());
}

The functions contained within the module definition (i.e. greet, get_num, create_abc and increment, the value property getter), and the TestStruct custom type (with friendly name ABC) are automatically registered into the Engine when Engine::register_global_module is called.

let x = greet("world");
x == "hello, world!";

let x = greet(get_num().to_string());
x == "hello, 42!";

let x = get_num();
x == 42;

let abc = create_abc(x);

type_of(abc) == "ABC";

abc.value == 42;

abc.increment();

abc.value == 43;

Only functions

When using a module as a package, only functions registered at the top level can be accessed.

Variables as well as sub-modules are ignored.

Register with Engine::register_static_module

Another simple way to register the plugin module into an Engine is, again:

  1. use the exported_module! macro to turn it into a normal Rhai module,
  2. call Engine::register_static_module to register it under a particular module namespace
fn main() {
    let mut engine = Engine::new();

    // The macro call creates a Rhai module from the plugin module.
    let module = exported_module!(my_module);

    // A module can simply be registered as a static module namespace.
    engine.register_static_module("service", module.into());
}

The functions contained within the module definition (i.e. greet, get_num and increment), plus the constant MY_NUMBER, are automatically registered under the module namespace service:

let x = service::greet("world");
x == "hello, world!";

service::MY_NUMBER == 42;

let x = service::greet(service::get_num().to_string());
x == "hello, 42!";

let x = service::get_num();
x == 42;

let abc = service::create_abc(x);

type_of(abc) == "ABC";

abc.value == 42;

service::increment(abc);

abc.value == 43;

Tip: Default global

The default for all getters/setters and indexers defined in a plugin module is #[rhai_fn(global)] unless specifically overridden by #[rhai_fn(internal)].

All functions (usually methods) defined in the module and marked with #[rhai_fn(global)], all type iterators and all custom types are automatically exposed to the global namespace, so iteration, getters/setters and indexers for custom types can work as expected.

Therefore, in the example above, the increment method (defined with #[rhai_fn(global)]) works fine when called in method-call style:

let x = 42;
x.increment();
x == 43;

Load Dynamically

See also

See the module section for more information.

Using this directly as a dynamically-loadable Rhai module is almost the same, except that a module resolver must be used to serve the module, and the module is loaded via import statements.

Combine into Custom Package

Finally, the plugin module can also be used to develop a custom package, using combine_with_exported_module! which automatically flattens the module namespace so that all functions in sub-modules are promoted to the top level namespace, all sub-modules are eliminated, and all variables are ignored.

Tip: Feature gating

Due to flattening, sub-modules are often used conveniently as a grouping mechanism, especially to put feature gates or compile-time gates (i.e. #[cfg(...)]) on a large collection of functions without having to duplicate the gates onto each individual function.

#[export_module]
mod my_module {
    // Always available
    pub fn func0() {}

    // The following functions are only available under 'foo'.
    // Use a sub-module for convenience, since all functions underneath
    // will be flattened into the namespace.
    #[cfg(feature = "foo")]
    pub mod group_foo {
        pub fn func1() {}
        pub fn func2() {}
        pub fn func3() {}
    }

    // The following functions are only available under 'bar'
    #[cfg(feature = "bar")]
    pub mod group_bar {
        pub fn func4() {}
        pub fn func5() {}
        pub fn func6() {}
    }
}

// The above is equivalent to:
#[export_module]
mod my_module_alternate {
    pub fn func0() {}

    #[cfg(feature = "foo")]
    pub fn func1() {}
    #[cfg(feature = "foo")]
    pub fn func2() {}
    #[cfg(feature = "foo")]
    pub fn func3() {}

    #[cfg(feature = "bar")]
    pub fn func4() {}
    #[cfg(feature = "bar")]
    pub fn func5() {}
    #[cfg(feature = "bar")]
    pub fn func6() {}
}

// Registered functions:
//   func0 - always available
//   func1, func2, func3 - available under 'foo'
//   func4, func5, func6 - available under 'bar'
//   func0, func1, func2, func3, func4, func5, func6 - available under 'foo' and 'bar'
combine_with_exported_module!(module, "my_module_ID", my_module);

Functions Overloading and Operators

Tip: NativeCallContext parameter

The first parameter of a function can also be NativeCallContext.

Operators and overloaded functions can be specified via applying the #[rhai_fn(name = "...")] attribute to individual functions.

The text string given as the name parameter to #[rhai_fn] is used to register the function with the Engine, disregarding the actual name of the function.

With #[rhai_fn(name = "...")], multiple functions may be registered under the same name in Rhai, so long as they have different parameters.

Tip: Operators

Operators (which require function names that are not valid for Rust) can also be registered this way.

Duplicated functions

Registering the same function name with the same parameter types will cause a parse error.

use rhai::plugin::*;        // a "prelude" import for macros

#[export_module]
mod my_module {
    // This is the '+' operator for 'TestStruct'.
    #[rhai_fn(name = "+")]
    pub fn add(obj: &mut TestStruct, value: i64) {
        obj.prop += value;
    }
    // This function is 'calc (i64)'.
    pub fn calc(num: i64) -> i64 {
        ...
    }
    // This function is 'calc (i64, bool)'.
    #[rhai_fn(name = "calc")]
    pub fn calc_with_option(num: i64, option: bool) -> i64 {
        ...
    }
}

Getters, Setters and Indexers

Tip: Global namespace

Getters/setters and indexers default to #[rhai_fn(global)] unless overridden by #[rhai_fn(internal)].

Functions can be marked as getters/setters and indexers for custom types via the #[rhai_fn] attribute, which is applied on a function level.

AttributeDescription
#[rhai_fn(get = "property")]property getter
#[rhai_fn(set = "property")]property setter
#[rhai_fn(index_get)]index getter
#[rhai_fn(index_set)]index setter
use rhai::plugin::*;        // a "prelude" import for macros

#[export_module]
mod my_module {
    // This is a normal function 'greet'.
    pub fn greet(name: &str) -> String {
        format!("hello, {}!", name)
    }
    // This is a getter for 'TestStruct::prop'.
    #[rhai_fn(get = "prop", pure)]
    pub fn get_prop(obj: &mut TestStruct) -> i64 {
        obj.prop
    }
    // This is a setter for 'TestStruct::prop'.
    #[rhai_fn(set = "prop")]
    pub fn set_prop(obj: &mut TestStruct, value: i64) {
        obj.prop = value;
    }
    // This is an index getter for 'TestStruct'.
    #[rhai_fn(index_get)]
    pub fn get_index(obj: &mut TestStruct, index: i64) -> bool {
        obj.list[index]
    }
    // This is an index setter for 'TestStruct'.
    #[rhai_fn(index_set)]
    pub fn get_index(obj: &mut TestStruct, index: i64, state: bool) {
        obj.list[index] = state;
    }
}

Multiple Registrations

Parameters to the #[rhai_fn(...)] attribute can be applied multiple times, separated by commas.

Tip: Overloaded names

Multiple registrations is useful for name = "...", get = "..." and set = "..." to give multiple alternative names to the same function.

use rhai::plugin::*;        // a "prelude" import for macros

#[export_module]
mod my_module {
    // This function can be called in five ways
    #[rhai_fn(name = "get_prop_value", name = "prop", name = "+", set = "prop", index_get)]
    pub fn prop_function(obj: &mut TestStruct, index: i64) -> i64 {
        obj.prop[index]
    }
}

The above function can be called in five ways:

Parameter for #[rhai_fn(...)]TypeCall style
name = "get_prop_value"method functionget_prop_value(x, 0), x.get_prop_value(0)
name = "prop"method functionprop(x, 0), x.prop(0)
name = "+"operatorx + 42
set = "prop"setterx.prop = 42
index_getindex getterx[0]

Pure Functions

Apply the #[rhai_fn(pure)] attribute on a method function (i.e. one taking a &mut first parameter) to mark it as pure – i.e. it does not modify the &mut parameter.

This is often done to avoid expensive cloning for methods or property getters that return information about a custom type and does not modify it.

Must not modify &mut parameter

Pure functions MUST NOT modify the &mut parameter. There is no checking.

Error: Constants Not OK for non-pure

Non-pure functions raise a runtime error when passed a constant value as the first &mut parameter.

Tip: Constants OK for pure

Pure functions can be passed a constant value as the first &mut parameter.

use rhai::plugin::*;        // a "prelude" import for macros

#[export_module]
mod my_module {
    // This function can be passed a constant
    #[rhai_fn(name = "add1", pure)]
    pub fn add_scaled(array: &mut rhai::Array, x: i64) -> i64 {
        array.iter().map(|v| v.as_int().unwrap()).fold(0, |(r, v)| r += v * x)
    }
    // This function CANNOT be passed a constant
    #[rhai_fn(name = "add2")]
    pub fn add_scaled2(array: &mut rhai::Array, x: i64) -> i64 {
        array.iter().map(|v| v.as_int().unwrap()).fold(0, |(r, v)| r += v * x)
    }
    // This getter can be applied to a constant
    #[rhai_fn(get = "first1", pure)]
    pub fn get_first(array: &mut rhai::Array) -> i64 {
        array[0]
    }
    // This getter CANNOT be applied to a constant
    #[rhai_fn(get = "first2")]
    pub fn get_first2(array: &mut rhai::Array) -> i64 {
        array[0]
    }
    // The following is a syntax error because a setter is SUPPOSED to
    // mutate the object.  Therefore the 'pure' attribute cannot be used.
    #[rhai_fn(get = "values", pure)]
    pub fn set_values(array: &mut rhai::Array, value: i64) {
        // ...
    }
}

When applied to a Rhai script:

// Constant
const VECTOR = [1, 2, 3, 4, 5, 6, 7];

let r = VECTOR.add1(2);     // ok!

let r = VECTOR.add2(2);     // runtime error: constant modified

let r = VECTOR.first1;      // ok!

let r = VECTOR.first2;      // runtime error: constant modified

Fallible Functions

To register fallible functions (i.e. functions that may return errors), apply the #[rhai_fn(return_raw)] attribute on functions that return Result<T, Box<EvalAltResult>> where T is any clonable type.

use rhai::plugin::*;        // a "prelude" import for macros

#[export_module]
mod my_module {
    /// This overloads the '/' operator for i64.
    #[rhai_fn(name = "/", return_raw)]
    pub fn double_and_divide(x: i64, y: i64) -> Result<i64, Box<EvalAltResult>> {
        if y == 0 {
            Err("Division by zero!".into())
        } else {
            Ok((x * 2) / y)
        }
    }
}

Missing #[rhai_fn(return_raw)]

A compilation error — usually something that says Result does not implement Clone — is generated if a fallible function is missing #[rhai_fn(return_raw)].

It is another compilation error for the reverse — a function with #[rhai_fn(return_raw)] does not have the appropriate return type.

#[export_module] Parameters

Parameters can be applied to the #[export_module] attribute to override its default behavior.

ParameterDescription
noneexports only public (i.e. pub) functions
export_allexports all functions (including private, non-pub functions); use #[rhai_fn(skip)] on individual functions to avoid export
export_prefix = "..."exports functions (including private, non-pub functions) with names starting with a specific prefix

Inner Attributes

rhai_fn vs rhai_mod

#[rhai_fn] is applied to functions, while #[rhai_mod] is applied to sub-modules.

Inner attributes can be applied to the inner items of a module to tweak the export process.

Parameters should be set on inner attributes to specify the desired behavior.

Attribute ParameterUse withApply toDescription
skip#[rhai_fn]
#[rhai_mod]
function or sub-moduledo not export this function/sub-module
global#[rhai_fn]functionexpose this function to the global namespace
internal#[rhai_fn]functionkeep this function within the internal module namespace
name = "..."#[rhai_fn]
#[rhai_mod]
function or sub-moduleregisters function/sub-module under the specified name
get = "..."#[rhai_fn]pub fn (&mut Type) -> ValueTyperegisters a property getter for the named property
set = "..."#[rhai_fn]pub fn (&mut Type, ValueType)registers a property setter for the named property
index_get#[rhai_fn]pub fn (&mut Type, IndexType) -> ValueTyperegisters an index getter
index_set#[rhai_fn]pub fn (&mut Type, IndexType, ValueType)registers an index setter
return_raw#[rhai_fn]pub fn (...) -> Result<Type, Box<EvalAltResult>>marks this as a fallible function
pure#[rhai_fn]pub fn (&mut Type, ...) -> ...marks this as a pure function

Export a Rust Function to Rhai

Sometimes only a few ad hoc functions are required and it is simpler to register individual functions instead of a full-blown plugin module.

Macros

MacroSignatureDescription
#[export_fn]apply to Rust function defined in a Rust moduleexports the function
register_exported_fn!register_exported_fn!(&mut engine, "name", function)registers the function into an Engine under a specific name
set_exported_fn!set_exported_fn!(&mut module, "name", function)registers the function into a Module under a specific name
set_exported_global_fn!set_exported_global_fn!(&mut module, "name", function)registers the function into a Module under a specific name, exposing it to the global namespace

#[export_fn] and register_exported_fn!

Tip: NativeCallContext parameter

The first parameter of a function can also be NativeCallContext.

Apply #[export_fn] onto a function defined at Rust module level to convert it into a Rhai plugin function.

To register the plugin function, simply call register_exported_fn!.

Global scope only

The function cannot be nested inside another function – it can only be defined directly under a Rust module.

Tip: Overloading

The name of the function can be any text string, so it is possible to register overloaded functions as well as operators.

use rhai::plugin::*;        // import macros

#[export_fn]
fn increment(num: &mut i64) {
    *num += 1;
}

fn main() {
    let mut engine = Engine::new();

    // 'register_exported_fn!' registers the function as 'inc' with the Engine.
    register_exported_fn!(engine, "inc", increment);
}

Pure Functions

Some functions are pure – i.e. they do not mutate any parameter, even though the first parameter may be passed in as &mut (e.g. for a method function).

This is often done to avoid expensive cloning for methods or property getters that return information about a custom type and does not modify it.

Apply the #[export_fn(pure)] attribute on a plugin function to mark it as pure.

Must not modify &mut parameter

Pure functions MUST NOT modify the &mut parameter. There is no checking.

Error: Constants Not OK for non-pure

Non-pure functions raise a runtime error when passed a constant value as the first &mut parameter.

Tip: Constants OK for pure

Pure functions can be passed a constant value as the first &mut parameter.

use rhai::plugin::*;        // a "prelude" import for macros

// This method is pure, so 'len' can be used on a constant 'TestStruct'.
#[export_fn(pure)]
pub fn len(my_type: &mut TestStruct) -> i64 {
    my_type.len()
}

// This method is not pure, so 'clear' will raise an error
// when used on a constant 'TestStruct'.
#[export_fn]
pub fn clear(my_type: &mut TestStruct) {
    my_type.clear();
}

Fallible Functions

To register fallible functions (i.e. functions that may return errors), apply the #[export_fn(return_raw)] attribute on plugin functions that return Result<T, Box<EvalAltResult>> where T is any clonable type.

use rhai::plugin::*;        // a "prelude" import for macros

#[export_fn(return_raw)]
pub fn double_and_divide(x: i64, y: i64) -> Result<i64, Box<EvalAltResult>> {
    if y == 0 {
        Err("Division by zero!".into())
    } else {
        Ok((x * 2) / y)
    }
}

fn main() {
    let mut engine = Engine::new();

    // Overloads the operator '+' with the Engine.
    register_exported_fn!(engine, "+", double_and_divide);
}

Missing #[rhai_fn(return_raw)]

A compilation error — usually something that says Result does not implement Clone — is generated if a fallible function is missing #[rhai_fn(return_raw)].

It is another compilation error for the reverse — a function with #[rhai_fn(return_raw)] does not have the appropriate return type.

Packages

The built-in library of Rhai is provided as various packages that can be turned into shared modules, which in turn can be registered into the global namespace of an Engine via Engine::register_global_module.

Packages reside under rhai::packages::* and the trait rhai::packages::Package must be loaded in order for packages to be used.

Trivia: Packages are modules!

Internally, a package is a module, with some conveniences to make it easier to define and use as a standard library for an Engine.

Packages typically contain Rust functions that are callable within a Rhai script. All top-level functions in a package are available under the global namespace (i.e. they’re available without namespace qualifiers).

Sub-modules and variables are ignored in packages.

Share a Package Among Multiple Engine’s

Engine::register_global_module and Engine::register_static_module both require shared modules.

Once a package is created (e.g. via Package::new), it can create shared modules (via Package::as_shared_module) and register into multiple instances of Engine, even across threads (under the sync feature).

Tip: Sharing package

A package only has to be created once and essentially shared among multiple Engine instances.

This is particularly useful when spawning large number of raw Engine’s.

use rhai::Engine;
use rhai::packages::Package         // load the 'Package' trait to use packages
use rhai::packages::CorePackage;    // the 'core' package contains basic functionalities (e.g. arithmetic)

// Create a package - can be shared among multiple 'Engine' instances
let package = CorePackage::new();

let mut engines_collection: Vec<Engine> = Vec::new();

// Create 100 'raw' Engines
for _ in 0..100 {
    let mut engine = Engine::new_raw();

    // Register the package into the global namespace.
    // 'Package::as_shared_module' converts the package into a shared module.
    engine.register_global_module(package.as_shared_module());

    engines_collection.push(engine);
}

Built-In Packages

Engine::new creates an Engine with the StandardPackage loaded.

Engine::new_raw creates an Engine with no package loaded.

PackageDescriptionIn CoreIn Standard
LanguageCorePackagecore functions for the Rhai languageyesyes
ArithmeticPackagearithmetic operators (e.g. +, -, *, /) for numeric types that are not built in (e.g. u16)yesyes
BitFieldPackagebasic bit-field functionsnoyes
BasicIteratorPackagenumeric ranges (e.g. range(1, 100, 5)), iterators for arrays, strings, bit-fields and object mapsyesyes
LogicPackagelogical and comparison operators (e.g. ==, >) for numeric types that are not built in (e.g. u16)noyes
BasicStringPackagebasic string functions (e.g. print, debug, len) that are not built inyesyes
BasicTimePackagebasic time functions (e.g. timestamps)noyes
MoreStringPackageadditional string functions, including converting common types to stringnoyes
BasicMathPackagebasic math functions (e.g. sin, sqrt)noyes
BasicArrayPackagebasic array functions (not available under no_index)noyes
BasicBlobPackagebasic BLOB functions (not available under no_index)noyes
BasicMapPackagebasic object map functions (not available under no_object)noyes
BasicFnPackagebasic methods for function pointersyesyes
DebuggingPackagebasic functions for debugging (requires debugging)yesyes
CorePackagebasic essentialsyesyes
StandardPackagestandard library (default for Engine::new)noyes

CorePackage

If only minimal functionalities are required, register the CorePackage instead.

use rhai::Engine;
use rhai::packages::{Package, CorePackage};

let mut engine = Engine::new_raw();
let package = CorePackage::new();

// Register the package into the 'Engine' by converting it into a shared module.
engine.register_global_module(package.as_shared_module());

Create a Custom Package

See also

See also the One Engine Instance Per Call pattern.

The macro def_package! can be used to create a custom package.

A custom package can aggregate many other packages into a single self-contained unit. More functions can be added on top of others.

Custom packages are extremely useful when multiple raw Engine instances must be created such that they all share the same set of functions.

def_package!

def_package! {
    /// Package description doc-comment
    pub name(variable) {
                        :
        // package initialization code block
                        :
    }

    // Multiple packages can be defined at the same time

    /// Package description doc-comment
    pub(crate) name(variable) {
                        :
        // package initialization code block
                        :
    }

    /// A private package description doc-comment
    name(variable) {
                        :
        // private package initialization code block
                        :
    }

    :
}

where:

ElementDescription
descriptiondoc-comment for the package
pub etc.visibility of the package
namename of the package, usually ending in …Package
variablea variable name holding a reference to the module forming the package, usually module or lib
code blocka code block that initializes the package

Examples

// Import necessary types and traits.
use rhai::def_package;      // 'def_package!' macro
use rhai::packages::{
    ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage
};

def_package! {
    /// My own personal super package
    pub MyPackage(module) {
        // Aggregate other packages simply by calling 'init' on each.
        ArithmeticPackage::init(module);
        LogicPackage::init(module);
        BasicArrayPackage::init(module);
        BasicMapPackage::init(module);

        // Register additional Rust functions using 'Module::set_native_fn'.
        let hash = module.set_native_fn("foo", |s: &str| Ok(foo(s)));

        // Remember to update the parameter names/types and return type
        // metadata when using the 'metadata' feature because
        // 'Module::set_native_fn' by default does not set function metadata.
        module.update_fn_metadata(hash, &["s: &str", "i64"]);
    }
}

Create a Custom Package from a Plugin Module

Trivia

This is exactly how Rhai’s built-in packages, such as BasicMathPackage, are actually implemented.

By far the easiest way to create a custom package is to call plugin::combine_with_exported_module! from within def_package! which simply merges in all the functions defined within a plugin module.

Due to specific requirements of a package, plugin::combine_with_exported_module! flattens all sub-modules (i.e. all functions and type iterators defined within sub-modules are pulled up to the top level instead) and so there will not be any sub-modules added to the package.

Variables in the plugin module are ignored.

// Import necessary types and traits.
use rhai::def_package;
use rhai::packages::{
    ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage
};
use rhai::plugin::*;

// Define plugin module.
#[export_module]
mod my_plugin_module {
    pub const MY_NUMBER: i64 = 42;

    pub fn greet(name: &str) -> String {
        format!("hello, {}!", name)
    }

    // Non-public functions are by default not exported.
    fn get_private_num() -> i64 {
        42
    }

    pub fn get_num() -> i64 {
        get_private_num()
    }

    // This is a sub-module, but if using 'combine_with_exported_module!',
    // it will be flattened and all functions registered at the top level.
    //
    // Because of the flattening, sub-modules are very convenient for
    // putting feature gates onto large groups of functions.
    #[cfg(feature = "sub-num-feature")]
    pub mod my_sub_module {
        pub fn get_sub_num() -> i64 {
            0
        }
    }
}

def_package! {
    /// My own personal super package
    pub MyPackage(module) {
        // Aggregate other packages simply by calling 'init' on each.
        ArithmeticPackage::init(module);
        LogicPackage::init(module);
        BasicArrayPackage::init(module);
        BasicMapPackage::init(module);

        // Merge all registered functions and constants from the plugin module
        // into the custom package.
        //
        // The sub-module 'my_sub_module' is flattened and its functions
        // registered at the top level.
        //
        // The text string name in the second parameter can be anything
        // and is reserved for future use; it is recommended to be an
        // ID string that uniquely identifies the plugin module.
        //
        // The constant variable, 'MY_NUMBER', is ignored.
        //
        // This call ends up registering three functions at the top level of
        // the package:
        // 1) 'greet'
        // 2) 'get_num'
        // 3) 'get_sub_num' (flattened from 'my_sub_module')
        //
        combine_with_exported_module!(module, "my-mod", my_plugin_module));
    }
}

Create a Custom Package as an Independent Crate

Creating a custom package as an independent crate allows it to be shared by multiple projects.

Key concepts

  • Create a Rust crate that specifies rhai as dependency.

  • The main lib.rs module can contain the package being constructed.

Example

The project rhai-rand shows a simple example of creating a custom package as an independent crate.

Implementation

Cargo.toml:

[package]
name = "my-package"     # 'my-package' crate

[dependencies]
rhai = "1.8.0"    # assuming 1.8.0 is the latest version

lib.rs:

use rhai::def_package;
use rhai::plugin::*;

// This is a plugin module
#[export_module]
mod my_module {
    // Constants are ignored when used as a package
    pub const MY_NUMBER: i64 = 42;

    pub fn greet(name: &str) -> String {
        format!("hello, {}!", name)
    }
    pub fn get_num() -> i64 {
        42
    }

    // This is a sub-module, but if using combine_with_exported_module!, it will
    // be flattened and all functions registered at the top level.
    pub mod my_sub_module {
        pub fn get_sub_num() -> i64 {
            0
        }
    }
}

// Define the package 'MyPackage' which is exported for the crate.
def_package! {
    /// My own personal super package in a new crate!
    pub MyPackage(module) {
        combine_with_exported_module!(module, "my-functions", my_module));
    }
}

External Packages

Following are external packages that can be used with Rhai for additional functionalities.

PackageDescription
rhai-randgenerate random numbers, shuffling and sampling

rhai-rand: Random Number Generation, Shuffling and Sampling

rhai-rand is an independent Rhai package that provides:

  • random number generation using the rand crate
  • array shuffling and sampling

On crates.io: rhai-rand

On GitHub: rhaiscript/rhai-rand

Package name: RandomPackage

Dependency

Cargo.toml:

[dependencies]
rhai = "1.8.0"
rhai-rand = "0.1"       # use rhai-rand crate

Load Package into Engine

use rhai::Engine;
use rhai::packages::Package;    // needed for 'as_shared_module'
use rhai_rand::RandomPackage;

let mut engine = Engine::new();

// Create new 'RandomPackage' instance
let random = RandomPackage::new();

// Load the package
engine.register_global_module(random.as_shared_module());

Features

FeatureDescriptionDefault?Should not be used with Rhai feature
floatenables random floating-point number generationyesno_float
arrayenables methods for arraysyesno_index
metadataenables functions metadata (turns on metadata in Rhai)no

Example – working with no_float in Rhai

Cargo.toml:

[dependencies]
# Rhai is set for 'no_float', meaning no floating-point support
rhai = { version="1.8.0", features = ["no_float"] }

# Use 'default-features = false' to clear defaults, then only add 'array'
rhai-rand = { version="0.1", default-features = false, features = ["array"] }

Package Functions

The following functions are defined.

FunctionReturn typeFeatureDescription
rand()INTgenerates a random integer number
rand(start..end)INTgenerates a random integer number within the exclusive range start..end
rand(start..=end)INTgenerates a random integer number within the inclusive range start..=end
rand(start, end)INTgenerates a random integer number within the inclusive range start..=end
rand_float()FLOATfloatgenerates a random floating-point number between 0.0 and 1.0 (exclusive)
rand_float(start, end)FLOATfloatgenerates a random floating-point number within the inclusive range start..=end
rand_decimal()Decimalgenerates a random decimal number
rand_decimal(start, end)Decimalgenerates a random decimal number within the inclusive range start..=end
rand_bool()boolgenerates a random boolean
rand_bool(p)boolfloatgenerates a random boolean with the probability p of being true

Arrays

The following methods are defined for arrays (requires the array feature).

MethodParameter(s)Return typeDescription
shufflenoneshuffles the items in the array
samplenoneDynamicreturns a random item from the array
samplenumber of items to sample (empty if ≤ 0, all if ≥ length)Arrayreturns a non-repeating shuffled random sample of items from the array

Comments

Comments are C-style, including /**/ pairs for block comments and // for comments to the end of the line.

Comments can be nested.

let /* intruder comment */ name = "Bob";

// This is a very important one-line comment

/* This comment spans
   multiple lines, so it
   only makes sense that
   it is even more important */

/* Fear not, Rhai satisfies all nesting needs with nested comments:
   /*/*/*/*/**/*/*/*/*/
*/

Doc-Comments

Similar to Rust, comments starting with /// (three slashes) or /** (two asterisks) are doc-comments.

Doc-comments can only appear in front of function definitions, not any other elements. Therefore, doc-comments are not available under no_function.

Requires metadata

Doc-comments are only supported under the metadata feature.

If metadata is not active, doc-comments are treated as normal comments.

/// This is a valid one-line doc-comment
fn foo() {}

/** This is a
 ** valid block
 ** doc-comment
 **/
fn bar(x) {
   /// Syntax error: this doc-comment is invalid
   x + 1
}

/** Syntax error: this doc-comment is invalid */
let x = 42;

/// Syntax error: this doc-comment is also invalid
{
   let x = 42;
}

Tip: Special cases

Long streams of //////… and /*****… do NOT form doc-comments. This is consistent with popular [comment] block styles for C-like languages.

///////////////////////////////  <- this is not a doc-comment
// This is not a doc-comment //  <- this is not a doc-comment
///////////////////////////////  <- this is not a doc-comment

// However, watch out for comment lines starting with '///'

//////////////////////////////////////////  <- this is not a doc-comment
/// This, however, IS a doc-comment!!! ///  <- doc-comment!
//////////////////////////////////////////  <- this is not a doc-comment

/****************************************
 *                                      *
 * This is also not a doc-comment block *
 * so we don't have to put this in      *
 * front of a function.                 *
 *                                      *
 ****************************************/

Using Doc-Comments

Doc-comments are stored within the script’s AST after compilation.

The AST::iter_functions method provides a ScriptFnMetadata instance for each function defined within the script, which includes doc-comments.

Doc-comments never affect the evaluation of a script nor do they incur significant performance overhead. However, third party tools can take advantage of this information to auto-generate documentation for Rhai script functions.

Values and Types

The following primitive types are supported natively.

CategoryEquivalent Rust typestype_of()to_string()
System integerrhai::INT (default i64, i32 under only_i32)"i32" or "i64""42", "123" etc.
Other integer numberu8, i8, u16, i16, u32, i32, u64, i64"i32", "u64" etc."42", "123" etc.
Integer numeric rangestd::ops::Range<rhai::INT>, std::ops::RangeInclusive<rhai::INT>"range", "range=""2..7", "0..=15" etc.
Floating-point number (disabled with no_float)rhai::FLOAT (default f64, f32 under f32_float)"f32" or "f64""123.4567" etc.
Fixed precision decimal number (requires decimal)rust_decimal::Decimal"decimal""42", "123.4567" etc.
Boolean valuebool"bool""true" or "false"
Unicode characterchar"char""A", "x" etc.
Immutable Unicode stringrhai::ImmutableString (Rc<SmartString>, Arc<SmartString> under sync)"string""hello" etc.
Array (disabled with no_index)rhai::Array (Vec<Dynamic>)"array""[ 1, 2, 3 ]" etc.
Byte array – BLOB (disabled with no_index)rhai::Blob (Vec<u8>)"blob""[01020304abcd]" etc.
Object map (disabled with no_object)rhai::Map (BTreeMap<SmartString, Dynamic>)"map""#{ "a": 1, "b": true }" etc.
Timestamp (disabled with no_std)std::time::Instant (instant::Instant if WASM build)"timestamp""<timestamp>"
Function pointerrhai::FnPtr"Fn""Fn(foo)" etc.
Dynamic value (i.e. can be anything)rhai::Dynamicthe actual typeactual value
Shared value (a reference-counted, shared Dynamic value, created via automatic currying, disabled with no_closure)the actual typeactual value
Nothing/void/nil/null/Unit (or whatever it is called)()"()""" (empty string)

All types are distinct

All types are treated strictly distinct by Rhai, meaning that i32 and i64 and u32 are completely different. They cannot even be added together.

This is very similar to Rust.

Default Types

The default integer type is i64. If other integer types are not needed, it is possible to exclude them and make a smaller build with the only_i64 feature.

If only 32-bit integers are needed, enabling the only_i32 feature will remove support for all integer types other than i32, including i64. This is useful on some 32-bit targets where using 64-bit integers incur a performance penalty.

Default integer is i64

Rhai’s default integer type is i64, which is DIFFERENT from Rust’s i32.

It is very easy to unsuspectingly set an i32 into Rhai, which still works but will incur a significant runtime performance hit since the Engine will treat i32 as an opaque custom type (unless using the only_i32 feature).

Tip: Floating-point numbers

If no floating-point is needed or supported, use the no_float feature to remove it.

Some applications require fixed-precision decimal numbers, which can be enabled via the decimal feature.

Strings

Strings in Rhai are immutable, meaning that they can be shared but not modified.

Internally, the ImmutableString type is a wrapper over Rc<String> or Arc<String> (depending on sync).

Any modification done to a Rhai string causes the string to be cloned and the modifications made to the copy.

Tip: Convert to string

The to_string() function converts a standard type into a string for display purposes.

The to_debug() function converts a standard type into a string in debug format.

Dynamic Values

A Dynamic value can be any type, as long as it implements Clone.

Send + Sync

Under the sync feature, all types must also be Send + Sync.

let x = 42;         // value is an integer

x = 123.456;        // value is now a floating-point number

x = "hello";        // value is now a string

x = x.len > 0;      // value is now a boolean

x = [x];            // value is now an array

x = #{x: x};        // value is now an object map

Use type_of() to Get Value Type

Because type_of() a Dynamic value returns the type of the actual value, it is usually used to perform type-specific actions based on the actual value’s type.

let mystery = get_some_dynamic_value();

switch type_of(mystery) {
    "()" => print("Hey, I got the unit () here!"),
    "i64" => print("Hey, I got an integer here!"),
    "f64" => print("Hey, I got a float here!"),
    "decimal" => print("Hey, I got a decimal here!"),
    "range" => print("Hey, I got an exclusive range here!"),
    "range=" => print("Hey, I got an inclusive range here!"),
    "string" => print("Hey, I got a string here!"),
    "bool" => print("Hey, I got a boolean here!"),
    "array" => print("Hey, I got an array here!"),
    "blob" => print("Hey, I got a BLOB here!"),
    "map" => print("Hey, I got an object map here!"),
    "Fn" => print("Hey, I got a function pointer here!"),
    "timestamp" => print("Hey, I got a time-stamp here!"),
    "TestStruct" => print("Hey, I got the TestStruct custom type here!"),
    _ => print(`I don't know what this is: ${type_of(mystery)}`)
}

Type Checking and Casting

Tip: Dynamic::try_cast

The try_cast method does not panic but returns None upon failure.

A Dynamic value’s actual type can be checked via Dynamic::is.

The cast method then converts the value into a specific, known type.

Use clone_cast to clone a reference to Dynamic.

let list: Array = engine.eval("...")?;      // return type is 'Array'
let item = list[0].clone();                 // an element in an 'Array' is 'Dynamic'

item.is::<i64>() == true;                   // 'is' returns whether a 'Dynamic' value is of a particular type

let value = item.cast::<i64>();             // if the element is 'i64', this succeeds; otherwise it panics
let value: i64 = item.cast();               // type can also be inferred

let value = item.try_cast::<i64>()?;        // 'try_cast' does not panic when the cast fails, but returns 'None'

let value = list[0].clone_cast::<i64>();    // use 'clone_cast' on '&Dynamic'
let value: i64 = list[0].clone_cast();

Type Name and Matching Types

The type_name method gets the name of the actual type as a static string slice, which can be match-ed against.

This is a very simple and direct way to act on a Dynamic value based on the actual type of the data value.

let list: Array = engine.eval("...")?;      // return type is 'Array'
let item = list[0];                         // an element in an 'Array' is 'Dynamic'

match item.type_name() {                    // 'type_name' returns the name of the actual Rust type
    "()" => ...
    "i64" => ...
    "f64" => ...
    "rust_decimal::Decimal" => ...
    "core::ops::range::Range<i64>" => ...
    "core::ops::range::RangeInclusive<i64>" => ...
    "alloc::string::String" => ...
    "bool" => ...
    "char" => ...
    "rhai::FnPtr" => ...
    "std::time::Instant" => ...
    "crate::path::to::module::TestStruct" => ...
        :
}

Always full path name

type_name always returns the full Rust path name of the type, even when the type has been registered with a friendly name via Engine::register_type_with_name.

This behavior is different from that of the type_of function in Rhai.

Methods and Traits

The following methods are available when working with Dynamic:

MethodNot available underReturn typeDescription
type_name&strname of the value’s type
into_sharedno_closureDynamicturn the value into a shared value
flatten_cloneDynamicclone the value (a shared value, if any, is cloned into a separate copy)
flattenDynamicclone the value into a separate copy if it is shared and there are multiple outstanding references, otherwise shared values are turned unshared
read_lock<T>no_closure (pass thru’)Option< guard to T>lock the value for reading
write_lock<T>no_closure (pass thru’)Option< guard to T>lock the value exclusively for writing

Constructor instance methods

MethodNot available underValue typeData type
from_boolboolbool
from_intINTinteger number
from_floatno_floatFLOATfloating-point number
from_decimalnon-decimalDecimalDecimal
from_str&strstring
from_charcharcharacter
from_arrayno_indexVec<T>array
from_blobno_indexVec<u8>BLOB
from_mapno_objectMapobject map
from_timestampno_stdstd::time::Instant (instant::Instant if WASM build)timestamp
from<T>Tcustom type

Detection methods

MethodNot available underReturn typeDescription
is<T>boolis the value of type T?
is_variantboolis the value a trait object (i.e. not one of Rhai’s standard types)?
is_read_onlyboolis the value constant? A constant value should not be modified.
is_sharedno_closureboolis the value shared via a closure?
is_lockedno_closureboolis the value shared and locked (i.e. currently being read)?

Casting methods

The following methods cast a Dynamic into a specific type:

MethodNot available underReturn type (error is the actual data type)
cast<T>T (panics on failure)
try_cast<T>Option<T>
clone_cast<T>cloned copy of T (panics on failure)
as_unitResult<(), &str>
as_intResult<INT, &str>
as_floatno_floatResult<FLOAT, &str>
as_decimalnon-decimalResult<Decimal, &str>
as_boolResult<bool, &str>
as_charResult<char, &str>
into_stringResult<String, &str>
into_immutable_stringResult<ImmutableString, &str>
into_arrayno_indexResult<Array, &str>
into_blobno_indexResult<Blob, &str>
into_typed_array<T>no_indexResult<Vec<T>, &str>

Constructor traits

The following constructor traits are implemented for Dynamic where T: Clone:

TraitNot available underData type
From<()>()
From<INT>integer number
From<FLOAT>no_floatfloating-point number
From<Decimal>non-decimalDecimal
From<bool>bool
From<S: Into<ImmutableString>>
e.g. From<String>, From<&str>
ImmutableString
From<char>character
From<Vec<T>>no_indexarray
From<&[T]>no_indexarray
From<BTreeMap<K: Into<SmartString>, T>>
e.g. From<BTreeMap<String, T>>
no_objectobject map
From<BTreeSet<K: Into<SmartString>>>
e.g. From<BTreeSet<String>>
no_objectobject map
From<HashMap<K: Into<SmartString>, T>>
e.g. From<HashMap<String, T>>
no_object or no_stdobject map
From<HashSet<K: Into<SmartString>>>
e.g. From<HashSet<String>>
no_object or no_stdobject map
From<FnPtr>function pointer
From<Instant>no_stdtimestamp
From<Rc<RefCell<Dynamic>>>sync or no_closureDynamic
From<Arc<RwLock<Dynamic>>> (sync)non-sync or no_closureDynamic
FromIterator<X: IntoIterator<Item=T>>no_indexarray

type_of()

The type_of function detects the actual type of a value.

This is useful because all variables are Dynamic in nature.

// Use 'type_of()' to get the actual types of values
type_of('c') == "char";
type_of(42) == "i64";

let x = 123;
x.type_of() == "i64";       // method-call style is also OK
type_of(x) == "i64";

x = 99.999;
type_of(x) == "f64";

x = "hello";
if type_of(x) == "string" {
    do_something_first_with_string(x);
}

switch type_of(x) {
    "string" => do_something_with_string(x),
    "char" => do_something_with_char(x),
    "i64" => do_something_with_int(x),
    "f64" => do_something_with_float(x),
    "bool" => do_something_with_bool(x),
    _ => throw `I cannot work with ${type_of(x)}!!!`
}

Custom types

type_of() a custom type returns:

  • the registered name, if registered via Engine::register_type_with_name

  • the full Rust type name, if registered via Engine::register_type

struct TestStruct1;
struct TestStruct2;

engine
    // type_of(struct1) == "crate::path::to::module::TestStruct1"
    .register_type::<TestStruct1>()
    // type_of(struct2) == "MyStruct"
    .register_type_with_name::<TestStruct2>("MyStruct");

Dynamic Value Tag

Each Dynamic value can contain a tag that is i32 and can contain any arbitrary data.

On 32-bit targets, however, the tag is only i16.

The tag defaults to zero.

Value out of bounds

It is an error to set a tag to a value beyond the bounds of i32 (i16 on 32-bit targets).

Examples

let x = 42;

x.tag == 0;             // tag defaults to zero

x.tag = 123;            // set tag value

set_tag(x, 123);        // 'set_tag' function also works

x.tag == 123;           // get updated tag value

x.tag() == 123;         // method also works

tag(x) == 123;          // function call style also works

x.tag[3..5] = 2;        // tag can be used as a bit-field

x.tag[3..5] == 2;

let y = x;

y.tag == 123;           // the tag is copied across assignment

y.tag = 3000000000;     // runtime error: 3000000000 is too large for 'i32'

Practical Applications

Attaching arbitrary information together with a value has a lot of practical uses.

Identify code path

For example, it is easy to attach an ID number to a value to indicate how or why that value is originally set.

This is tremendously convenient for debugging purposes where it is necessary to figure out which code path a particular value went through.

After the script is verified, all tag assignment statements can simply be removed.

const ROUTE1 = 1;
const ROUTE2 = 2;
const ROUTE3 = 3;
const ERROR_ROUTE = 9;

fn some_complex_calculation(x) {
    let result;

    if some_complex_condition(x) {
        result = 42;
        result.tag = ROUTE1;        // record route #1
    } else if some_other_very_complex_condition(x) == 1 {
        result = 123;
        result.tag = ROUTE2;        // record route #2
    } else if some_non_understandable_calculation(x) > 0 {
        result = 0;
        result.tag = ROUTE3;        // record route #3
    } else {
        result = -1;
        result.tag = ERROR_ROUTE;   // record error
    }

    result  // this value now contains the tag
}

let my_result = some_complex_calculation(key);

// The code path that 'my_result' went through is now in its tag.

// It is now easy to trace how 'my_result' gets its final value.

print(`Result = ${my_result} and reason = ${my_result.tag}`);

Identify data source

It is convenient to use the tag value to record the source of a piece of data.

let x = [0, 1, 2, 3, 42, 99, 123];

// Store the index number of each value into its tag before
// filtering out all even numbers, leaving only odd numbers
let filtered = x.map(|v, i| { v.tag = i; v }).filter(|v| v.is_odd());

// The tag now contains the original index position

for (data, i) in filtered {
    print(`${i + 1}: Value ${data} from position #${data.tag + 1}`);
}

Identify code conditions

The tag value may also contain a bit-field of up to 32 (16 under 32-bit targets) individual bits, recording up to 32 (or 16 under 32-bit targets) logic conditions that contributed to the value.

Again, after the script is verified, all tag assignment statements can simply be removed.


fn some_complex_calculation(x) {
    let result = 42;

    // Check first condition
    if some_complex_condition() {
        result += 1;
        result.tag[0] = true;   // Set first bit in bit-field
    }

    // Check second condition
    if some_other_very_complex_condition(x) == 1 {
        result *= 10;
        result.tag[1] = true;   // Set second bit in bit-field
    }

    // Check third condition
    if some_non_understandable_calculation(x) > 0 {
        result -= 42;
        result.tag[2] = true;   // Set third bit in bit-field
    }

    // Check result
    if result > 100 {
        result = 0;
        result.tag[3] = true;   // Set forth bit in bit-field
    }
}

let my_result = some_complex_calculation(key);

// The tag of 'my_result' now contains a bit-field indicating
// the result of each condition.

// It is now easy to trace how 'my_result' gets its final value.
// Use indexing on the tag to get at individual bits.

print(`Result = ${my_result}`);
print(`First condition = ${my_result.tag[0]}`);
print(`Second condition = ${my_result.tag[1]}`);
print(`Third condition = ${my_result.tag[2]}`);
print(`Result check = ${my_result.tag[3]}`);

Return auxillary info

Sometimes it is useful to return auxillary info from a function.

// Verify Bell's Inequality by calculating a norm
// and comparing it with a hypotenuse.
// https://en.wikipedia.org/wiki/Bell%27s_theorem
//
// Returns the smaller of the norm or hypotenuse.
// Tag is 1 if norm <= hypo, 0 if otherwise.
fn bells_inequality(x, y, z) {
    let norm = sqrt(x ** 2 + y ** 2);
    let result;

    if norm <= z {
        result = norm;
        result.tag = 1;
    } else {
        result = z;
        result.tag = 0;
    }

    result
}

let dist = bells_inequality(x, y, z);

print(`Value = ${dist}`);

if dist.tag == 1 {
    print("Local realism maintained! Einstein rules!");
} else {
    print("Spooky action at a distance detected! Einstein will hate this...");
}

Poor-man’s tuples

Rust has tuples but Rhai does not (nor does JavaScript in this sense).

Similar to the JavaScript situation, practical alternatives using Rhai include returning an object map or an array.

Both of these alternatives, however, incur overhead that may be wasteful when the amount of additional information is small – e.g. in many cases, a single bool, or a small number.

To return a number of small values from functions, the tag value as a bit-field is an ideal container without resorting to a full-blown object map or array.

// This function essentially returns a tuple of four numbers:
// (result, a, b, c)
fn complex_calc(x, y, z) {
    let a = x + y;
    let b = x - y + z;
    let c = (a + b) * z / y;
    let r = do_complex_calculation(a, b, c);

    // Store 'a', 'b' and 'c' into tag if they are small
    r.tag[0..8] = a;
    r.tag[8..16] = b;
    r.tag[16..32] = c;

    r
}

// Deconstruct the tuple
let result = complex_calc(x, y, z);
let a = r.tag[0..8];
let b = r.tag[8..16];
let c = r.tag[16..32];

TL;DR

Tell me, really, what is the point?

Due to byte alignment requirements on modern CPU’s, there are unused spaces in a Dynamic type, of the order of 4 bytes on 64-bit targets (2 bytes on 32-bit).

It is empty space that can be put to good use and not wasted, especially when Rhai does not have built-in support of tuples in order to return multiple values from functions.

Serialization and Deserialization of Dynamic with serde

Rhai’s Dynamic type supports serialization and deserialization by serde via the serde feature.

Tip

Dynamic works both as a serialization format as well as a data type that is serializable.

Serialize/Deserialize a Dynamic

With the serde feature turned on, Dynamic implements serde::Serialize and serde::Deserialize, so it can easily be serialized and deserialized with serde.

let value: Dynamic = ...;

// Serialize 'Dynamic' to JSON
let json = serde_json::to_string(&value);

// Deserialize 'Dynamic' from JSON
let result: Dynamic = serde_json::from_str(&json);

Custom types are serialized as text strings of the value’s type name.

Dynamic as Serialization Format

A Dynamic can be seamlessly converted to and from any type that implements serde::Serialize and/or serde::Deserialize, acting as a serialization format.

Serialize Any Type to Dynamic

The function rhai::serde::to_dynamic automatically converts any Rust type that implements serde::Serialize into a Dynamic.

For primary types, this is usually not necessary because using Dynamic::from is much easier and is essentially the same thing. The only difference is treatment for integer values. Dynamic::from keeps different integer types intact, while rhai::serde::to_dynamic converts them all into INT (i.e. the system integer type which is i64 or i32 depending on the only_i32 feature).

Rust struct’s (or any type that is marked as a serde map) are converted into object maps while Rust Vec’s (or any type that is marked as a serde sequence) are converted into arrays.

While it is also simple to serialize a Rust type to JSON via serde, then use Engine::parse_json to convert it into an object map, rhai::serde::to_dynamic serializes it to Dynamic directly via serde without going through the JSON step.

use rhai::{Dynamic, Map};
use rhai::serde::to_dynamic;

#[derive(Debug, serde::Serialize)]
struct Point {
    x: f64,
    y: f64
}

#[derive(Debug, serde::Serialize)]
struct MyStruct {
    a: i64,
    b: Vec<String>,
    c: bool,
    d: Point
}

let x = MyStruct {
    a: 42,
    b: vec![ "hello".into(), "world".into() ],
    c: true,
    d: Point { x: 123.456, y: 999.0 }
};

// Convert the 'MyStruct' into a 'Dynamic'
let map: Dynamic = to_dynamic(x);

map.is::<Map>() == true;

Deserialize a Dynamic into Any Type

The function rhai::serde::from_dynamic automatically converts a Dynamic value into any Rust type that implements serde::Deserialize.

In particular, object maps are converted into Rust struct’s (or any type that is marked as a serde map) while arrays are converted into Rust Vec’s (or any type that is marked as a serde sequence).

use rhai::{Engine, Dynamic};
use rhai::serde::from_dynamic;

#[derive(Debug, serde::Deserialize)]
struct Point {
    x: f64,
    y: f64
}

#[derive(Debug, serde::Deserialize)]
struct MyStruct {
    a: i64,
    b: Vec<String>,
    c: bool,
    d: Point
}

let engine = Engine::new();

let result: Dynamic = engine.eval(
r#"
    {
        a: 42,
        b: [ "hello", "world" ],
        c: true,
        d: #{ x: 123.456, y: 999.0 }
    }
"#)?;

// Convert the 'Dynamic' object map into 'MyStruct'
let x: MyStruct = from_dynamic(&result)?;

Cannot deserialize shared values

A Dynamic containing a shared value cannot be deserialized. It will give a type error.

Use Dynamic::flatten to obtain a cloned copy before deserialization (if the value is not shared, it is simply returned and not cloned).

Shared values are turned off via the no_closure feature.

Tip: Lighter alternative

The serde crate is quite heavy.

If only simple JSON parsing (i.e. only deserialization) of a hash object into a Rhai object map is required, the Engine::parse_json method is available as a cheap alternative, but it does not provide the same level of correctness, nor are there any configurable options.

Tip: Working with BLOB’s

BLOB’s, or byte-arrays, are normally serialized and deserialized as simple arrays.

For higher efficiency, it is necessary to specify BLOB fields via the serde_bytes attribute from the serde_bytes crate.

use serde::{Deserialize, Serialize};

// Use 'serde_bytes' to serialize the data as Dynamic BLOB's
#[derive(Deserialize, Serialize)]
struct TypeWithBlobs<'a> {
    #[serde(with = "serde_bytes")]
    bytes: &'a [u8],

    #[serde(with = "serde_bytes")]
    byte_buf: Vec<u8>,
}

let blobs = from_dynamic::<TypeWithBlobs>(&blob)?;

// Use 'serde_bytes::Bytes' to get a slice to a stream of bytes
let bytes_ref: &[u8] = from_dynamic::<serde_bytes::Bytes>(&blob)?.as_ref();

// Use 'serde_bytes::ByteBuf' to get a 'Vec<u8>'
let bytes: Vec<u8> = from_dynamic::<serde_bytes::ByteBuf>(&blob)?.into_vec();

Numbers

Integers

Tip: Bit-fields

Integers can also be conveniently manipulated as bit-fields.

Integer numbers follow C-style format with support for decimal, binary (0b), octal (0o) and hex (0x) notations.

The default system integer type (also aliased to INT) is i64. It can be turned into i32 via the only_i32 feature.

Floating-Point Numbers

Tip: Notations

Both decimal and scientific notations can be used to represent floating-point numbers.

Floating-point numbers are also supported if not disabled with no_float.

The default system floating-point type is f64 (also aliased to FLOAT). It can be turned into f32 via the f32_float feature.

Decimal Numbers

When rounding errors cannot be accepted, such as in financial calculations, the decimal feature turns on support for the Decimal type, which is a fixed-precision floating-point number with no rounding errors.

Number Literals

_ separators can be added freely and are ignored within a number – except at the very beginning or right after a decimal point (.).

SampleFormatValue typeno_floatno_float + decimal
_123improper separator
123_345, -42decimalINTINTINT
0o07_76octalINTINTINT
0xab_cd_efhexINTINTINT
0b0101_1001binaryINTINTINT
123._456improper separator
123_456.78_9normal floating-pointFLOATsyntax errorDecimal
-42.ending with decimal pointFLOATsyntax errorDecimal
123_456_.789e-10scientific notationFLOATsyntax errorDecimal
.456missing leading 0
123.456e_10improper separator
123.e-10missing decimal 0

Warning – No Implicit Type Conversions

Unlike most C-like languages, Rhai does not provide implicit type conversions between different numeric types.

For example, a u8 is never implicitly converted to i64 when used as a parameter in a function call or as a comparison operand. f32 is never implicitly converted to f64.

This is exactly the same as Rust where all numeric types are distinct. Rhai is written in Rust afterall.

Warning

Integer variables pushed inside a custom Scope must be the correct type.

It is extremely easy to mess up numeric types since the Rust default integer type is i32 while for Rhai it is i64 (unless under only_i32).

use rhai::{Engine, Scope, INT};

let engine = Engine::new();

let mut scope = Scope::new();

scope.push("r", 42);            // 'r' is i32 (Rust default integer type)
scope.push("x", 42_u8);         // 'x' is u8
scope.push("y", 42_i64);        // 'y' is i64
scope.push("z", 42 as INT);     // 'z' is i64 (or i32 under 'only_i32')
scope.push("f", 42.0_f32);      // 'f' is f32

// Rhai integers are i64 (i32 under 'only_i32')
engine.eval::<String>("type_of(42)")? == "i64";

// false - i32 is never equal to i64
engine.eval_with_scope::<bool>(&mut scope, "r == 42")?;

// false - u8 is never equal to i64
engine.eval_with_scope::<bool>(&mut scope, "x == 42")?;

// true - i64 is equal to i64
engine.eval_with_scope::<bool>(&mut scope, "y == 42")?;

// true - INT is i64
engine.eval_with_scope::<bool>(&mut scope, "z == 42")?;

// false - f32 is never equal to f64
engine.eval_with_scope::<bool>(&mut scope, "f == 42.0")?;

Floating-Point vs. Decimal

Tip: no_float + decimal

When both no_float and decimal features are turned on, Decimal replaces the standard floating-point type.

Floating-point number literals in scripts parse to Decimal values.

Decimal (enabled via the decimal feature) represents a fixed-precision floating-point number which is popular with financial calculations and other usage scenarios where round-off errors are not acceptable.

Decimal takes up more space (16 bytes) than a standard FLOAT (4-8 bytes) and is much slower in calculations due to the lack of CPU hardware support. Use it only when necessary.

For most situations, the standard floating-point number type FLOAT (f64 or f32 with f32_float) is enough and is faster than Decimal.

It is possible to use both FLOAT and Decimal together with just the decimal feature – use parse_decimal or to_decimal to create a Decimal value.

Numeric Operators

Numeric operators generally follow C styles.

Unary Operators

OperatorDescription
+positive
-negative
let number = +42;

number = -5;

number = -5 - +5;

-(-42) == +42;      // two '-' equals '+'
                    // beware: '++' and '--' are reserved symbols

Binary Operators

OperatorDescriptionResult typeINTFLOATDecimal
+, +=plusnumericyesyes, also with INTyes, also with INT
-, -=minusnumericyesyes, also with INTyes, also with INT
*, *=multiplynumericyesyes, also with INTyes, also with INT
/, /=divide (integer division if acting on integer types)numericyesyes, also with INTyes, also with INT
%, %=modulo (remainder)numericyesyes, also with INTyes, also with INT
**, **=power/exponentiationnumericyesyes, also FLOAT**INTno
<<, <<=left bit-shiftnumericyesnono
>>, >>=right bit-shiftnumericyesnono
&, &=bit-wise Andnumericyesnono
|, |=bit-wise Ornumericyesnono
^, ^=bit-wise Xornumericyesnono
==equals toboolyesyes, also with INTyes, also with INT
!=not equals toboolyesyes, also with INTyes, also with INT
>greater thanboolyesyes, also with INTyes, also with INT
>=greater than or equals toboolyesyes, also with INTyes, also with INT
<less thanboolyesyes, also with INTyes, also with INT
<=less than or equals toboolyesyes, also with INTyes, also with INT
..exclusive rangerangeyesnono
..=inclusive rangerangeyesnono

Examples

let x = (1 + 2) * (6 - 4) / 2;  // arithmetic, with parentheses

let reminder = 42 % 10;         // modulo

let power = 42 ** 2;            // power

let left_shifted = 42 << 3;     // left shift

let right_shifted = 42 >> 3;    // right shift

let bit_op = 42 | 99;           // bit masking

Floating-Point Interoperates with Integers

When one of the operands to a binary arithmetic operator is floating-point, it works with INT for the other operand and the result is floating-point.

let x = 41.0 + 1;               // 'FLOAT' + 'INT'

type_of(x) == "f64";            // result is 'FLOAT'

let x = 21 * 2.0;               // 'FLOAT' * 'INT'

type_of(x) == "f64";

(x == 42) == true;              // 'FLOAT' == 'INT'

(10 < x) == true;               // 'INT' < 'FLOAT'

Decimal Interoperates with Integers

When one of the operands to a binary arithmetic operator is Decimal, it works with INT for the other operand and the result is Decimal.

let d = parse_decimal("2");

let x = d + 1;                  // 'Decimal' + 'INT'

type_of(x) == "decimal";        // result is 'Decimal'

let x = 21 * d;                 // 'Decimal' * 'INT'

type_of(x) == "decimal";

(x == 42) == true;              // 'Decimal' == 'INT'

(10 < x) == true;               // 'INT' < 'Decimal'

Unary Before Binary

In Rhai, unary operators take precedence over binary operators. This is especially important to remember when handling operators such as ** which in some languages bind tighter than the unary - operator.

-2 + 2 == 0;

-2 - 2 == -4;

-2 * 2 == -4;

-2 / 2 == -1;

-2 % 2 == 0;

-2 ** 2 = 4;            // means: (-2) ** 2
                        // in some languages this means: -(2 ** 2)

Numeric Functions

Integer Functions

The following standard functions (defined in the ArithmeticPackage but excluded if using a raw Engine) operate on integers only.

FunctionDescription
is_odd method and propertyreturns true if the value is an odd number, otherwise false
is_even method and propertyreturns true if the value is an even number, otherwise false

The following standard functions (defined in the BasicMathPackage but excluded if using a raw Engine) operate on integers only.

FunctionNot available underDescription
to_floatno_floatconvert the value into f64 (f32 under f32_float)
to_decimalnon-decimalconvert the value into Decimal

Signed Numeric Functions

The following standard functions (defined in the ArithmeticPackage but excluded if using a raw Engine) operate on i8, i16, i32, i64, f32, f64 and Decimal (requires decimal) only.

FunctionDescription
absabsolute value
signreturns (INT) −1 if negative, +1 if positive, 0 if zero
is_zero method and propertyreturns true if the value is zero, otherwise false

Floating-Point Functions

The following standard functions (defined in the BasicMathPackage but excluded if using a raw Engine) operate on f64 (f32 under f32_float) and Decimal (requires decimal) only.

CategorySupports DecimalFunctions
Trigonometryyessin, cos, tan
Trigonometrynosinh, cosh, tanh in radians, hypot(x,y)
Arc-trigonometrynoasin, acos, atan(v), atan(x,y), asinh, acosh, atanh in radians
Square rootyessqrt
Exponentialyesexp (base e)
Logarithmicyesln (base e)
Logarithmicyeslog (base 10)
Logarithmicnolog(x,base)
Roundingyesfloor, ceiling, round, int, fraction methods and properties
Conversionyesto_int, to_decimal (requires decimal), to_float (not under no_float)
Conversionnoto_degrees, to_radians
Testingnois_nan, is_finite, is_infinite methods and properties

Decimal Rounding Functions

The following rounding methods (defined in the BasicMathPackage but excluded if using a raw Engine) operate on Decimal only, which requires the decimal feature.

Rounding typeBehaviorMethods
Nonefloor, ceiling, int, fraction methods and properties
Banker’s roundinground to integerround method and property
Banker’s roundinground to specified number of decimal pointsround(decimal points)
Round upaway from zeroround_up(decimal points)
Round downtowards zeroround_down(decimal points)
Round half-upmid-point away from zeroround_half_up(decimal points)
Round half-downmid-point towards zeroround_half_down(decimal points)

Parsing Functions

The following standard functions (defined in the BasicMathPackage but excluded if using a raw Engine) parse numbers.

FunctionNo available underDescription
parse_intconverts a string to INT with an optional radix
parse_floatno_float and non-decimalconverts a string to FLOAT (Decimal under no_float and decimal)
parse_decimalnon-decimalconverts a string to Decimal

Formatting Functions

The following standard functions (defined in the BasicStringPackage but excluded if using a raw Engine) convert integer numbers into a string of hex, octal or binary representations.

FunctionDescription
to_binaryconverts an integer number to binary
to_octalconverts an integer number to octal
to_hexconverts an integer number to hex

These formatting functions are defined for all available integer numbers – i.e. INT, u8, i8, u16, i16, u32, i32, u64, i64, u128 and i128 unless disabled by feature flags.

Floating-point Constants

The following functions return standard mathematical constants.

FunctionDescription
PIreturns the value of π
Ereturns the value of e

Value Conversions

Convert Between Integer and Floating-Point

FunctionNot available underFrom typeTo type
to_intFLOAT, DecimalINT
to_floatno_floatINT, DecimalFLOAT
to_decimalnon-decimalINT, FLOATDecimal

That’s it; for other conversions, register custom conversion functions.

let x = 42;                     // 'x' is an integer

let y = x * 100.0;              // integer and floating-point can inter-operate

let y = x.to_float() * 100.0;   // convert integer to floating-point with 'to_float'

let z = y.to_int() + x;         // convert floating-point to integer with 'to_int'

let d = y.to_decimal();         // convert floating-point to Decimal with 'to_decimal'

let w = z.to_decimal() + x;     // Decimal and integer can inter-operate

let c = 'X';                    // character

print(`c is '${c}' and its code is ${c.to_int()}`); // prints "c is 'X' and its code is 88"

Parse String into Number

FunctionFrom typeTo type
parse_intstringINT
parse_int with radix 2-36stringINT (specified radix)
parse_float (not no_float)stringFLOAT
parse_float (no_float+decimal)stringDecimal
parse_decimal (requires decimal)stringDecimal
let x = parse_float("123.4");   // parse as floating-point
x == 123.4;
type_of(x) == "f64";

let x = parse_decimal("123.4"); // parse as Decimal value
type_of(x) == "decimal";

let x = 1234.to_decimal() / 10; // alternate method to create a Decimal value
type_of(x) == "decimal";

let dec = parse_int("42");      // parse as integer
dec == 42;
type_of(dec) == "i64";

let dec = parse_int("42", 10);  // radix = 10 is the default
dec == 42;
type_of(dec) == "i64";

let bin = parse_int("110", 2);  // parse as binary (radix = 2)
bin == 0b110;
type_of(bin) == "i64";

let hex = parse_int("ab", 16);  // parse as hex (radix = 16)
hex == 0xab;
type_of(hex) == "i64";

Format Numbers

FunctionFrom typeTo typeFormat
to_binaryINTstringbinary (i.e. only 1 and 0)
to_octalINTstringoctal (i.e. 07)
to_hexINTstringhex (i.e. 0f)
let x = 0x1234abcd;

x == 305441741;

x.to_string() == "305441741";

x.to_binary() == "10010001101001010101111001101";

x.to_octal() == "2215125715";

x.to_hex() == "1234abcd";

Ranges

Syntax

Numeric ranges can be constructed by the .. (exclusive) or ..= (inclusive) operators.

Exclusive range

start .. end

An exclusive range does not include the last (i.e. “end”) value.

The Rust type of an exclusive range is std::ops::Range<INT>.

type_of() an exclusive range returns "range".

Inclusive range

start ..= end

An inclusive range includes the last (i.e. “end”) value.

The Rust type of an inclusive range is std::ops::RangeInclusive<INT>.

type_of() an inclusive range returns "range=".

Usage Scenarios

Ranges are commonly used in the following scenarios.

ScenarioExample
for statementsfor n in 0..100 { ... }
in expressionsif n in 0..100 { ... }
switch expressionsswitch n { 0..100 => ... }
Bit-fields accesslet x = n[2..6];
Bits iterationfor bit in n.bits(2..=9) { ... }
Array range-based API’sarray.extract(2..8)
BLOB range-based API’sblob.parse_le_int(4..8)
String range-based API’sstring.sub_string(4..=12)
Characters iterationfor ch in string.bits(4..=12) { ... }
Custom typesmy_obj.action(3..=15, "foo");

Use as Parameter Type

Native Rust functions that take parameters of type std::ops::Range<INT> or std::ops::RangeInclusive<INT>, when registered into an Engine, accept ranges as arguments.

Different types

.. (exclusive range) and ..= (inclusive range) are different types to Rhai and they do not interoperate.

Two different versions of the same API must be registered to handle both range styles.

use std::ops::{Range, RangeInclusive};

/// The actual work function
fn do_work(obj: &mut TestStruct, from: i64, to: i64, inclusive: bool) {
    ...
}

let mut engine = Engine::new();

engine
    /// Version of API that accepts an exclusive range
    .register_fn("do_work", |obj: &mut TestStruct, range: Range<i64>|
        do_work(obj, range.start, range.end, false)
    )
    /// Version of API that accepts an inclusive range
    .register_fn("do_work", |obj: &mut TestStruct, range: RangeInclusive<i64>|
        do_work(obj, range.start(), range.end(), true)
    );

engine.run(
"
    let obj = new_ts();

    obj.do_work(0..12);         // use exclusive range

    obj.do_work(0..=11);        // use inclusive range
")?;

Indexers Using Ranges

Indexers commonly use ranges as parameters.

use std::ops::{Range, RangeInclusive};

let mut engine = Engine::new();

engine
    /// Version of indexer that accepts an exclusive range
    .register_indexer_get_set(
        |obj: &mut TestStruct, range: Range<i64>| -> bool { ... },
        |obj: &mut TestStruct, range: Range<i64>, value: bool| { ... },
    )
    /// Version of indexer that accepts an inclusive range
    .register_indexer_get_set(
        |obj: &mut TestStruct, range: RangeInclusive<i64>| -> bool { ... },
        |obj: &mut TestStruct, range: RangeInclusive<i64>, value: bool| { ... },
    );

engine.run(
"
    let obj = new_ts();

    let x = obj[0..12];         // use exclusive range

    obj[0..=11] = !x;           // use inclusive range
")?;

Built-in Functions

The following methods (mostly defined in the BasicIteratorPackage but excluded if using a raw Engine) operate on ranges.

FunctionParameter(s)Description
start method and propertybeginning of the range
end method and propertyend of the range
contains, in operatornumber to checkdoes this range contain the specified number?
is_inclusive method and propertyis the range inclusive?
is_exclusive method and propertyis the range exclusive?

TL;DR

What happened to the open-ended ranges?

Rust has open-ended ranges, such as start.., ..end and ..=end. They are not available in Rhai.

They are not needed because Rhai can overload functions.

Typically, an API accepting ranges as parameters would have equivalent versions that accept a starting position and a length (the standard start + len pair), as well as a versions that accept only the starting position (the length assuming to the end).

In fact, usually all versions redirect to a call to one single version.

For example, a naive implementation of the extract method for arrays (without any error handling) would look like:

use std::ops::{Range, RangeInclusive};

// Version with exclusive range
#[rhai_fn(name = "extract", pure)]
pub fn extract_range(array: &mut Array, range: Range<i64>) -> Array {
    array[range].to_vec()
}
// Version with inclusive range
#[rhai_fn(name = "extract", pure)]
pub fn extract_range2(array: &mut Array, range: RangeInclusive<i64>) -> Array {
    extract_range(array, range.start()..range.end() + 1)
}
// Version with start
#[rhai_fn(name = "extract", pure)]
pub fn extract_to_end(array: &mut Array, start: i64) -> Array {
    extract_range(array, start..start + array.len())
}
// Version with start+len
#[rhai_fn(name = "extract", pure)]
pub fn extract(array: &mut Array, start: i64, len: i64) -> Array {
    extract_range(array, start..start + len)
}

Therefore, there should always be a function that can do what open-ended ranges are intended for.

The left-open form (i.e. ..end and ..=end) is trivially replaced by using zero as the starting position with a length that corresponds to the end position (for ..end).

The right-open form (i.e. start..) is trivially replaced by the version taking a single starting position.

let x = [1, 2, 3, 4, 5];

x.extract(0..3);    // normal range argument
                    // copies 'x' from positions 0-2

x.extract(2);       // copies 'x' from position 2 onwards
                    // equivalent to '2..'

x.extract(0, 2);    // copies 'x' from beginning for 2 items
                    // equivalent to '..2'

Integer as Bit-Fields

Note

Nothing here cannot be done via standard bit-manipulation (i.e. shifting and masking).

Built-in support is more elegant and performant since it usually replaces a sequence of multiple steps.

Since bit-wise operators are defined on integer numbers, individual bits can also be accessed and manipulated via an indexing syntax.

If a bit is set (i.e. 1), the index access returns true.

If a bit is not set (i.e. 0), the index access returns false.

When a range is used, the bits within the range are shifted and extracted as an integer value.

Bit-fields are very commonly used in embedded systems which must squeeze data into limited memory. Built-in support makes handling them efficient.

Indexing an integer as a bit-field is disabled for the no_index feature.

Syntax

From Least-Significant Bit (LSB)

Bits in a bit-field are accessed with zero-based, non-negative integer indices:

integer [ index from 0 to 63 or 31 ]

integer [ index from 0 to 63 or 31 ] = true or false ;

Ranges can also be used:

integer [ start .. end ]
integer [ start ..= end ]

integer [ start .. end ] = new integer value ;
integer [ start ..= end ] = new integer value ;

Number of bits

The maximum bit number that can be accessed is 63 (or 31 under only_i32).

Bits outside of the range are ignored.

From Most-Significant Bit (MSB)

A negative index accesses a bit in the bit-field counting from the end, or from the most-significant bit, with −1 being the highest bit.

integer [ index from −1 to −64 or −32 ]

integer [ index from −1 to −64 or −32 ] = true or false ;

Ranges always count from the least-significant bit (LSB) and has no support for negative positions.

Number of bits

The maximum bit number that can be accessed is −64 (or −32 under only_i32).

Bit-Field Functions

The following standard functions (defined in the BitFieldPackage but excluded if using a raw Engine) operate on INT bit-fields.

These functions are available even under the no_index feature.

FunctionParameter(s)Description
get_bitbit number, counting from MSB if < 0returns the state of a bit: true if 1, false if 0
set_bit
  1. bit number, counting from MSB if < 0
  2. new state: true if 1, false if 0
sets the state of a bit
get_bits
  1. starting bit number, counting from MSB if < 0
  2. number of bits to extract, none if < 1, to MSB if ≥ length
extracts a number of bits, shifted towards LSB
get_bitsrange of bitsextracts a number of bits, shifted towards LSB
set_bits
  1. starting bit number, counting from MSB if < 0
  2. number of bits to set, none if < 1, to MSB if ≥ length
    3) new value
sets a number of bits from the new value
set_bits
  1. range of bits
  2. new value
sets a number of bits from the new value
bits method and property
  1. (optional) starting bit number, counting from MSB if < 0
  2. (optional) number of bits to extract, none if < 1, to MSB if ≥ length
allows iteration over the bits of a bit-field
bitsrange of bitsallows iteration over the bits of a bit-field

Example

// Assume the following bits fields in a single 16-bit word:
// ┌─────────┬────────────┬──────┬─────────┐
// │  15-12  │    11-4    │  3   │   2-0   │
// ├─────────┼────────────┼──────┼─────────┤
// │    0    │ 0-255 data │ flag │ command │
// └─────────┴────────────┴──────┴─────────┘

let value = read_start_hw_register(42);

let command = value.get_bits(0, 3);         // Command = bits 0-2

let flag = value[3];                        // Flag = bit 3

let data = value[4..=11];                   // Data = bits 4-11
let data = value.get_bits(4..=11);          // <- above is the same as this

let reserved = value.get_bits(-4);          // Reserved = last 4 bits

if reserved != 0 {
    throw reserved;
}

switch command {
    0 => print(`Data = ${data}`),
    1 => value[4..=11] = data / 2,
    2 => value[3] = !flag,
    _ => print(`Unknown: ${command}`)
}

Strings and Characters

Safety

Always limit the maximum length of strings.

String in Rhai contain any text sequence of valid Unicode characters.

Internally strings are stored in UTF-8 encoding.

type_of() a string returns "string".

String and Character Literals

String and character literals follow JavaScript-style syntax.

TypeQuotesEscapes?Continuation?Interpolation?
Normal string"..."yeswith \no
Multi-line literal string`...`nonowith ${...}
Character'...'yesnono

Tip: Building strings

Strings can be built up from other strings and types via the + operator (provided by the MoreStringPackage but excluded if using a raw Engine).

This is particularly useful when printing output.

Standard Escape Sequences

There is built-in support for Unicode (\uxxxx or \Uxxxxxxxx) and hex (\xxx) escape sequences for normal strings and characters.

Hex sequences map to ASCII characters, while \u maps to 16-bit common Unicode code points and \U maps the full, 32-bit extended Unicode code points.

Escape sequences are not supported for multi-line literal strings wrapped by back-ticks (`).

Escape sequenceMeaning
\\back-slash (\)
\ttab
\rcarriage-return (CR)
\nline-feed (LF)
\" or ""double-quote (")
\'single-quote (')
\xxxASCII character in 2-digit hex
\uxxxxUnicode character in 4-digit hex
\UxxxxxxxxUnicode character in 8-digit hex

Line Continuation

For a normal string wrapped by double-quotes ("), a back-slash (\) character at the end of a line indicates that the string continues onto the next line without any line-break.

Whitespace up to the indentation of the opening double-quote is ignored in order to enable lining up blocks of text.

Spaces are not added, so to separate one line with the next with a space, put a space before the ending back-slash (\) character.

let x = "hello, world!\
         hello world again! \
         this is the ""last"" time!!!";
// ^^^^^^ these whitespaces are ignored

// The above is the same as:
let x = "hello, world!hello world again! this is the \"last\" time!!!";

A string with continuation does not open up a new line. To do so, a new-line character must be manually inserted at the appropriate position.

let x = "hello, world!\n\
         hello world again!\n\
         this is the last time!!!";

// The above is the same as:
let x = "hello, world!\nhello world again!\nthis is the last time!!!";

Multi-Line Literal Strings

A string wrapped by a pair of back-tick (`) characters is interpreted literally, meaning that every single character that lies between the two back-ticks is taken verbatim. This include new-lines, whitespaces, escape characters etc.

let x = `hello, world! "\t\x42"
  hello world again! 'x'
     this is the last time!!! `;

// The above is the same as:
let x = "hello, world! \"\\t\\x42\"\n  hello world again! 'x'\n     this is the last time!!! ";

If a back-tick (`) appears at the end of a line, then it is understood that the entire text block starts from the next line; the starting new-line character is stripped.

let x = `
        hello, world! "\t\x42"
  hello world again! 'x'
     this is the last time!!!
`;

// The above is the same as:
let x = "        hello, world! \"\\t\\x42\"\n  hello world again! 'x'\n     this is the last time!!!\n";

To actually put a back-tick (`) character inside a multi-line literal string, use two back-ticks together (i.e. ``).

let x = `I have a quote " as well as a back-tick `` here.`;

// The above is the same as:
let x = "I have a quote \" as well as a back-tick ` here.";

String Interpolation

Multi-line literal strings support string interpolation wrapped in ${}.

Interpolation is not supported for normal string or character literals.

${} acts as a statements block and can contain anything that is allowed within a statements block, including another interpolated string! The last result of the block is taken as the value for interpolation.

Rhai uses to_string() to convert any value into a string, then physically joins all the sub-strings together.

For convenience, if any interpolated value is a BLOB, however, it is automatically treated as a UTF-8 encoded string. That is because it is rarely useful to interpolate a BLOB into a string, but extremely useful to be able to directly manipulate UTF-8 encoded text.

let x = 42;
let y = 123;

let s = `x = ${x} and y = ${y}.`;                   // <- interpolated string

let s = ("x = " + {x} + " and y = " + {y} + ".");   // <- de-sugars to this

s == "x = 42 and y = 123.";

let s = `
Undeniable logic:
1) Hello, ${let w = `${x} world`; if x > 1 { w += "s" } w}!
2) If ${y} > ${x} then it is ${y > x}!
`;

s == "Undeniable logic:\n1) Hello, 42 worlds!\n2) If 123 > 42 then it is true!\n";

let blob = blob(3, 0x21);

print(blob);                            // prints [212121]

print(`Data: ${blob}`);                 // prints "Data: !!!"
                                        // BLOB is treated as UTF-8 encoded string

print(`Data: ${blob.to_string()}`);     // prints "Data: [212121]"

Indexing

Strings can be indexed into to get access to any individual character. This is similar to many modern languages but different from Rust.

From beginning

Individual characters within a string can be accessed with zero-based, non-negative integer indices:

string [ index from 0 to (total number of characters − 1) ]

From end

A negative index accesses a character in the string counting from the end, with −1 being the last character.

string [ index from −1 to −(total number of characters) ]

Character indexing can be SLOW

Internally, a Rhai string is still stored compactly as a Rust UTF-8 string in order to save memory.

Therefore, getting the character at a particular index involves walking through the entire UTF-8 encoded bytes stream to extract individual Unicode characters, counting them on the way.

Because of this, indexing can be a slow procedure, especially for long strings. Along the same lines, getting the length of a string (which returns the number of characters, not bytes) can also be slow.

Examples

let name = "Bob";
let middle_initial = 'C';
let last = "Davis";

let full_name = `${name} ${middle_initial}. ${last}`;
full_name == "Bob C. Davis";

// String building with different types
let age = 42;
let record = `${full_name}: age ${age}`;
record == "Bob C. Davis: age 42";

// Unlike Rust, Rhai strings can be indexed to get a character
// (disabled with 'no_index')
let c = record[4];
c == 'C';

ts.s = record;                          // custom type properties can take strings

let c = ts.s[4];
c == 'C';

let c = ts.s[-4];                       // negative index counts from the end
c == 'e';

let c = "foo"[0];                       // indexing also works on string literals...
c == 'f';

let c = ("foo" + "bar")[5];             // ... and expressions returning strings
c == 'r';

// Escape sequences in strings
record += " \u2764\n";                  // escape sequence of '❤' in Unicode
record == "Bob C. Davis: age 42 ❤\n";  // '\n' = new-line

// Unlike Rust, Rhai strings can be directly modified character-by-character
// (disabled with 'no_index')
record[4] = '\x58'; // 0x58 = 'X'
record == "Bob X. Davis: age 42 ❤\n";

// Use 'in' to test if a substring (or character) exists in a string
"Davis" in record == true;
'X' in record == true;
'C' in record == false;

// Strings can be iterated with a 'for' statement, yielding characters
for ch in record {
    print(ch);
}

The ImmutableString Type

Why SmartString?

SmartString is used because many strings in scripts are short (fewer than 24 ASCII characters).

All strings in Rhai are implemented as ImmutableString, which is an alias to Rc<SmartString> (or Arc<SmartString> under the sync feature).

An ImmutableString is immutable (i.e. never changes) and therefore can be shared among many users. Cloning an ImmutableString is cheap since it only copies an immutable reference.

Modifying an ImmutableString causes it first to be cloned, and then the modification made to the copy. Therefore, direct string modifications are expensive.

Avoid String Parameters

ImmutableString should be used in place of String for function parameters because using String is very inefficient (the argument is cloned during every function call).

A alternative is to use &str which de-sugars to ImmutableString.

A function with the first parameter being &mut String does not match a string argument passed to it, which has type ImmutableString. In fact, &mut String is treated as an opaque custom type.

fn slow(s: String) -> i64 { ... }               // string is cloned each call

fn fast1(s: ImmutableString) -> i64 { ... }     // cloning 'ImmutableString' is cheap

fn fast2(s: &str) -> i64 { ... }                // de-sugars to above

fn bad(s: &mut String) { ... }                  // '&mut String' will not match string values

Differences from Rust Strings

Internally Rhai strings are stored as UTF-8 just like Rust (they are Rust Strings!), but nevertheless there are major differences.

In Rhai a string is semantically the same as an array of Unicode characters and can be directly indexed (unlike Rust).

This is similar to most other languages (e.g. JavaScript, C#) where strings are stored internally not as UTF-8 but as arrays of UCS-16 or UCS-32.

Individual characters within a Rhai string can also be replaced just as if the string is an array of Unicode characters.

In Rhai, there are also no separate concepts of String and &str (a string slice) as in Rust.

Performance considerations

Although Rhai exposes a string as a simple array of characters which can be directly indexed to get at a particular character, such convenient syntax is an illusion.

Internally the string is still stored in UTF-8 (native Rust Strings).

All indexing operations actually require walking through the entire UTF-8 string to find the offset of the particular character position, and therefore is much slower than the simple array indexing for other scripting languages.

This implementation detail is hidden from the user but has a performance implication.

Avoid large scale character-based processing of strings; instead, build an actual array of characters (via the split() method) which can then be manipulated efficiently.

Standard String Functions

The following standard methods (mostly defined in the MoreStringPackage but excluded if using a raw Engine) operate on strings (and possibly characters).

FunctionParameter(s)Description
len method and propertynonereturns the number of characters (not number of bytes) in the string
bytes method and propertynonereturns the number of bytes making up the UTF-8 string; for strings containing only ASCII characters, this is much faster than len
to_blob
(not available under no_index)
noneconverts the string into an UTF-8 encoded byte-stream and returns it as a BLOB.
to_chars
(not available under no_index)
nonesplits the string by individual characters, returning them as an array
getposition, counting from end if < 0gets the character at a certain position (() if the position is not valid)
set
  1. position, counting from end if < 0
  2. new character
sets a certain position to a new character (no effect if the position is not valid)
pad
  1. target length
  2. character/string to pad
pads the string with a character or a string to at least a specified length
append, += operatoritem to appendadds the display text of an item to the end of the string
removecharacter/string to removeremoves a character or a string from the string
pop(optional) number of characters to remove, none if ≤ 0, entire string if ≥ lengthremoves the last character (if no parameter) and returns it (() if empty); otherwise, removes the last number of characters and returns them as a string
clearnoneempties the string
truncatetarget lengthcuts off the string at exactly a specified number of characters
to_uppernoneconverts the string/character into upper-case as a new string/character and returns it
to_lowernoneconverts the string/character into lower-case as a new string/character and returns it
make_uppernoneconverts the string/character into upper-case
make_lowernoneconverts the string/character into lower-case
trimnonetrims the string of whitespace at the beginning and end
containscharacter/sub-string to search forchecks if a certain character or sub-string occurs in the string
starts_withstringreturns true if the string starts with a certain string
ends_withstringreturns true if the string ends with a certain string
index_of
  1. character/sub-string to search for
  2. (optional) start position, counting from end if < 0, end if ≥ length
returns the position that a certain character or sub-string occurs in the string, or −1 if not found
sub_string
  1. start position, counting from end if < 0
  2. (optional) number of characters to extract, none if ≤ 0, to end if omitted
extracts a sub-string
sub_stringrange of characters to extract, from beginning if ≤ 0, to end if ≥ lengthextracts a sub-string
split
(not available under no_index)
nonesplits the string by whitespaces, returning an array of string segments
split
(not available under no_index)
position to split at (in number of characters), counting from end if < 0, end if ≥ lengthsplits the string into two segments at the specified character position, returning an array of two string segments
split
(not available under no_index)
  1. delimiter character/string
  2. (optional) maximum number of segments, 1 if < 1
splits the string by the specified delimiter, returning an array of string segments
split_rev
(not available under no_index)
  1. delimiter character/string
  2. (optional) maximum number of segments, 1 if < 1
splits the string by the specified delimiter in reverse order, returning an array of string segments
crop
  1. start position, counting from end if < 0
  2. (optional) number of characters to retain, none if ≤ 0, to end if omitted
retains only a portion of the string
croprange of characters to retain, from beginning if ≤ 0, to end if ≥ lengthretains only a portion of the string
replace
  1. target character/sub-string
  2. replacement character/string
replaces a sub-string with another
chars method and property
  1. (optional) start position, counting from end if < 0
  2. (optional) number of characters to iterate, none if ≤ 0
allows iteration of the characters inside the string

Beware that functions that involve indexing into a string to get at individual characters, e.g. sub_string, require walking through the entire UTF-8 encoded bytes stream to extract individual Unicode characters and counting them, which can be slow for long strings.

Standard Operators

The following standard operators inter-operate between strings and/or characters.

When one (or both) of the operands is a character, it is first converted into a one-character string before running the operator.

OperatorDescription
+, +=character/string concatenation
-, -=remove character/sub-string from string
==equals to
!=not equals to
>greater than
>=greater than or equals to
<less than
<=less than or equals to

For convenience, when BLOB’s are appended to a string, it is treated as UTF-8 encoded data and automatically first converted into the appropriate string value.

That is because it is rarely useful to append a BLOB into a string, but extremely useful to be able to directly manipulate UTF-8 encoded text.

OperatorDescription
+, +=
(not available under no_index)
append a BLOB (as a UTF-8 encoded string) to a string

Examples

let full_name == " Bob C. Davis ";
full_name.len == 14;

full_name.trim();
full_name.len == 12;
full_name == "Bob C. Davis";

full_name.pad(15, '$');
full_name.len == 15;
full_name == "Bob C. Davis$$$";

let n = full_name.index_of('$');
n == 12;

full_name.index_of("$$", n + 1) == 13;

full_name.sub_string(n, 3) == "$$$";
full_name.sub_string(n..n+3) == "$$$";

full_name.truncate(6);
full_name.len == 6;
full_name == "Bob C.";

full_name.replace("Bob", "John");
full_name.len == 7;
full_name == "John C.";

full_name.contains('C') == true;
full_name.contains("John") == true;

full_name.crop(5);
full_name == "C.";

full_name.crop(0, 1);
full_name == "C";

full_name.clear();
full_name.len == 0;

Arrays

Safety

Always limit the maximum size of arrays.

Arrays are first-class citizens in Rhai.

All elements stored in an array are Dynamic, and the array can freely grow or shrink with elements added or removed.

The Rust type of a Rhai array is rhai::Array which is an alias to Vec<Dynamic>.

type_of() an array returns "array".

Arrays are disabled via the no_index feature.

Literal Syntax

Array literals are built within square brackets [] and separated by commas ,:

[ value, value,, value ]

[ value, value,, value , ] // trailing comma is OK

Element Access Syntax

From beginning

Like C, arrays are accessed with zero-based, non-negative integer indices:

array [ index position from 0 to (length−1) ]

From end

A negative position accesses an element in the array counting from the end, with −1 being the last element.

array [ index position from −1 to −length ]

Built-in Functions

The following methods (mostly defined in the BasicArrayPackage but excluded if using a raw Engine) operate on arrays.

FunctionParameter(s)Description
getposition, counting from end if < 0gets a copy of the element at a certain position (() if the position is not valid)
set
  1. position, counting from end if < 0
  2. new element
sets a certain position to a new value (no effect if the position is not valid)
push, += operator
  1. array
  2. element to append (not another array)
appends an element to the end
append, += operator
  1. array
  2. array to append
concatenates the second array to the end of the first
+ operator
  1. first array
  2. second array
concatenates the first array with the second
== operator
  1. first array
  2. second array
are two arrays the same (elements compared with the == operator, if defined)?
!= operator
  1. first array
  2. second array
are two arrays different (elements compared with the == operator, if defined)?
insert
  1. position, counting from end if < 0, end if ≥ length
  2. element to insert
inserts an element at a certain position
popnoneremoves the last element and returns it (() if empty)
shiftnoneremoves the first element and returns it (() if empty)
extract
  1. start position, counting from end if < 0, end if ≥ length
  2. (optional) number of items to extract, none if ≤ 0, to end if omitted
extracts a portion of the array into a new array
extractrange of items to extract, from beginning if ≤ 0, to end if ≥ lengthextracts a portion of the array into a new array
removeposition, counting from end if < 0removes an element at a particular position and returns it (() if the position is not valid)
reversenonereverses the array
len method and propertynonereturns the number of elements
pad
  1. target length
  2. element to pad
pads the array with an element to at least a specified length
clearnoneempties the array
truncatetarget lengthcuts off the array at exactly a specified length (discarding all subsequent elements)
choptarget lengthcuts off the head of the array, leaving the tail at exactly a specified length
split
  1. array
  2. position to split at, counting from end if < 0, end if ≥ length
splits the array into two arrays, starting from a specified position
drain
  1. function pointer to predicate (usually a closure), or the function name as a string
removes all items (returning them) that return true when called with the predicate function taking the following parameters:
  1. array item
  2. (optional) offset position
drain
  1. start position, counting from end if < 0, end if ≥ length
  2. number of items to remove, none if ≤ 0
removes a portion of the array, returning the removed items as a new array
drainrange of items to remove, from beginning if ≤ 0, to end if ≥ lengthremoves a portion of the array, returning the removed items as a new array
retain
  1. function pointer to predicate (usually a closure), or the function name as a string
removes all items (returning them) that do not return true when called with the predicate function taking the following parameters:
  1. array item
  2. (optional) offset position
retain
  1. start position, counting from end if < 0, end if ≥ length
  2. number of items to retain, none if ≤ 0
retains a portion of the array, removes all other items and returning them as a new array
retainrange of items to retain, from beginning if ≤ 0, to end if ≥ lengthretains a portion of the array, removes all other bytes and returning them as a new array
splice
  1. start position, counting from end if < 0, end if ≥ length
  2. number of items to remove, none if ≤ 0
  3. array to insert
replaces a portion of the array with another (not necessarily of the same length as the replaced portion)
splice
  1. range of items to remove, from beginning if ≤ 0, to end if ≥ length
  2. array to insert
replaces a portion of the array with another (not necessarily of the same length as the replaced portion)
filterfunction pointer to predicate (usually a closure), or the function name as a stringconstructs a new array with all items that return true when called with the predicate function taking the following parameters:
  1. array item
  2. (optional) offset position
contains, in operatorelement to finddoes the array contain an element? The == operator (if defined) is used to compare custom types
index_of
  1. element to find (not a function pointer)
  2. (optional) start position, counting from end if < 0, end if ≥ length
returns the position of the first item in the array that equals the supplied element (using the == operator, if defined), or −1 if not found
index_of
  1. function pointer to predicate (usually a closure), or the function name as a string
  2. (optional) start position, counting from end if < 0, end if ≥ length
returns the position of the first item in the array that returns true when called with the predicate function, or −1 if not found:
  1. array item
  2. (optional) offset position
dedup(optional) function pointer to predicate (usually a closure), or the function name as a string; if omitted, the == operator is used, if definedremoves all but the first of consecutive items in the array that return true when called with the predicate function (non-consecutive duplicates are not removed):
1st & 2nd parameters: two items in the array
mapfunction pointer to conversion function (usually a closure), or the function name as a stringconstructs a new array with all items mapped to the result of applying the conversion function taking the following parameters:
  1. array item
  2. (optional) offset position
reduce
  1. function pointer to accumulator function (usually a closure), or the function name as a string
  2. (optional) the initial value
reduces the array into a single value via the accumulator function taking the following parameters:
  1. accumulated value (() initially)
  2. array item
  3. (optional) offset position
reduce_rev
  1. function pointer to accumulator function (usually a closure), or the function name as a string
  2. (optional) the initial value
reduces the array (in reverse order) into a single value via the accumulator function taking the following parameters:
  1. accumulated value (() initially)
  2. array item
  3. (optional) offset position
somefunction pointer to predicate (usually a closure), or the function name as a stringreturns true if any item returns true when called with the predicate function taking the following parameters:
  1. array item
  2. (optional) offset position
allfunction pointer to predicate (usually a closure), or the function name as a stringreturns true if all items return true when called with the predicate function taking the following parameters:
  1. array item
  2. (optional) offset position
sortfunction pointer to a comparison function (usually a closure), or the function name as a stringsorts the array with a comparison function taking the following parameters:
  1. first item
  2. second item
    return value: INT < 0 if first < second, > 0 if first > second, 0 if first == second
sortnonesorts a homogeneous array containing only elements of the same comparable built-in type (INT, FLOAT, Decimal, string, character, bool, ())

Tip: Use custom types with arrays

To use a custom type with arrays, a number of functions need to be manually implemented, in particular the == operator in order to support the in operator which uses == (via the contains method) to compare elements.

See the section on custom types for more details.

Examples

let y = [2, 3];             // y == [2, 3]

let y = [2, 3,];            // y == [2, 3]

y.insert(0, 1);             // y == [1, 2, 3]

y.insert(999, 4);           // y == [1, 2, 3, 4]

y.len == 4;

y[0] == 1;
y[1] == 2;
y[2] == 3;
y[3] == 4;

(1 in y) == true;           // use 'in' to test if an item exists in the array

(42 in y) == false;         // 'in' uses the 'contains' function, which uses the
                            // '==' operator (that users can override)
                            // to check if the target item exists in the array

y.contains(1) == true;      // the above de-sugars to this

y[1] = 42;                  // y == [1, 42, 3, 4]

(42 in y) == true;

y.remove(2) == 3;           // y == [1, 42, 4]

y.len == 3;

y[2] == 4;                  // elements after the removed element are shifted

ts.list = y;                // arrays can be assigned completely (by value copy)

ts.list[1] == 42;

[1, 2, 3][0] == 1;          // indexing on array literal

[1, 2, 3][-1] == 3;         // negative position counts from the end

fn abc() {
    [42, 43, 44]            // a function returning an array
}

abc()[0] == 42;

y.push(4);                  // y == [1, 42, 4, 4]

y += 5;                     // y == [1, 42, 4, 4, 5]

y.len == 5;

y.shift() == 1;             // y == [42, 4, 4, 5]

y.chop(3);                  // y == [4, 4, 5]

y.len == 3;

y.pop() == 5;               // y == [4, 4]

y.len == 2;

for item in y {             // arrays can be iterated with a 'for' statement
    print(item);
}

y.pad(6, "hello");          // y == [4, 4, "hello", "hello", "hello", "hello"]

y.len == 6;

y.truncate(4);              // y == [4, 4, "hello", "hello"]

y.len == 4;

y.clear();                  // y == []

y.len == 0;

// The examples below use 'a' as the master array

let a = [42, 123, 99];

a.map(|v| v + 1);           // returns [43, 124, 100]

a.map(|v, i| v + i);        // returns [42, 124, 101]

a.filter(|v| v > 50);       // returns [123, 99]

a.filter(|v, i| i == 1);    // returns [123]

a.filter("is_odd");         // returns [123, 99]

a.filter(Fn("is_odd"));     // <- previous statement is equivalent to this...

a.filter(|v| is_odd(v));    // <- or this

a.some(|v| v > 50);         // returns true

a.some(|v, i| v < i);       // returns false

a.none(|v| v != 0);         // returns false

a.none(|v, i| v == i);      // returns true

a.all(|v| v > 50);          // returns false

a.all(|v, i| v > i);        // returns true

// Reducing - initial value provided directly
a.reduce(|sum, v| sum + v, 0) == 264;

// Reducing - initial value is '()'
a.reduce(
    |sum, v| if sum.type_of() == "()" { v } else { sum + v }
) == 264;

// Reducing - initial value has index position == 0
a.reduce(|sum, v, i|
    if i == 0 { v } else { sum + v }
) == 264;

// Reducing - initial value provided directly
a.reduce_rev(|sum, v| sum + v, 0) == 264;

// Reducing - initial value is '()'
a.reduce_rev(
    |sum, v| if sum.type_of() == "()" { v } else { sum + v }
) == 264;

// Reducing - initial value has index position == 0
a.reduce_rev(|sum, v, i|
    if i == 2 { v } else { sum + v }
) == 264;

// In-place modification

a.splice(1..=1, [1, 3, 2]); // a == [42, 1, 3, 2, 99]

a.extract(1..=3);           // returns [1, 3, 2]

a.sort(|x, y| y - x);       // a == [99, 42, 3, 2, 1]

a.sort();                   // a == [1, 2, 3, 42, 99]

a.drain(|v| v <= 1);        // a == [2, 3, 42, 99]

a.drain(|v, i| i ≥ 3);      // a == [2, 3, 42]

a.retain(|v| v > 10);       // a == [42]

a.retain(|v, i| i > 0);     // a == []

BLOB’s

Safety

Always limit the maximum size of arrays.

BLOB’s (Binary Large OBjects), used to hold packed arrays of bytes, have built-in support in Rhai.

A BLOB has no literal representation, but is created via the blob function, or simply returned as the result of a function call (e.g. generate_thumbnail_image that generates a thumbnail version of a large image as a BLOB).

All items stored in a BLOB are bytes (i.e. u8) and the BLOB can freely grow or shrink with bytes added or removed.

The Rust type of a Rhai BLOB is rhai::Blob which is an alias to Vec<u8>.

type_of() a BLOB returns "blob".

BLOB’s are disabled via the no_index feature.

Element Access Syntax

From beginning

Like arrays, BLOB’s are accessed with zero-based, non-negative integer indices:

blob [ index position from 0 to (length−1) ]

From end

A negative position accesses an element in the BLOB counting from the end, with −1 being the last element.

blob [ index position from −1 to −length ]

Byte values

The value of a particular byte in a BLOB is mapped to an INT (which can be 64-bit or 32-bit depending on the only_i32 feature).

Only the lowest 8 bits are significant, all other bits are ignored.

Create a BLOB

The function blob allows creating an empty BLOB, optionally filling it to a required size with a particular value (default zero).

let x = blob();             // empty BLOB

let x = blob(10);           // BLOB with ten zeros

let x = blob(50, 42);       // BLOB with 50x 42's

Tip: Initialize with byte stream

To quickly initialize a BLOB with a particular byte stream, the write_be method can be used to write eight bytes at a time (four under only_i32) in big-endian byte order.

If fewer than eight bytes are needed, remember to right-pad the number as big-endian byte order is used.

let buf = blob(12, 0);      // BLOB with 12x zeros

// Write eight bytes at a time, in big-endian order
buf.write_be(0, 8, 0xab_cd_ef_12_34_56_78_90);
buf.write_be(8, 8, 0x0a_0b_0c_0d_00_00_00_00);
                            //   ^^^^^^^^^^^ remember to pad unused bytes

print(buf);                 // prints "[abcdef1234567890 0a0b0c0d]"

buf[3] == 0x12;
buf[10] == 0x0c;

// Under 'only_i32', write four bytes at a time:
buf.write_be(0, 4, 0xab_cd_ef_12);
buf.write_be(4, 4, 0x34_56_78_90);
buf.write_be(8, 4, 0x0a_0b_0c_0d);

Writing ASCII Bytes

Non-ASCII

Non-ASCII characters (i.e. characters not within 1-127) are ignored.

For many embedded applications, it is necessary to encode an ASCII string as a byte stream.

Use the write_ascii method to write ASCII strings into any specific range within a BLOB.

The following is an example of a building a 16-byte command to send to an embedded device.

// Assume the following 16-byte command for an embedded device:
// ┌─────────┬───────────────┬──────────────────────────────────┬───────┐
// │    0    │       1       │              2-13                │ 14-15 │
// ├─────────┼───────────────┼──────────────────────────────────┼───────┤
// │ command │ string length │ ASCII string, max. 12 characters │  CRC  │
// └─────────┴───────────────┴──────────────────────────────────┴───────┘

let buf = blob(16, 0);      // initialize command buffer

let text = "foo & bar";     // text string to send to device

buf[0] = 0x42;              // command code
buf[1] = s.len();           // length of string

buf.write_ascii(2..14, text);   // write the string

let crc = buf.calc_crc();   // calculate CRC

buf.write_le(14, 2, crc);   // write CRC

print(buf);                 // prints "[4209666f6f202620 626172000000abcd]"
                            //          ^^ command code              ^^^^ CRC
                            //            ^^ string length
                            //              ^^^^^^^^^^^^^^^^^^^ foo & bar

device.send(buf);           // send command to device

What if I need UTF-8?

The write_utf8 function writes a string in UTF-8 encoding.

UTF-8, however, is not very common for embedded applications.

Built-in Functions

The following functions (mostly defined in the BasicBlobPackage but excluded if using a raw Engine) operate on BLOB’s.

FunctionsParameter(s)Description
blob constructor function
  1. (optional) initial length of the BLOB
  2. (optional) initial byte value
creates a new BLOB, optionally of a particular length filled with an initial byte value (default = 0)
to_arraynoneconverts the BLOB into an array of integers
getposition, counting from end if < 0gets a copy of the byte at a certain position (0 if the position is not valid)
set
  1. position, counting from end if < 0
  2. new byte value
sets a certain position to a new value (no effect if the position is not valid)
push, += operator
  1. BLOB
  2. byte to append
appends a byte to the end
append, += operator
  1. BLOB
  2. BLOB to append
concatenates the second BLOB to the end of the first
append, += operator
  1. BLOB
  2. string/character to append
concatenates a string or character (as UTF-8 encoded byte-stream) to the end of the BLOB
+ operator
  1. first BLOB
  2. second BLOB
concatenates the first BLOB with the second
== operator
  1. first BLOB
  2. second BLOB
are two BLOB’s the same?
!= operator
  1. first BLOB
  2. second BLOB
are two BLOB’s different?
insert
  1. position, counting from end if < 0, end if ≥ length
  2. byte to insert
inserts a byte at a certain position
popnoneremoves the last byte and returns it (0 if empty)
shiftnoneremoves the first byte and returns it (0 if empty)
extract
  1. start position, counting from end if < 0, end if ≥ length
  2. (optional) number of bytes to extract, none if ≤ 0
extracts a portion of the BLOB into a new BLOB
extractrange of bytes to extract, from beginning if ≤ 0, to end if ≥ lengthextracts a portion of the BLOB into a new BLOB
removeposition, counting from end if < 0removes a byte at a particular position and returns it (0 if the position is not valid)
reversenonereverses the BLOB byte by byte
len method and propertynonereturns the number of bytes in the BLOB
pad
  1. target length
  2. byte value to pad
pads the BLOB with a byte value to at least a specified length
clearnoneempties the BLOB
truncatetarget lengthcuts off the BLOB at exactly a specified length (discarding all subsequent bytes)
choptarget lengthcuts off the head of the BLOB, leaving the tail at exactly a specified length
contains, in operatorbyte value to finddoes the BLOB contain a particular byte value?
split
  1. BLOB
  2. position to split at, counting from end if < 0, end if ≥ length
splits the BLOB into two BLOB’s, starting from a specified position
drain
  1. start position, counting from end if < 0, end if ≥ length
  2. number of bytes to remove, none if ≤ 0
removes a portion of the BLOB, returning the removed bytes as a new BLOB
drainrange of bytes to remove, from beginning if ≤ 0, to end if ≥ lengthremoves a portion of the BLOB, returning the removed bytes as a new BLOB
retain
  1. start position, counting from end if < 0, end if ≥ length
  2. number of bytes to retain, none if ≤ 0
retains a portion of the BLOB, removes all other bytes and returning them as a new BLOB
retainrange of bytes to retain, from beginning if ≤ 0, to end if ≥ lengthretains a portion of the BLOB, removes all other bytes and returning them as a new BLOB
splice
  1. start position, counting from end if < 0, end if ≥ length
  2. number of bytes to remove, none if ≤ 0
  3. BLOB to insert
replaces a portion of the BLOB with another (not necessarily of the same length as the replaced portion)
splice
  1. range of bytes to remove, from beginning if ≤ 0, to end if ≥ length
  2. BLOB to insert
replaces a portion of the BLOB with another (not necessarily of the same length as the replaced portion)
parse_le_int
  1. start position, counting from end if < 0, end if ≥ length
  2. number of bytes to parse, 8 if > 8 (4 under only_i32), none if ≤ 0
parses an integer at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
parse_le_intrange of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under only_i32)parses an integer at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
parse_be_int
  1. start position, counting from end if < 0, end if ≥ length
  2. number of bytes to parse, 8 if > 8 (4 under only_i32), none if ≤ 0
parses an integer at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
parse_be_intrange of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under only_i32)parses an integer at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
parse_le_float
(not available under no_float)
  1. start position, counting from end if < 0, end if ≥ length
  2. number of bytes to parse, 8 if > 8 (4 under f32_float), none if ≤ 0
parses a floating-point number at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
parse_le_float
(not available under no_float)
range of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under f32_float)parses a floating-point number at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
parse_be_float
(not available under no_float)
  1. start position, counting from end if < 0, end if ≥ length
  2. number of bytes to parse, 8 if > 8 (4 under f32_float), none if ≤ 0
parses a floating-point number at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
parse_be_float
(not available under no_float)
range of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under f32_float)parses a floating-point number at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
write_le
  1. start position, counting from end if < 0, end if ≥ length
  2. number of bytes to write, 8 if > 8 (4 under only_i32 or f32_float), none if ≤ 0
  3. integer or floating-point value
writes a value at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
write_le
  1. range of bytes to write, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under only_i32 or f32_float)
  2. integer or floating-point value
writes a value at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
write_be
  1. start position, counting from end if < 0, end if ≥ length
  2. number of bytes to write, 8 if > 8 (4 under only_i32 or f32_float), none if ≤ 0
  3. integer or floating-point value
writes a value at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
write_be
  1. range of bytes to write, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under only_i32 or f32_float)
  2. integer or floating-point value
writes a value at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored)
write_utf8
  1. start position, counting from end if < 0, end if ≥ length
  2. number of bytes to write, none if ≤ 0, to end if ≥ length
  3. string to write
writes a string to the particular offset in UTF-8 encoding
write_utf8
  1. range of bytes to write, from beginning if ≤ 0, to end if ≥ length, to end if ≥ length
  2. string to write
writes a string to the particular offset in UTF-8 encoding
write_ascii
  1. start position, counting from end if < 0, end if ≥ length
  2. number of characters to write, none if ≤ 0, to end if ≥ length
  3. string to write
writes a string to the particular offset in 7-bit ASCII encoding (non-ASCII characters are skipped)
write_ascii
  1. range of bytes to write, from beginning if ≤ 0, to end if ≥ length, to end if ≥ length
  2. string to write
writes a string to the particular offset in 7-bit ASCII encoding (non-ASCII characters are skipped)

Object Maps

Safety

Always limit the maximum size of object maps.

Object maps are hash dictionaries. Properties are all Dynamic and can be freely added and retrieved.

The Rust type of a Rhai object map is rhai::Map. Currently it is an alias to BTreeMap<SmartString, Dynamic>.

type_of() an object map returns "map".

Object maps are disabled via the no_object feature.

Why SmartString?

SmartString is used because most object map properties are short (at least shorter than 23 characters) and ASCII-based, so they can usually be stored inline without incurring the cost of an allocation.

Why BTreeMap and not HashMap?

The vast majority of object maps contain just a few properties.

BTreeMap performs significantly better than HashMap when the number of entries is small.

Literal Syntax

Object map literals are built within braces #{} with name:value pairs separated by commas ,:

#{ property : value,, property : value }

#{ property : value,, property : value , } // trailing comma is OK

The property name can be a simple identifier following the same naming rules as variables, or a string literal without interpolation.

Property Access Syntax

Dot notation

The dot notation allows only property names that follow the same naming rules as variables.

object . property

Elvis notation

The Elvis notation is similar to the dot notation except that it returns () if the object itself is ().

// returns () if object is ()
object ?. property

// no action if object is ()
object ?. property = value ;

Index notation

The index notation allows setting/getting properties of arbitrary names (even the empty string).

object [ property ]

Non-existing property

Tip: Force error

It is possible to force Rhai to return an EvalAltResult:: ErrorPropertyNotFound via Engine:: set_fail_on_invalid_map_property.

Trying to read a non-existing property returns () instead of causing an error.

This is similar to JavaScript where accessing a non-existing property returns undefined.

Use the Elvis operator (?.) to short-circuit further processing if the object is ().

x.a.b.foo();        // <- error if 'x', 'x.a' or 'x.a.b' is ()

x.a.b = 42;         // <- error if 'x' or 'x.a' is ()

x?.a?.b?.foo();     // <- ok! returns () if 'x', 'x.a' or 'x.a.b' is ()

x?.a?.b = 42;       // <- ok even if 'x' or 'x.a' is ()

Built-in Functions

The following methods (defined in the BasicMapPackage but excluded if using a raw Engine) operate on object maps.

FunctionParameter(s)Description
getproperty namegets a copy of the value of a certain property (() if the property does not exist); behavior is not affected by Engine::fail_on_invalid_map_property
set
  1. property name
  2. new element
sets a certain property to a new value (property is added if not already exists)
lennonereturns the number of properties
clearnoneempties the object map
removeproperty nameremoves a certain property and returns it (() if the property does not exist)
+= operator, mixinsecond object mapmixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values)
+ operator
  1. first object map
  2. second object map
merges the first object map with the second
== operator
  1. first object map
  2. second object map
are the two object maps the same (elements compared with the == operator, if defined)?
!= operator
  1. first object map
  2. second object map
are the two object maps different (elements compared with the == operator, if defined)?
fill_withsecond object mapadds in all properties of the second object map that do not exist in the object map
contains, in operatorproperty namedoes the object map contain a property of a particular name?
keysnonereturns an array of all the property names (in random order), not available under no_index
valuesnonereturns an array of all the property values (in random order), not available under no_index
to_jsonnonereturns a JSON representation of the object map (() is mapped to null, all other data types must be supported by JSON)

Examples

let y = #{              // object map literal with 3 properties
    a: 1,
    bar: "hello",
    "baz!$@": 123.456,  // like JavaScript, you can use any string as property names...
    "": false,          // even the empty string!

    `hello`: 999,       // literal strings are also OK

    a: 42,              // <- syntax error: duplicated property name

    `a${2}`: 42,        // <- syntax error: property name cannot have string interpolation
};

y.a = 42;               // access via dot notation
y.a == 42;

y.baz!$@ = 42;          // <- syntax error: only proper variable names allowed in dot notation
y."baz!$@" = 42;        // <- syntax error: strings not allowed in dot notation
y["baz!$@"] = 42;       // access via index notation is OK

"baz!$@" in y == true;  // use 'in' to test if a property exists in the object map
("z" in y) == false;

ts.obj = y;             // object maps can be assigned completely (by value copy)
let foo = ts.list.a;
foo == 42;

let foo = #{ a:1, };    // trailing comma is OK

let foo = #{ a:1, b:2, c:3 }["a"];
let foo = #{ a:1, b:2, c:3 }.a;
foo == 1;

fn abc() {
    { a:1, b:2, c:3 }  // a function returning an object map
}

let foo = abc().b;
foo == 2;

let foo = y["a"];
foo == 42;

y.contains("a") == true;
y.contains("xyz") == false;

y.xyz == ();            // a non-existing property returns '()'
y["xyz"] == ();

y.len == ();            // an object map has no property getter function
y.len() == 3;           // method calls are OK

y.remove("a") == 1;     // remove property

y.len() == 2;
y.contains("a") == false;

for name in y.keys() {  // get an array of all the property names via 'keys'
    print(name);
}

for val in y.values() { // get an array of all the property values via 'values'
    print(val);
}

y.clear();              // empty the object map

y.len() == 0;

No Support for Property Getters

In order not to affect the speed of accessing properties in an object map, new property getters cannot be registered because they conflict with the syntax of property access.

A property getter function registered via Engine::register_get, for example, for a Map will never be found – instead, the property will be looked up in the object map.

Properties should be registered as methods instead:

map.len                 // access property 'len', returns '()' if not found

map.len()               // 'len' method - returns the number of properties

map.keys                // access property 'keys', returns '()' if not found

map.keys()              // 'keys' method - returns array of all property names

map.values              // access property 'values', returns '()' if not found

map.values()            // 'values' method - returns array of all property values

Parse an Object Map from JSON

Do It Without serde

Object map vs. JSON

A valid JSON object hash does not start with a hash character # while a Rhai object map does. That’s the only difference!

The syntax for an object map is extremely similar to the JSON representation of a object hash, with the exception of null values which can technically be mapped to ().

Use the Engine::parse_json method to parse a piece of JSON into an object map.

// JSON string - notice that JSON property names are always quoted
//               notice also that comments are acceptable within the JSON string
let json = r#"{
                "a": 1,                 // <- this is an integer number
                "b": true,
                "c": 123.0,             // <- this is a floating-point number
                "$d e f!": "hello",     // <- any text can be a property name
                "^^^!!!": [1,42,"999"], // <- value can be array or another hash
                "z": null               // <- JSON 'null' value
              }"#;

// Parse the JSON expression as an object map
// Set the second boolean parameter to true in order to map 'null' to '()'
let map = engine.parse_json(json, true)?;

map.len() == 6;       // 'map' contains all properties in the JSON string

// Put the object map into a 'Scope'
let mut scope = Scope::new();
scope.push("map", map);

let result = engine.eval_with_scope::<i64>(&mut scope, r#"map["^^^!!!"].len()"#)?;

result == 3;          // the object map is successfully used in the script

Warning: Must be object hash

The JSON text must represent a single object hash – i.e. must be wrapped within braces {}.

It cannot be a primitive type (e.g. number, string etc.). Otherwise it cannot be converted into an object map and a type error is returned.

Representation of numbers

JSON numbers are all floating-point while Rhai supports integers (INT) and floating-point (FLOAT) (except under no_float).

Most common generators of JSON data distinguish between integer and floating-point values by always serializing a floating-point number with a decimal point (i.e. 123.0 instead of 123 which is assumed to be an integer).

This style can be used successfully with Rhai object maps.

Sub-objects

Sub-objects are handled transparently by Engine::parse_json.

It is not necessary to replace { with #{ in order to fake a Rhai object map literal.

// JSON with sub-object 'b'.
let json = r#"{"a":1, "b":{"x":true, "y":false}}"#;

// 'parse_json' handles this just fine.
let map = engine.parse_json(json, false)?;

// 'map' contains two properties: 'a' and 'b'
map.len() == 2;

TL;DR

How is it done?

Internally, Engine::parse_json cheats by treating the JSON text as a Rhai script.

That is why it even supports comments and arithmetic expressions in the JSON text, although it is not a good idea to rely on non-standard JSON formats.

A token remap filter is used to convert { into #{ and null to ().

Use serde

Remember, Engine::parse_json is nothing more than a cheap alternative to true JSON parsing.

If strict correctness is needed, or for more configuration possibilities, turn on the serde feature to pull in serde which enables serialization and deserialization to/from multiple formats, including JSON.

Beware, though… the serde crate is quite heavy.

Special Support for OOP via Object Maps

See also

See the pattern on Simulating Object-Oriented Programming for more details.

Object maps can be used to simulate object-oriented programming (OOP) by storing data as properties and methods as properties holding function pointers.

If an object map’s property holds a function pointer, the property can simply be called like a normal method in method-call syntax.

This is a short-hand to avoid the more verbose syntax of using the call function keyword.

When a property holding a function pointer or a closure is called like a method, it is replaced as a method call on the object map itself.

let obj = #{
                data: 40,
                action: || this.data += x    // 'action' holds a closure
           };

obj.action(2);                               // calls the function pointer with 'this' bound to 'obj'

obj.call(obj.action, 2);                     // <- the above de-sugars to this

obj.data == 42;

// To achieve the above with normal function pointer call will fail.

fn do_action(map, x) { map.data += x; }      // 'map' is a copy

obj.action = Fn("do_action");

obj.action.call(obj, 2);                     // a copy of 'obj' is passed by value

obj.data == 42;                              // 'obj.data' is not changed

Timestamps

Timestamps are provided by the BasicTimePackage (excluded if using a raw Engine) via the timestamp function.

Timestamps are not available under no_std.

The Rust type of a timestamp is std::time::Instant (instant::Instant in WASM builds).

type_of() a timestamp returns "timestamp".

Built-in Functions

The following methods (defined in the BasicTimePackage but excluded if using a raw Engine) operate on timestamps.

FunctionParameter(s)Description
elapsed method and propertynonereturns the number of seconds since the timestamp
+ operatornumber of seconds to addreturns a new timestamp with a specified number of seconds added
+= operatornumber of seconds to addadds a specified number of seconds to the timestamp
- operatornumber of seconds to subtractreturns a new timestamp with a specified number of seconds subtracted
-= operatornumber of seconds to subtractsubtracts a specified number of seconds from the timestamp
- operator
  1. later timestamp
  2. earlier timestamp
returns the number of seconds between the two timestamps

The following methods are defined in the LanguageCorePackage but excluded if using a raw Engine.

FunctionNot available underParameter(s)Description
sleepno_stdnumber of seconds to sleepblocks the current thread for a specified number of seconds

Examples

let now = timestamp();

// Do some lengthy operation...

if now.elapsed > 30.0 {
    print("takes too long (over 30 seconds)!")
}

Keywords

The following are reserved keywords in Rhai.

Active keywordsReserved keywordsUsageInactive under feature
true, falseconstants
let, constvar, staticvariables
is_sharedshared valuesno_closure
istype checking
if, elsegoto, exitcontrol flow
switchmatch, caseswitching and matching
do, while, loop, until, for, in, continue, breaklooping
fn, private, is_def_fn, thispublic, protected, newfunctionsno_function
returnreturn values
throw, try, catchthrow/catch exceptions
import, export, asuse, with, module, package, supermodulesno_module
globalautomatic global moduleno_function, no_module
Fn, call, curryfunction pointers
spawn, thread, go, sync, async, await, yieldthreading/async
type_of, print, debug, eval, is_def_varspecial functions
default, void, null, nilspecial values

Warning

Keywords cannot become the name of a function or variable, even when they are disabled.

Statements

Statements are terminated by semicolons ; and they are mandatory, except for the last statement in a block (enclosed by {} pairs) where it can be omitted.

Semicolons can also be omitted for statement types that always end in a block – for example the if, while, for, loop and switch statements.

let a = 42;             // normal assignment statement
let a = foo(42);        // normal function call statement
foo < 42;               // normal expression as statement

let a = { 40 + 2 };     // 'a' is set to the value of the statement block, which is the value of the last statement
//              ^ the last statement does not require a terminating semicolon (but also works with it)
//                ^ semicolon required here to terminate the 'let' statement
//                  it is a syntax error without it, even though it ends with '}'
//                  that is because the 'let' statement doesn't end in a block

if foo { a = 42 }
//               ^ no need to terminate an if-statement with a semicolon
//                 that is because the 'if' statement ends in a block

4 * 10 + 2              // a statement which is just one expression - no ending semicolon is OK
                        // because it is the last statement of the whole block

Statement Block

Syntax

Statement blocks in Rhai are formed by enclosing zero or more statements within braces {}.

{ statement; statement;statement }

{ statement; statement;statement; } // trailing semi-colon is optional

Closed scope

A statement block forms a closed scope.

Any variable and/or constant defined within the block are removed outside the block, so are modules imported within the block.

let x = 42;
let y = 18;

{
    import "hello" as h;
    const HELLO = 99;
    let y = 0;

    h::greet();         // ok

    print(y + HELLO);   // prints 99 (y is zero)

        :    
        :    
}                       // <- 'HELLO' and 'y' go away here...

print(x + y);           // prints 60 (y is still 18)

print(HELLO);           // <- error: 'HELLO' not found

h::greet();             // <- error: module 'h' not found

Statement Expression

Differs from Rust

This is different from Rust where, if the last statement is terminated by a semicolon, the block’s return value defaults to ().

Like Rust, a statement can be used anywhere where an expression is expected.

These are called, for lack of a more creative name, “statement expressions.”

The last statement of a statement block is always the block’s return value when used as a statement, regardless of whether it is terminated by a semicolon or not.

If the last statement has no return value (e.g. variable definitions, assignments) then it is assumed to be ().

let x = {
    let foo = calc_something();
    let bar = foo + baz;
    bar.further_processing();       // <- this is the return value
};                                  // <- semicolon is needed here...

// The above is equivalent to:
let result;
{
    let foo = calc_something();
    let bar = foo + baz;
    result = bar.further_processing();
}
let x = result;

// Statement expressions can be inserted inside normal expressions
// to avoid duplicated calculations
let x = foo(bar) + { let v = calc(); process(v, v.len, v.abs) } + baz;

// The above is equivalent to:
let foo_result = foo(bar);
let calc_result;
{
    let v = calc();
    result = process(v, v.len, v.abs);  // <- avoid calculating 'v'
}
let x = foo_result + calc_result + baz;

// Statement expressions are also useful as function call arguments
// when side effects are desired
do_work(x, y, { let z = foo(x, y); print(z); z });
           // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
           //       statement expression

Statement expressions can be disabled via Engine::set_allow_statement_expression.

Variables

Valid Names

Tip: Unicode Standard Annex #31 identifiers

The unicode-xid-ident feature expands the allowed characters for variable names to the set defined by Unicode Standard Annex #31.

Variables in Rhai follow normal C naming rules – must contain only ASCII letters, digits and underscores _.

Character setDescription
AZUpper-case ASCII letters
azLower-case ASCII letters
09Digit characters
_Underscore character

However, unlike Rust, a variable name must also contain at least one ASCII letter, and an ASCII letter must come before any digits. In other words, the first character that is not an underscore _ must be an ASCII letter and not a digit.

Why this restriction?

To reduce confusion (and subtle bugs) because, for instance, _1 can easily be misread (or mistyped) as -1.

Rhai is dynamic without type checking, so there is no compiler to catch these typos.

Therefore, some names acceptable to Rust, like _, _42foo, _1 etc., are not valid in Rhai.

For example: c3po and _r2d2_ are valid variable names, but 3abc and ____49steps are not.

Variable names are case sensitive.

Variable names also cannot be the same as a keyword (active or reserved).

Avoid names longer than 11 letters on 32-Bit

Rhai uses SmartString which avoids allocations unless a string is over its internal limit (23 ASCII characters on 64-bit, but only 11 ASCII characters on 32-bit).

On 64-bit systems, most variable names are shorter than 23 letters, so this is unlikely to become an issue.

However, on 32-bit systems, take care to limit, where possible, variable names to within 11 letters. This is particularly true for local variables inside a hot loop, where they are created and destroyed in rapid succession.

// The following is SLOW on 32-bit
for my_super_loop_variable in array {
    print(`Super! ${my_super_loop_variable}`);
}

// Suggested revision:
for loop_var in array {
    print(`Super! ${loop_var}`);
}

Declare a Variable

Variables are declared using the let keyword.

Tip: No initial value

Variables do not have to be given an initial value. If none is provided, it defaults to ().

Variables are local

A variable defined within a statement block is local to that block.

Tip: is_def_var

Use is_def_var to detect if a variable is defined.

let x;              // ok - value is '()'
let x = 3;          // ok
let _x = 42;        // ok
let x_ = 42;        // also ok
let _x_ = 42;       // still ok

let _ = 123;        // <- syntax error: illegal variable name
let _9 = 9;         // <- syntax error: illegal variable name

let x = 42;         // variable is 'x', lower case
let X = 123;        // variable is 'X', upper case

print(x);           // prints 42
print(X);           // prints 123

{
    let x = 999;    // local variable 'x' shadows the 'x' in parent block

    print(x);       // prints 999
}

print(x);           // prints 42 - the parent block's 'x' is not changed

let x = 0;          // new variable 'x' shadows the old 'x'

print(x);           // prints 0

is_def_var("x") == true;

is_def_var("_x") == true;

is_def_var("y") == false;

Use Before Definition

By default, variables do not need to be defined before they are used.

If a variable accessed by a script is not defined previously within the same script, it is assumed to be provided via an external custom Scope passed to the Engine via the Engine::XXX_with_scope API.

let engine = Engine::new();

engine.run("print(answer)")?;       // <- error: variable 'answer' not found

// Create custom scope
let mut scope = Scope::new();

// Add variable to custom scope
scope.push("answer", 42_i64);

// Run with custom scope
engine.run_with_scope(&mut scope,
    "print(answer)"                 // <- prints 42
)?;

No Scope

If no Scope is used to evaluate the script (e.g. when using Engine::run instead of Engine::run_with_scope), an undefined variable causes a runtime error when accessed.

Strict Variables Mode

With Engine::set_strict_variables, it is possible to turn on Strict Variables mode.

When strict variables mode is active, accessing a variable not previously defined within the same script directly causes a parse error when compiling the script.

let x = 42;

print(x);           // prints 42

print(foo);         // <- parse error under strict variables mode:
                    //    variable 'foo' is undefined

Tip

Turn on strict variables mode if no Scope is to be provided for script evaluation runs. This way, variable access errors are caught during compile time instead of runtime.

Variable Shadowing

In Rhai, new variables automatically shadow existing ones of the same name. There is no error.

This behavior is consistent with Rust.

let x = 42;
let y = 123;

print(x);           // prints 42

let x = 88;         // <- 'x' is shadowed here

// At this point, it is no longer possible to access the
// original 'x' on the first line...

print(x);           // prints 88

let x = 0;          // <- 'x' is shadowed again

// At this point, it is no longer possible to access both
// previously-defined 'x'...

print(x);           // prints 0

{
    let x = 999;    // <- 'x' is shadowed in a block
    
    print(x);       // prints 999
}

print(x);           // prints 0 - shadowing within the block goes away

print(y);           // prints 123 - 'y' is not shadowed

Tip: Disable shadowing

Set Engine::set_allow_shadowing to false to turn variables shadowing off.

let x = 42;

let x = 123;        // <- syntax error: variable 'x' already defined
                    //    when variables shadowing is disallowed

Strict Variables Mode

Scope constants

Constants in the external Scope, when provided, count as definition.

By default, Rhai looks up access to variables from the enclosing block scope, working its way outwards until it reaches the top (global) level, then it searches the Scope (if any) that is passed into the Engine::eval_with_scope call.

Setting Engine::set_strict_variables to true turns on Strict Variables Mode, which requires that:

Within Strict Variables mode, any attempt to access a variable or module before definition/import results in a parse error.

This way, variable access errors (usually typos) are caught during compile time instead of runtime.

let x = 42;

let y = x * z;          // <- parse error under strict variables mode:
                        //    variable 'z' is not yet defined

let z = x + w;          // <- parse error under strict variables mode:
                        //    variable 'w' is undefined

foo::bar::baz();        // <- parse error under strict variables mode:
                        //    module 'foo' is not yet defined

fn test1() {
    foo::bar::baz();    // <- parse error under strict variables mode:
                        //    module 'foo' is defined
}

import "my_module" as foo;

foo::bar::baz();        // ok!

print(foo::xyz);        // ok!

let x = abc::def;       // <- parse error under strict variables mode:
                        //    module 'abc' is undefined

fn test2() {
    foo:bar::baz();     // ok!
}

TL;DR

Why isn’t there a Strict Functions mode?

Why can’t function calls be checked for validity as well?

Rust functions in Rhai can be overloaded. This means that multiple versions of the same Rust function can exist under the same name, each accepting different numbers and/or types of arguments.

While it is possible to check, at compile time, whether a variable has been previously declared, it is impossible to predict, at compile time, the types of arguments to function calls, unless the function in question takes no parameters.

Therefore, it is impossible to check, at compile time, whether a function call is valid given that the types of arguments are unknown until runtime. QED.

Not to mention that it is also impossible to check for a function called via a function pointer.

Variable Definition Filter

Although it is easy to disable variable shadowing via Engine::set_allow_shadowing, sometimes more fine-grained control is needed.

For example, it may be the case that not all variables shadowing must be disallowed, but that only a particular variable name needs to be protected and not others. Or only under very special circumstances.

Under this scenario, it is possible to provide a filter closure to the Engine via Engine::on_def_var that traps variable definitions (i.e. let or const statements) in a Rhai script.

The filter is called when a variable or constant is defined both during runtime and compilation.

let mut engine = Engine::new();

// Register a variable definition filter.
engine.on_def_var(|is_runtime, info, context| {
    match (info.name, info.is_const) {
        // Disallow defining 'MYSTIC_NUMBER' as a constant!
        ("MYSTIC_NUMBER", true) => Ok(false),
        // Disallow defining constants not at global level!
        (_, true) if info.nesting_level > 0 => Ok(false),
        // Throw any exception you like...
        ("hello", _) => Err(EvalAltResult::ErrorVariableNotFound(info.name.to_string(), Position::NONE).into()),
        // Return Ok(true) to continue with normal variable definition.
        _ => Ok(true)
    }
});

Function Signature

The function signature passed to Engine::on_def_var takes the following form.

Fn(is_runtime: bool, info: VarDefInfo, context: EvalContext) -> Result<bool, Box<EvalAltResult>>

where:

ParameterTypeDescription
is_runtimebooltrue if the variable definition event happens during runtime, false if during compilation
infoVarDefInfoinformation on the variable being defined
contextEvalContextthe current evaluation context

and VarDefInfo is a simple struct that contains the following fields:

FieldTypeDescription
name&strvariable name
is_constbooltrue if the definition is a const; false if it is a let
nesting_levelusizethe current nesting level; the global level is zero
will_shadowboolwill this variable shadow an existing variable of the same name?

and EvalContext is a type that encapsulates the current evaluation context.

Return value

The return value is Result<bool, Box<EvalAltResult>> where:

ValueDescription
Ok(true)normal variable definition should continue
Ok(false)throws a runtime or compilation error
Err(Box<EvalAltResult>)error that is reflected back to the Engine

Error during compilation

During compilation (i.e. when is_runtime is false), EvalAltResult::ErrorParsing is passed through as the compilation error.

All other errors map to ParseErrorType::ForbiddenVariable.

Variable Resolver

By default, Rhai looks up access to variables from the enclosing block scope, working its way outwards until it reaches the top (global) level, then it searches the Scope that is passed into the Engine::eval call.

There is a built-in facility for advanced users to hook into the variable resolution service and to override its default behavior.

To do so, provide a closure to the Engine via Engine::on_var.

let mut engine = Engine::new();

// Register a variable resolver.
engine.on_var(|name, index, context| {
    match name {
        "MYSTIC_NUMBER" => Ok(Some(42_i64.into())),
        // Override a variable - make it not found even if it exists!
        "DO_NOT_USE" => Err(EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::NONE).into()),
        // Silently maps 'chameleon' into 'innocent'.
        "chameleon" => context.scope().get_value("innocent").map(Some).ok_or_else(|| 
            EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::NONE).into()
        ),
        // Return Ok(None) to continue with the normal variable resolution process.
        _ => Ok(None)
    }
});

Benefits of using a variable resolver

  1. Avoid having to maintain a custom Scope with all variables regardless of need (because a script may not use them all).

  2. Short-circuit variable access, essentially overriding standard behavior.

  3. Lazy-load variables when they are accessed, not up-front. This benefits when the number of variables is very large, when they are timing-dependent, or when they are expensive to load.

  4. Rename system variables on a script-by-script basis without having to construct different Scope’s.

Returned values are constants

Variable values returned by a variable resolver are treated as constants.

This is to avoid needing a mutable reference to the underlying data provider which may not be possible to obtain.

To change these variables, better push them into a custom Scope instead of using a variable resolver.

Tip: Returning shared values

It is possible to return a shared value from a variable resolver.

This is one way to implement Mutable Global State.

Function Signature

The function signature passed to Engine::on_var takes the following form.

Fn(name: &str, index: usize, context: EvalContext) -> Result<Option<Dynamic>, Box<EvalAltResult>>

where:

ParameterTypeDescription
name&strvariable name
indexusizean offset from the bottom of the current Scope that the variable is supposed to reside.
Offsets start from 1, with 1 meaning the last variable in the current Scope. Essentially the correct variable is at position scope.len() - index.
If index is zero, then there is no pre-calculated offset position and a search through the current Scope must be performed.
contextEvalContextmutable reference to the current evaluation context

and EvalContext is a type that encapsulates the current evaluation context.

Return value

The return value is Result<Option<Dynamic>, Box<EvalAltResult>> where:

ValueDescription
Ok(None)normal variable resolution process should continue, i.e. continue searching through the Scope
Ok(Some(value))value (a Dynamic) of the variable, treated as a constant
Err(Box<EvalAltResult>)error that is reflected back to the Engine, normally EvalAltResult::ErrorVariableNotFound to indicate that the variable does not exist, but it can be any EvalAltResult.

Constants

Constants can be defined using the const keyword and are immutable.

const X;            // 'X' is a constant '()'

const X = 40 + 2;   // 'X' is a constant 42

print(X * 2);       // prints 84

X = 123;            // <- syntax error: constant modified

Tip: Naming

Constants follow the same naming rules as variables, but as a convention are often named with all-capital letters.

Manually Add Constant into Custom Scope

Tip: Singleton

A constant value holding a custom type essentially acts as a singleton.

It is possible to add a constant into a custom Scope via Scope::push_constant so it’ll be available to scripts running with that Scope.

use rhai::{Engine, Scope};

#[derive(Debug, Clone)]
struct TestStruct(i64);                                     // custom type

let mut engine = Engine::new();

engine
    .register_type_with_name::<TestStruct>("TestStruct")    // register custom type
    .register_get("value", |obj: &mut TestStruct| obj.0),   // property getter
    .register_fn("update_value",
        |obj: &mut TestStruct, value: i64| obj.0 = value    // mutating method
    );

let script =
"
    MY_NUMBER.update_value(42);
    print(MY_NUMBER.value);
";

let ast = engine.compile(script)?;

let mut scope = Scope::new();                               // create custom scope

scope.push_constant("MY_NUMBER", TestStruct(123_i64));      // add constant variable

// Beware: constant objects can still be modified via a method call!
engine.run_ast_with_scope(&mut scope, &ast)?;               // prints 42

// Running the script directly, as below, is less desirable because
// the constant 'MY_NUMBER' will be propagated and copied into each usage
// during the script optimization step
engine.run_with_scope(&mut scope, script)?;

Caveat – Constants Can be Modified via Rust

Tip: Plugin functions

In plugin functions, &mut parameters disallow constant values by default.

This is different from the Engine::register_XXX API.

However, if a plugin function is marked with #[export_fn(pure)] or #[rhai_fn(pure)], it is assumed pure (i.e. will not modify its arguments) and so constants are allowed.

A custom type stored as a constant cannot be modified via script, but can be modified via a registered Rust function that takes a first &mut parameter – because there is no way for Rhai to know whether the Rust function modifies its argument!

By default, native Rust functions with a first &mut parameter always allow constants to be passed to them. This is because using &mut can avoid unnecessary cloning of a custom type value, even though it is actually not modified – for example returning the size of a collection type.

In line with intuition, Rhai is smart enough to always pass a cloned copy of a constant as the first &mut argument if the function is called in normal function call style.

If it is called as a method, however, the Rust function will be able to modify the constant’s value.

Also, property setters and indexers are always assumed to mutate the first &mut parameter and so they always raise errors when passed constants by default.

// For the below, assume 'increment' is a Rust function with '&mut' first parameter

const X = 42;       // a constant

increment(X);       // call 'increment' in normal FUNCTION-CALL style
                    // since 'X' is constant, a COPY is passed instead

X == 42;            // value is 'X" is unchanged

X.increment();      // call 'increment' in METHOD-CALL style

X == 43;            // value of 'X' is changed!
                    // must use 'Dynamic::is_read_only' to check if parameter is constant

fn double() {
    this *= 2;      // function doubles 'this'
}

let y = 1;          // 'y' is not constant and mutable

y.double();         // double it...

y == 2;             // value of 'y' is changed as expected

X.double();         // since 'X' is constant, a COPY is passed to 'this'

X == 43;            // value of 'X' is unchanged by script

Implications on script optimization

Rhai assumes that constants are never changed, even via Rust functions.

This is important to keep in mind because the script optimizer by default does constant propagation as a operation.

If a constant is eventually modified by a Rust function, the optimizer will not see the updated value and will propagate the original initialization value instead.

Dynamic::is_read_only can be used to detect whether a Dynamic value is constant or not within a Rust function.

Automatic Global Module

When a constant is declared at global scope, it is added to a special module called global.

Functions can access those constants via the special global module.

Naturally, the automatic global module is not available under no_function nor no_module.

const CONSTANT = 42;        // this constant is automatically added to 'global'

{
    const INNER = 0;        // this constant is not at global level
}                           // <- it goes away here

fn foo(x) {
    x *= global::CONSTANT;  // ok! 'CONSTANT' exists in 'global'

    x * global::INNER       // <- error: constant 'INNER' not found in 'global'
}

Override global

It is possible to override the automatic global module by importing another module under the name global.

import "foo" as global;     // import a module as 'global'

const CONSTANT = 42;        // this constant is NOT added to 'global'

fn foo(x) {
    global::CONSTANT        // <- error: constant 'CONSTANT' not found in 'global'
}

Assignments

Value assignments to variables use the = symbol.

let foo = 42;

bar = 123 * 456 - 789;

x[1][2].prop = do_calculation();

The left-hand-side (LHS) of an assignment statement must be a valid l-value, which must be rooted in a variable, potentially extended via indexing or properties.

Assigning to invalid l-value

Expressions that are not valid l-values cannot be assigned to.

x = 42;                 // variable is an l-value

x[1][2][3] = 42         // variable indexing is an l-value

x.prop1.prop2 = 42;     // variable property is an l-value

foo(x) = 42;            // syntax error: function call is not an l-value

x.foo() = 42;           // syntax error: method call is not an l-value

(x + y) = 42;           // syntax error: binary expression is not an l-value

Compound Assignments

Compound assignments are assignments with a binary operator attached.

number += 8;            // number = number + 8

number -= 7;            // number = number - 7

number *= 6;            // number = number * 6

number /= 5;            // number = number / 5

number %= 4;            // number = number % 4

number **= 3;           // number = number ** 3

number <<= 2;           // number = number << 2

number >>= 1;           // number = number >> 1

number &= 0x00ff;       // number = number & 0x00ff;

number |= 0x00ff;       // number = number | 0x00ff;

number ^= 0x00ff;       // number = number ^ 0x00ff;

The Flexible +=

The the + and += operators are often overloaded to perform build-up operations for different data types.

Build strings

let my_str = "abc";

my_str += "ABC";
my_str += 12345;

my_str == "abcABC12345"

Concatenate arrays

let my_array = [1, 2, 3];

my_array += [4, 5];

my_array == [1, 2, 3, 4, 5];

Concatenate BLOB’s

let my_blob = blob(3, 0x42);

my_blob += blob(5, 0x89);

my_blob.to_string() == "[4242428989898989]";

Mix two object maps together

let my_obj = #{ a:1, b:2 };

my_obj += #{ c:3, d:4, e:5 };

my_obj == #{ a:1, b:2, c:3, d:4, e:5 };

Add seconds to timestamps

let now = timestamp();

now += 42.0;

(now - timestamp()).round() == 42.0;

Logic Operators

Comparison Operators

OperatorDescription
(x operator y)
x, y same type or are numericx, y different types
==x is equals to yerror if not definedfalse if not defined
!=x is not equals to yerror if not definedtrue if not defined
>x is greater than yerror if not definedfalse if not defined
>=x is greater than or equals to yerror if not definedfalse if not defined
<x is less than yerror if not definedfalse if not defined
<=x is less than or equals to yerror if not definedfalse if not defined

Comparison operators between most values of the same type are built in for all standard types.

Others are defined in the LogicPackage but excluded if using a raw Engine.

Floating-point numbers interoperate with integers

Comparing a floating-point number (FLOAT) with an integer is also supported.

42 == 42.0;         // true

42.0 == 42;         // true

42.0 > 42;          // false

42 >= 42.0;         // true

42.0 < 42;          // false

Decimal numbers interoperate with integers

Comparing a Decimal number with an integer is also supported.

let d = parse_decimal("42");

42 == d;            // true

d == 42;            // true

d > 42;             // false

42 >= d;            // true

d < 42;             // false

Strings interoperate with characters

Comparing a string with a character is also supported, with the character first turned into a string before performing the comparison.

'x' == "x";         // true

"" < 'a';           // true

'x' > "hello";      // false

Comparing different types defaults to false

Comparing two values of different data types defaults to false unless the appropriate operator functions have been registered.

The exception is != (not equals) which defaults to true. This is in line with intuition.

42 > "42";          // false: i64 cannot be compared with string

42 <= "42";         // false: i64 cannot be compared with string

let ts = new_ts();  // custom type

ts == 42;           // false: different types cannot be compared

ts != 42;           // true: different types cannot be compared

ts == ts;           // error: '==' not defined for the custom type

Safety valve: Comparing different numeric types has no default

Beware that the above default does NOT apply to numeric values of different types (e.g. comparison between i64 and u16, i32 and f64) – when multiple numeric types are used it is too easy to mess up and for subtle errors to creep in.

// Assume variable 'x' = 42_u16, 'y' = 42_u16 (both types of u16)

x == y;             // true: '==' operator for u16 is built-in

x == "hello";       // false: different non-numeric operand types default to false

x == 42;            // error: ==(u16, i64) not defined, no default for numeric types

42 == y;            // error: ==(i64, u16) not defined, no default for numeric types

Caution: Beware operators for custom types

Tip: Always the full set

It is strongly recommended that, when defining operators for custom types, always define the full set of six operators together, or at least the == and != pair.

Operators are completely separate from each other. For example:

  • != does not equal !(==)

  • > does not equal !(<=)

  • <= does not equal < plus ==

  • <= does not imply <

Therefore, if a custom type misses an operator definition, it simply raises an error or returns the default.

This behavior can be counter-intuitive.

let ts = new_ts();  // custom type with '<=' and '==' defined

ts <= ts;           // true: '<=' defined

ts < ts;            // error: '<' not defined, even though '<=' is

ts == ts;           // true: '==' defined

ts != ts;           // error: '!=' not defined, even though '==' is

Boolean Operators

Note

All boolean operators are built in for the bool data type.

OperatorDescriptionArityShort-circuits?
! (prefix)NOTunaryno
&&ANDbinaryyes
&ANDbinaryno
||ORbinaryyes
|ORbinaryno

Double boolean operators && and || short-circuit – meaning that the second operand will not be evaluated if the first one already proves the condition wrong.

Single boolean operators & and | always evaluate both operands.

a() || b();         // b() is not evaluated if a() is true

a() && b();         // b() is not evaluated if a() is false

a() | b();          // both a() and b() are evaluated

a() & b();          // both a() and b() are evaluated

Null-Coalescing Operator

OperatorDescriptionArityShort-circuits?
??Null-coalescebinaryyes

The null-coalescing operator (??) returns the first operand if it is not (), or the second operand if the first operand is ().

It short-circuits – meaning that the second operand will not be evaluated if the first operand is not ().

a ?? b              // returns 'a' if it is not (), otherwise 'b'

a() ?? b();         // b() is only evaluated if a() is ()

In Operator

Trivia

The in operator is simply syntactic sugar for a call to the contains function.

The in operator is used to check for containment – i.e. whether a particular collection data type contains a particular item.

42 in array;

array.contains(42);     // <- the above is equivalent to this

Built-in Support for Standard Data Types

Data typeCheck for
Numeric rangeinteger number
Arraycontained item
Object mapproperty name
Stringsub-string or character

Examples

let array = [1, "abc", 42, ()];

42 in array == true;                // check array for item

let map = #{
    foo: 42,
    bar: true,
    baz: "hello"
};

"foo" in map == true;               // check object map for property name

'w' in "hello, world!" == true;     // check string for character

"wor" in "hello, world" == true;    // check string for sub-string

42 in -100..100 == true;            // check range for number

Array Items Comparison

The default implementation of the in operator for arrays uses the == operator (if defined) to compare items.

== defaults to false

For a custom type, == defaults to false when comparing it with a value of of the same type.

See the section on Logic Operators for more details.

let ts = new_ts();                  // assume 'new_ts' returns a custom type

let array = [1, 2, 3, ts, 42, 999];
//                    ^^ custom type

42 in array == true;                // 42 cannot be compared with 'ts'
                                    // so it defaults to 'false'
                                    // because == operator is not defined

Custom Implementation of contains

The in operator maps directly to a call to a function contains with the two operands switched.

// This expression...
item in container

// maps to this...
contains(container, item)

// or...
container.contains(item)

Support for the in operator can be easily extended to other types by registering a custom binary function named contains with the correct parameter types.

let mut engine = Engine::new();

engine.register_type::<TestStruct>()
      .register_fn("new_ts", || TestStruct::new())
      .register_fn("contains", |ts: &mut TestStruct, item: i64| -> bool {
          // Remember the parameters are switched from the 'in' expression
          ts.contains(item)
      });

// Now the 'in' operator can be used for 'TestStruct' and integer

engine.run(
r#"
    let ts = new_ts();

    if 42 in ts {                   // this calls the 'contains' function
        print("I got 42!");
    }

    let err = "hello" in ts;        // <- runtime error: 'contains' not found
                                    //    for 'TestStruct' and string
"#)?;

If Statement

if statements follow C syntax.

if foo(x) {
    print("It's true!");
} else if bar == baz {
    print("It's true again!");
} else if baz.is_foo() {
    print("Yet again true.");
} else if foo(bar - baz) {
    print("True again... this is getting boring.");
} else {
    print("It's finally false!");
}

Braces are mandatory

Unlike C, the condition expression does not need to be enclosed in parentheses (), but all branches of the if statement must be enclosed within braces {}, even when there is only one statement inside the branch.

Like Rust, there is no ambiguity regarding which if clause a branch belongs to.

// Rhai is not C!
if (decision) print(42);
//            ^ syntax error, expecting '{'

If Expression

Like Rust, if statements can also be used as expressions, replacing the ? : conditional operators in other C-like languages.

Tip: Disable if expressions

if expressions can be disabled via Engine::set_allow_if_expression.

// The following is equivalent to C: int x = 1 + (decision ? 42 : 123) / 2;
let x = 1 + if decision { 42 } else { 123 } / 2;
x == 22;

let x = if decision { 42 }; // no else branch defaults to '()'
x == ();

Switch Statement

The switch statement allows matching on literal values, and it mostly follows Rust’s match syntax.

switch calc_secret_value(x) {
    1 => print("It's one!"),
    2 => {
        print("It's two!");
        print("Again!");
    }
    3 => print("Go!"),
    // _ is the default when no case matches. It must be the last case.
    _ => print(`Oops! Something's wrong: ${x}`)
}

Default Case

A default case (i.e. when no other cases match) can be specified with _.

Must be last

The default case must be the last case in the switch statement.

switch wrong_default {
    1 => 2,
    _ => 9,     // <- syntax error: default case not the last
    2 => 3,
    3 => 4,     // <- ending with extra comma is OK
}

Array and Object Map Literals Also Work

The switch expression can match against any literal, including array and object map literals.

// Match on arrays
switch [foo, bar, baz] {
    ["hello", 42, true] => ...,
    ["hello", 123, false] => ...,
    ["world", 1, true] => ...,
    _ => ...
}

// Match on object maps
switch map {
    #{ a: 1, b: 2, c: true } => ...,
    #{ a: 42, d: "hello" } => ...,
    _ => ...
}

Tip: Working with enums

Switching on arrays is very useful when working with Rust enums (see this section for more details).

Case Conditions

Similar to Rust, each case (except the default case at the end) can provide an optional condition that must evaluate to true in order for the case to match.

Unlike Rust, however, case conditions do not allow the case values to duplicate.

let result = switch calc_secret_value(x) {
    1 if some_external_condition(x, y, z) => 100,

    2 if x < foo => 200,
    2 if bar() => 999,      // <- syntax error: still cannot have duplicated cases

    3 => if CONDITION {     // <- put condition inside statement block for
        123                 //    duplicated cases
    } else {
        0
    }

    _ if CONDITION => 8888  // <- syntax error: default case cannot have condition
};

Range Cases

Because of their popularity, literal integer ranges can also be used as switch cases.

Numeric ranges are only searched when the switch value is itself an integer (i.e. they never match any other data types).

Must come after integer cases

Range cases must come after all integer cases.

let x = 42;

switch x {
    'x' => ...,             // no match: wrong data type

    1 => ...,               // <- specific integer cases are checked first
    2 => ...,               // <- but these do not match

    0..50 if x > 45 => ..., // no match: condition is 'false'

    -10..20 => ...,         // no match: not in range

    0..50 => ...,           // <- MATCH!!! duplicated range cases are OK

    30..100 => ...,         // no match: even though it is within range,
                            // the previous case matches first

    42 => ...,              // <- syntax error: integer cases cannot follow range cases
}

Tip: Ranges can overlap

When more then one range contain the switch value, the first one with a fulfilled condition (if any) is evaluated.

Numeric range cases are tried in the order that they appear in the original script.

Difference From if-else if Chain

Although a switch expression looks almost the same as an if-else if chain, there are subtle differences between the two.

Look-up Table vs x == y

A switch expression matches through hashing via a look-up table. Therefore, matching is very fast. Walking down an if-else if chain is much slower.

On the other hand, operators can be overloaded in Rhai, meaning that it is possible to override the == operator for integers such that x == y returns a different result from the built-in default.

switch expressions do not use the == operator for comparison; instead, they hash the data values and jump directly to the correct statements via a pre-compiled look-up table. This makes matching extremely efficient, but it also means that overloading the == operator will have no effect.

Therefore, in environments where it is desirable to overload the == operator for standard types – though it is difficult to think of valid scenarios where you’d want 1 == 1 to return something other than true – avoid using the switch expression.

Efficiency

Because the switch expression works through a look-up table, it is very efficient even for large number of cases; in fact, switching is an O(1) operation regardless of the size of the data and number of cases to match.

A long if-else if chain becomes increasingly slower with each additional case because essentially an O(n) linear scan is performed.

Switch Expression

Like if, switch also works as an expression.

Tip

This means that a switch expression can appear anywhere a regular expression can, e.g. as function call arguments.

Tip: Disable switch expressions

switch expressions can be disabled via Engine::set_allow_switch_expression.

let x = switch foo { 1 => true, _ => false };

func(switch foo {
    "hello" => 42,
    "world" => 123,
    _ => 0
});

// The above is somewhat equivalent to:

let x = if foo == 1 { true } else { false };

if foo == "hello" {
    func(42);
} else if foo == "world" {
    func(123);
} else {
    func(0);
}

While Loop

while loops follow C syntax.

Like C, continue can be used to skip to the next iteration, by-passing all following statements; break can be used to break out of the loop unconditionally.

Tip: Disable while loops

while loops can be disabled via Engine::set_allow_looping.

let x = 10;

while x > 0 {
    x -= 1;
    if x < 6 { continue; }  // skip to the next iteration
    print(x);
    if x == 5 { break; }    // break out of while loop
}

Do Loop

do loops have two opposite variants: dowhile and dountil.

Like the while loop, continue can be used to skip to the next iteration, by-passing all following statements; break can be used to break out of the loop unconditionally.

Tip: Disable do loops

do loops can be disabled via Engine::set_allow_looping.

let x = 10;

do {
    x -= 1;
    if x < 6 { continue; }  // skip to the next iteration
    print(x);
    if x == 5 { break; }    // break out of do loop
} while x > 0;


do {
    x -= 1;
    if x < 6 { continue; }  // skip to the next iteration
    print(x);
    if x == 5 { break; }    // break out of do loop
} until x == 0;

Infinite Loop

Infinite loops follow Rust syntax.

Like Rust, continue can be used to skip to the next iteration, by-passing all following statements; break can be used to break out of the loop unconditionally.

Tip: Disable loop

loop can be disabled via Engine::set_allow_looping.

let x = 10;

loop {
    x -= 1;

    if x > 5 { continue; }  // skip to the next iteration

    print(x);

    if x == 0 { break; }    // break out of loop
}

Remember the break statement

A loop statement without a break statement inside its loop block is infinite. There is no way for the loop to stop iterating.

For Loop

Iterating through a numeric range or an array, or any type with a registered type iterator, is provided by the forin loop.

There are two alternative syntaxes, one including a counter variable:

for variable in expression {}

for ( variable , counter ) in expression {}

Tip: Disable for loops

for loops can be disabled via Engine::set_allow_looping.

Counter Variable

The counter variable, if specified, starts from zero, incrementing upwards.

let a = [42, 123, 999, 0, true, "hello", "world!", 987.6543];

// Loop through the array
for (item, count) in a {
    if x.type_of() == "string" {
        continue;                   // skip to the next iteration
    }

    // 'item' contains a copy of each element during each iteration
    // 'count' increments (starting from zero) for each iteration
    print(`Item #${count + 1} = ${item}`);

    if x == 42 { break; }           // break out of for loop
}

Break or Continue

Like C, continue can be used to skip to the next iteration, by-passing all following statements; break can be used to break out of the loop unconditionally.

Iterate Through Arrays

Iterating through an array yields cloned copies of each element.

let a = [1, 3, 5, 7, 9, 42];

// Loop through the array
for x in a {
    if x > 10 { continue; }         // skip to the next iteration

    print(x);

    if x == 42 { break; }           // break out of for loop
}

Iterate Through Strings

Iterating through a string yields individual characters.

The chars method also allow iterating through characters in a string, optionally accepting the character position to start from (counting from the end if negative), as well as the number of characters to iterate (defaults to all).

char also accepts a range which can be created via the .. (exclusive) and ..= (inclusive) operators.

let s = "hello, world!";

// Iterate through all the characters.
for ch in s {
    print(ch);
}

// Iterate starting from the 3rd character and stopping at the 7th.
for ch in s.chars(2, 5) {
    if ch > 'z' { continue; }       // skip to the next iteration

    print(ch);

    if x == '@' { break; }          // break out of for loop
}

// Iterate starting from the 3rd character and stopping at the end.
for ch in s.chars(2..s.len) {
    if ch > 'z' { continue; }       // skip to the next iteration

    print(ch);

    if x == '@' { break; }          // break out of for loop
}

Iterate Through Numeric Ranges

Ranges are created via the .. (exclusive) and ..= (inclusive) operators.

The range function similarly creates exclusive ranges, plus allowing optional step values.

// Iterate starting from 0 and stopping at 49
// The step is assumed to be 1 when omitted for integers
for x in 0..50 {
    if x > 10 { continue; }         // skip to the next iteration

    print(x);

    if x == 42 { break; }           // break out of for loop
}

// The 'range' function is just the same
for x in range(0, 50) {
    if x > 10 { continue; }         // skip to the next iteration

    print(x);

    if x == 42 { break; }           // break out of for loop
}

// The 'range' function also takes a step
for x in range(0, 50, 3) {          // step by 3
    if x > 10 { continue; }         // skip to the next iteration

    print(x);

    if x == 42 { break; }           // break out of for loop
}

// The 'range' function can also step backwards
for x in range(50, 0, -3) {         // step down by -3
    if x < 10 { continue; }         // skip to the next iteration

    print(x);

    if x == 42 { break; }           // break out of for loop
}

// It works also for floating-point numbers
for x in range(5.0, 0.0, -2.0) {    // step down by -2.0
    if x < 10 { continue; }         // skip to the next iteration

    print(x);

    if x == 4.2 { break; }          // break out of for loop
}

Iterate Through Bit-Fields

The bits function allows iterating through an integer as a bit-field.

bits optionally accepts the bit number to start from (counting from the most-significant-bit if negative), as well as the number of bits to iterate (defaults all).

bits also accepts a range which can be created via the .. (exclusive) and ..= (inclusive) operators.

let x = 0b_1001110010_1101100010_1100010100;
let num_on = 0;

// Iterate through all the bits
for bit in x.bits() {
    if bit { num_on += 1; }
}

print(`There are ${num_on} bits turned on!`);

const START = 3;

// Iterate through all the bits from 3 through 12
for (bit, index) in x.bits(START, 10) {
    print(`Bit #${index} is ${if bit { "ON" } else { "OFF" }}!`);

    if index >= 7 { break; }        // break out of for loop
}

// Iterate through all the bits from 3 through 12
for (bit, index) in x.bits(3..=12) {
    print(`Bit #${index} is ${if bit { "ON" } else { "OFF" }}!`);

    if index >= 7 { break; }        // break out of for loop
}

Iterate Through Object Maps

Two methods, keys and values, return arrays containing cloned copies of all property names and values of an object map, respectively.

These arrays can be iterated.

let map = #{a:1, b:3, c:5, d:7, e:9};

// Property names are returned in unsorted, random order
for x in map.keys() {
    if x > 10 { continue; }         // skip to the next iteration

    print(x);

    if x == 42 { break; }           // break out of for loop
}

// Property values are returned in unsorted, random order
for val in map.values() {
    print(val);
}

Make a Custom Type Iterable

Built-in type iterators

Type iterators are already defined for built-in standard types such as strings, ranges, bit-fields, arrays and object maps.

That’s why they can be used with the for loop.

If a custom type is iterable, the for loop can be used to iterate through its items in sequence, as long as it has a type iterator registered.

Engine::register_iterator<T> allows registration of a type iterator for any type that implements IntoIterator.

With a type iterator registered, the custom type can be iterated through.

// Custom type
#[derive(Debug, Clone)]
struct TestStruct { fields: Vec<i64> }

// Implement 'IntoIterator' trait
impl IntoIterator<Item = i64> for TestStruct {
    type Item = i64;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.fields.into_iter()
    }
}

let mut engine = Engine::new();

// Register API and type iterator for 'TestStruct'
engine.register_type_with_name::<TestStruct>("TestStruct")
      .register_fn("new_ts", || TestStruct { fields: vec![1, 2, 3, 42] })
      .register_iterator::<TestStruct>();

engine.run(
"
    // 'TestStruct' is iterable
    let ts = new_ts();

    // Use 'for' statement to loop through items in 'ts'
    for value in ts {
        ...
    }
")?;

Return Value

The return statement is used to immediately stop evaluation and exist the current context (typically a function call) yielding a return value.

return;             // equivalent to return ();

return 123 + 456;   // returns 579

A return statement at global level stops the entire script evaluation, the return value is taken as the result of the script evaluation.

A return statement inside a function call exits with a return value to the caller.

Throw Exception on Error

All Engine evaluation API methods return Result<T, Box<rhai::EvalAltResult>> with EvalAltResult holding error information.

To deliberately return an error, use the throw keyword.

if some_bad_condition_has_happened {
    throw error;    // 'throw' any value as the exception
}

throw;              // defaults to '()'

Exceptions thrown via throw in the script can be captured in Rust by matching EvalAltResult::ErrorRuntime(value, position) with the exception value captured by value.

let result = engine.eval::<i64>(
"
    let x = 42;

    if x > 0 {
        throw x;
    }
").expect_err();

println!("{}", result);     // prints "Runtime error: 42 (line 5, position 15)"

Catch a Thrown Exception

It is possible to catch an exception instead of having it abort the evaluation of the entire script via the trycatch statement common to many C-like languages.

fn code_that_throws() {
    throw 42;
}

try
{
    code_that_throws();
}
catch (err)         // 'err' captures the thrown exception value
{
    print(err);     // prints 42
}

Catch Exceptions

When an exception is thrown via a throw statement, evaluation of the script halts and the Engine returns with EvalAltResult::ErrorRuntime containing the exception value thrown.

It is possible, via the trycatch statement, to catch exceptions, optionally with an error variable.

try {} catch {}

try {} catch ( error variable ) {}

// Catch an exception and capturing its value
try
{
    throw 42;
}
catch (err)         // 'err' captures the thrown exception value
{
    print(err);     // prints 42
}

// Catch an exception without capturing its value
try
{
    print(42/0);    // deliberate divide-by-zero exception
}
catch               // no error variable - exception value is discarded
{
    print("Ouch!");
}

// Exception in the 'catch' block
try
{
    print(42/0);    // throw divide-by-zero exception
}
catch
{
    print("You seem to be dividing by zero here...");

    throw "die";    // a 'throw' statement inside a 'catch' block
                    // throws a new exception
}

Tip: Re-throw exception

Like the trycatch syntax in most languages, it is possible to re-throw an exception within the catch block simply by another throw statement without a value.

try
{
    // Call something that will throw an exception...
    do_something_bad_that_throws();
}
catch
{
    print("Oooh! You've done something real bad!");

    throw;          // 'throw' without a value within a 'catch' block
                    // re-throws the original exception
}

Catchable exceptions

Many script-oriented exceptions can be caught via trycatch.

Error typeError value
Runtime error thrown by a throw statementvalue in throw statement
Arithmetic errorobject map
Variable not foundobject map
Function not foundobject map
Module not foundobject map
Unbound thisobject map
Data type mismatchobject map
Assignment to a calculated/constant valueobject map
Array/string/bit-field indexing out-of-boundsobject map
Indexing with an inappropriate data typeobject map
Error in property accessobject map
for statement on a type without a type iteratorobject map
Data race detectedobject map
Other runtime errorobject map

The error value in the catch clause is an object map containing information on the particular error, including its type, line and character position (if any), and source etc.

When the no_object feature is turned on, however, the error value is a simple string description.

Non-catchable exceptions

Some exceptions cannot be caught.

Error typeNotes
System error – e.g. script file not foundsystem errors are not recoverable
Syntax error during parsinginvalid script
Custom syntax mismatch errorincompatible Engine instance
Script evaluation metrics exceeding limitssafety protection
Script evaluation manually terminatedsafety protection

Functions

Rhai supports defining functions in script, with a syntax that is very similar to Rust without types.

fn add(x, y) {
    x + y
}

fn sub(x, y,) {     // trailing comma in parameters list is OK
    x - y
}

add(2, 3) == 5;

sub(2, 3,) == -1;   // trailing comma in arguments list is OK

Tip: Disable functions

Defining functions can be disabled via the no_function feature.

Tip: is_def_fn

Use is_def_fn (not available under no_function) to detect if a Rhai function is defined (and therefore callable) based on its name and the number of parameters (arity).

fn foo(x) { x + 1 }

is_def_fn("foo", 1) == true;

is_def_fn("foo", 0) == false;

is_def_fn("foo", 2) == false;

is_def_fn("bar", 1) == false;

Implicit Return

Just like in Rust, an implicit return can be used. In fact, the last statement of a block is always the block’s return value regardless of whether it is terminated with a semicolon ;. This is different from Rust.

fn add(x, y) {      // implicit return:
    x + y;          // value of the last statement (no need for ending semicolon)
                    // is used as the return value
}

fn add2(x) {
    return x + 2;   // explicit return
}

add(2, 3) == 5;

add2(42) == 44;

Global Definitions Only

Functions can only be defined at the global level, never inside a block or another function.

Again, this is different from Rust.

// Global level is OK
fn add(x, y) {
    x + y
}

// The following will not compile
fn do_addition(x) {
    fn add_y(n) {   // <- syntax error:  cannot define inside another function
        n + y
    }

    add_y(x)
}

No Access to External Scope

Functions are not closures. They do not capture the calling environment and can only access their own parameters.

They cannot access variables external to the function itself.

let x = 42;

fn foo() {
    x               // <- error: variable 'x' not found
}

But Can Call Other Functions and Access Modules

All functions in the same AST can call each other.

fn foo(x) {         // function defined in the global namespace
    x + 1
}

fn bar(x) {
    foo(x)          // ok! function 'foo' can be called
}

In addition, modules imported at global level can be accessed.

import "hello" as hey;
import "world" as woo;

{
    import "x" as xyz;  // <- this module is not at global level
}                       // <- it goes away here

fn foo(x) {
    hey::process(x);    // ok! imported module 'hey' can be accessed

    print(woo::value);  // ok! imported module 'woo' can be accessed

    xyz::do_work();     // <- error: module 'xyz' not found
}

Automatic Global Module

When a constant is declared at global scope, it is added to a special module called global.

Functions can access those constants via the special global module.

Naturally, the automatic global module is not available under no_function nor no_module.

const CONSTANT = 42;        // this constant is automatically added to 'global'

let hello = 1;              // variables are not added to 'global'

{
    const INNER = 0;        // this constant is not at global level
}                           // <- it goes away here

fn foo(x) {
    x * global::hello       // <- error: variable 'hello' not found in 'global'

    x * global::CONSTANT    // ok! 'CONSTANT' exists in 'global'

    x * global::INNER       // <- error: constant 'INNER' not found in 'global'
}

Use Before Definition Allowed

Unlike C/C++, functions in Rhai can be defined anywhere at global level.

A function does not need to be defined prior to being used in a script; a statement in the script can freely call a function defined afterwards.

This is similar to Rust and many other modern languages, such as JavaScript’s function keyword.

let x = foo(41);    // <- I can do this!

fn foo(x) {         // <- define 'foo' after use
    x + 1
}

Arguments are Passed by Value

Functions defined in script always take Dynamic parameters (i.e. they can be of any types). Therefore, functions with the same name and same number of parameters are equivalent.

All arguments are passed by value, so all Rhai script-defined functions are pure (i.e. they never modify their arguments).

Any update to an argument will not be reflected back to the caller.

fn change(s) {      // 's' is passed by value
    s = 42;         // only a COPY of 's' is changed
}

let x = 500;

change(x);

x == 500;           // 'x' is NOT changed!

this – Simulating an Object Method

Functions are pure

The only way for a script-defined function to change an external value is via this.

Arguments passed to script-defined functions are always by value because functions are pure.

However, functions can also be called in method-call style:

object . method ( parameters)

When a function is called this way, the keyword this binds to the object in the method call and can be changed.

fn change() {       // note that the method does not need a parameter
    this = 42;      // 'this' binds to the object in method-call
}

let x = 500;

x.change();         // call 'change' in method-call style, 'this' binds to 'x'

x == 42;            // 'x' is changed!

change();           // <- error: 'this' is unbound

Elvis Operator

The Elvis operator can be used to short-circuit the method call when the object itself is ().

object ?. method ( parameters)

In the above, the method is never called if object is ().

Function Overloading

Functions defined in script can be overloaded by arity (i.e. they are resolved purely upon the function’s name and number of parameters, but not parameter types since all parameters are the same type – Dynamic).

New definitions overwrite previous definitions of the same name and number of parameters.

fn foo(x, y, z) {
    print(`Three!!! ${x}, ${y}, ${z}`);
}
fn foo(x) {
    print(`One! ${x}`);
}
fn foo(x, y) {
    print(`Two! ${x}, ${y}`);
}
fn foo() {
    print("None.");
}
fn foo(x) {     // <- overwrites previous definition
    print(`HA! NEW ONE! ${x}`);
}

foo(1,2,3);     // prints "Three!!! 1,2,3"

foo(42);        // prints "HA! NEW ONE! 42"

foo(1,2);       // prints "Two!! 1,2"

foo();          // prints "None."

Function Namespaces

Each Function is a Separate Compilation Unit

Functions in Rhai are pure and they form individual compilation units.

This means that individual functions can be separated, exported, re-grouped, imported, and generally mix-’n-matched with other completely unrelated scripts.

For example, the AST::merge and AST::combine methods (or the equivalent + and += operators) allow combining all functions in one AST into another, forming a new, unified, group of functions.

Namespace Types

In general, there are two main types of namespaces where functions are looked up:

NamespaceQuantitySourceLookupSub-modules?Variables?
Globalone
  1. AST being evaluated
  2. Engine::register_XXX API
  3. global registered modules
  4. functions in imported modules marked global
  5. functions in registered static modules marked global
simple nameignoredignored
Modulemany
  1. Module registered via Engine::register_static_module
  2. Module loaded via import statement
namespace-qualified nameyesyes

Module Namespaces

There can be multiple module namespaces at any time during a script evaluation, usually loaded via the import statement.

Static module namespaces can also be registered into an Engine via Engine::register_static_module.

Functions and variables in module namespaces are isolated and encapsulated within their own environments.

They must be called or accessed in a namespace-qualified manner.

import "my_module" as m;        // new module namespace 'm' created via 'import'

let x = m::calc_result();       // namespace-qualified function call

let y = m::MY_NUMBER;           // namespace-qualified variable/constant access

let z = calc_result();          // <- error: function 'calc_result' not found
                                //    in global namespace!

Global Namespace

There is one global namespace for every Engine, which includes (in the following search order):

  • all functions defined in the AST currently being evaluated,

  • all native Rust functions and iterators registered via the Engine::register_XXX API,

  • all functions and iterators defined in global modules that are registered into the Engine via register_global_module,

  • functions defined in modules registered into the Engine via register_static_module that are specifically marked for exposure to the global namespace (e.g. via the #[rhai(global)] attribute in a plugin module).

  • functions defined in imported modules that are specifically marked for exposure to the global namespace (e.g. via the #[rhai(global)] attribute in a plugin module).

Anywhere in a Rhai script, when a function call is made, the function is searched within the global namespace, in the above search order.

Therefore, function calls in Rhai are late bound – meaning that the function called cannot be determined or guaranteed; there is no way to lock down the function being called. This aspect is very similar to JavaScript before ES6 modules.

// Compile a script into AST
let ast1 = engine.compile(
r#"
    fn get_message() {
        "Hello!"                // greeting message
    }

    fn say_hello() {
        print(get_message());   // prints message
    }

    say_hello();
"#)?;

// Compile another script with an overriding function
let ast2 = engine.compile(r#"fn get_message() { "Boo!" }"#)?;

// Combine the two AST's
ast1 += ast2;                   // 'message' will be overwritten

engine.run_ast(&ast1)?;         // prints 'Boo!'

Therefore, care must be taken when cross-calling functions to make sure that the correct function is called.

The only practical way to ensure that a function is a correct one is to use modules – i.e. define the function in a separate module and then import it:

┌──────────────┐
│ message.rhai │
└──────────────┘

fn get_message() { "Hello!" }


┌─────────────┐
│ script.rhai │
└─────────────┘

import "message" as msg;

fn say_hello() {
    print(msg::get_message());
}
say_hello();

Function Pointers

Trivia

A function pointer simply stores the name of the function as a string.

It is possible to store a function pointer in a variable just like a normal value.

A function pointer is created via the Fn function, which takes a string parameter.

Call a function pointer via the call method.

Built-in Functions

The following standard methods (mostly defined in the BasicFnPackage but excluded if using a raw Engine) operate on function pointers.

FunctionParameter(s)Description
name method and propertynonereturns the name of the function encapsulated by the function pointer
is_anonymous method and propertynonedoes the function pointer refer to an anonymous function? Not available under no_function.
callargumentscalls the function matching the function pointer’s name with the arguments

Examples

fn foo(x) { 41 + x }

let func = Fn("foo");       // use the 'Fn' function to create a function pointer

print(func);                // prints 'Fn(foo)'

let func = fn_name.Fn();    // <- error: 'Fn' cannot be called in method-call style

func.type_of() == "Fn";     // type_of() as function pointer is 'Fn'

func.name == "foo";

func.call(1) == 42;         // call a function pointer with the 'call' method

foo(1) == 42;               // <- the above de-sugars to this

call(func, 1);              // normal function call style also works for 'call'

let len = Fn("len");        // 'Fn' also works with registered native Rust functions

len.call("hello") == 5;

let fn_name = "hello";      // the function name does not have to exist yet

let hello = Fn(fn_name + "_world");

hello.call(0);              // error: function not found - 'hello_world (i64)'

Warning – Not First-Class Functions

Beware that function pointers are not first-class functions.

They are syntactic sugar only, capturing only the name of a function to call. They do not hold the actual functions.

The actual function must be defined in the appropriate namespace for the call to succeed.

Cannot export function pointer

Exporting a function pointer (or an anonymous function or closure) from a module referring to a local function fails at runtime.

That is because the target function is not supposed to be found in the caller’s namespace.

┌────────────────┐
│ my_module.rhai │
└────────────────┘

fn increment(x) {
    x + 1
}

export let inc = Fn("increment");   // exports a function pointer


┌───────────┐
│ main.rhai │
└───────────┘

import "my_module" as my_mod;

print(my_mod::increment(41));       // ok!

let x = my_mod::inc.call(41);       // runtime error:
                                    //    function 'increment' not found

Warning – Global Namespace Only

See also

See Function Namespaces for more details.

Because of their dynamic nature, function pointers cannot refer to functions in import-ed modules.

They can only refer to functions within the global namespace.

import "foo" as f;          // assume there is 'f::do_work()'

f::do_work();               // works!

let p = Fn("f::do_work");   // error: invalid function name

fn do_work_now() {          // call it from a local function
    f::do_work();
}

let p = Fn("do_work_now");

p.call();                   // works!

Dynamic Dispatch

The purpose of function pointers is to enable rudimentary dynamic dispatch, meaning to determine, at runtime, which function to call among a group.

Although it is possible to simulate dynamic dispatch via a number and a large if-then-else-if statement, using function pointers significantly simplifies the code.

let x = some_calculation();

// These are the functions to call depending on the value of 'x'
fn method1(x) { ... }
fn method2(x) { ... }
fn method3(x) { ... }

// Traditional - using decision variable
let func = sign(x);

// Dispatch with if-statement
if func == -1 {
    method1(42);
} else if func == 0 {
    method2(42);
} else if func == 1 {
    method3(42);
}

// Using pure function pointer
let func = if x < 0 {
    Fn("method1")
} else if x == 0 {
    Fn("method2")
} else if x > 0 {
    Fn("method3")
};

// Dynamic dispatch
func.call(42);

// Using functions map
let map = [ Fn("method1"), Fn("method2"), Fn("method3") ];

let func = sign(x) + 1;

// Dynamic dispatch
map[func].call(42);

Bind the this Pointer

When call is called as a method but not on a function pointer, it is possible to dynamically dispatch to a function call while binding the object in the method call to the this pointer of the function.

To achieve this, pass the function pointer as the first argument to call:

fn add(x) {                 // define function which uses 'this'
    this += x;
}

let func = Fn("add");       // function pointer to 'add'

func.call(1);               // error: 'this' pointer is not bound

let x = 41;

func.call(x, 1);            // error: function 'add (i64, i64)' not found

call(func, x, 1);           // error: function 'add (i64, i64)' not found

x.call(func, 1);            // 'this' is bound to 'x', dispatched to 'func'

x == 42;

Beware that this only works for method-call style. Normal function-call style cannot bind the this pointer (for syntactic reasons).

Therefore, obviously, binding the this pointer is unsupported under no_object.

Call a Function Pointer within a Rust Function (as a Callback)

It is completely normal to register a Rust function with an Engine that takes parameters whose types are function pointers. The Rust type in question is rhai::FnPtr.

A function pointer in Rhai is essentially syntactic sugar wrapping the name of a function to call in script. Therefore, the script’s execution context (i.e. NativeCallContext) is needed in order to call a function pointer.

use rhai::{Engine, FnPtr, NativeCallContext};

let mut engine = Engine::new();

// A function expecting a callback in form of a function pointer.
fn super_call(context: NativeCallContext, callback: FnPtr, value: i64)
                -> Result<String, Box<EvalAltResult>>
{
    // Use 'FnPtr::call_within_context' to call the function pointer using the call context.
    // 'FnPtr::call_within_context' automatically casts to the required result type.
    callback.call_within_context(&context, (value,))
    //                                     ^^^^^^^^ arguments passed in tuple
}

engine.register_result_fn("super_call", super_call);

Call a Function Pointer Directly

The FnPtr::call method allows the function pointer to be called directly on any Engine and AST, making it possible to reuse the FnPtr data type in may different calls and scripting environments.

use rhai::{Engine, FnPtr};

let engine = Engine::new();

// Compile script to AST
let ast = engine.compile(
r#"
    let test = "hello";
    |x| test + x            // this creates a closure
"#)?;

// Save the closure together with captured variables
let fn_ptr = engine.eval_ast::<FnPtr>(&ast)?;

// 'f' captures: the Engine, the AST, and the closure
let f = move |x: i64| -> Result<String, _> {
            fn_ptr.call(&engine, &ast, (x,))
        };

// 'f' can be called like a normal function
let result = f(42)?;

result == "hello42";

Function Pointer Currying

Automatic currying

Anonymous functions defined via a closure syntax capture external variables that are not shadowed inside the function’s scope.

This is accomplished via automatic currying.

It is possible to curry a function pointer by providing partial (or all) arguments.

Currying is done via the curry keyword and produces a new function pointer which carries the curried arguments.

When the curried function pointer is called, the curried arguments are inserted starting from the left.

The actual call arguments should be reduced by the number of curried arguments.

fn mul(x, y) {                  // function with two parameters
    x * y
}

let func = Fn("mul");

func.call(21, 2) == 42;         // two arguments are required for 'mul'

let curried = func.curry(21);   // currying produces a new function pointer which
                                // carries 21 as the first argument

let curried = curry(func, 21);  // function-call style also works

curried.call(2) == 42;          // <- de-sugars to 'func.call(21, 2)'
                                //    only one argument is now required

Anonymous Functions

Many functions in the standard API expect function pointer as parameters.

For example:

// Function 'double' defined here - used only once
fn double(x) { 2 * x }

// Function 'square' defined here - again used only once
fn square(x) { x * x }

let x = [1, 2, 3, 4, 5];

// Pass a function pointer to 'double'
let y = x.map(Fn("double"));

// Pass a function pointer to 'square'
let z = y.map(Fn("square"));

Sometimes it gets tedious to define separate functions only to dispatch them via single function pointers – essentially, those functions are only ever called in one place.

This scenario is especially common when simulating object-oriented programming (OOP).

// Define functions one-by-one
fn obj_inc(x, y) { this.data += x * y; }
fn obj_dec(x) { this.data -= x; }
fn obj_print() { print(this.data); }

// Define object
let obj = #{
    data: 42,
    increment: Fn("obj_inc"),           // use function pointers to
    decrement: Fn("obj_dec"),           // refer to method functions
    print: Fn("obj_print")
};

Syntax

Anonymous functions have a syntax similar to Rust’s closures (they are not the same).

|param 1, param 2,, param n| statement

|param 1, param 2,, param n| { statements}

No parameters:

|| statement

|| { statements}

Anonymous functions can be disabled via Engine::set_allow_anonymous_function.

Rewrite Using Anonymous Functions

The above can be rewritten using anonymous functions.

let x = [1, 2, 3, 4, 5];

let y = x.map(|x| 2 * x);

let z = y.map(|x| x * x);

let obj = #{
    data: 42,
    increment: |x, y| this.data += x * y,   // one statement
    decrement: |x| this.data -= x,          // one statement
    print_obj: || {
        print(this.data);                   // full function body
    }
};

This de-sugars to:

// Automatically generated...
fn anon_fn_0001(x) { 2 * x }
fn anon_fn_0002(x) { x * x }
fn anon_fn_0003(x, y) { this.data += x * y; }
fn anon_fn_0004(x) { this.data -= x; }
fn anon_fn_0005() { print(this.data); }

let x = [1, 2, 3, 4, 5];

let y = x.map(Fn("anon_fn_0001"));

let z = y.map(Fn("anon_fn_0002"));

let obj = #{
    data: 42,
    increment: Fn("anon_fn_0003"),
    decrement: Fn("anon_fn_0004"),
    print: Fn("anon_fn_0005")
};

NOT real closures

Remember: though having the same syntax as Rust closures, anonymous functions are themselves NOT real closures.

In particular, they capture their execution environment via automatic currying (disabled via no_closure).

Simulating Closures

Capture External Variables via Automatic Currying

Tip: is_shared

Use Dynamic::is_shared to check whether a particular Dynamic value is shared.

Since anonymous functions de-sugar to standard function definitions, they retain all the behaviors of Rhai functions, including being pure, having no access to external variables.

The anonymous function syntax, however, automatically captures variables that are not defined within the current scope, but are defined in the external scope – i.e. the scope where the anonymous function is created.

Variables that are accessible during the time the anonymous function is created can be captured, as long as they are not shadowed by local variables defined within the function’s scope.

The captured variables are automatically converted into reference-counted shared values (Rc<RefCell<Dynamic>>, or Arc<RwLock<Dynamic>> under sync).

Therefore, similar to closures in many languages, these captured shared values persist through reference counting, and may be read or modified even after the variables that hold them go out of scope and no longer exist.

Tip: Disable closures

Automatic currying can be turned off via the no_closure feature.

Examples

let x = 1;                          // a normal variable

x.is_shared() == false;

let f = |y| x + y;                  // variable 'x' is auto-curried (captured) into 'f'

x.is_shared() == true;              // 'x' is now a shared value!

f.call(2) == 3;                     // 1 + 2 == 3

x = 40;                             // changing 'x'...

f.call(2) == 42;                    // the value of 'x' is 40 because 'x' is shared

// The above de-sugars into something like this:

fn anon_0001(x, y) { x + y }        // parameter 'x' is inserted

make_shared(x);                     // convert variable 'x' into a shared value

let f = Fn("anon_0001").curry(x);   // shared 'x' is curried

Beware: Captured Variables are Truly Shared

The example below is a typical tutorial sample for many languages to illustrate the traps that may accompany capturing external variables in closures.

It prints 9, 9, 9, … 9, 9, not 0, 1, 2, … 8, 9, because there is ever only one captured variable, and all ten closures capture the same variable.

let list = [];

for i in 0..10 {
    list.push(|| print(i));         // the for loop variable 'i' is captured
}

list.len() == 10;                   // 10 closures stored in the array

list[0].type_of() == "Fn";          // make sure these are closures

for f in list {
    f.call();                       // all references to 'i' are the same variable!
}

Therefore – Be Careful to Prevent Data Races

Rust does not have data races, but that doesn’t mean Rhai doesn’t.

Avoid performing a method call on a captured shared variable (which essentially takes a mutable reference to the shared object) while using that same variable as a parameter in the method call – this is a sure-fire way to generate a data race error.

If a shared value is used as the this pointer in a method call to a closure function, then the same shared value must not be captured inside that function, or a data race will occur and the script will terminate with an error.

let x = 20;

x.is_shared() == false;             // 'x' is not shared, so no data race is possible

let f = |a| this += x + a;          // 'x' is captured in this closure

x.is_shared() == true;              // now 'x' is shared

x.call(f, 2);                       // <- error: data race detected on 'x'

Data Races in sync Builds Can Become Deadlocks

Under the sync feature, shared values are guarded with a RwLock, meaning that data race conditions no longer raise an error.

Instead, they wait endlessly for the RwLock to be freed, and thus can become deadlocks.

On the other hand, since the same thread (i.e. the Engine thread) that is holding the lock is attempting to read it again, this may also panic depending on the O/S.

let x = 20;

let f = |a| this += x + a;          // 'x' is captured in this closure

// Under `sync`, the following may wait forever, or may panic,
// because 'x' is locked as the `this` pointer but also accessed
// via a captured shared value.
x.call(f, 2);

TL;DR

How is it actually implemented?

The actual implementation of closures de-sugars to:

  1. Keeping track of what variables are accessed inside the anonymous function,

  2. If a variable is not defined within the anonymous function’s scope, it is looked up outside the function and in the current execution scope – where the anonymous function is created.

  3. The variable is added to the parameters list of the anonymous function, at the front.

  4. The variable is then converted into a reference-counted shared value.

    An anonymous function which captures an external variable is the only way to create a reference-counted shared value in Rhai.

  5. The shared value is then curried into the function pointer itself, essentially carrying a reference to that shared value and inserting it into future calls of the function.

    This process is called Automatic Currying, and is the mechanism through which Rhai simulates normal closures.

Why automatic currying?

In concept, a closure closes over captured variables from the outer scope – that’s why they are called closures. When this happen, a typical language implementation hoists those variables that are captured away from the stack frame and into heap-allocated storage. This is because those variables may be needed after the stack frame goes away.

These heap-allocated captured variables only go away when all the closures that need them are finished with them. A garbage collector makes this trivial to implement – they are automatically collected as soon as all closures needing them are destroyed.

In Rust, this can be done by reference counting instead, with the potential pitfall of creating reference loops that will prevent those variables from being deallocated forever. Rhai avoids this by clone-copying most data values, so reference loops are hard to create.

Rhai does the hoisting of captured variables into the heap by converting those values into reference-counted locked values, also allocated on the heap. The process is identical.

Closures are usually implemented as a data structure containing two items:

  1. A function pointer to the function body of the closure,
  2. A data structure containing references to the captured shared variables on the heap.

Usually a language implementation passes the structure containing references to captured shared variables into the function pointer, the function body taking this data structure as an additional parameter.

This is essentially what Rhai does, except that Rhai passes each variable individually as separate parameters to the function, instead of creating a structure and passing that structure as a single parameter. This is the only difference.

Therefore, in most languages, essentially all closures are implemented as automatic currying of shared variables hoisted into the heap, automatically passing those variables as parameters into the function. Rhai just brings this directly up to the front.

Functions Metadata

The metadata of a function means all relevant information related to a function’s definition including:

  1. Its callable name

  2. Its access mode (public or private)

  3. Its parameter names and types (if any)

  4. Its return value and type (if any)

  5. Its nature (i.e. native Rust or Rhai-scripted)

  6. Its namespace (module or global)

  7. Its purpose, in the form of doc-comments

  8. Usage notes, warnings, examples etc., in the form of doc-comments

A function’s signature encapsulates the first four pieces of information in a single concise line of definition:

[private] name (param 1:type 1, param 2:type 2,, param n:type n) -> return type

Requires metadata

Exporting metadata requires the metadata feature.

Get Functions Metadata in Scripts

The built-in function get_fn_metadata_list returns an array of object maps, each containing the metadata of one script-defined function in scope.

get_fn_metadata_list has a few versions taking different parameters:

SignatureDescription
get_fn_metadata_list()returns an array for all script-defined functions
get_fn_metadata_list(name)returns an array containing all script-defined functions matching a specified name
get_fn_metadata_list(name, params)returns an array containing all script-defined functions matching a specified name and accepting the specified number of parameters

Functions from the following sources are returned, in order:

  1. Encapsulated script environment (e.g. when loading a module from a script file),
  2. Current script,
  3. Modules registered via Engine::register_global_module (latest registrations first)
  4. Modules imported via the import statement (latest imports first),
  5. Modules added via Engine::register_static_module (latest registrations first)

The return value is an array of object maps (so get_fn_metadata_list is also not available under no_index or no_object), containing the following fields.

FieldTypeOptional?Description
namespacestringyesthe module namespace if the function is defined within a module
accessstringno"public" if the function is public,
"private" if it is private
namestringnofunction name
paramsarray of stringsnoparameter names
is_anonymousboolnois this function an anonymous function?

Get Scripted Functions Metadata from AST

Use AST::iter_functions to iterate through all the script-defined functions in an AST.

ScriptFnMetadata

The type returned from the iterator is ScriptFnMetadata with the following fields:

FieldRequiresTypeDescription
name&strName of function
paramsVec<&str>Number of parameters
accessFnAccessFnAccess::Public (public)
FnAccess::Private (private)
commentsmetadataVec<&str>Doc-comments, if any, one per line

Get Native Function Signatures

Engine::gen_fn_signatures

As part of a reflections API, Engine::gen_fn_signatures returns a list of function signatures (as Vec<String>), each corresponding to a particular native function available to that Engine instance.

name (param 1:type 1, param 2:type 2,, param n:type n) -> return type

The metadata feature must be used to turn on this API.

Sources

Functions from the following sources are included, in order:

  1. Native Rust functions registered into the global namespace via the Engine::register_XXX API
  2. Public (i.e. non-private) functions (native Rust or Rhai scripted) in global sub-modules registered via Engine::register_static_module.
  3. Native Rust functions in external packages registered via Engine::register_global_module
  4. Native Rust functions in built-in packages (optional)

Functions Metadata

Beware, however, that not all function signatures contain parameters and return value information.

Engine::register_XXX

For instance, functions registered via Engine::register_XXX contain no information on the names of parameter because Rust simply does not make such metadata available natively.

Type names, however, are provided.

A function registered under the name foo with three parameters.

foo(_: i64, _: char, _: &str) -> String

An operator function. Notice that function names do not need to be valid identifiers.

+=(_: &mut i64, _: i64)

A property setter. Notice that function names do not need to be valid identifiers. In this case, the first parameter should be &mut T of the custom type and the return value is ():

set$prop(_: &mut TestStruct, _: i64)

Script-Defined Functions

Script-defined function signatures contain parameter names. Since all parameters, as well as the return value, are Dynamic the types are simply not shown.

foo(x, y, z)

is probably defined simply as:

/// This is a doc-comment, included in this function's metadata.
fn foo(x, y, z) {
    ...
}

which is really the same as:

foo(x: Dynamic, y: Dynamic, z: Dynamic) -> Result<Dynamic, Box<EvalAltResult>>

Plugin Functions

Functions defined in plugin modules are the best. They contain all metadata describing the functions, including doc-comments.

For example, a plugin function combine:

/// This is a doc-comment, included in this function's metadata.
combine(list: &mut MyStruct<i64>, num: usize, name: &str) -> bool

Notice that function names do not need to be valid identifiers.

For example, an operator defined as a fallible function in a plugin module via #[rhai_fn(name="+=", return_raw)] returns Result<bool, Box<EvalAltResult>>:

+=(list: &mut MyStruct<i64>, value: &str) -> Result<bool, Box<EvalAltResult>>

For example, a property getter defined in a plugin module:

get$prop(obj: &mut MyStruct<i64>) -> String

Export Functions Metadata to JSON

Engine::gen_fn_metadata_to_json
Engine::gen_fn_metadata_with_ast_to_json

As part of a reflections API, Engine::gen_fn_metadata_to_json and the corresponding Engine::gen_fn_metadata_with_ast_to_json export the full list of functions metadata in JSON format.

Requires metadata

The metadata feature is required for this API, which also pulls in the serde_json crate.

Sources

Functions from the following sources are included:

  1. Script-defined functions in an AST (for Engine::gen_fn_metadata_with_ast_to_json)
  2. Native Rust functions registered into the global namespace via the Engine::register_XXX API
  3. Public (i.e. non-private) functions (native Rust or Rhai scripted) in static modules registered via Engine::register_static_module
  4. Native Rust functions in external packages registered via Engine::register_global_module
  5. Native Rust functions in built-in packages (optional)

JSON Schema

The JSON schema used to hold functions metadata is very simple, containing a nested structure of modules and a list of functions.

Modules Schema

{
    "modules":
    {
        /* namespace 'sub_module_1' */
        "sub_module_1":
        {
            "modules":
            {
                /* namespace 'sub_module_1::sub_sub_module_A' */
                "sub_sub_module_A":
                {
                    /* functions exported in 'sub_module_1::sub_sub_module_A' */
                    "functions":
                    [
                        { ... function metadata ... },
                        { ... function metadata ... },
                        { ... function metadata ... },
                        { ... function metadata ... },
                        ...
                    ]
                },
                /* namespace 'sub_module_1::sub_sub_module_B' */
                "sub_sub_module_B":
                {
                    ...
                }
            }
        },
        "sub_module_2":
        {
            ...
        },
        ...
    },
    /* functions registered globally or in the 'AST' */
    "functions":
    [
        { ... function metadata ... },
        { ... function metadata ... },
        { ... function metadata ... },
        { ... function metadata ... },
        ...
    ]
}

Function Metadata Schema

{
    "baseHash": 9876543210,  /* partial hash with only number of parameters */
    "fullHash": 1234567890,  /* full hash with actual parameter types */
    "namespace": "internal" | "global",
    "access": "public" | "private",
    "name": "fn_name",
    "type": "native" | "script",
    "numParams": 42,  /* number of parameters */
    "params":  /* omitted if no parameters */
    [
        { "name": "param_1", "type": "type_1" },
        { "name": "param_2" },  /* no type name */
        { "type": "type_3" },   /* no parameter name */
        ...
    ],
    "returnType": "ret_type",  /* omitted if () or unknown */
    "signature": "[private] fn_name(param_1: type_1, param_2, _: type_3) -> ret_type",
    "docComments":  /* omitted if none */
    [
        "/// doc-comment line 1",
        "/// doc-comment line 2",
        "/** doc-comment block */",
        ...
    ]
}

print and debug

The print and debug functions default to printing to stdout, with debug using standard debug formatting.

print("hello");         // prints "hello" to stdout

print(1 + 2 + 3);       // prints "6" to stdout

let x = 42;

print(`hello${x}`);     // prints "hello42" to stdout

debug("world!");        // prints "world!" to stdout using debug formatting

Override print and debug with Callback Functions

When embedding Rhai into an application, it is usually necessary to trap print and debug output (for logging into a tracking log, for example) with the Engine::on_print and Engine::on_debug methods.

// Any function or closure that takes an '&str' argument can be used to override 'print'.
engine.on_print(|x| println!("hello: {}", x));

// Any function or closure that takes a '&str', an 'Option<&str>' and a 'Position' argument
// can be used to override 'debug'.
engine.on_debug(|x, src, pos| {
    let src = src.unwrap_or("unknown");
    println!("DEBUG of {} at {:?}: {}", src, pos, x)
});

// Example: quick-'n-dirty logging
let logbook = Arc::new(RwLock::new(Vec::<String>::new()));

// Redirect print/debug output to 'log'
let log = logbook.clone();
engine.on_print(move |s| {
    let entry = format!("entry: {}", s);
    log.write().unwrap().push(entry);
});

let log = logbook.clone();
engine.on_debug(move |s, src, pos| {
    let src = src.unwrap_or("unknown");
    let entry = format!("DEBUG of {} at {:?}: {}", src, pos, s);
    log.write().unwrap().push(entry);
});

// Evaluate script
engine.run(script)?;

// 'logbook' captures all the 'print' and 'debug' output
for entry in logbook.read().unwrap().iter() {
    println!("{}", entry);
}

on_debug Callback Signature

The function signature passed to Engine::on_debug takes the following form.

Fn(text: &str, source: Option<&str>, pos: Position)

where:

ParameterTypeDescription
text&strtext to display
sourceOption<&str>source of the current evaluation, if any
posPositionposition (line number and character offset) of the debug call

The source of a script evaluation is any text string provided to an AST via AST::set_source.

Tip

If a module is loaded via an import statement, then the source of functions defined within the module will be the module’s path.

Modules

Rhai allows organizing code (functions, both Rust-based or script-based, and variables) into modules. Modules can be disabled via the no_module feature.

A module has the type Module and holds a collection of functions, variables, type iterators and sub-modules. It may be created entirely from Rust functions, or it may encapsulate a Rhai script together with the functions and variables defined by that script.

Other scripts can then load this module and use the functions and variables exported as if they were defined inside the same script.

Export Variables, Functions and Sub-Modules From a Script

See also

See Create a Module from AST for more details.

The easiest way to expose a collection of functions as a self-contained module is to do it via a Rhai script itself.

The script text is evaluated.

Variables are then selectively exposed via the export statement.

Functions defined by the script are automatically exported, unless marked as private.

Modules loaded within this module at the global level become sub-modules and are also automatically exported.

Export Global Variables

The export statement, which can only be at global level, exposes a selected variable as member of a module.

Variables not exported are private and hidden. They are merely used to initialize the module, but cannot be accessed from outside.

Everything exported from a module is constant (i.e. read-only).

// This is a module script.

let hidden = 123;       // variable not exported - default hidden
let x = 42;             // this will be exported below

export x;               // the variable 'x' is exported under its own name

export const x = 42;    // convenient short-hand to declare a constant and export it
                        // under its own name

export let x = 123;     // variables can be exported as well, though it'll still be constant

export x as answer;     // the variable 'x' is exported under the alias 'answer'
                        // another script can load this module and access 'x' as 'module::answer'

{
    let inner = 0;      // local variable - it disappears when the statement block ends,
                        //                  therefore it is not 'global' and cannot be exported

    export inner;       // <- syntax error: cannot export a local variable
}

Tip: Multiple exports

Variables can be exported under multiple names. For example, the following exports three variables:

  • x as x and hello
  • y as foo and bar
  • z as z
export x;
export x as hello;
export y as foo;
export x as world;
export y as bar;
export z;

Do not export closures

A function pointer, anonymous function or closure, is not a first-class function. It is syntactic sugar only, capturing the name of a function to call.

Exporting them causes a runtime error.

Export Functions

Private functions

private functions are commonly called within the module only. They cannot be accessed otherwise.

All functions are automatically exported, unless it is explicitly opt-out with the private prefix.

Functions declared private are hidden to the outside.

// This is a module script.

fn inc(x) { x + 1 }     // script-defined function - default public

private fn foo() {}     // private function - hidden

Sub-Modules

All loaded modules are automatically exported as sub-modules.

Tip: Skip exporting a module

To prevent a module from being exported, load it inside a block statement so that it goes away at the end of the block.

// This is a module script.

import "hello" as foo;      // <- exported

{
    import "world" as bar;  // <- not exported
}

Import a Module

See also

See Module Resolvers for more details.

Before a module can be used (via an import statement) in a script, there must be a module resolver registered into the Engine, the default being the FileModuleResolver.

import Statement

Tip

A module that is only import-ed but not given any name is simply run.

This is a very simple way to run another script file from within a script.

A module can be imported via the import statement, and be given a name.

Its members can be accessed via :: similar to C++.

import "crypto_banner";         // run the script file 'crypto_banner.rhai' without creating an imported module

import "crypto" as lock;        // run the script file 'crypto.rhai' and import it as a module named 'lock'

const SECRET_NUMBER = 42;

let mod_file = `crypto_${SECRET_NUMBER}`;

import mod_file as my_mod;      // load the script file "crypto_42.rhai" and import it as a module named 'my_mod'
                                // notice that module path names can be dynamically constructed!
                                // any expression that evaluates to a string is acceptable after the 'import' keyword

lock::encrypt(secret);          // use functions defined under the module via '::'

lock::hash::sha256(key);        // sub-modules are also supported

print(lock::status);            // module variables are constants

lock::status = "off";           // <- runtime error: cannot modify a constant

Imports are scoped

Modules imported via import statements are only accessible inside the relevant block scope.

import "hacker" as h;           // import module - visible globally

if secured {                    // <- new block scope
    let mod = "crypt";

    import mod + "o" as c;      // import module (the path needs not be a constant string)

    let x = c::encrypt(key);    // use a function in the module

    h::hack(x);                 // global module 'h' is visible here
}                               // <- module 'c' disappears at the end of the block scope

h::hack(something);             // this works as 'h' is visible

c::encrypt(something);          // <- this causes a run-time error because
                                //    module 'c' is no longer available!

fn foo(something) {
    h::hack(something);         // <- this also works as 'h' is visible
}

for x in 0..1000 {
    import "crypto" as c;       // <- importing a module inside a loop is a Very Bad Idea™

    c.encrypt(something);
}

Place import statements at the top

import statements can appear anywhere a normal statement can be, but in the vast majority of cases they are usually grouped at the top (beginning) of a script for manageability and visibility.

It is not advised to deviate from this common practice unless there is a Very Good Reason™.

Especially, do not place an import statement within a loop; doing so will repeatedly re-load the same module during every iteration of the loop!

Recursive imports

Beware of import cycles – i.e. recursively loading the same module. This is a sure-fire way to cause a stack overflow in the Engine, unless stopped by setting a limit for maximum number of modules.

For instance, importing itself always causes an infinite recursion:

┌────────────┐
│ hello.rhai │
└────────────┘

import "hello" as foo;          // import itself - infinite recursion!

foo::do_something();

Modules cross-referencing also cause infinite recursion:

┌────────────┐
│ hello.rhai │
└────────────┘

import "world" as foo;
foo::do_something();


┌────────────┐
│ world.rhai │
└────────────┘

import "hello" as bar;
bar::do_something_else();

eval Function

Or “How to Shoot Yourself in the Foot even Easier”

Saving the best for last, there is the ever-dreaded… eval function!

let x = 10;

fn foo(x) { x += 12; x }

let script =
"
    let y = x;
    y += foo(y);
    x + y
";

let result = eval(script);      // <- look, JavaScript, we can also do this!

result == 42;

x == 10;                        // prints 10 - arguments are passed by value
y == 32;                        // prints 32 - variables defined in 'eval' persist!

eval("{ let z = y }");          // to keep a variable local, use a statement block

print(z);                       // <- error: variable 'z' not found

"print(42)".eval();             // <- nope... method-call style doesn't work with 'eval'

eval executes inside the current scope!

Script segments passed to eval execute inside the current Scope, so they can access and modify everything, including all variables that are visible at that position in code!

let script = "x += 32";

let x = 10;
eval(script);       // variable 'x' is visible!
print(x);           // prints 42

// The above is equivalent to:
let script = "x += 32";
let x = 10;
x += 32;
print(x);

eval can also be used to define new variables and do other things normally forbidden inside a function call.

let script = "let x = 42";
eval(script);
print(x);           // prints 42

Treat it as if the script segments are physically pasted in at the position of the eval call.

Cannot define new functions

New functions cannot be defined within an eval call, since functions can only be defined at the global level!

eval is evil

For those who subscribe to the (very sensible) motto of eval is evil”, disable eval via Engine::disable_symbol.

// Disable usage of 'eval'
engine.disable_symbol("eval");

TL;DR

Do you regret implementing eval in Rhai?

Or course we do.

Having the possibility of an eval call disrupts any predictability in the Rhai script, thus disabling a large number of optimizations.

Why did it then???!!!

Brendan Eich puts it well: “it is just too easy to implement.” (source wanted)

Safety and Protection Against DoS Attacks

For scripting systems open to untrusted user-land scripts, it is always best to limit the amount of resources used by a script so that it does not consume more resources that it is allowed to.

Most Important Resources

Memory

Continuously grow a string, an array, a BLOB or object map until all memory is consumed.

CPU

Run an infinite tight loop that consumes all CPU cycles.

Time

Run indefinitely, thereby blocking the calling system which is waiting for a result.

Stack

  • Infinite recursive call that exhausts the call stack.

  • Create a large array or object map literal that exhausts the stack during parsing.

  • Create a degenerated deep expression with so many levels that the parser exhausts the call stack when parsing the expression; or even deeply-nested statement blocks, if nested deep enough.

  • Load a self-referencing module.

Overflows or Underflows

  • Numeric overflows and/or underflows.

  • Divide by zero.

  • Bad floating-point representations.

Files

Continuously import an external module within an infinite loop, thus putting heavy load on the file-system (or even the network if the file is not local).

Even when modules are not created from files, they still typically consume a lot of resources to load.

Private data

Read from and/or write to private, secret, sensitive data.

Such security breach may put the entire system at risk.

The internals feature

The internals feature allows third-party access to Rust internal data types and functions (for example, the AST and related types).

This is usually a Very Bad Idea™ because:

  • Messing up Rhai’s internal data structures will easily create panics that bring down the host environment, violating the Don’t Panic guarantee.

  • Allowing access to internal types may open up new attack vectors.

  • Internal Rhai types and functions are volatile, so they may change from version to version and break code.

Use internals only if the operating environment has absolutely no safety concerns – you’d be surprised how few scenarios this assumption holds.

One example of such an environment is a Rhai scripting Engine compiled to WASM where the AST is further translated to include environment-specific modifications.

Don’t Panic Guarantee – Any Panic is a Bug

OK, panic anyway

All these safe-guards can be turned off via the unchecked feature, which disables all safety checks (even fatal ones).

This increases script evaluation performance somewhat, but very easy for a malicious script to bring down the host system.

Don’t Panic

Rhai is designed to not bring down the host system, regardless of what a script may do to it. This is a central design goal – Rhai provides a Don’t Panic guarantee.

When using Rhai, any panic outside of API’s with explicitly documented panic conditions is considered a bug in Rhai and should be reported as such.

Sand-Boxing – Block Access to External Data

Rhai is sand-boxed so a script can never read from outside its own environment.

Furthermore, an Engine created non-mut cannot mutate any state, including itself (and therefore it is also re-entrant).

It is highly recommended that Engine’s be created immutable as much as possible.

let mut engine = Engine::new();

// Use the fluent API to configure an 'Engine'
engine.register_get("field", get_field)
      .register_set("field", set_field)
      .register_fn("do_work", action);

// Then turn it into an immutable instance
let engine = engine;

// 'engine' is immutable...

Tip: Use Rhai to control external environment

How does a sand-boxed, immutable Engine control the external environment?

This is necessary in order to use Rhai as a dynamic control layer over a Rust core system.

There are two general patterns, both involving wrapping the external system in a shared, interior-mutated object (e.g. Rc<RefCell<T>>):

Maximum Length of Strings

Rhai by default does not limit how long a string can be.

This can be changed via the Engine::set_max_string_size method, with zero being unlimited (the default).

A script attempting to create a string literal longer than the maximum length will terminate with a parse error.

Any script operation that produces a string longer than the maximum also terminates the script with an error result.

This check can be disabled via the unchecked feature for higher performance (but higher risks as well).

let mut engine = Engine::new();

engine.set_max_string_size(500);    // allow strings only up to 500 bytes long (in UTF-8 format)

engine.set_max_string_size(0);      // allow unlimited string length

Maximum length

Be conservative when setting a maximum limit and always consider the fact that a registered function may grow a string’s length without Rhai noticing until the very end.

For instance, the built-in + operator for strings concatenates two strings together to form one longer string; if both strings are slightly below the maximum length limit, the resultant string may be almost twice the maximum length.

Maximum Size of Arrays

Rhai by default does not limit how large an array or a BLOB can be.

This can be changed via the Engine::set_max_array_size method, with zero being unlimited (the default).

A script attempting to create an array literal larger than the maximum will terminate with a parse error.

Any script operation that produces an array or a BLOB larger than the maximum also terminates the script with an error result.

This check can be disabled via the unchecked feature for higher performance (but higher risks as well).

let mut engine = Engine::new();

engine.set_max_array_size(500);     // allow arrays only up to 500 items

engine.set_max_array_size(0);       // allow unlimited arrays

Maximum size

Be conservative when setting a maximum limit and always consider the fact that a registered function may grow an array’s or BLOB’s size without Rhai noticing until the very end.

For instance, the built-in + operator for arrays and BLOB’s concatenates two of them together to form one larger array or BLOB; if both sources are slightly below the maximum size limit, the result may be almost twice the maximum size.

As a malicious script may also create a deeply-nested array which consumes huge amounts of memory while each individual array still stays under the maximum size limit, Rhai also recursively adds up the sizes of all strings, arrays, [blobs] and object maps contained within each array to make sure that the aggregate sizes of none of these data structures exceed their respective maximum size limits (if any).

// Small, innocent array...
let small_array = [42];             // 1-deep... 1 item, 1 array

// ... becomes huge when multiplied!
small_array.push(small_array);      // 2-deep... 2 items, 2 arrays
small_array.push(small_array);      // 3-deep... 4 items, 4 arrays
small_array.push(small_array);      // 4-deep... 8 items, 8 arrays
small_array.push(small_array);      // 5-deep... 16 items, 16 arrays
          :
          :
small_array.push(small_array);      // <- Rhai raises an error somewhere here
small_array.push(small_array);      //    when the TOTAL number of items in
small_array.push(small_array);      //    the entire array tree exceeds limit

// Or this abomination...
let a = [ 42 ];

loop {
    a[0] = a;       // <- only 1 item, but infinite number of arrays
}

Maximum Size of Object Maps

Rhai by default does not limit how large (i.e. the number of properties) an object map can be.

This can be changed via Engine::set_max_map_size, with zero being unlimited (the default).

A script attempting to create an object map literal with more properties than the maximum will terminate with a parse error.

Any script operation that produces an object map with more properties than the maximum also terminates the script with an error result.

This check can be disabled via the unchecked feature for higher performance (but higher risks as well).

let mut engine = Engine::new();

engine.set_max_map_size(500);       // allow object maps with only up to 500 properties

engine.set_max_map_size(0);         // allow unlimited object maps

Maximum size

Be conservative when setting a maximum limit and always consider the fact that a registered function may grow an object map’s size without Rhai noticing until the very end.

For instance, the built-in + operator for object maps concatenates two object maps together to form one larger object map; if both object maps are slightly below the maximum size limit, the resultant object map may be almost twice the maximum size.

As a malicious script may create a deeply-nested object map which consumes huge amounts of memory while each individual object map still stays under the maximum size limit, Rhai also recursively adds up the sizes of all strings, arrays and object maps contained within each object map to make sure that the aggregate sizes of none of these data structures exceed their respective maximum size limits (if any).

// Small, innocent object map...
let small_map: #{ x: 42 };          // 1-deep... 1 item, 1 object map

// ... becomes huge when multiplied!
small_map.y = small_map;            // 2-deep... 2 items, 2 object maps
small_map.y = small_map;            // 3-deep... 4 items, 4 object maps
small_map.y = small_map;            // 4-deep... 8 items, 8 object maps
small_map.y = small_map;            // 5-deep... 16 items, 16 object maps
          :
          :
small_map.y = small_map;            // <- Rhai raises an error somewhere here
small_map.y = small_map;            //    when the TOTAL number of items in
small_map.y = small_map;            //    the entire array tree exceeds limit

// Or this abomination...
let map = #{ x: 42 };

loop {
    map.x = map;    // <- only 1 item, but infinite number of object maps
}

Maximum Number of Operations

In Rhai, it is trivial to construct infinite loops, or scripts that run for a very long time.

loop { ... }                        // infinite loop

while 1 < 2 { ... }                 // loop with always-true condition

Rhai by default does not limit how much time or CPU a script consumes.

This can be changed via the Engine::set_max_operations method, with zero being unlimited (the default).

The operations count is intended to be a very course-grained measurement of the amount of CPU that a script has consumed, allowing the system to impose a hard upper limit on computing resources.

A script exceeding the maximum operations count terminates with an error result. This can be disabled via the unchecked feature for higher performance (but higher risks as well).

let mut engine = Engine::new();

engine.set_max_operations(500);     // allow only up to 500 operations for this script

engine.set_max_operations(0);       // allow unlimited operations

TL;DR: What does one operation mean?

The concept of one single operation in Rhai is volatile – it roughly equals one expression node, loading one variable/constant, one operator call, one iteration of a loop, or one function call etc. with sub-expressions, statements and function calls executed inside these contexts accumulated on top.

A good rule-of-thumb is that one simple non-trivial expression consumes on average 5-10 operations.

One operation can take an unspecified amount of time and real CPU cycles, depending on the particulars. For example, loading a constant consumes very few CPU cycles, while calling an external Rust function, though also counted as only one operation, may consume much more computing resources.

To help visualize, think of an operation as roughly equals to one instruction of a hypothetical CPU which includes specialized instructions, such as function call, load module etc., each taking up one CPU cycle to execute.

Track Progress and Force-Termination

Operations count vs. progress percentage

Operations count does not indicate the amount of work already done – thus it is not real progress tracking.

The real progress can be estimated based on the expected number of operations in a typical run.

It is impossible to know when, or even whether, a script run will end (a.k.a. the Halting Problem).

When dealing with third-party untrusted scripts that may be malicious, in order to track evaluation progress and force-terminate a script prematurely (for any reason), provide a closure to the Engine via Engine::on_progress.

The closure passed to Engine::on_progress will be called once for every operation.

Progress tracking is disabled with the unchecked feature.

Example

let mut engine = Engine::new();

engine.on_progress(|count| {    // parameter is number of operations already performed
    if count % 1000 == 0 {
        println!("{}", count);  // print out a progress log every 1,000 operations
    }
    None                        // return 'None' to continue running the script
                                // return 'Some(token)' to immediately terminate the script
});

Function signature

The signature of the closure to pass to Engine::on_progress is as follows.

Fn(operations: u64) -> Option<Dynamic>

Return value

ValueEffect
Some(token)terminate immediately, with token (a Dynamic value) as termination token
Nonecontinue script evaluation

Termination Token

Token

The termination token is commonly used to provide information on the reason behind the termination decision.

The Dynamic value returned is a termination token.

A script that is manually terminated returns with the error EvalAltResult::ErrorTerminated(token, position) wrapping this value.

If the termination token is not needed, simply return Some(Dynamic::UNIT) to terminate the script run with () as the token.

Maximum Number of Modules

Rhai by default does not limit how many modules can be loaded via import statements.

This can be changed via the Engine::set_max_modules method. Notice that setting the maximum number of modules to zero does not indicate unlimited modules, but disallows loading any module altogether.

A script attempting to load more than the maximum number of modules will terminate with an error result.

This limit can also be used to stop import-loops (i.e. cycles of modules referring to each other).

This check can be disabled via the unchecked feature for higher performance (but higher risks as well).

let mut engine = Engine::new();

engine.set_max_modules(5);      // allow loading only up to 5 modules

engine.set_max_modules(0);      // disallow loading any module (maximum = zero)

engine.set_max_modules(1000);   // set to a large number for effectively unlimited modules

Maximum Call Stack Depth

In Rhai, it is trivial for a function call to perform infinite recursion such that all stack space is exhausted.

// This is a function that, when called, recurses forever.
fn recurse_forever() {
    recurse_forever();
}

Rhai, by default, limits function calls to a maximum depth of 64 levels (8 levels in debug build).

This limit may be changed via the Engine::set_max_call_levels method.

A script exceeding the maximum call stack depth will terminate with an error result.

This check can be disabled via the unchecked feature for higher performance (but higher risks as well).

let mut engine = Engine::new();

engine.set_max_call_levels(10);     // allow only up to 10 levels of function calls

engine.set_max_call_levels(0);      // allow no function calls at all (max depth = zero)

Additional considerations

When setting this limit, care must be also be taken to the evaluation depth of each statement within a function.

It is entirely possible for a malicious script to embed a recursive call deep inside a nested expression or statement block (see maximum statement depth).

fn bad_function(n) {
    // Bail out long before reaching the limit
    if n > 10 {
        return;
    }

    // Nest many, many levels deep...
    if check_1() {
        if check_2() {
            if check_3() {
                if check_4() {
                        :
                    if check_n() {
                        bad_function(n+1);  // <- recursive call!
                    }
                        :
                }
            }
        }
    }
}

// The function call below may still overflow the stack!
bad_function(0);

Maximum Expression Nesting Depth

Rhai by default limits statement and expression nesting to a maximum depth of 64 (which should be plenty) when they are at global level, but only a depth of 32 when they are within function bodies.

For debug builds, these limits are set further downwards to 32 and 16 respectively.

That is because it is possible to overflow the Engine’s stack when it tries to recursively parse an extremely deeply-nested code stream.

// The following, if long enough, can easily cause stack overflow during parsing.
let a = (1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(...)+1)))))))))));

This limit may be changed via Engine::set_max_expr_depths.

There are two limits to set, one for the maximum depth at global level, and the other for function bodies.

A script exceeding the maximum nesting depths will terminate with a parse error. The malicious AST will not be able to get past parsing in the first place.

This check can be disabled via the unchecked feature for higher performance (but higher risks as well).

let mut engine = Engine::new();

engine.set_max_expr_depths(50, 5);  // allow nesting up to 50 layers of expressions/statements
                                    // at global level, but only 5 inside functions

Warning

Multiple layers of expressions may be generated for a simple language construct, even though it may correspond to only one AST node.

That is because the Rhai parser internally runs a recursive chain of function calls and it is important that a malicious script does not panic the parser in the first place.

Beware of recursion

Functions are placed under stricter limits because of the multiplicative effect of recursion.

A function can effectively call itself while deep inside an expression chain within the function body, thereby overflowing the stack even when the level of recursion is within limit.

fn deep_calc(a, n) {
    (a+(a+(a+(a+(a+(a+(a+(a+(a+ ... (a+deep_calc(a,n+1)) ... )))))))))
    //                                 ^^^^^^^^^^^^^^^^ recursive call!
}

let a = 42;

let result = (a+(a+(a+(a+(a+(a+(a+(a+(a+ ... (a+deep_calc(a,0)) ... )))))))));

In the contrived example above, each recursive call to the function deep_calc adds the total number of nested expression layers to Rhai’s evaluation stack. Sooner or later (most likely sooner than the limit for maximum depth of function calls is reached), a stack overflow can be expected.

In general, make sure that C x ( 5 + F ) + S layered calls do not cause a stack overflow, where:

  • C = maximum call stack depth,
  • F = maximum statement depth for functions,
  • S = maximum statement depth at global level.

Turning Off Safety Checks

Checked Arithmetic

Don’t Panic

Scripts under normal builds of Rhai never crash the host system – any panic is a bug.

By default, all arithmetic calculations in Rhai are checked, meaning that the script terminates with a runtime error whenever it detects a numeric over-flow/under-flow condition or an invalid floating-point operation.

This checking can be turned off via the unchecked feature for higher performance (but higher risks as well).

let x = 1_000_000_000_000;

x * x;      // Normal build - runtime error: multiplication overflow

x * x;      // 'unchecked' debug build - panic!
            // 'unchecked' release build - overflow with no error

x / 0;      // Normal build - runtime error: division by zero

x / 0;      // 'unchecked' build - panic!

Other Safety Checks

In addition to overflows, there are many other safety checks performed by Rhai at runtime. unchecked turns them all off as well, such as…

Infinite loops

// Normal build - runtime error: exceeds maximum number of operations
loop {
    foo();
}

// 'unchecked' build - never terminates!
loop {
    foo();
}

Infinite recursion

fn foo() {
    foo();
}

foo();      // Normal build - runtime error: exceeds maximum stack depth

foo();      // 'unchecked' build - panic due to stack overflow!

Gigantic data structures

let x = [];

// Normal build - runtime error: array exceeds maximum size
loop {
    x += 42;
}

// 'unchecked' build - panic due to out-of-memory!
loop {
    x += 42;
}

Improper range iteration

// Normal build - runtime error: zero step
for x in range(0, 10, 0) { ... }

// 'unchecked' build - never terminates!
for x in range(0, 10, 0) { ... }

// Normal build - empty range
for x in range(0, 10, -1) { ... }

// 'unchecked' build - panic due to numeric underflow!
for x in range(0, 10, -1) { ... }

Script Optimization

Rhai includes an optimizer that tries to optimize a script after parsing. This can reduce resource utilization and increase execution speed.

Script optimization can be turned off via the no_optimize feature.

Optimization Levels

There are three levels of optimization: None, Simple and Full. The default is Simple.

An Engine’s optimization level is set via Engine::set_optimization_level.

// Turn on aggressive optimizations
engine.set_optimization_level(rhai::OptimizationLevel::Full);

None

None is obvious – no optimization on the AST is performed.

Simple (Default)

Simple performs only relatively safe optimizations without causing side-effects (i.e. it only relies on static analysis and built-in operators for constant standard types, and will not perform any external function calls).

Warning

After constants propagation is performed, if the constants are then modified (yes, it is possible, via Rust functions), the modified values will not show up in the optimized script.

Only the initialization values of constants are ever retained.

Warning

Overriding a built-in operator in the Engine afterwards has no effect after the optimizer replaces an expression with its calculated value.

Full

Full is much more aggressive, including calling external functions on constant arguments to determine their results.

One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators.

Optimization Passes

Script optimization is performed via multiple passes. Each pass does a specific optimization.

The optimization is completed when no passes can simplify the AST any further.

Built-in Optimization Passes

PassDescription
Dead code eliminationEliminates code that cannot be reached
Constants propagationReplaces constants with values
Compound assignments rewriteRewrites assignments into compound assignments
Eager operator evaluationEagerly calls operators with constant arguments
Eager function evaluationEagerly calls functions with constant arguments

Dead Code Elimination

Who writes dead code?

Nobody deliberately writes scripts with dead code (we hope).

They are, however, extremely common in template-based machine-generated scripts.

Rhai attempts to eliminate dead code.

“Dead code” is code that does nothing and has no side effects.

Example is an pure expression by itself as a statement (allowed in Rhai). The result of the expression is calculated then immediately discarded and not used.

{
    let x = 999;            // NOT eliminated: variable may be used later on (perhaps even an 'eval')
    
    123;                    // eliminated: no effect
    
    "hello";                // eliminated: no effect
    
    [1, 2, x, 4];           // eliminated: no effect
    
    if 42 > 0 {             // '42 > 0' is replaced by 'true' and the first branch promoted
        foo(42);            // promoted, NOT eliminated: the function 'foo' may have side-effects
    } else {
        bar(x);             // eliminated: branch is never reached
    }
    
    let z = x;              // eliminated: local variable, no side-effects, and only pure afterwards
    
    666                     // NOT eliminated: this is the return value of the block,
                            // and the block is the last one so this is the return value of the whole script
}

The above script optimizes to:

{
    let x = 999;
    foo(42);
    666
}

Constants Propagation

Usage

Effective in template-based machine-generated scripts to turn on/off certain sections.

Constants propagation is commonly used to:

const ABC = true;
const X = 41;

if ABC || calc(X+1) { print("done!"); }     // 'ABC' is constant so replaced by 'true'...
                                            // 'X' is constant so replaced by 41... 

if true || calc(42) { print("done!"); }     // '41+1' is replaced by 42
                                            // since '||' short-circuits, 'calc' is never called

if true { print("done!"); }                 // <- the line above is equivalent to this

print("done!");                             // <- the line above is further simplified to this
                                            //    because the condition is always true

Tip: Custom Scope constants

Constant values can be provided in a custom Scope object to the Engine for optimization purposes.

use rhai::{Engine, Scope};

let engine = Engine::new();

let mut scope = Scope::new();

// Add constant to custom scope
scope.push_constant("ABC", true);

// Evaluate script with custom scope
engine.run_with_scope(&mut scope,
r#"
    if ABC {    // 'ABC' is replaced by 'true'
        print("done!");
    }
"#)?;

Tip: Customer module constants

Constants defined in modules that are registered into an Engine via Engine::register_global_module are used in optimization.

use rhai::{Engine, Module};

let mut engine = Engine::new();

let mut module = Module::new();

// Add constant to module
module.set_var("ABC", true);

// Register global module
engine.register_global_module(module.into());

// Evaluate script
engine.run(
r#"
    if ABC {    // 'ABC' is replaced by 'true'
        print("done!");
    }
"#)?;

Caveat: Constants in custom scope and modules are also propagated into functions

Constants defined at global level typically cannot be seen by script functions because they are pure.

const MY_CONSTANT = 42;     // <- constant defined at global level

print(MY_CONSTANT);         // <- optimized to: print(42)

fn foo() {
    MY_CONSTANT             // <- not optimized: 'foo' cannot see 'MY_CONSTANT'
}

print(foo());               // error: 'MY_CONSTANT' not found

When constants are provided in a custom Scope (e.g. via Engine::compile_with_scope, Engine::eval_with_scope or Engine::run_with_scope), or in a module registered via Engine::register_global_module, instead of defined within the same script, they are also propagated to functions.

This is usually the intuitive usage and behavior expected by regular users, even though it means that a script will behave differently (essentially a runtime error) when script optimization is disabled.

use rhai::{Engine, Scope};

let engine = Engine::new();

let mut scope = Scope::new();

// Add constant to custom scope
scope.push_constant("MY_CONSTANT", 42_i64);

engine.run_with_scope(&mut scope,
"
    print(MY_CONSTANT);     // optimized to: print(42)

    fn foo() {
        MY_CONSTANT         // optimized to: fn foo() { 42 }
    }

    print(foo());           // prints 42
")?;

The script will act differently when script optimization is disabled because script functions are pure and typically cannot see constants within the custom Scope.

Therefore, constants in functions now throw a runtime error.

use rhai::{Engine, Scope, OptimizationLevel};

let mut engine = Engine::new();

// Turn off script optimization, no constants propagation is performed
engine.set_optimization_level(OptimizationLevel::None);

let mut scope = Scope::new();

// Add constant to custom scope
scope.push_constant("MY_CONSTANT", 42_i64);

engine.run_with_scope(&mut scope,
"
    print(MY_CONSTANT);     // prints 42

    fn foo() {
        MY_CONSTANT         // <- 'foo' cannot see 'MY_CONSTANT'
    }

    print(foo());           // error: 'MY_CONSTANT' not found
")?;

Caveat: Beware of large constants

Constants propagation replaces each usage of the constant with a clone of its value.

This may have negative implications to performance if the constant value is expensive to clone (e.g. if the type is very large).

let mut scope = Scope::new();

// Push a large constant into the scope...
let big_type = AVeryLargeType::take_long_time_to_create();
scope.push_constant("MY_BIG_TYPE", big_type);

// Causes each usage of 'MY_BIG_TYPE' in the script below to be replaced
// by cloned copies of 'AVeryLargeType'.
let result = engine.run_with_scope(&mut scope,
"
    let value = MY_BIG_TYPE.value;
    let data = MY_BIG_TYPE.data;
    let len = MY_BIG_TYPE.len();
    let has_options = MY_BIG_TYPE.has_options();
    let num_options = MY_BIG_TYPE.options_len();
")?;

To avoid this, compile the script first to an AST without the constants, then evaluate the AST (e.g. with Engine::eval_ast_with_scope or Engine::run_ast_with_scope) together with the constants.

Caveat: Constants may be modified by Rust methods

If the constants are modified later on (yes, it is possible, via Rust methods), the modified values will not show up in the optimized script. Only the initialization values of constants are ever retained.

const MY_SECRET_ANSWER = 42;

MY_SECRET_ANSWER.update_to(666);    // assume 'update_to(&mut i64)' is a Rust function

print(MY_SECRET_ANSWER);            // prints 42 because the constant is propagated

This is almost never a problem because real-world scripts seldom modify a constant, but the possibility is always there.

Compound Assignment Rewrite

Avoid cloning

Arguments passed as value are always cloned.

Usually, a compound assignment (e.g. += for append) takes a mutable first parameter (i.e. &mut) while the corresponding simple operator (i.e. +) does not.

The script optimizer rewrites normal assignments into compound assignments wherever possible in order to avoid unnecessary cloning.

let big = create_some_very_big_type();

big = big + 1;
//    ^ 'big' is cloned here

// The above is equivalent to:
let temp_value = big + 1;
big = temp_value;

big += 1;           // <- 'big' is NOT cloned

Warning: Simple references only

Only simple variable references are optimized.

No common sub-expression elimination is performed by Rhai.

x = x + 1;          // <- this statement...

x += 1;             // <- ... is rewritten to this

x[y] = x[y] + 1;    // <- but this is not,
                    //    so MUCH slower...

x[y] += 1;          // <- ... than this

Eager Operator Evaluation

Most operators are actually function calls, and those functions can be overridden, so whether they are optimized away depends on the situation:

No external functions

Rhai guarantees that no external function will be run, which may trigger side-effects (unless the optimization level is OptimizationLevel::Full).

// The following is most likely generated by machine.

const DECISION = 1;             // this is an integer, one of the standard types

if DECISION == 1 {              // this is optimized into 'true'
    :
} else if DECISION == 2 {       // this is optimized into 'false'
    :
} else if DECISION == 3 {       // this is optimized into 'false'
    :
} else {
    :
}

// Or an equivalent using 'switch':

switch DECISION {
    1 => ...,                   // this statement is promoted
    2 => ...,                   // this statement is eliminated
    3 => ...,                   // this statement is eliminated
    _ => ...                    // this statement is eliminated
}

Pre-Evaluation of Constant Expressions

Because of the eager evaluation of operators for standard types, many constant expressions will be evaluated and replaced by the result.

let x = (1+2) * 3 - 4/5 % 6;    // will be replaced by 'let x = 9'

let y = (1 > 2) || (3 <= 4);    // will be replaced by 'let y = true'

For operators that are not optimized away due to one of the above reasons, the function calls are simply left behind.

// Assume 'new_state' returns some custom type that is NOT one of the standard types.
// Also assume that the '==' operator is defined for that custom type.
const DECISION_1 = new_state(1);
const DECISION_2 = new_state(2);
const DECISION_3 = new_state(3);

if DECISION == 1 {              // NOT optimized away because the operator is not built-in
    :                           // and may cause side-effects if called!
    :
} else if DECISION == 2 {       // same here, NOT optimized away
    :
} else if DECISION == 3 {       // same here, NOT optimized away
    :
} else {
    :
}

Alternatively, turn the optimizer to OptimizationLevel::Full.

Eager Function Evaluation When Using Full Optimization Level

When the optimization level is OptimizationLevel::Full, the Engine assumes all functions to be pure and will eagerly evaluated all function calls with constant arguments, using the result to replace the call.

This also applies to all operators (which are implemented as functions).

// When compiling the following with OptimizationLevel::Full...

const DECISION = 1;
                            // this condition is now eliminated because 'sign(DECISION) > 0'
if DECISION.sign() > 0 {    // is a call to the 'sign' and '>' functions, and they return 'true'
    print("hello!");        // this block is promoted to the parent level
} else {
    print("boo!");          // this block is eliminated because it is never reached
}

print("hello!");            // <- the above is equivalent to this
                            //    ('print' and 'debug' are handled specially)

Side-Effect Considerations for Full Optimization Level

All of Rhai’s built-in functions (and operators which are implemented as functions) are pure (i.e. they do not mutate state nor cause any side-effects, with the exception of print and debug which are handled specially) so using OptimizationLevel::Full is usually quite safe unless custom types and functions are registered.

If custom functions are registered, they may be called (or maybe not, if the calls happen to lie within a pruned code block).

If custom functions are registered to overload built-in operators, they will also be called when the operators are used (in an if statement, for example), potentially causing side-effects.

Rule of thumb

  • Always register custom types and functions after compiling scripts if OptimizationLevel::Full is used.

  • DO NOT depend on knowledge that the functions have no side-effects, because those functions can change later on and, when that happens, existing scripts may break in subtle ways.

Volatility Considerations for Full Optimization Level

Even if a custom function does not mutate state nor cause side-effects, it may still be volatile, i.e. it depends on the external environment and is not pure.

A perfect example is a function that gets the current time – obviously each run will return a different value!

print(get_current_time(true));      // prints the current time
                                    // notice the call to 'get_current_time'
                                    // has constant arguments

// The above, under full optimization level, is rewritten to:

print("10:25AM");                   // the function call is replaced by
                                    // its result at the time of optimization!

The optimizer, when using OptimizationLevel::Full, merrily assumes that all functions are non-volatile, so when it finds constant arguments (or none) it eagerly executes the function call and replaces it with the result.

This causes the script to behave differently from the intended semantics.

Warning

Avoid using OptimizationLevel::Full if volatile custom functions are involved.

Subtle Semantic Changes After Optimization

Some optimizations can alter subtle semantics of the script, causing the script to behave differently when run with or without optimization.

Typically, this involves some form of error that may arise in the original, unoptimized script but is optimized away by the script optimizer.

DO NOT depend on runtime errors

Needless to say, it is usually a Very Bad Idea™ to depend on a script failing with a runtime error or such kind of subtleties.

If it turns out to be necessary (why? I would never guess), turn script optimization off by setting the optimization level to OptimizationLevel::None.

Disappearing Runtime Errors

For example:

if true {           // condition always true
    123.456;        // eliminated
    hello;          // eliminated, EVEN THOUGH the variable doesn't exist!
    foo(42)         // promoted up-level
}

foo(42)             // <- the above optimizes to this

If the original script were evaluated instead, it would have been an error – the variable hello does not exist, so the script would have been terminated at that point with a runtime error.

In fact, any errors inside a statement that has been eliminated will silently disappear.

print("start!");
if my_decision { /* do nothing... */ }  // eliminated due to no effect
print("end!");

// The above optimizes to:

print("start!");
print("end!");

In the script above, if my_decision holds anything other than a boolean value, the script should have been terminated due to a type error.

However, after optimization, the entire if statement is removed (because an access to my_decision produces no side-effects), thus the script silently runs to completion without errors.

Eliminated Useless Work

Another example is more subtle – that of an empty loop body.

// ... say, the 'Engine' is limited to no more than 10,000 operations...

// The following should fail because it exceeds the operations limit:
for n in 0..42000 {
    // empty loop
}

// The above is optimized away because the loop body is empty
// and the iterations simply do nothing.
()

Normally, and empty loop body inside a for statement with a pure iterator does nothing and can be safely eliminated.

Thus the script now runs silently to completion without errors.

Without optimization, the script may fail by exceeding the maximum number of operations allowed.

Re-Optimize an AST

Sometimes it is more efficient to store one single, large script with delimited code blocks guarded by constant variables. This script is compiled once to an AST.

Then, depending on the execution environment, constants are passed into the Engine and the AST is re-optimized based on those constants via Engine::optimize_ast, effectively pruning out unused code sections.

The final, optimized AST is then used for evaluations.

// Compile master script to AST
let master_ast = engine.compile(
"
    fn do_work() {
        // Constants in scope are also propagated into functions
        print(SCENARIO);
    }

    switch SCENARIO {
        1 => do_work(),
        2 => do_something(),
        3 => do_something_else(),
        _ => do_nothing()
    }
")?;

for n in 0..5_i64 {
    // Create a new scope - put constants in it to aid optimization
    let mut scope = Scope::new();
    scope.push_constant("SCENARIO", n);

    // Re-optimize the AST
    let new_ast = engine.optimize_ast(&scope, master_ast.clone(), OptimizationLevel::Simple);

    // Run it
    engine.run_ast(&new_ast)?;
}

Constants propagation

Beware that constants inside the custom Scope will also be propagated to functions defined within the script while normally such functions are pure and cannot see variables/constants within the global Scope.

Manage AST’s

When compiling a Rhai script to an AST, the following data are packaged together as a single unit:

DataTypeDescriptionRequires featureAccess API
Source nameImmutableStringoptional text name to identify the source of the scriptsource(&self),
clone_source(&self),
set_source(&mut self, source),
clear_source(&mut self)
StatementsVec<Stmt>list of script statements at global levelinternalsstatements(&self),
statements_mut(&mut self)
FunctionsShared<Module>functions defined in the scriptinternals,
not no_function
shared_lib(&self)
Embedded module resolverStaticModuleResolverembedded module resolver for self-contained ASTinternals,
not no_module
resolver(&self)

Most of the AST API is available only under the internals feature.

Tip: Source name

Use the source name to identify the source script in errors – useful when multiple modules are imported recursively.

AST public API

For the complete AST API, refer to the documentation online.

Extract Only Functions

The following methods, not available under no_function, allow manipulation of the functions encapsulated within an AST:

MethodDescription
clone_functions_only(&self)clone the AST into a new AST with only functions, excluding statements
clone_functions_only_filtered(&self, filter)clone the AST into a new AST with only functions that pass the filter predicate, excluding statements
retain_functions(&mut self, filter)remove all functions in the AST that do not pass a particular predicate filter; statements are untouched
iter_functions(&self)return an iterator on all the functions in the AST
clear_functions(&mut self)remove all functions from the AST, leaving only statements

Extract Only Statements

The following methods allow manipulation of the statements in an AST:

MethodDescription
clone_statements_only(&self)clone the AST into a new AST with only the statements, excluding functions
clear_statements(&mut self)remove all statements from the AST, leaving only functions
iter_literal_variables(&self, constants, variables)return an iterator on all top-level literal constant and/or variable definitions in the AST

Merge and Combine AST’s

The following methods merge one AST with another:

MethodDescription
merge(&self, &ast),
+ operator
append the second AST to this AST, yielding a new AST that is a combination of the two; statements are simply appended, functions in the second AST of the same name and arity override similar functions in this AST
merge_filtered(&self, &ast, filter)append the second AST (but only functions that pass the predicate filter) to this AST, yielding a new AST that is a combination of the two; statements are simply appended, functions in the second AST of the same name and arity override similar functions in this AST
combine(&mut self, ast),
+= operator
append the second AST to this AST; statements are simply appended, functions in the second AST of the same name and arity override similar functions in this AST
combine_filtered(&mut self, ast, filter)append the second AST (but only functions that pass the predicate filter) to this AST; statements are simply appended, functions in the second AST of the same name and arity override similar functions in this AST

When statements are appended, beware that this may change the semantics of the script.

// First script
let ast1 = engine.compile(
"
     fn foo(x) { 42 + x }
     foo(1)
")?;

// Second script
let ast2 = engine.compile(
"
     fn foo(n) { `hello${n}` }
     foo("!")
")?;

// Merge them
let merged = ast1.merge(&ast2);

// Notice that using the '+' operator also works:
let merged = &ast1 + &ast2;

merged in the above example essentially contains the following script program:

fn foo(n) { `hello${n}` }   // <- definition of first 'foo' is overwritten
foo(1)                      // <- notice this will be "hello1" instead of 43,
                            //    but it is no longer the return value
foo("!")                    // <- returns "hello!"

Walk an AST

The internals feature allows access to internal Rhai data structures, particularly the nodes that make up the AST.

AST node types

There are a few useful types when walking an AST:

TypeDescription
ASTNodean enum with two variants: Expr or Stmt
Expran expression
Stmta statement
BinaryExpra sub-type containing the LHS and RHS of a binary expression
FnCallExpra sub-type containing information on a function call
CustomExpra sub-type containing information on a custom syntax expression

The AST::walk method takes a callback function and recursively walks the AST in depth-first manner, with the parent node visited before its children.

Callback function signature

The signature of the callback function takes the following form.

FnMut(&[ASTNode]) -> bool

The single argument passed to the method contains a slice of ASTNode types representing the path from the current node to the root of the AST.

Return true to continue walking the AST, or false to terminate.

Children visit order

The order of visits to the children of each node type:

Node typeChildren visit order
if statement
  1. condition expression
  2. then statements
  3. else statements (if any)
switch statement
  1. match element
  2. each of the case conditions and statements, in order
  3. each of the range conditions and statements, in order
  4. default statements (if any)
while, do, loop statement
  1. condition expression
  2. statements body
for statement
  1. collection expression
  2. statements body
return statementreturn value expression
throw statementexception value expression
trycatch statement
  1. try statements body
  2. catch statements body
import statementpath expression
Array literaleach of the element expressions, in order
Object map literaleach of the element expressions, in order
Interpolated stringeach of the string/expression segments, in order
Indexing
  1. LHS expression
  2. RHS (index) expression
Field access/method call
  1. LHS expression
  2. RHS expression
&&, ||, ??
  1. LHS expression
  2. RHS expression
Function call, operator expressioneach of the argument expressions, in order
let, const statementvalue expression
Assignment statement
  1. l-value expression
  2. value expression
Statements blockeach of the statements, in order
Custom syntax expressioneach of the inputs stream, in order
All otherssingle child (if any)

Use the Low-Level API to Register a Rust Function

When a native Rust function is registered with an Engine using the register_XXX API, Rhai transparently converts all function arguments from Dynamic into the correct types before calling the function.

For more power and flexibility, there is a low-level API to work directly with Dynamic values without the conversions.

Raw Function Registration

The Engine::register_raw_fn method is marked volatile, meaning that it may be changed without warning.

If this is acceptable, then using this method to register a Rust function opens up more opportunities.

engine.register_raw_fn(
    "increment_by",                                         // function name
    &[                                                      // a slice containing parameter types
        std::any::TypeId::of::<i64>(),                      // type of first parameter
        std::any::TypeId::of::<i64>()                       // type of second parameter
    ],
    |context, args| {                                       // fixed function signature
        // Arguments are guaranteed to be correct in number and of the correct types.

        // But remember this is Rust, so you can keep only one mutable reference at any one time!
        // Therefore, get a '&mut' reference to the first argument _last_.
        // Alternatively, use `args.split_first_mut()` etc. to split the slice first.

        let y = *args[1].read_lock::<i64>().unwrap();       // get a reference to the second argument
                                                            // then copy it because it is a primary type

        let y = std::mem::take(args[1]).cast::<i64>();      // alternatively, directly 'consume' it

        let y = args[1].as_int().unwrap();                  // alternatively, use 'as_xxx()'

        let x = args[0].write_lock::<i64>().unwrap();       // get a '&mut' reference to the first argument

        *x += y;                                            // perform the action

        Ok(Dynamic::UNIT)                                   // must be 'Result<Dynamic, Box<EvalAltResult>>'
    }
);

// The above is the same as (in fact, internally they are equivalent):

engine.register_fn("increment_by", |x: &mut i64, y: i64| *x += y);

Function Signature

The function signature passed to Engine::register_raw_fn takes the following form.

Fn(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<T, Box<EvalAltResult>>

where:

ParameterTypeDescription
Timpl Clonereturn type of the function
contextNativeCallContextthe current native call context, useful for recursively calling functions on the same Engine
args&mut [&mut Dynamic]a slice containing &mut references to Dynamic values.
The slice is guaranteed to contain enough arguments of the correct types.

Return value

The return value is the result of the function call.

Remember, in Rhai, all arguments except the first one are always passed by value (i.e. cloned). Therefore, it is unnecessary to ever mutate any argument except the first one, as all mutations will be on the cloned copy.

Extract The First &mut Argument (If Any)

To extract the first &mut argument passed by reference from the args parameter (&mut [&mut Dynamic]), use the following to get a mutable reference to the underlying value:

let value: &mut T = &mut *args[0].write_lock::<T>().unwrap();

*value = ...    // overwrite the existing value of the first `&mut` parameter

When there is a mutable reference to the first &mut argument, there can be no other immutable references to args, otherwise the Rust borrow checker will complain.

Therefore, always extract the mutable reference last, after all other arguments are taken.

Extract Other Pass-By-Value Arguments

To extract an argument passed by value from the args parameter (&mut [&mut Dynamic]), use the following statements.

Argument typeAccess statement (n = argument position)ResultOriginal value
INTargs[n].as_int().unwrap()INTuntouched
FLOATargs[n].as_float().unwrap()FLOATuntouched
Decimalargs[n].as_decimal().unwrap()Decimaluntouched
boolargs[n].as_bool().unwrap()booluntouched
charargs[n].as_char().unwrap()charuntouched
()args[n].as_unit().unwrap()()untouched
String&*args[n].read_lock::<ImmutableString>().unwrap()&ImmutableStringuntouched
String (consumed)std::mem::take(args[n]).cast::<ImmutableString>()ImmutableString()
Others&*args[n].read_lock::<T>().unwrap()&Tuntouched
Others (consumed)std::mem::take(args[n]).cast::<T>()T()

Example – Pass a Callback to a Rust Function

The low-level API is useful when there is a need to interact with the scripting Engine within a function.

The following example registers a function that takes a function pointer as an argument, then calls it within the same Engine. This way, a callback function can be provided to a native Rust function.

The example also showcases the use of FnPtr::call_raw, a low-level API which allows binding the this pointer to the function pointer call.

use rhai::{Engine, FnPtr};

let mut engine = Engine::new();

// Register a Rust function
engine.register_raw_fn(
    "bar",
    &[
        std::any::TypeId::of::<i64>(),                      // parameter types
        std::any::TypeId::of::<FnPtr>(),
        std::any::TypeId::of::<i64>(),
    ],
    |context, args| {
        // 'args' is guaranteed to contain enough arguments of the correct types

        let fp = std::mem::take(args[1]).cast::<FnPtr>();   // 2nd argument - function pointer
        let value = std::mem::take(args[2]);                // 3rd argument - function argument

        // The 1st argument holds the 'this' pointer.
        // This should be done last as it gets a mutable reference to 'args'.
        let this_ptr = args.get_mut(0).unwrap();

        // Use 'FnPtr::call_raw' to call the function pointer with the context
        // while also binding the 'this' pointer!
        fp.call_raw(&context, Some(this_ptr), [value])
    },
);

let result = engine.eval::<i64>(
r#"
    fn foo(x) { this += x; }        // script-defined function 'foo'

    let x = 41;                     // object
    x.bar(Fn("foo"), 1);            // pass 'foo' as function pointer
    x
"#)?;

Tip: Hold multiple references

In order to access a value argument that is expensive to clone while holding a mutable reference to the first argument, use one of the following tactics:

  1. If it is a primary type other than string, use as_xxx() as above;

  2. Directly consume that argument via std::mem::take as above;

  3. Use split_first_mut to partition the slice:

// Partition the slice
let (first, rest) = args.split_first_mut().unwrap();

// Mutable reference to the first parameter, of type '&mut A'
let this_ptr = &mut *first.write_lock::<A>().unwrap();

// Immutable reference to the second value parameter, of type '&B'
// This can be mutable but there is no point because the parameter is passed by value
let value_ref = &*rest[0].read_lock::<B>().unwrap();

TL;DR

Why read_lock and write_lock?

The Dynamic API that casts it to a reference to a particular data type is read_lock (for an immutable reference) and write_lock (for a mutable reference).

As the naming shows, something is locked in order to allow this access, and that something is a shared value created by capturing variables from closures.

Shared values are implemented as Rc<RefCell<Dynamic>> (Arc<RwLock<Dynamic>> under sync).

If the value is not a shared value, or if running under no_closure where there is no capturing, this API de-sugars to a simple reference cast.

In other words, there is no locking and reference counting overhead for the vast majority of non-shared values.

If the value is a shared value, then it is first locked and the returned lock guard allows access to the underlying value in the specified type.

EvalContext

Usage

Many functions in advanced API’s contain an EvalContext parameter in order to allow the current evaluation state to be accessed and/or modified.

EvalContext is a type that encapsulates the current evaluation context and exposes the following methods.

MethodReturn typeDescription
scope()&Scopereference to the current Scope
scope_mut()&mut &mut Scopemutable reference to the current Scope; variables can be added to/removed from it
engine()&Enginereference to the current Engine
source()Option<&str>reference to the current source, if any
tag()&Dynamicreference to the custom state that is persistent during the current run
tag_mut()&mut Dynamicmutable reference to the custom state that is persistent during the current run
iter_imports()impl Iterator<Item = (&str,&Module)>iterator of the current stack of modules imported via import statements, in reverse order (i.e. later modules come first); not available under no_module
global_runtime_state()&GlobalRuntimeStatereference to the current global runtime state (including the stack of modules imported via import statements)
global_runtime_state_mut()&mut &mut GlobalRuntimeStatemutable reference to the current global runtime state; use this to access the debugger field in order to set/clear break-points
iter_namespaces()impl Iterator<Item =&Module>iterator of the namespaces (as modules) containing all script-defined functions, in reverse order (i.e. later modules come first)
namespaces()&[&Module]reference to the namespaces (as modules) containing all script-defined functions
this_ptr()Option<&Dynamic>reference to the current bound this pointer, if any
this_ptr_mut()&mut Option<&mut Dynamic>mutable reference to the current bound this pointer, if any
call_level()usizethe current nesting level of function calls

Call a Function Within the Caller’s Scope

Peeking Out of The Pure Box

Only scripts

This is only meaningful for scripted functions.

Native Rust functions can never access any Scope.

Rhai functions are pure, meaning that they depend on on their arguments and have no access to the calling environment.

When a function accesses a variable that is not defined within that function’s Scope, it raises an evaluation error.

It is possible, through a special syntax, to actually run the function call within the Scope of the parent caller – i.e. the Scope that makes the function call – and access/mutate variables defined there.

fn foo(y) {             // function accesses 'x' and 'y', but 'x' is not defined
    x += y;             // 'x' is modified in this function
    let z = 0;          // 'z' is defined in this function's scope
    x
}

let x = 1;              // 'x' is defined here in the parent scope

foo(41);                // error: variable 'x' not found

// Calling a function with a '!' causes it to run within the caller's scope

foo!(41) == 42;         // the function can access and mutate the value of 'x'!

x == 42;                // 'x' is changed!

z == 0;                 // <- error: variable 'z' not found

x.method!();            // <- syntax error: not allowed in method-call style

// Also works for function pointers

let f = Fn("foo");

call!(f, 42) == 84;     // must use function-call style

x == 84;                // 'x' is changed once again

f.call!(41);            // <- syntax error: not allowed in method-call style

// But not allowed for module functions

import "hello" as h;

h::greet!();            // <- syntax error: not allowed in namespace-qualified calls

The caller’s scope can be mutated

Changes to variables in the calling Scope persist.

With this syntax, it is possible for a Rhai function to mutate its calling environment.

New variables are not retained

Variables or constants defined within the function are not retained. They remain local to the function.

Although the syntax resembles a Rust macro invocation, it is still a function call.

Caveat emptor

Functions relying on the calling Scope is often a Very Bad Idea™ because it makes code almost impossible to reason about and maintain, as their behaviors are volatile and unpredictable.

Rhai functions are normally pure, meaning that you can rely on the fact that they never mutate the outside environment. Using this syntax breaks this guarantee.

Functions called in this manner behave more like macros that are expanded inline than actual function calls, thus the syntax is also similar to Rust’s macro invocations.

This usage should be at the last resort.

YOU HAVE BEEN WARNED.

Use Rhai as a Domain-Specific Language (DSL)

Rhai can be successfully used as a domain-specific language (DSL).

Expressions Only

In many DSL scenarios, only evaluation of expressions is needed.

The Engine::eval_expression_XXX API can be used to restrict a script to expressions only.

Unicode Standard Annex #31 Identifiers

Variable names and other identifiers do not necessarily need to be ASCII-only.

The unicode-xid-ident feature, when turned on, causes Rhai to allow variable names and identifiers that follow Unicode Standard Annex #31.

This is sometimes useful in a non-English DSL.

Disable Keywords and/or Operators

In some DSL scenarios, it is necessary to further restrict the language to exclude certain language features that are not necessary or dangerous to the application.

For example, a DSL may disable the while loop while keeping all other statement types intact.

It is possible, in Rhai, to surgically disable keywords and operators.

Custom Operators

Some DSL scenarios require special operators that make sense only for that specific environment. In such cases, it is possible to define custom operators in Rhai.

let animal = "rabbit";
let food = "carrot";

animal eats food            // custom operator 'eats'

eats(animal, food)          // <- the above actually de-sugars to this

let x = foo # bar;          // custom operator '#'

let x = #(foo, bar)         // <- the above actually de-sugars to this

Although a custom operator always de-sugars to a simple function call, nevertheless it makes the DSL syntax much simpler and expressive.

Custom Syntax

For advanced DSL scenarios, it is possible to define entire expression syntax – essentially custom statement types.

For example, the following is a SQL-like syntax for some obscure DSL operation:

let table = [..., ..., ..., ...];

// Syntax = calculate $ident$ ( $expr$ -> $ident$ ) => $ident$ : $expr$
let total = calculate sum(table->price) => row : row.weight > 50;

// Note: There is nothing special about those symbols; to make it look exactly like SQL:
// Syntax = SELECT $ident$ ( $ident$ ) AS $ident$ FROM $expr$ WHERE $expr$
let total = SELECT sum(price) AS row FROM table WHERE row.weight > 50;

After registering this custom syntax with Rhai, it can be used anywhere inside a script as a normal expression.

For its evaluation, the callback function will receive the following list of inputs:

  • inputs[0] = "sum" – math operator
  • inputs[1] = "price" – field name
  • inputs[2] = "row" – loop variable name
  • inputs[3] = Expression(table) – data source
  • inputs[4] = Expression(row.weight > 50) – filter predicate

Other identifiers, such as "calculate", "FROM", as well as symbols such as -> and : etc., are parsed in the order defined within the custom syntax.

Remap Tokens During Parsing

The Rhai Engine first parses a script into a stream of tokens.

Tokens have the type Token which is only exported under internals.

The function Engine::on_parse_token, available only under internals, allows registration of a mapper function that converts (remaps) a Token into another.

Function Signature

Tip: Raising errors

Raise a parse error by returning Token::LexError as the mapped token.

The function signature passed to Engine::on_parse_token takes the following form.

Fn(token: Token, pos: Position, state: &TokenizeState) -> Token

where:

ParameterTypeDescription
tokenTokenthe next symbol parsed
posPositionlocation of the token
state&TokenizeStatecurrent state of the tokenizer

Example

use rhai::{Engine, FLOAT, Token};

let mut engine = Engine::new();

// Register a token mapper function.
engine.on_parse_token(|token, pos, state| {
    match token {
        // Change 'begin' ... 'end' to '{' ... '}'
        Token::Identifier(s) if &s == "begin" => Token::LeftBrace,
        Token::Identifier(s) if &s == "end" => Token::RightBrace,

        // Change all integer literals to floating-point
        Token::IntegerConstant(n) => Token::FloatConstant((n as FLOAT).into()),
        
        // Disallow '()'
        Token::Unit => Token::LexError(
            LexError::ImproperSymbol("()".to_string(), "".to_string()).into()
        ),

        // Pass through all other tokens unchanged
        _ => token
    }
});

Disable Certain Keywords and/or Operators

For certain embedded usage, it is sometimes necessary to restrict the language to a strict subset of Rhai to prevent usage of certain language features.

Rhai supports surgically disabling a keyword or operator via Engine::disable_symbol.

use rhai::Engine;

let mut engine = Engine::new();

engine
    .disable_symbol("if")       // disable the 'if' keyword
    .disable_symbol("+=");      // disable the '+=' operator

// The following all return parse errors.

engine.compile("let x = if true { 42 } else { 0 };")?;
//                      ^ 'if' is rejected as a reserved keyword

engine.compile("let x = 40 + 2; x += 1;")?;
//                                ^ '+=' is not recognized as an operator
//                         ^ other operators are not affected

Disable Looping

For certain scripts, especially those in embedded usage for straight calculations, or where Rhai script AST’s are eventually transcribed into some other instruction set, looping may be undesirable as it may not be supported by the application itself.

Rhai looping constructs include the while, loop, do and for statements.

Although it is possible to disable these keywords via Engine::disable_symbol, it is simpler to disable all looping via Engine::set_allow_looping.

use rhai::Engine;

let mut engine = Engine::new();

// Disable looping
engine.set_allow_looping(false);

// The following all return parse errors.

engine.compile("while x == y { x += 1; }")?;

engine.compile(r#"loop { print("hello world!"); }"#)?;

engine.compile("do { x += 1; } until x > 10;")?;

engine.compile("for n in 0..10 { print(n); }")?;

Custom Operators

See also

See this section for details on operator precedence.

For use as a DSL (Domain-Specific Languages), it is sometimes more convenient to augment Rhai with customized operators performing specific logic.

Engine::register_custom_operator registers a keyword as a custom operator, giving it a particular precedence (which cannot be zero).

Example

use rhai::Engine;

let mut engine = Engine::new();

// Register a custom operator '#' and give it a precedence of 160
// (i.e. between +|- and *|/)
// Also register the implementation of the custom operator as a function
engine.register_custom_operator("#", 160)?
      .register_fn("#", |x: i64, y: i64| (x * y) - (x + y));

// The custom operator can be used in expressions
let result = engine.eval_expression::<i64>("1 + 2 * 3 # 4 - 5 / 6")?;
//                                                    ^ custom operator

// The above is equivalent to: 1 + ((2 * 3) # 4) - (5 / 6)
result == 15;

Alternatives to a Custom Operator

Custom operators are merely syntactic sugar. They map directly to registered functions.

let mut engine = Engine::new();

// Define 'foo' operator
engine.register_custom_operator("foo", 160)?;

engine.eval::<i64>("1 + 2 * 3 foo 4 - 5 / 6")?;       // use custom operator

engine.eval::<i64>("1 + foo(2 * 3, 4) - 5 / 6")?;     // <- above is equivalent to this

A script using custom operators can always be pre-processed, via a pre-processor application, into a syntax that uses the corresponding function calls.

Using Engine::register_custom_operator merely enables a convenient shortcut.

Must be a Valid Identifier or Reserved Symbol

All custom operators must be identifiers that follow the same naming rules as variables.

Alternatively, they can also be reserved symbols, disabled operators or keywords.

engine.register_custom_operator("foo", 20)?;          // 'foo' is a valid custom operator

engine.register_custom_operator("#", 20)?;            // the reserved symbol '#' is also
                                                      // a valid custom operator

engine.register_custom_operator("+", 30)?;            // <- error: '+' is an active operator

engine.register_custom_operator("=>", 30)?;           // <- error: '=>' is an active symbol

Binary Operators Only

All custom operators must be binary (i.e. they take two operands). Unary custom operators are not supported.

// Register unary '#' operator
engine.register_custom_operator("#", 160)?
      .register_fn("#", |x: i64| x * x);

engine.eval::<i64>("# 42")?;                          // <- syntax error

Operator Precedence

All operators in Rhai has a precedence indicating how tightly they bind.

A higher precedence binds more tightly than a lower precedence, so * and / binds before + and - etc.

When registering a custom operator, the operator’s precedence must also be provided.

The following precedence table shows the built-in precedence of standard Rhai operators:

CategoryOperatorsBindingPrecedence (0-255)
Logic and bit masks||, |, ^left30
Logic and bit masks&&, &left60
Comparisons==, !=left90
Containmentinleft110
Comparisons>, >=, <, <=left130
Null-coalesce??left135
Ranges.., ..=left140
Arithmetic+, -left150
Arithmetic*, /, %left180
Arithmetic**right190
Bit-shifts<<, >>left210
Unary operators+, -, !righthighest
Object field access., ?.righthighest

Extend Rhai with Custom Syntax

For the ultimate adventurous, there is a built-in facility to extend the Rhai language with custom-defined syntax.

But before going off to define the next weird statement type, heed this warning:

Don’t Do It™

Stick with standard language syntax as much as possible.

Having to learn Rhai is bad enough, no sane user would ever want to learn yet another obscure language syntax just to do something.

Try custom operators first. A custom syntax should be considered a last resort.

Where this might be useful

  • Where an operation is used a LOT and a custom syntax saves a lot of typing.

  • Where a custom syntax significantly simplifies the code and significantly enhances understanding of the code’s intent.

  • Where certain logic cannot be easily encapsulated inside a function.

  • Where you just want to confuse your user and make their lives miserable, because you can.

How to Do It

Step One – Design The Syntax

A custom syntax is simply a list of symbols.

These symbol types can be used:

  • Standard keywords
  • Standard operators
  • Reserved symbols.
  • Identifiers following the variable naming rules.
  • $expr$ – any valid expression, statement or statement block.
  • $block$ – any valid statement block (i.e. must be enclosed by {}).
  • $ident$ – any variable name.
  • $symbol$ – any symbol, active or reserved.
  • $bool$ – a boolean value.
  • $int$ – an integer number.
  • $float$ – a floating-point number (if not no_float).
  • $string$ – a string literal.

The first symbol must be an identifier

There is no specific limit on the combination and sequencing of each symbol type, except the first symbol which must be a custom keyword that follows the naming rules of variables.

The first symbol also cannot be a normal keyword unless it is disabled. Any valid identifier that is not an active keyword works fine, even if it is a reserved keyword.

The first symbol must be unique

Rhai uses the first symbol as a clue to parse custom syntax.

Therefore, at any one time, there can only be one custom syntax starting with each unique symbol.

Any new custom syntax definition using the same first symbol simply overwrites the previous one.

Example

exec [ $ident$ $symbol$ $int$ ] <- $expr$ : $block$

The above syntax is made up of a stream of symbols:

PositionInput slotSymbolDescription
1execcustom keyword
2[the left bracket symbol
20$ident$a variable name
31$symbol$the operator
42$int$an integer number
5]the right bracket symbol
6<-the left-arrow symbol (which is a reserved symbol in Rhai).
73$expr$an expression, which may be enclosed with {}, or not.
8:the colon symbol
94$block$a statement block, which must be enclosed with {}.

This syntax matches the following sample code and generates five inputs (one for each non-keyword):

// Assuming the 'exec' custom syntax implementation declares the variable 'hello':
let x = exec [hello < 42] <- foo(1, 2) : {
            hello += bar(hello);
            baz(hello);
        };

print(x);       // variable 'x'  has a value returned by the custom syntax

print(hello);   // variable declared by a custom syntax persists!

Step Two – Implementation

Any custom syntax must include an implementation of it.

Function signature

The signature of an implementation function is as follows.

Fn(context: &mut EvalContext, inputs: &[Expression]) -> Result<Dynamic, Box<EvalAltResult>>

where:

ParameterTypeDescription
context&mut EvalContextmutable reference to the current evaluation context
inputs&[Expression]a list of input expression trees

and EvalContext is a type that encapsulates the current evaluation context.

Return value

Return value is the result of evaluating the custom syntax expression.

Access arguments

The most important argument is inputs where the matched identifiers ($ident$), expressions/statements ($expr$) and statement blocks ($block$) are provided.

To access a particular argument, use the following patterns:

Argument typePattern (n = slot in inputs)Result typeDescription
$ident$inputs[n].get_string_value().unwrap()&strvariable name
$symbol$inputs[n].get_literal_value::<ImmutableString>().unwrap()ImmutableStringsymbol literal
$expr$&inputs[n]&Expressionan expression tree
$block$&inputs[n]&Expressionan expression tree
$bool$inputs[n].get_literal_value::<bool>().unwrap()boolboolean value
$int$inputs[n].get_literal_value::<INT>().unwrap()INTinteger number
$float$inputs[n].get_literal_value::<FLOAT>().unwrap()FLOATfloating-point number
$string$inputs[n].get_literal_value::<ImmutableString>().unwrap()

inputs[n].get_string_value().unwrap()
ImmutableString

&str
string text

Get literal constants

Several argument types represent literal constants that can be obtained directly via Expression::get_literal_value<T> or Expression::get_string_value (for strings).

let expression = &inputs[0];

// Use 'get_literal_value' with a turbo-fish type to extract the value
let string_value = expression.get_literal_value::<ImmutableString>().unwrap();
let string_slice = expression.get_string_value().unwrap();

let float_value = expression.get_literal_value::<FLOAT>().unwrap();

// Or assign directly to a variable with type...
let int_value: i64 = expression.get_literal_value().unwrap();

// Or use type inference!
let bool_value = expression.get_literal_value().unwrap();

if bool_value { ... }       // 'bool_value' inferred to be 'bool'

Evaluate an expression tree

Use the EvalContext::eval_expression_tree method to evaluate an arbitrary expression tree within the current evaluation context.

let expression = &inputs[0];
let result = context.eval_expression_tree(expression)?;

Declare variables

New variables maybe declared (usually with a variable name that is passed in via $ident$).

It can simply be pushed into the Scope.

However, beware that all new variables must be declared prior to evaluating any expression tree. In other words, any Scope calls that change the list of must come before any EvalContext::eval_expression_tree calls.

let var_name = inputs[0].get_string_value().unwrap();
let expression = &inputs[1];

context.scope_mut().push(var_name, 0_i64);      // do this BEFORE 'context.eval_expression_tree'!

let result = context.eval_expression_tree(expression)?;

Step Three – Register the Custom Syntax

Use Engine::register_custom_syntax to register a custom syntax.

Again, beware that the first symbol must be unique. If there already exists a custom syntax starting with that symbol, the previous syntax will be overwritten.

The syntax is passed simply as a slice of &str.

// Custom syntax implementation
fn implementation_func(context: &mut EvalContext, inputs: &[Expression]) -> Result<Dynamic, Box<EvalAltResult>> {
    let var_name = inputs[0].get_string_value().unwrap();
    let stmt = &inputs[1];
    let condition = &inputs[2];

    // Push new variable into the scope BEFORE 'context.eval_expression_tree'
    context.scope_mut().push(var_name.to_string(), 0_i64);

    let mut count = 0_i64;

    loop {
        // Evaluate the statement block
        context.eval_expression_tree(stmt)?;

        count += 1;

        // Declare a new variable every three turns...
        if count % 3 == 0 {
            context.scope_mut().push(format!("{}{}", var_name, count), count);
        }

        // Evaluate the condition expression
        let expr_result = !context.eval_expression_tree(condition)?;

        match expr_result.as_bool() {
            Ok(true) => (),
            Ok(false) => break,
            Err(err) => return Err(EvalAltResult::ErrorMismatchDataType(
                            "bool".to_string(),
                            err.to_string(),
                            condition.position(),
                        ).into()),
        }
    }

    Ok(Dynamic::UNIT)
}

// Register the custom syntax (sample): exec<x> -> { x += 1 } while x < 0
engine.register_custom_syntax(
    &[ "exec", "<", "$ident$", ">", "->", "$block$", "while", "$expr$" ], // the custom syntax
    true,  // variables declared within this custom syntax
    implementation_func
)?;

Remember that a custom syntax acts as an expression, so it can show up practically anywhere:

// Use as an expression:
let foo = (exec<x> -> { x += 1 } while x < 42) * 100;

// New variables are successfully declared...
x == 42;
x3 == 3;
x6 == 6;

// Use as a function call argument:
do_something(exec<x> -> { x += 1 } while x < 42, 24, true);

// Use as a statement:
exec<x> -> { x += 1 } while x < 0;
//                               ^ terminate statement with ';' unless the custom
//                                 syntax already ends with '}'

Step Four – Disable Unneeded Statement Types

When a DSL needs a custom syntax, most likely than not it is extremely specialized. Therefore, many statement types actually may not make sense under the same usage scenario.

So, while at it, better disable those built-in keywords and operators that should not be used by the user. The would leave only the bare minimum language surface exposed, together with the custom syntax that is tailor-designed for the scenario.

A keyword or operator that is disabled can still be used in a custom syntax.

In an extreme case, it is possible to disable every keyword in the language, leaving only custom syntax (plus possibly expressions). But again, Don’t Do It™ – unless you are certain of what you’re doing.

Step Five – Document

For custom syntax, documentation is crucial.

Make sure there are lots of examples for users to follow.

Step Six – Profit!

Practical Example – Recreating JavaScript’s var Statement

The following example recreates a statement similar to the var variable declaration syntax in JavaScript, which creates a global variable if one doesn’t already exist. There is currently no equivalent in Rhai.

// Register the custom syntax: var x = ???
engine.register_custom_syntax(&[ "var", "$ident$", "=", "$expr$" ], true, |context, inputs| {
    let var_name = inputs[0].get_string_value().unwrap().to_string();
    let expr = &inputs[1];

    // Evaluate the expression
    let value = context.eval_expression_tree(expr)?;

    // Push a new variable into the scope if it doesn't already exist.
    // Otherwise just set its value.
    if !context.scope().is_constant(var_name).unwrap_or(false) {
        context.scope_mut().set_value(var_name.to_string(), value);
        Ok(Dynamic::UNIT)
    } else {
        Err(format!("variable {} is constant", var_name).into())
    }
})?;

Really Advanced – Custom Parsers

Sometimes it is desirable to have multiple custom syntax starting with the same symbol.

This is especially common for command-style syntax where the second symbol calls a particular command:

// The following simulates a command-style syntax, all starting with 'perform'.
perform hello world;        // A fixed sequence of symbols
perform action 42;          // Perform a system action with a parameter
perform update system;      // Update the system
perform check all;          // Check all system settings
perform cleanup;            // Clean up the system
perform add something;      // Add something to the system
perform remove something;   // Delete something from the system

Alternatively, a custom syntax may have variable length, with a termination symbol:

// The following is a variable-length list terminated by '>'  
tags < "foo", "bar", 123, ... , x+y, true >

For even more flexibility in order to handle these advanced use cases, there is a low level API for custom syntax that allows the registration of an entire mini-parser.

Use Engine::register_custom_syntax_raw to register a custom syntax parser together with the implementation function.

How Custom Parsers Work

Leading Symbol

Under this API, the leading symbol for a custom parser is no longer restricted to be valid identifiers.

It can either be:

Function Signature

The custom syntax parser has the following signature.

Fn(symbols: &[ImmutableString], look_ahead: &str) -> Result<Option<ImmutableString>, ParseError>

where:

ParameterTypeDescription
symbols&[ImmutableString]a slice of symbols that have been parsed so far, possibly containing $expr$ and/or $block$; $ident$ and other literal markers are replaced by the actual text
look_ahead&stra string slice containing the next symbol that is about to be read

Most strings are ImmutableString’s so it is usually more efficient to just clone the appropriate one (if any matches, or keep an internal cache for commonly-used symbols) as the return value.

Parameters

A custom parser takes as input parameters two pieces of information:

  • The symbols (as ImmutableStrings) parsed so far:

    Argument typeValue
    text stringtext value
    $ident$identifier name
    $symbol$symbol literal
    $expr$$expr$
    $block$$block$
    $bool$true or false
    $int$value of number
    $float$value of number
    $string$string text

    The custom parser can inspect this symbols stream to determine the next symbol to parse.

  • The look-ahead symbol, which is the symbol that will be parsed next.

    If the look-ahead is an expected symbol, the customer parser just returns it to continue parsing, or it can return $ident$ to parse it as an identifier, or even $expr$ to start parsing an expression.

    If the look-ahead is {, then the custom parser may also return $block$ to start parsing a statements block.

    If the look-ahead is unexpected, the custom parser should then return the symbol expected and Rhai will fail with a parse error containing information about the expected symbol.

Return value

The return value is Result<Option<ImmutableString>, ParseError> where:

ValueDescription
Ok(None)parsing complete and there are no more symbols to match
Ok(Some(symbol))the next symbol to match, which can also be $expr$, $ident$ or $block$
Err(error)error that is reflected back to the Engine – normally ParseError(ParseErrorType::BadInput(LexError::ImproperSymbol(message)), Position::NONE) to indicate that there is a syntax error, but it can be any ParseError.

A custom parser always returns Some with the next symbol expected (which can be $ident$, $expr$, $block$ etc.) or None if parsing should terminate (without reading the look-ahead symbol).

Tip: $$ return symbol

A return symbol starting with $$ is treated specially.

Like None, it also terminates parsing, but at the same time it adds this symbol as text into the inputs stream at the end.

This is typically used to inform the implementation function which custom syntax variant was actually parsed.

Example

engine.register_custom_syntax_raw(
    // The leading symbol - which needs not be an identifier.
    "perform",
    // The custom parser implementation - always returns the next symbol expected
    // 'look_ahead' is the next symbol about to be read
    //
    // Return symbols starting with '$$' terminate parsing but also allows us
    // to determine which syntax variant was actually parsed so we can perform the
    // appropriate action.
    //
    // The return type is 'Option<ImmutableString>' to allow common text strings
    // to be interned and shared easily, reducing allocations during parsing.
    |symbols, look_ahead| match symbols.len() {
        // perform ...
        1 => Ok(Some("$ident$".into())),
        // perform command ...
        2 => match symbols[1].as_str() {
            "action" => Ok(Some("$expr$".into())),
            "hello" => Ok(Some("world".into())),
            "update" | "check" | "add" | "remove" => Ok(Some("$ident$".into())),
            "cleanup" => Ok(Some("$$cleanup".into())),
            cmd => Err(LexError::ImproperSymbol(format!("Improper command: {}", cmd))
                       .into_err(Position::NONE)),
        },
        // perform command arg ...
        3 => match (symbols[1].as_str(), symbols[2].as_str()) {
            ("action", _) => Ok(Some("$$action".into())),
            ("hello", "world") => Ok(Some("$$hello-world".into())),
            ("update", arg) => match arg {
                "system" => Ok(Some("$$update-system".into())),
                "client" => Ok(Some("$$update-client".into())),
                _ => Err(LexError::ImproperSymbol(format!("Cannot update {}", arg))
                         .into_err(Position::NONE))
            },
            ("check", arg) => Ok(Some("$$check".into())),
            ("add", arg) => Ok(Some("$$add".into())),
            ("remove", arg) => Ok(Some("$$remove".into())),
            (cmd, arg) => Err(LexError::ImproperSymbol(
                format!("Invalid argument for command {}: {}", cmd, arg)
            ).into_err(Position::NONE)),
        },
        _ => unreachable!(),
    },
    // No variables declared/removed by this custom syntax
    false,
    // Implementation function
    implementation_func
);

Debugging Interface

For systems open to external user-created scripts, it is usually desirable to provide a debugging experience to the user. The alternative is to provide a custom implementation of debug via Engine::on_debug that traps debug output to show in a side panel, for example, which is actually extremely simple.

Nevertheless, in some systems, it may not be convenient, or even possible, for the user to debug his or her scripts simply via good-old print or debug statements – the system does not have any facility for printed output, for instance.

Or the system may require more advanced debugging facilities than mere print statements – such as break-points.

For these advanced scenarios, Rhai contains a Debugging interface, turned on via the debugging feature (which implies the internals feature).

The debugging interface resides under the debugger sub-module.

The Rhai Debugger

The rhai-dbg bin tool shows a simple example of employing the debugging interface to create a debugger for Rhai scripts!

Built-in Functions

The following functions (defined in the DebuggingPackage but excluded if using a raw Engine) provides runtime information for debugging purposes.

FunctionParameter(s)Not available underDescription
back_tracenoneno_function, no_indexreturns an array of object maps or strings, each containing one level of function call;
returns an empty array if no debugger is registered
// This recursive function prints its own call stack during each run
fn foo(x) {
    print(back_trace());        // prints the current call stack

    if x > 0 {
        foo(x - 1)
    }
}

Register with the Debugger

Hooking up a debugging interface is as simple as providing closures to the Engine’s built-in debugger via Engine::register_debugger.

use rhai::debugger::{ASTNode, DebuggerCommand};

let mut engine = Engine::new();

engine.register_debugger(
    // Provide a callback to initialize the debugger state
    || { ... },
    // Provide a callback for each debugging step
    |context, event, node, source, pos| {
        ...

        DebuggerCommand::StepOver
    }
);

Tip: Accessing the Debugger

The type debugger::Debugger allows for manipulating break-points, among others.

The Engine’s debugger instance can be accessed via context.global_runtime_state_mut().debugger.

Callback Functions Signature

There are two callback functions to register for the debugger.

The first is simply a function to initialize the state of the debugger (a Dynamic value), with the following signature.

Fn() -> Dynamic

The second callback is a function which will be called by the debugger during each step, with the following signature.

Fn(context: EvalContext, event: debugger::DebuggerEvent, node: ASTNode, source: &str, pos: Position) -> Result<debugger::DebuggerCommand, Box<EvalAltResult>>

where:

ParameterTypeDescription
contextEvalContextthe current evaluation context
eventDebuggerEventan enum indicating the event that triggered the debugger
nodeASTNodean enum with two variants: Expr or Stmt, corresponding to the current expression node or statement node in the AST
source&strthe source of the current AST, or empty if none
posPositionposition of the current node, same as node.position()

and EvalContext is a type that encapsulates the current evaluation context.

Event

The event parameter of the second closure passed to Engine::register_debugger contains a debugger::DebuggerEvent which is an enum with the following variants.

DebuggerEvent variantDescription
Startthe debugger is triggered at the beginning of evaluation
Stepthe debugger is triggered at the next step of evaluation
BreakPoint(n)the debugger is triggered by the n-th break-point
FunctionExitWithValue(r)the debugger is triggered by a function call returning with value r which is &Dynamic
FunctionExitWithError(err)the debugger is triggered by a function call exiting with error err which is &EvalAltResult
Endthe debugger is triggered at the end of evaluation

Return value

Tip: Initialization

When a script starts evaluation, the debugger always stops at the very first AST node with the event parameter set to DebuggerStatus::Start.

This allows initialization to be done (e.g. setting up break-points).

The second closure passed to Engine::register_debugger will be called when stepping into or over expressions and statements, or when break-points are hit.

The return type of the closure is Result<debugger::DebuggerCommand, Box<EvalAltResult>>.

If an error is returned, the script evaluation at that particular instance returns with that particular error. It is thus possible to abort the script evaluation by returning an error that is not catchable, such as EvalAltResult::ErrorTerminated.

If no error is returned, then the return debugger::DebuggerCommand variant determines the continued behavior of the debugger.

DebuggerCommand variantBehaviorgdb equivalent
Continuecontinue with normal script evaluationcontinue
StepIntorun to the next expression or statement, diving into functionsstep
StepOverrun to the next expression or statement, skipping over functions
Nextrun to the next statement, skipping over functionsnext
FunctionExitrun to the end of the current function call; debugger is triggered before the function call returns and the Scope clearedfinish

Debugger State

Sometimes it is useful to keep a persistent state within the debugger.

The Engine::register_debugger API accepts a function that returns the initial value of the debugger’s state, which is a Dynamic and can hold any value.

This state value is the stored into the debugger’s custom state.

Access the Debugger State

Use EvalContext::global_runtime_state_mut().debugger to gain access to the current debugger::Debugger instance.

The following debugger::Debugger methods allow access to the custom debugger state.

MethodParameter typeReturn typeDescription
statenone&Dynamicreturns the custom state
state_mutnone&mut Dynamicreturns a mutable reference to the custom state
set_stateimpl Into<Dynamic>nonesets the value of the custom state

Example

engine.register_debugger(
    || {
        // Say, use an object map for the debugger state
        let mut state = Map::new();
        // Initialize properties
        state.insert("hello".into(), 42_64.into());
        state.insert("foo".into(), false.into());
        Dynamic::from_map(state)
    },
    |context, node, source, pos| {
        // Print debugger state - which is an object map
        let state = context.global_runtime_state_mut().debugger.state();
        println!("Current state = {}", state);

        // Get the state as an object map
        let mut state = context.global_runtime_state_mut()
                               .debugger.state_mut()
                               .write_lock::<Map>().unwrap();

        // Read state
        let hello = state.get("hello").unwrap().as_int().unwrap();

        // Modify state
        state.insert("hello".into(), (hello + 1).into());
        state.insert("foo".into(), true.into());
        state.insert("something_new".into(), "hello, world!".into());

        // Continue with debugging
        Ok(DebuggerCommand::StepInto)
    }
);

Call Stack

Call stack frames

Each “frame” in the call stack corresponds to one layer of function call (script-defined or native Rust).

A call stack frame has the type debugger::CallStackFrame.

The debugger keeps a call stack of function calls with argument values.

This call stack can be examined to determine the control flow at any particular point.

The Debugger::call_stack method returns a slice of all call stack frames.

use rhai::debugger::*;

let debugger = &mut context.global_runtime_state_mut().debugger;

// Get depth of the call stack.
let depth = debugger.call_stack().len();

// Display all function calls
for frame in debugger.call_stack().iter() {
    println!("{}", frame);
}

Break-Points

A break-point always stops the current evaluation and calls the debugging callback.

A break-point is represented by the debugger::BreakPoint type, which is an enum with the following variants.

BreakPoint variantNot available underDescription
AtPosition { source, pos, enabled }no_positionbreaks at the specified position in the specified source (empty if none);
if pos is at beginning of line, breaks anywhere on the line
AtFunctionName { name, enabled }breaks when a function matching the specified name is called (can be operator)
AtFunctionCall { name, args, enabled }breaks when a function matching the specified name (can be operator) and the specified number of arguments is called
AtProperty { name, enabled }no_objectbreaks at the specified property access

Access Break-Points

The following debugger::Debugger methods allow access to break-points for manipulation.

MethodReturn typeDescription
break_points&[BreakPoint]returns a slice of all BreakPoint’s
break_points_mut&mut Vec<BreakPoint>returns a mutable reference to all BreakPoint’s

Example

use rhai::debugger::*;

let debugger = &mut context.global_runtime_state_mut().debugger;

// Get number of break-points.
let num_break_points = debugger.break_points().len();

// Add a new break-point on calls to 'foo(_, _, _)'
debugger.break_points_mut().push(
    BreakPoint::AtFunctionCall { name: "foo".into(), args: 3 }
);

// Display all break-points
for bp in debugger.break_points().iter() {
    println!("{}", bp);
}

// Clear all break-points
debugger.break_points_mut().clear();

Implement a Debugging Server

Sometimes it is desirable to embed a debugging server inside the application such that an external debugger interface can connect to the application’s running instance at runtime.

This way, when scripts are run within the application, it is easy for an external interface to debug those scripts as they run.

Such connections may take the form of any communication channel, for example a TCP/IP connection, a named pipe, or an MPSC channel.

Example

Server side

The following example assumes bi-direction, blocking messaging channels, such as a WebSocket connection, with a server that accepts connections and creates those channels.

use rhai::debugger::{ASTNode, DebuggerCommand};

let mut engine = Engine::new();

engine.register_debugger(
    // Use the initialization callback to set up the communications channel
    // and listen to it
    || {
        // Create server that will listen to requests
        let mut server = MyCommServer::new();
        server.listen("localhost:8080");

        // Wrap it up in a shared locked cell so it can be 'Clone'
        let server = Rc::new(RefCell::new(server));

        // Store the channel in the debugger state
        Dynamic::from(server)
    },
    // Trigger the server during each debugger stop point
    |context, event, node, source, pos| {
        // Get the state
        let mut state = context.tag_mut();

        // Get the server
        let mut server = state.write_lock::<MyCommServer>().unwrap();

        // Send the event to the server - blocking call
        server.send_message(...);

        // Receive command - blocking call
        match server.receive_message() {
            None => DebuggerCommand::StepOver,
            // Decode command
            Ok(...) => { ... }
            Ok(...) => { ... }
            Ok(...) => { ... }
            Ok(...) => { ... }
            Ok(...) => { ... }
                :
                :
        }
    }
);

Client side

The client can be any system that can work with WebSockets for messaging.

// Connect to the application's debugger
let webSocket = new WebSocket("wss://localhost:8080");

webSocket.on_message = (event) => {
    let msg = JSON.parse(event.data);

    switch msg.type {
        // handle debugging events from the application...
        case "step": {
                :
        }
                :
                :
    }
};

// Send command to the application
webSocket.send("step-over");

Object-Oriented Programming (OOP)

Rhai does not have objects per se and is not object-oriented (in the traditional sense), but it is possible to simulate object-oriented programming.

To OOP or not to OOP, that is the question.

Regardless of whether object-oriented programming (OOP) should be treated as a pattern or an anti-pattern (the programming world is split 50-50 on this), there are always users who would like to write Rhai in “the OOP way.”

Rust itself is not object-oriented in the traditional sense; JavaScript also isn’t, but that didn’t prevent generations of programmers trying to shoehorn a class-based inheritance system onto it.

So… as soon as Rhai gained in usage, way way before version 1.0, PR’s started coming in to make it possible to write Rhai in “the OOP way.”

Use Object Maps to Simulate OOP

Rhai’s object maps has special support for OOP.

Rhai conceptMaps to OOP
Object mapsobjects
Object map properties holding valuesproperties
Object map properties that hold function pointersmethods

When a property of an object map is called like a method function, and if it happens to hold a valid function pointer (perhaps defined via an anonymous function or more commonly as a closure), then the call will be dispatched to the actual function with this binding to the object map itself.

Use Closures to Define Methods

Anonymous functions or closures defined as values for object map properties take on a syntactic shape which resembles very closely that of class methods in an OOP language.

Closures also capture variables from the defining environment, which is a very common language feature. Capturing is accomplished via a feature called automatic currying and can be turned off via the no_closure feature.

let factor = 1;

// Define the object
let obj = #{
        data: 0,                            // object field
        increment: |x| this.data += x,      // 'this' binds to 'obj'
        update: |x| this.data = x * factor, // 'this' binds to 'obj', 'factor' is captured
        action: || print(this.data)         // 'this' binds to 'obj'
    };

// Use the object
obj.increment(1);
obj.action();                               // prints 1

obj.update(42);
obj.action();                               // prints 42

factor = 2;

obj.update(42);
obj.action();                               // prints 84

Simulating Inheritance with Polyfills

The fill_with method of object maps can be conveniently used to polyfill default method implementations from a base class, as per OOP lingo.

Do not use the mixin method because it overwrites existing fields.

// Define base class
let BaseClass = #{
    factor: 1,
    data: 42,

    get_data: || this.data * 2,
    update: |x| this.data += x * this.factor
};

let obj = #{
    // Override base class field
    factor: 100,

    // Override base class method
    // Notice that the base class can also be accessed, if in scope
    get_data: || this.call(BaseClass.get_data) * 999,
}

// Polyfill missing fields/methods
obj.fill_with(BaseClass);

// By this point, 'obj' has the following:
//
// #{
//      factor: 100
//      data: 42,
//      get_data: || this.call(BaseClass.get_data) * 999,
//      update: |x| this.data += x * this.factor
// }

// obj.get_data() => (this.data (42) * 2) * 999
obj.get_data() == 83916;

obj.update(1);

obj.data == 142

Prototypical Inheritance via Mixin

Some languages like JavaScript has prototypical inheritance, which bases inheritance on a prototype object.

It is possible to simulate this form of inheritance using object maps, leveraging the fact that, in Rhai, all values are cloned and there are no pointers. This significantly simplifies coding logic.

// Define prototype 'class'

const PrototypeClass = #{
    field: 42,

    get_field: || this.field,
    set_field: |x| this.field = x
};

// Create instances of the 'class'

let obj1 = PrototypeClass;                  // a copy of 'PrototypeClass'

obj1.get_field() == 42;

let obj2 = PrototypeClass;                  // a copy of 'PrototypeClass'

obj2.mixin(#{                               // override fields and methods
    field: 1,
    get_field: || this.field * 2
};

obj2.get_field() == 2;

let obj2 = PrototypeClass + #{              // compact syntax with '+'
    field: 1,
    get_field: || this.field * 2
};

obj2.get_field() == 2;

// Inheritance chain

const ParentClass = #{
    field: 123,
    new_field: 0,
    action: || print(this.new_field * this.field)
};

const ChildClass = #{
    action: || {
        this.field = this.new_field;
        this.new_field = ();
    }
}

let obj3 = PrototypeClass + ParentClass + ChildClass;

// Alternate formulation

const ParentClass = PrototypeClass + #{
    field: 123,
    new_field: 0,
    action: || print(this.new_field * this.field)
};

const ChildClass = ParentClass + #{
    action: || {
        this.field = this.new_field;
        this.new_field = ();
    }
}

let obj3 = ChildClass;                      // a copy of 'ChildClass'

Working With Rust Enums

Why enums are hard

Rust enum variants are not considered separate types.

Although Rhai integrates fine with Rust enums (treated transparently as custom types), it is impossible (short of registering a complete API) to distinguish between individual variants and to extract internal data from them.

Enums in Rust can hold data and are typically used with pattern matching.

Unlike Rust, Rhai does not have built-in pattern matching capabilities, so working with enum variants that contain embedded data is not an easy proposition.

Since Rhai is dynamic and variables can hold any type of data, they are essentially enums by nature.

Multiple distinct types can be stored in a single Dynamic without merging them into an enum as variants.

This section outlines a number of possible solutions to work with Rust enums.

Simulate an Enum API

A plugin module is extremely handy in creating an entire API for a custom enum type.

use rhai::plugin::*;
use rhai::{Dynamic, Engine, EvalAltResult};

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum MyEnum {
    Foo,
    Bar(i64),
    Baz(String, bool),
}

// Create a plugin module with functions constructing the 'MyEnum' variants
#[export_module]
mod MyEnumModule {
    // Constructors for 'MyEnum' variants
    pub const Foo: &MyEnum = MyEnum::Foo;
    pub fn Bar(value: i64) -> MyEnum { MyEnum::Bar(value) }
    pub fn Baz(val1: String, val2: bool) -> MyEnum { MyEnum::Baz(val1, val2) }
    // Access to fields
    #[rhai_fn(global, get = "enum_type", pure)]
    pub fn get_type(my_enum: &mut MyEnum) -> String {
        match my_enum {
            MyEnum::Foo => "Foo".to_string(),
            MyEnum::Bar(_) => "Bar".to_string(),
            MyEnum::Baz(_, _) => "Baz".to_string(),
        }
    }
    #[rhai_fn(global, get = "field_0", pure)]
    pub fn get_field_0(my_enum: &mut MyEnum) -> Dynamic {
        match my_enum {
            MyEnum::Foo => Dynamic::UNIT,
            MyEnum::Bar(x) => Dynamic::from(x),
            MyEnum::Baz(x, _) => Dynamic::from(x),
        }
    }
    #[rhai_fn(global, get = "field_1", pure)]
    pub fn get_field_1(my_enum: &mut MyEnum) -> Dynamic {
        match my_enum {
            MyEnum::Foo | MyEnum::Bar(_) => Dynamic::UNIT,
            MyEnum::Baz(_, x) => Dynamic::from(x),
        }
    }
    // Printing
    #[rhai_fn(global, name = "to_string", name = "to_debug", pure)]
    pub fn to_string(my_enum: &mut MyEnum) -> String {
        format!("{:?}", my_enum)
    }
    // '==' and '!=' operators
    #[rhai_fn(global, name = "==", pure)]
    pub fn eq(my_enum: &mut MyEnum, my_enum2: MyEnum) -> bool {
        my_enum == &my_enum2
    }
    #[rhai_fn(global, name = "!=", pure)]
    pub fn neq(my_enum: &mut MyEnum, my_enum2: MyEnum) -> bool {
        my_enum != &my_enum2
    }
}

let mut engine = Engine::new();

// Load the module as the module namespace "MyEnum"
engine.register_type_with_name::<MyEnum>("MyEnum")
      .register_static_module("MyEnum", exported_module!(MyEnumModule).into());

With this API in place, working with enums feels almost the same as in Rust:

let x = MyEnum::Foo;

let y = MyEnum::Bar(42);

let z = MyEnum::Baz("hello", true);

x == MyEnum::Foo;

y != MyEnum::Bar(0);

// Detect enum types

x.enum_type == "Foo";

y.enum_type == "Bar";

z.enum_type == "Baz";

// Extract enum fields

y.field_0 == 42;

y.field_1 == ();

z.field_0 == "hello";

z.field_1 == true;

Since enums are internally treated as custom types, they are not literals and cannot be used as a match case in switch statements. This is quite a limitation because the equivalent match statement is commonly used in Rust to work with enums and bind variables to variant-internal data.

It is possible, however, to switch through enum variants based on their types:

switch my_enum.enum_type {
  "Foo" => ...,
  "Bar" => {
    let value = foo.field_0;
    ...
  }
  "Baz" => {
    let val1 = foo.field_0;
    let val2 = foo.field_1;
    ...
  }
}

Use switch Through Arrays

Another way to work with Rust enums in a switch statement is through exposing the internal data (or at least those that act as effective discriminants) of each enum variant as a variable-length array, usually with the name of the variant as the first item for convenience:

use rhai::Array;

engine.register_get("enum_data", |my_enum: &mut MyEnum| {
    match my_enum {
        MyEnum::Foo => vec![ "Foo".into() ] as Array,

        // Say, skip the data field because it is not
        // used as a discriminant
        MyEnum::Bar(value) => vec![ "Bar".into() ] as Array,

        // Say, all fields act as discriminants
        MyEnum::Baz(val1, val2) => vec![
            "Baz".into(), val1.clone().into(), (*val2).into()
        ] as Array
    }
});

Then it is a simple matter to match an enum via a switch expression.

// Assume 'value' = 'MyEnum::Baz("hello", true)'
// 'enum_data' creates a variable-length array with 'MyEnum' data
let x = switch value.enum_data {
    ["Foo"] => 1,
    ["Bar"] => value.field_1,
    ["Baz", "hello", false] => 4,
    ["Baz", "hello", true] => 5,
    _ => 9
};

x == 5;

// Which is essentially the same as:
let x = switch [value.type, value.field_0, value.field_1] {
    ["Foo", (), ()] => 1,
    ["Bar", 42, ()] => 42,
    ["Bar", 123, ()] => 123,
            :
    ["Baz", "hello", false] => 4,
    ["Baz", "hello", true] => 5,
    _ => 9
}

Usually, a helper method returns an array of values that can uniquely determine the switch case based on actual usage requirements – which means that it probably skips fields that contain data instead of discriminants.

Then switch is used to very quickly match through a large number of array shapes and jump to the appropriate case implementation.

Data fields can then be extracted from the enum independently.

Simulate Macros to Simplify Scripts

Usage scenario

  • Scripts need to access existing data in variables.

  • The particular fields to access correspond to long/complex expressions (e.g. long indexing and/or property chains foo[x][y].bar[z].baz).

  • Usage is prevalent inside the scripts, requiring extensive duplications of code that are prone to typos and errors.

  • There are a few such variables to modify at the same time – otherwise, it would be simpler to bind the this pointer to the variable.

Key concepts

  • Pick a macro syntax that is unlikely to conflict with content in literal strings.

  • Before script evaluation/compilation, globally replace macros with their corresponding expansions.

Pick a Macro Syntax

Warning: Not real macros

The technique described here is to simulate macros. They are not REAL macros.

Pick a syntax that is intuitive for the domain but unlikely to occur naturally inside string literals.

Sample SyntaxSample usage
#FOO#FOO = 42;
$Bar$Bar.work();
<Baz>print(<Baz>);
#HELLO#let x = #HELLO#;
%HEY%%HEY% += 1;

Tip: Avoid normal words

Avoid normal syntax that may show up inside a string literal.

For example, if using Target as a macro:

// This script...
Target.do_damage(10);

if Target.hp <= 0 {
    print("Target is destroyed!");
}

// Will turn to this...
entities["monster"].do_damage(10);

if entities["monster"].hp <= 0 {
    // Text in string literal erroneously replaced!
    print("entities["monster"] is destroyed!");
}

Global Search/Replace

// Replace macros with expansions
let script = script.replace("#FOO", "foo[x][y].bar[z].baz");

let mut scope = Scope::new();

// Add global variables
scope.push("foo", ...);
scope.push_constant("x", ...);
scope.push_constant("y", ...);
scope.push_constant("z", ...);

// Run the script as normal
engine.run_with_scope(&mut scope, script)?;

Example script

print(`Found entity FOO at (${x},${y},${z})`);

let speed = #FOO.speed;

if speed < 42 {
    #FOO.speed *= 2;
} else {
    #FOO.teleport(#FOO.home());
}

print(`FOO is now at (${ #FOO.current_location() })`);

Character positions

After macro expansion, the character positions of different script elements will be shifted based on the length of the expanded text.

Therefore, error positions may no longer point to the correct locations in the original, unexpanded scripts.

Line numbers are not affected.

Dynamic Constants Provider

Usage scenario

  • A system has a large number of constants, but only a minor set will be used by any script.

  • The system constants are expensive to load.

  • The system constants set is too massive to push into a custom Scope.

  • The values of system constants are volatile and call-dependent.

Key concepts

  • Use a variable resolver to intercept variable access.

  • Only load a variable when it is being used.

  • Perform a lookup based on variable name, and provide the correct data value.

  • May even perform back-end network access or look up the latest value from a database.

Implementation

let mut engine = Engine::new();

// Create shared data provider.
// Assume that SystemValuesProvider::get(&str) -> Option<value> gets a value.
let provider = Arc::new(SystemValuesProvider::new());

// Clone the shared provider
let db = provider.clone();

// Register a variable resolver.
// Move the shared provider into the closure.
engine.on_var(move |name, _, _, _| Ok(db.get(name).map(Dynamic::from)));

Values are constants

All values provided by a variable resolver are constants due to their dynamic nature. They cannot be assigned to.

In order to change values in an external system, register a dedicated API for that purpose.

Global Constants

Usage scenario

Key concepts

Example

Assume that the following Rhai script needs to work (but it doesn’t).

// These are constants

const FOO = 1;
const BAR = 123;
const MAGIC_NUMBER = 42;

fn get_magic() {
    MAGIC_NUMBER        // <- oops! 'MAGIC_NUMBER' not found!
}

fn calc_foo(x) {
    x * global::FOO     // <- works but cumbersome; not desirable!
}

let magic = get_magic() * BAR;

let x = calc_foo(magic);

print(x);

Step 1 – Compile Script into AST

Compile the script into AST form.

Normally, it is useful to disable optimizations at this stage since the AST will be re-optimized later.

Strict Variables Mode must be OFF for this to work.

// Turn Strict Variables Mode OFF (if necessary)
engine.set_strict_variables(false);

// Turn optimizations OFF
engine.set_optimization_level(OptimizationLevel::None);

let ast = engine.compile("...")?;

Step 2 – Extract Constants

Use AST::iter_literal_variables to extract top-level constants from the AST.

let mut scope = Scope::new();

// Extract all top-level constants without running the script
ast.iter_literal_variables(true, false).for_each(|(name, _, value)|
    scope.push_constant(name, value);
);

// 'scope' now contains: FOO, BAR, MAGIC_NUMBER

Step 3a – Propagate Constants

Re-optimize the AST using the new constants.

// Turn optimization back ON
engine.set_optimization_level(OptimizationLevel::Simple);

let ast = engine.optimize_ast(&scope, ast, engine.optimization_level());

Step 3b – Recompile Script (Alternative)

If Strict Variables Mode is used, however, it is necessary to re-compile the script in order to detect undefined variable usages.

// Turn Strict Variables Mode back ON
engine.set_strict_variables(true);

// Turn optimization back ON
engine.set_optimization_level(OptimizationLevel::Simple);

// Re-compile the script using constants in 'scope'
let ast = engine.compile_with_scope(&scope, "...")?;

Step 4 – Run the Script

At this step, the AST is now optimized with constants propagated into all access sites.

The script essentially becomes:

// These are constants

const FOO = 1;
const BAR = 123;
const MAGIC_NUMBER = 42;

fn get_magic() {
    42      // <- constant replaced by value
}

fn calc_foo(x) {
    x * global::FOO
}

let magic = get_magic() * 123;  // <- constant replaced by value

let x = calc_foo(magic);

print(x);

Run it via Engine::run_ast or Engine::eval_ast.

// The 'scope' is no longer necessary
engine.run_ast(&ast)?;

Mutable Global State

Don’t Do It™

Consider JavaScript

Generations of programmers struggled to get around mutable global state (a.k.a. the window object) in the initial design of JavaScript.

In contrast to global constants, mutable global states are strongly discouraged because:

  1. It is a sure-fire way to create race conditions – that is why Rust does not support it;

  2. It adds considerably to debug complexity – it is difficult to reason, in large code bases, where/when a state value is being modified;

  3. It forces hard (but obscure) dependencies between separate pieces of code that are difficult to break when the need arises;

  4. It is almost impossible to add new layers of redirection and/or abstraction afterwards without major surgery.

I don’t care! Just tell me how to do it, now!

This is not something that Rhai encourages. You Have Been Warned™.

There are two ways…

Option 1 – Get/Set Functions

This is similar to the Control Layer pattern.

Use get/set functions to read/write the global mutable state.

// The globally mutable shared value
let value = Rc::new(RefCell::new(42));

// Register an API to access the globally mutable shared value
let v = value.clone();
engine.register_fn("get_global_value", move || *v.borrow());

let v = value.clone();
engine.register_fn("set_global_value", move |value: i64| *v.borrow_mut() = value);

These functions can be used in script functions to access the shared global state.

fn foo() {
    let current = get_global_value();       // Get global state value
    current += 1;
    set_global_value(current);              // Modify global state value
}

This option is preferred because it is possible to modify the get/set functions later on to add/change functionalities without introducing breaking script changes.

Option 2 – Variable Resolver

Declare a variable resolver that returns a shared value which is the global state.

// Use a shared value as the global state
let value: Dynamic = 1.into();
let mut value = value.into_shared();        // convert into shared value

// Clone the shared value
let v = value.clone();

// Register a variable resolver.
engine.on_var(move |name, _, _| {
    match name
        "value" => Ok(Some(v.clone())),
        _ => Ok(None)
    }
});

// The shared global state can be modified
*value.write_lock::<i64>().unwrap() = 42;

The global state variable can now be used just like a normal local variable, including modifications.

fn foo() {
    value = value * 2;
//          ^ global variable can be read
//  ^ global variable can also be modified
}

Anti-Pattern

This option makes mutable global state so easy to implement that it should actually be considered an Anti-Pattern.

Loadable Configuration

Usage scenario

  • A system where settings and configurations are complex and logic-driven.

  • Where said system is too complex to configure via standard configuration file formats such as JSON, TOML or YAML.

  • The system is complex enough to require a full programming language to configure. Essentially configuration by code.

  • Yet the configuration must be flexible, late-bound and dynamically loadable, just like a configuration file.

Key concepts

  • Leverage the loadable modules of Rhai. The no_module feature must not be on.

  • Expose the configuration API. Use separate scripts to configure that API. Dynamically load scripts via the import statement.

  • Leverage function overloading to simplify the API design.

  • Since Rhai is sand-boxed, it cannot mutate the environment. To modify the external configuration object via an API, it must be wrapped in a RefCell (or RwLock/Mutex for sync) and shared to the Engine.

Implementation

Configuration type

#[derive(Debug, Clone, Default)]
struct Config {
    id: String,
    some_field: i64,
    some_list: Vec<String>,
    some_map: HashMap<String, bool>,
}

Make shared object

type SharedConfig = Rc<RefCell<Config>>;

let config = SharedConfig::default();

or in multi-threaded environments with the sync feature, use one of the following:

type SharedConfig = Arc<RwLock<Config>>;

type SharedConfig = Arc<Mutex<Config>>;

Register config API

The trick to building a Config API is to clone the shared configuration object and move it into each function registration via a closure.

Therefore, it is not possible to use a plugin module to achieve this, and each function must be registered one after another.

// Notice 'move' is used to move the shared configuration object into the closure.
let cfg = config.clone();
engine.register_fn("config_set_id", move |id: String| cfg.borrow_mut().id = id);

let cfg = config.clone();
engine.register_fn("config_get_id", move || cfg.borrow().id.clone());

let cfg = config.clone();
engine.register_fn("config_set", move |value: i64| cfg.borrow_mut().some_field = value);

// Remember Rhai functions can be overloaded when designing the API.

let cfg = config.clone();
engine.register_fn("config_add", move |value: String|
    cfg.borrow_mut().some_list.push(value)
);

let cfg = config.clone();
engine.register_fn("config_add", move |values: &mut Array|
    cfg.borrow_mut().some_list.extend(values.into_iter().map(|v| v.to_string()))
);

let cfg = config.clone();
engine.register_fn("config_add", move |key: String, value: bool|
    cfg.borrow_mut().some_map.insert(key, value)
);

let cfg = config.clone();
engine.register_fn("config_contains", move |value: String|
    cfg.borrow().some_list.contains(&value)
);

let cfg = config.clone();
engine.register_fn("config_is_set", move |value: String|
    cfg.borrow().some_map.get(&value).cloned().unwrap_or(false)
);

Configuration script

┌────────────────┐
│ my_config.rhai │
└────────────────┘

config_set_id("hello");

config_add("foo");          // add to list
config_add("bar", true);    // add to map

if config_contains("hey") || config_is_set("hey") {
    config_add("baz", false);   // add to map
}

Load the configuration

import "my_config";         // run configuration script without creating a module

let id = config_get_id();

id == "hello";

Consider a Custom Syntax

This is probably one of the few scenarios where a custom syntax can be recommended.

A properly-designed custom syntax can make the configuration file clean, simple to write, easy to understand and quick to modify.

For example, the above configuration example may be expressed by this custom syntax:

┌────────────────┐
│ my_config.rhai │
└────────────────┘

// Configure ID
id "hello";

// Add to list
list + "foo";

// Add to map
map "bar" => true;

if config contains "hey" || config is_set "hey" {
    map "baz" => false;
}

Notice that contains and is_set may also be implemented as a custom operator.

Multi-Layered Functions

Usage scenario

  • A system is divided into separate layers, each providing logic in terms of scripted functions.

  • A lower layer provides default implementations of certain functions.

  • Higher layers each provide progressively more specific implementations of the same functions.

  • A more specific function, if defined in a higher layer, always overrides the implementation in a lower layer.

  • This is akin to object-oriented programming but with functions.

  • This type of system is extremely convenient for dynamic business rules configuration, setting corporate-wide policies, granting permissions for specific roles etc. where specific, local rules need to override corporate-wide defaults.

Practical scenario

Assuming a LOB (line-of-business) system for a large MNC (multi-national corporation) with branches, facilities and offices across the globe.

The system needs to provide basic, corporate-wide policies to be enforced through the worldwide organization, but also cater for country- or region-specific rules, practices and regulations.

LayerDescription
corporatecorporate-wide policies
regionalregional policy overrides
countrycountry-specific modifications for legal compliance
officespecial treatments for individual office locations

Key concepts

  • Each layer is a separate script.

  • The lowest layer script is compiled into a base AST.

  • Higher layer scripts are also compiled into AST and combined into the base using AST::combine (or the += operator), overriding any existing functions.

Examples

Assume the following four scripts, one for each layer:

┌────────────────┐
│ corporate.rhai │
└────────────────┘

// Default implementation of 'foo'.
fn foo(x) { x + 1 }

// Default implementation of 'bar'.
fn bar(x, y) { x + y }

// Default implementation of 'no_touch'.
fn no_touch() { throw "do not touch me!"; }


┌───────────────┐
│ regional.rhai │
└───────────────┘

// Specific implementation of 'foo'.
fn foo(x) { x * 2 }

// New implementation for this layer.
fn baz() { print("hello!"); }


┌──────────────┐
│ country.rhai │
└──────────────┘

// Specific implementation of 'bar'.
fn bar(x, y) { x - y }

// Specific implementation of 'baz'.
fn baz() { print("hey!"); }


┌─────────────┐
│ office.rhai │
└─────────────┘

// Specific implementation of 'foo'.
fn foo(x) { x + 42 }

Load and combine them sequentially:

let engine = Engine::new();

// Compile the baseline layer.
let mut ast = engine.compile_file("corporate.rhai".into())?;

// Combine the first layer.
let lowest = engine.compile_file("regional.rhai".into())?;
ast += lowest;

// Combine the second layer.
let middle = engine.compile_file("country.rhai".into())?;
ast += middle;

// Combine the third layer.
let highest = engine.compile_file("office.rhai".into())?;
ast += highest;

// Now, 'ast' contains the following functions:
//
// fn no_touch() {                // from 'corporate.rhai'
//     throw "do not touch me!";
// }
// fn foo(x) { x + 42 }           // from 'office.rhai'
// fn bar(x, y) { x - y }         // from 'country.rhai'
// fn baz() { print("hey!"); }    // from 'country.rhai'

No super call

Unfortunately, there is no super call that calls the base implementation (i.e. no way for a higher-layer function to call an equivalent lower-layer function).

Hot Reloading

Usage scenario

  • A system where scripts are used for behavioral control.

  • All or parts of the control scripts need to be modified dynamically without re-initializing the host system.

  • New scripts must be active as soon as possible after modifications are detected.

Key concepts

  • The Rhai Engine is re-entrant, meaning that it is decoupled from scripts.

  • A new script only needs to be recompiled and the new AST replaces the old for new behaviors to be active.

  • Surgically patch scripts when only parts of the scripts are modified.

Implementation

Embed scripting engine and script into system

Say, a system has a Rhai Engine plus a compiled script (in AST form), with the AST kept with interior mutability…

// Main system object
struct System {
    engine: Engine,
    script: Rc<RefCell<AST>>,
      :
}

// Embed Rhai 'Engine' and control script
let engine = Engine::new();
let ast = engine.compile_file("config.rhai")?;

let mut system = System { engine, script: Rc::new(RefCell::new(ast)) };

// Handle events with script functions
system.on_event(|sys: &System, event: &str, data: Map| {
    let mut scope = Scope::new();

    // Call script function which is the same name as the event
    sys.engine.call_fn(&mut scope, sys.script.borrow(), event, (data,)).unwrap();

    result
});

Hot reload entire script upon change

If the control scripts are small enough and changes are infrequent, it is much simpler just to recompile the whole set of script and replace the original AST with the new one.

// Watch for script file change
system.watch(|sys: &System, file: &str| {
    // Compile the new script
    let ast = sys.engine.compile_file(file.into())?;

    // Hot reload - just replace the old script!
    *sys.script.borrow_mut() = ast;

    Ok(())
});

Hot patch specific functions

If the control scripts are large and complicated, and if the system can detect changes to specific functions, it is also possible to patch just the changed functions.

// Watch for changes in the script
system.watch_for_script_change(|sys: &mut System, fn_name: &str| {
    // Get the script file that contains the function
    let script = get_script_file_path(fn_name);

    // Compile the new script
    let mut patch_ast = sys.engine.compile_file(script)?;

    // Remove everything other than the specified function
    patch_ast.clear_statements();
    patch_ast.retain_functions(|_, _, name, _| name == fn_name);

    // Hot reload (via +=) only those functions in the script!
    *sys.script.borrow_mut() += patch_ast;
});

Tip: Multi-threaded considerations

For a multi-threaded environments, replace Rc with Arc, RefCell with RwLock or Mutex, and turn on the sync feature.

One Engine Instance Per Call

Usage scenario

  • A system where scripts are called a lot, in tight loops or in parallel.

  • Keeping a global Engine instance is sub-optimal due to contention and locking.

  • Scripts need to be executed independently from each other, perhaps concurrently.

  • Scripts are used to create Rust closures that are stored and may be called at any time, perhaps concurrently. In this case, the Engine instance is usually moved into the closure itself.

Key concepts

  • Rhai’s AST structure is sharable – meaning that one copy of the AST can be run on multiple instances of Engine simultaneously.

  • Rhai’s packages and modules are also sharable.

  • This means that Engine instances can be decoupled from the base system (packages and modules) as well as the scripts (AST) so they can be created very cheaply.

Procedure

Examples

use rhai::def_package;
use rhai::packages::{Package, StandardPackage};

// Define the custom package 'MyCustomPackage'.
def_package! {
    /// My own personal super-duper custom package
    pub MyCustomPackage(module) {
      // Aggregate other packages simply by calling 'init' on each.
      StandardPackage::init(module);

      // Register additional Rust functions using 'Module::set_native_fn'.
      let hash = module.set_native_fn("foo", |s: ImmutableString| {
          Ok(foo(s.into_owned()))
      });

      // Remember to update the parameter names/types and return type metadata
      // when using the 'metadata' feature.
      // 'Module::set_native_fn' by default does not set function metadata.
      module.update_fn_metadata(hash, &["s: ImmutableString", "i64"]);
  }
}

let ast = /* ... some AST ... */;

let custom_pkg = MyCustomPackage::new();

// The following loop creates 10,000 Engine instances!

for x in 0..10_000 {
    // Create a raw Engine - extremely cheap
    let mut engine = Engine::new_raw();

    // Register custom package - cheap
    engine.register_global_module(custom_pkg.as_shared_module());

    // Evaluate script
    engine.run_ast(&ast)?;
}

Multi-Threaded Synchronization

Usage scenarios

  • A system needs to communicate with an Engine running in a separate thread.

  • Multiple Engines running in separate threads need to coordinate/synchronize with each other.

Key concepts

  • An MPSC channel (or any other appropriate synchronization primitive) is used to send/receive messages to/from an Engine running in a separate thread.

  • An API is registered with the Engine that is essentially blocking until synchronization is achieved.

Example

use rhai::{Engine};

fn main() {
    // Channel: Script -> Master
    let (tx_script, rx_master) = std::sync::mpsc::channel();
    // Channel: Master -> Script
    let (tx_master, rx_script) = std::sync::mpsc::channel();

    // Spawn thread with Engine
    std::thread::spawn(move || {
        // Create Engine
        let mut engine = Engine::new();

        // Register API
        // Notice that the API functions are blocking
        engine.register_fn("get", move || rx_script.recv().unwrap())
              .register_fn("put", move |v: i64| tx_script.send(v).unwrap());

        // Run script
        engine.run(
        r#"
            print("Starting script loop...");

            loop {
                // The following call blocks until there is data
                // in the channel
                let x = get();
                print(`Script Read: ${x}`);

                x += 1;

                print(`Script Write: ${x}`);

                // The following call blocks until the data
                // is successfully sent to the channel
                put(x);
            }
        "#).unwrap();
    });

    // This is the main processing thread

    println!("Starting main loop...");

    let mut value = 0_i64;

    while value < 10 {
        println!("Value: {}", value);
        // Send value to script
        tx_master.send(value).unwrap();
        // Receive value from script
        value = rx_master.recv().unwrap();
    }
}

Considerations for sync

std::mpsc::Sender and std::mpsc::Receiver are not Sync, therefore they cannot be used in registered functions if the sync feature is enabled.

In that situation, it is possible to wrap the Sender and Receiver each in a Mutex or RwLock, which makes them Sync.

This, however, incurs the additional overhead of locking and unlocking the Mutex or RwLock during every function call, which is technically not necessary because there are no other references to them.

Async

The example above highlights the fact that Rhai scripts can call any Rust function, including ones that are blocking.

However, Rhai is essentially a blocking, single-threaded engine. Therefore it does not provide an async API.

That means, although it is simple to use Rhai within a multi-threading environment where blocking a thread is acceptable or even expected, it is currently not possible to call async functions within Rhai scripts because there is no mechanism in Engine to wrap the state of the call stack inside a future.

Fortunately an Engine is re-entrant so it can be shared among many async tasks. It is usually possible to split a script into multiple parts to avoid having to call async functions.

Creating an Engine is also relatively cheap (extremely cheap if creating a raw Engine), so it is also a valid pattern to spawn a new Engine instance for each task.

Scriptable Control Layer Over Rust Backend

Usage scenario

  • A system provides core functionalities, but no driving logic.

  • The driving logic must be dynamic and hot-loadable.

  • A script is used to drive the system and provide control intelligence.

Key concepts

  • Expose a Control API.

  • Leverage function overloading to simplify the API design.

  • Since Rhai is sand-boxed, it cannot mutate anything outside of its internal environment. To perform external actions via an API, the actual system must be wrapped in a RefCell (or RwLock/Mutex for sync) and shared to the Engine.

Using Rhai for games

Although this usage pattern appears a perfect fit for game logic, avoid writing the entire game in Rhai. Performance will not be acceptable.

Implement as much functionalities of the game engine in Rust as possible. Rhai integrates well with Rust so this is usually not a hinderance.

Lift as much out of Rhai as possible. Use Rhai only for the logic that must be dynamic or hot-loadable.

Implementation

There are two broad ways for Rhai to control an external system, both of which involve wrapping the system in a shared, interior-mutated object.

This is one way which does not involve exposing the data structures of the external system, but only through exposing an abstract API primarily made up of functions.

Use this when the API is relatively simple and clean, and the number of functions is small enough.

For a complex API involving lots of functions, or an API that has a clear object structure, use the Singleton Command Object pattern instead.

Functional API

Assume that a system provides the following functional API:

struct EnergizerBunny;

impl EnergizerBunny {
    pub fn new () -> Self { ... }
    pub fn go (&mut self) { ... }
    pub fn stop (&mut self) { ... }
    pub fn is_going (&self) { ... }
    pub fn get_speed (&self) -> i64 { ... }
    pub fn set_speed (&mut self, speed: i64) { ... }
}

Wrap API in shared object

pub type SharedBunny = Rc<RefCell<EnergizerBunny>>;

or in multi-threaded environments with the sync feature, use one of the following:

pub type SharedBunny = Arc<RwLock<EnergizerBunny>>;

pub type SharedBunny = Arc<Mutex<EnergizerBunny>>;

Register control API

The trick to building a Control API is to clone the shared API object and move it into each function registration via a closure.

Therefore, it is not possible to use a plugin module to achieve this, and each function must be registered one after another.

// Notice 'move' is used to move the shared API object into the closure.
let b = bunny.clone();
engine.register_fn("bunny_power", move |on: bool| {
    if on {
        if b.borrow().is_going() {
            println!("Still going...");
        } else {
            b.borrow_mut().go();
        }
    } else {
        if b.borrow().is_going() {
            b.borrow_mut().stop();
        } else {
            println!("Already out of battery!");
        }
    }
});

let b = bunny.clone();
engine.register_fn("bunny_is_going", move || b.borrow().is_going());

let b = bunny.clone();
engine.register_fn("bunny_get_speed", move ||
    if b.borrow().is_going() { b.borrow().get_speed() } else { 0 }
);

let b = bunny.clone();
engine.register_result_fn("bunny_set_speed", move |speed: i64|
    if speed <= 0 {
        return Err("Speed must be positive!".into());
    } else if speed > 100 {
        return Err("Bunny will be going too fast!".into());
    }

    if b.borrow().is_going() {
        b.borrow_mut().set_speed(speed)
    } else {
        return Err("Bunny is not yet going!".into());
    }

    Ok(())
);

Use the API

if !bunny_is_going() { bunny_power(true); }

if bunny_get_speed() > 50 { bunny_set_speed(50); }

Singleton Command Object

Usage scenario

  • A system provides core functionalities, but no driving logic.

  • The driving logic must be dynamic and hot-loadable.

  • A script is used to drive the system and provide control intelligence.

  • The API is multiplexed, meaning that it can act on multiple system-provided entities, or

  • The API lends itself readily to an object-oriented (OO) representation.

Key concepts

  • Expose a Command type with an API. The no_object feature must not be on.

  • Leverage function overloading to simplify the API design.

  • Since Rhai is sand-boxed, it cannot mutate anything outside of its internal environment. To perform external actions via an API, the command object type must be wrapped in a RefCell (or RwLock/Mutex for sync) and shared to the Engine.

  • Load each command object into a custom Scope as constant variables.

  • Control each command object in script via the constants.

Implementation

There are two broad ways for Rhai to control an external system, both of which involve wrapping the system in a shared, interior-mutated object.

This is the other way which involves directly exposing the data structures of the external system as a name singleton object in the scripting space.

Use this when the API is complex but has a clear object structure.

For a relatively simple API that is action-based and not object-based, use the Control Layer pattern instead.

Functional API

Assume the following command object type:

struct EnergizerBunny { ... }

impl EnergizerBunny {
    pub fn new () -> Self { ... }
    pub fn go (&mut self) { ... }
    pub fn stop (&mut self) { ... }
    pub fn is_going (&self) -> bool { ... }
    pub fn get_speed (&self) -> i64 { ... }
    pub fn set_speed (&mut self, speed: i64) { ... }
    pub fn turn (&mut self, left_turn: bool) { ... }
}

Wrap command object type as shared

pub type SharedBunny = Rc<RefCell<EnergizerBunny>>;

or in multi-threaded environments with the sync feature, use one of the following:

pub type SharedBunny = Arc<RwLock<EnergizerBunny>>;

pub type SharedBunny = Arc<Mutex<EnergizerBunny>>;

Develop a plugin with methods and getters/setters

The easiest way to develop a complete set of API for a custom type is via a plugin module.

Notice that putting pure in #[rhai_fn(...)] allows a getter/setter to operate on a constant without raising an error. Therefore, it is needed on all functions.

use rhai::plugin::*;

// Remember to put 'pure' on all functions, or they'll choke on constants!

#[export_module]
pub mod bunny_api {
    // Cust