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.18.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.14 sec on a single-core, 2.6 GHz Linux VM (see benchmarks).

  • 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

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.

Object maps

There is a built-in object map type which is adequate for most uses.

It is also 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.

Function pointers

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

Simulated closures

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

  • 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.

Highly customizable

This lack of formalism allows the tokenizer and parser themselves to be exposed as services in order to support a wide range of user customizations, such as:

  • No bytecodes/JIT – Rhai uses a heavily-optimized AST-walking interpreter which is fast enough for most real-life scenarios.

How it compares?

See Rhai performance benchmarks.

Benchmarking Rhai

Tip: Nothing beats a JIT

Needless to say V8 (JavaScript), which is a JIT compiler with type specialization, blows any interpreter (AST or bytecodes based) out of the water, as also should LuaJIT.

So if you absolutely must have top performance…

The purpose of Rhai is not to be blazing 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.

The following benchmarks were run on a 2.6GHz Linux VM comparing performance-optimized and full builds of Rhai with Python 3 and V8 (Node.js).

BenchmarkRhai
(Perf)
Rhai
(Full)
Python 3
(bytecodes)
V8
(JIT)
Description
Fibonacci2.25s3.2s0.6s0.07sstresses recursive function calls
1M loop0.13s0.2s0.08s0.05sa simple counting loop (1 million iterations) that must run as fast as possible
Prime numbers0.85s1.2s0.4s0.09sa closer-to-real-life calculation workload

In general, Rhai is roughly 2x slower than Python 3, which is a bytecodes interpreter, for typical real-life workloads.

TL;DR – Rhai is usually fast enough

Small data structures

Essential AST and runtime data structures are packed small and kept together to maximize cache friendliness.

Pre-calculations

Functions are dispatched based on pre-calculated hashes. Variables are mostly accessed through pre-calculated offsets to the variables file (a Scope). It is seldom necessary to look something up by name.

Caching

Function resolutions are cached so they do not incur lookup costs after a couple of calls.

No scope-chain

Maintaining a scope chain is deliberately avoided by design so function scopes do not pay any speed penalty. This allows variables data to be kept together in a contiguous block, avoiding allocations and fragmentation while being cache-friendly.

Immutable strings

Rhai uses immutable strings to bypass cloning issues.

No sharing

In a typical script evaluation run, no data is shared and nothing is locked (other than variables captured by closures).

DO NOT: Write the next 4D VR game entirely in Rhai

Rhai deliberately keeps the language small and lean by omitting advanced language features such as classes, inheritance, interfaces, generics, first-class functions/closures, pattern matching, monads (whatever), concurrency, async etc.

Focus is on flexibility and ease of use instead of a powerful, expressive language.

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

DO THIS: 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 (e.g. Python 3).

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.

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.66.0.

Dependencies

Rhai takes care to pull in as few dependencies as possible in order to avoid bloat when using the library.

Main Dependencies

CrateDescriptionWhy use it?
smallvecVec variant that stores a number of items inlinemost functions have very few parameters, and avoiding allocations result in significant performance improvement
num-traitsnumeric traitsfor use with macros defining arithmetic functions and operators
ahashfast hashing for datanot cryptographically secure, thus faster than standard Rust hashing; Rhai does a lot of hashing so this matters
once_cellglobal static datafixed hashing keys for static hashing
bitflagsbit fieldsstore flags in AST nodes to minimize memory usage
smartstringString variant that stores short strings inlinemost strings in scripts (e.g. keywords, properties, symbols, variables, function names etc.) are short, and avoiding allocations result in significant performance improvement

no-std Dependencies

CrateDescription
no-std-compatcreate std imports under no-std
libmno-std math library
core-errorError trait for no-std
hashbrownHashMap and HashSetfor no-std

Feature Dependencies

WASM Dependencies

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

This section shows how to install the Rhai crate into a Rust application.

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.18.0"    # assuming 1.18.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
stdnostandard features
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 (pulls in the rust_decimal crate)
unicode-xid-identnoallows Unicode Standard Annex #31 as identifiers (pulls in the unicode-xid crate)
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_timenodisables timestamps
no_functionnodisables script-defined functions; implies no_closure
no_modulenodisables loading external modules
no_closurenodisables capturing external variables in closures
no_custom_syntaxnodisables custom syntax and custom operators

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.

Specify default-features = false when compiling for no-std, which will remove the default std feature.

FeatureAdditive?Description
no_stdnobuilds for no-std; notice that additional dependencies will be pulled in to replace missing 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.18.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())?;

See also: 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.

Practical Use – Header Template Scripts

Sometimes it is desirable to include a standardized header template in a script that contains pre-defined functions, constants and imported modules.

// START OF THE HEADER TEMPLATE
// The following should run before every script...

import "hello" as h;
import "world" as w;

// Standard constants

const GLOBAL_CONSTANT = 42;
const SCALE_FACTOR = 1.2;

// Standard functions

fn foo(x, y) { ... }

fn bar() { ... }

fn baz() { ... }

// END OF THE HEADER TEMPLATE

// Everything below changes from run to run

foo(bar() + GLOBAL_CONSTANT, baz() * SCALE_FACTOR)

Option 1 – The easy way

Prepend the script header template onto independent scripts and run them as a whole.

Pros: Easy!

Cons: If the header template is long, work is duplicated every time to parse it.

let header_template = "..... // scripts... .....";

for index in 0..10000 {
    let user_script = db.get_script(index);

    // Just merge the two scripts...
    let combined_script = format!("{header_template}\n{user_script}\n");

    // Run away!
    let result = engine.eval::<i64>(combined_script)?;
    
    println!("{result}");
}

Option 2 – The hard way

Option 1 requires the script header template to be recompiled every time. This can be expensive if the header is very long.

This option compiles both the script header template and independent scripts as separate AST’s which are then joined together to form a combined AST.

Pros: No need to recompile the header template!

Cons: More work…

let header_template = "..... // scripts... .....";

let mut template_ast = engine.compile(header_template)?;

// If you don't want to run the template, only keep the functions
// defined inside (e.g. closures), clear out the statements.
template_ast.clear_statements();

for index in 0..10000 {
    let user_script = db.get_script(index);

    let user_ast = engine.compile(user_script)?;

    // Merge the two AST's
    let combined_ast = template_ast + user_ast;

    // Run away!
    let result = engine.eval_ast::<i64>(combined_ast)?;
    
    println!("{result}");

Option 3 – The not-so-hard way

Option 1 does repeated work, option 2 requires manipulating AST’s…

This option makes the scripted functions (not imported modules nor constants however) available globally by first making it a module (via Module::eval_ast_as_new) and then loading it into the Engine via Engine::register_global_module.

Pros: No need to recompile the header template!

Cons: No imported modules nor constants; if the header template is changed, a new Engine must be created.

let header_template = "..... // scripts... .....";

let template_ast = engine.compile(header_template)?;

let template_module = Module::eval_ast_as_new(Scope::new(), &template_ast, &engine)?;

engine.register_global_module(template_module.into());

for index in 0..10000 {
    let user_script = db.get_script(index);

    // Run away!
    let result = engine.eval::<i64>(user_script)?;
    
    println!("{result}");
}

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
Strings interneryesno
on_printyesnone
on_debugyesnone

Warning: No strings interner

A raw Engine disables the strings interner by default.

This may lead to a significant increase in memory usage if many strings are created in scripts.

Turn the strings interner back on via Engine::set_max_strings_interned.

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

// Enable the strings interner
engine.set_max_strings_interned(1024);

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

engine.on_debug(|text, source, pos| match (source, pos) {
    (Some(source), crate::Position::NONE) => println!("{source} | {text}"),
    (Some(source), pos) => println!("{source} @ {pos:?} | {text}"),
    (None, crate::Position::NONE) => println!("{text}"),
    (None, pos) => println!("{pos:?} | {text}"),
});

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

// Load the package into the [`Engine`]
package.register_into_engine(&mut engine);

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)
==, !=
>, >=, <, <=

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

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>.

Tip: The const generic parameter

Scope also has a const generic parameter, which is a number that defaults to 8. It indicates the number of entries that the Scope can keep inline without allocations.

The larger this number, the larger the Scope type gets, but allocations will happen far less frequently.

A smaller number makes Scope smaller, but allocation costs will be incurred when the number of entries exceed the inline capacity.

Scope API

MethodDescription
new instance methodcreate a new empty Scope
with_capacity instance methodcreate a new empty Scope with a specified initial capacity
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 the last variable within the Scope by name 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 the last variable/constant within the Scope by name
set_value<T>set the value of the last variable within the Scope by name, panics if it is constant
remove<T>remove the last variable/constant from the Scope by name, returning its value
getget a reference to the value of the last variable/constant within the Scope by name
get_mutget a reference to the value of the last variable within the Scope by name, None if it is constant
set_aliasexported the last variable/constant within the Scope by 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.

Serializing/Deserializing

With the serde feature, Scope is serializable and deserializable via serde.

Custom types stored in the Scope, however, are serialized as full type-name strings. Data in custom types are not serialized.

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 in _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. while, for, fn) – not even variable assignment – is supported and will be considered syntax errors.

This is true also for statement expressions and 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,
                    "{ let y = calc(x); x + y }"
             )?;

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

Tip: if-expressions and switch-expressions

if expressions are allowed if both statement blocks contain only a single expression each.

switch expressions are allowed if all match actions are expressions and not statements.

loop expressions are not allowed.

// The following are allowed.

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

let result = engine.eval_expression_with_scope::<i64>(&mut scope, "
                    switch x {
                        0 => x * 42,
                        1..=9 => foo(123) + bar(1),
                        10 => 0,
                    }
             ")?;

Engine Configuration Options

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

Compile-Time Language Features

MethodDescriptionDefault
set_optimization_level
(not available under no_optimize)
sets the amount of script optimizations performed (see script optimization)Simple
set_allow_if_expressionallows/disallows if-expressionsallow
set_allow_switch_expressionallows/disallows switch expressionsallow
set_allow_loop_expressionsallows/disallows loop expressionsallow
set_allow_statement_expressionallows/disallows statement expressionsallow
set_allow_anonymous_fn
(not available under no_function)
allows/disallows anonymous functionsallow
set_allow_loopingallows/disallows looping (i.e. while, loop, do and for statements)allow
set_allow_shadowingallows/disallows shadowing of variablesallow
set_strict_variablesenables/disables Strict Variables modedisabled
set_fast_operatorsenables/disables Fast Operators modeenabled
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
set_default_tagsets the default value of the custom state (which can be obtained via NativeCallContext::tag) for each evaluation run

Safety Limits

MethodNot available underDescription
set_max_expr_depthsuncheckedsets the maximum nesting levels of an expression/statement (see maximum statement depth)
set_max_call_levelsunchecked, no_functionsets 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_variablesuncheckedsets the maximum number of variables that a script is allowed to define within a single Scope (see maximum number of variables)
set_max_functionsunchecked, no_functionsets the maximum number of functions that a script is allowed to define (see maximum number of functions)
set_max_modulesunchecked, [no_modules]sets 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)
set_max_strings_internedsets the maximum number of strings to be interned (if zero, the strings interner is disabled)

Examples

Rhai comes with a number of examples showing how to integrate the scripting Engine within a Rust application, as well as a number of sample scripts that showcase different Rhai language features.

Rust Examples

Standard Examples

A number of examples can be found under examples.

ExampleDescription
arrays_and_structsshows how to register a Rust type and using it with arrays
callbackshows how to store a Rhai closure and call it later within Rust
custom_types_and_methodsshows how to register a Rust type and methods/getters/setters for it
custom_typesshows how to register a Rust type and methods/getters/setters using the CustomType trait.
definitionsshows how to generate definition files for use with the Rhai Language Server (requires the metadata feature)
hellosimple example that evaluates an expression and prints the result
pause_and_resumeshows how to pause/resume/stop an Engine running in a separate thread via an MPSC channel
reuse_scopeevaluates two pieces of code in separate runs, but using a common Scope
serdeexample to serialize and deserialize Rust types with serde (requires the serde feature)
simple_fnshows how to register a simple Rust function
stringsshows different ways to register Rust functions taking string arguments
threadingshows how to communicate in duplex with an Engine running in a separate thread via a pair of MPSC channels

Scriptable Event Handler With State Examples

Because of its popularity, the pattern Scriptable Event Handler With State has sample implementations for different styles.

Running Examples

Examples can be run with the following command:

cargo run --example {example_name}

no-std Examples

To illustrate no-std builds, a number of example applications are available under the no_std directory:

ExampleDescriptionOptimizationAllocatorPanics
no_std_testbare-bones test application that evaluates a Rhai expression and sets the result as the return valuesizewee_allocabort

Building the no-std examples

Nightly required

Currently, the nightly compiler must be used to build for no-std.

cd no_std/no_std_test

cargo +nightly build --release

./target/release/no_std_test

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
function_decl5.rhaimultiple functions as methods for different data types
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 checking code
no_moduledisable loading external modulesreduce code size
no_positiondisable position tracking during parsingreduce data size
no_custom_syntaxdisable custom syntaxreduce code size

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

See also: Benchmarks

See Rhai performance benchmarks.

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.

Fast Operators Mode

Make sure that Fast Operators Mode, which is enabled by default, is on. It ignores any user overloading of built-in operators.

For operator-heavy scripts, this may provide a substantial speed-up.

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 Capturing in Closures

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.

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

Use &mut functions

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.

Compound assignment

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

Use take

Another example: use the take function to extract a value out of a variable (replacing it with ()) without cloning.

let x = create_some_very_big_and_expensive_type();

let y = x;          // <- 'x' is cloned here

let y = x.take();   // <- '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.

In addition, a Engine::new_raw disables the strings interner, which might actually increase memory usage if many strings are created in scripts. Therefore, selectively turn on the strings interner via Engine::set_max_strings_interned.

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

But why?

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 that requires interop with JavaScript. 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 then be explicitly provided.

Target Environments

WASI: wasm32-wasi

There is no particular setting to tweak when building for WASI.

JavaScript: wasm32-unknown-unknown + wasm-bindgen/stdweb

Rhai requires a system-provided source of random numbers (for hashing).

Such random number source is available from JavaScript (implied by wasm-bindgen or stdweb).

The js feature on the getrandom crate is enabled automatically to provide the random number source. See also: https://docs.rs/getrandom/latest/getrandom/#webassembly-support for details.

Raw: wasm32-unknown-unknown

Rhai requires a system-provided source of random numbers (for hashing).

Non-JavaScript/non-browser environments may not have random numbers available, so it is necessary to opt out of default-features in order to enable static hashing which uses fixed (non-random) keys.

[dependencies]
rhai = { version = "1.18.0", default-features = false, features = [ "std" ] }

Size

Also look into minimal builds to reduce generated WASM size.

A typical, full-featured Rhai scripting engine compiles to a single WASM32 file that is less than 400KB (non-gzipped).

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

Standard packages can also be excluded to yield additional size savings.

Speed

In benchmark tests, a WASM build runs scripts roughly 30% 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
no_custom_syntaxif custom syntax is not used, this results in a small size saving

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
CustomTypetrait to build a custom type for use with an Enginebuild
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, resolve_raw
packages::Packagetrait implemented by packagesinit, init_engine, register_into_engine, register_into_engine_as, as_shared_module
plugin::PluginFunctiontrait implemented by plugin functionscall, is_method_call, has_context, is_pure

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.

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

// Notice that all three functions are overloaded into the same name with
// different number of parameters and/or parameter types.
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.

┌──────┐
│ Rust │
└──────┘

engine.register_fn("foo", |x: i64, y: i64| x * 2 + y * 3);

┌─────────────┐
│ Rhai script │
└─────────────┘

foo(42, 100);       // <- 42 * 2 + 100 * 3

Interact with external environment

An additional benefit to using closures is that they can capture external variables.

For example, capturing a type wrapped in shared mutability (e.g. Rc<RefCell<T>>) allows a script to interact with the external environment through that shared type.

See also: Control Layer.

┌──────┐
│ Rust │
└──────┘

/// A type that encapsulates some behavior.
#[derive(Clone)]
struct TestStruct { ... }

impl TestSTruct {
    /// Some action defined on that type.
    pub fn do_foo(&self, x: i64, y: bool) {
        // ... do something drastic with x and y
    }
}

/// Wrapped in shared mutability: Rc<RefCell<TestStruct>>.
let shared_obj = Rc::new(RefCell::new(TestStruct::new()));

/// Clone the shared reference and move it into the closure.
let embedded_obj = shared.clone();

engine.register_fn("foo", move |x: i64, y: bool| {
//                        ^^^^ 'embedded_obj' is captured into the closure

    embedded_obj.borrow().do_foo(x, y);
});

┌─────────────┐
│ Rhai script │
└─────────────┘

foo(42, true);      // <- equivalent to: shared_obj.borrow().do_foo(42, true);

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 numbers (i.e. arity) or different types.

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

Tip: Overloading as a form of default parameter values

Rhai does not support default values for function parameters.

However it is extremely easy to simulate default parameter values via multiple overloaded registrations of the same function name.

// The following definition of 'foo' is equivalent to the pseudo-code:
//   fn foo(x = 42_i64, y = "hello", z = true) -> i64 { ... }

fn foo3(x: i64, y: &str, z: bool) -> i64 { ... }
fn foo2(x: i64, y: &str) -> i64 { foo3(x, y, true) }
fn foo1(x: i64) -> i64 { foo2(x, "hello") }
fn foo0() -> i64 { foo1(42) }

engine.register_fn("foo", foo0)     // no parameters
      .register_fn("foo", foo1)     // 1 parameter
      .register_fn("foo", foo2)     // 2 parameters
      .register_fn("foo", foo3);    // 3 parameters

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

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

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.

Dynamic Return Value

Rhai supports registering functions that return Dynamic.

A Dynamic value can hold any clonable type.

use rhai::{Engine, Dynamic};

// The 'Dynamic' return type allows this function to
// return values of any supported type!
fn get_info(bag: &mut PropertyBag, key: &str) -> Dynamic {
    if let Some(prop_type) = bag.get_type(key) {
        match prop_type {
            // Use '.into()' for standard types
            "string" => bag.get::<&str>(key).into(),
            "int" => bag.get::<i64>(key).into(),
            "bool" => bag.get::<bool>(key).into(),
                            :
                            :
            // Use 'Dynamic::from' for custom types
            "bag" => Dynamic::from(bag.get::<PropertyBag>(key))
        }
    } else {
        // Return () upon error
        Dynamic::UNIT
    }
}

let mut engine = Engine::new();

engine.register_fn("get_info", get_info);

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

Tip: Alternative to fallible functions

Instead of registering a fallible function, it is usually more idiomatic to leverage the dynamic nature of Rhai and simply return () upon error.

use rhai::{Engine, Dynamic};

// Function that may fail - return () upon failure
fn safe_divide(x: i64, y: i64) -> Dynamic {
    if y == 0 {
        // Return () to indicate an error if y is zero
        Dynamic::UNIT
    } else {
        // Use '.into()' to convert standard types to 'Dynamic'
        (x / y).into()
    }
}

let mut engine = Engine::new();

engine.register_fn("divide", safe_divide);

// The following prints 'error!'
engine.run(r#"
    let result = divide(40, 0);
    
    if result == () {
        print("error!");
    } else {
        print(result);
    }
"#)?;

Register a Fallible Rust Function

Tip: Consider Dynamic

A lot of times it is not necessary to register fallible functions.

Simply have the function returns Dynamic. Upon error, return () which is idiomatic in Rhai.

See here for more details.

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

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

In other words, the error type must be Box<EvalAltResult>. It is Boxed in order to reduce the size of the Result type since the error path is rarely hit.

use rhai::{Engine, EvalAltResult};

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

engine.register_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.

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_native_fn(...)Result<T, Box<EvalAltResult>>call a registered native Rust 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
call_native_fn_raw(...)Result<Dynamic, Box<EvalAltResult>>call a registered native Rust function with the supplied arguments; this is an advanced method

Example Implementations

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 new_array(context: NativeCallContext, size: i64) -> Result<Array, Box<EvalAltResult>>
{
    let array = Array::new();

    if size <= 0 {
        return array;
    }

    let size = size as usize;
    let max_size = context.engine().max_array_size();

    // Make sure the function does not generate a data structure larger than
    // the allowed limit for the Engine!
    if max_size > 0 && size > max_size {
        return Err(EvalAltResult::ErrorDataTooLarge(
            "Size to grow".to_string(),
            max_size, size,
            context.position(),
        ).into());
    }

    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 (or more commonly call_native_fn).

use rhai::{Engine, NativeCallContext};

let mut engine = Engine::new();

// A function expecting a callback in form of a function pointer.
engine.register_fn("super_call", |context: NativeCallContext, value: i64| {
    // Call a function within the current evaluation!
    // 'call_native_fn' ensures that only registered native Rust functions
    // are called, so a scripted function named 'double' cannot hijack
    // the process.
    // To also include scripted functions, use 'call_fn' instead.
    context.call_native_fn::<i64>("double", (value,))
    //                                      ^^^^^^^^ arguments passed in tuple
});

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_within_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))
}

Advanced Usage – Restore NativeCallContext

The NativeCallContext type encapsulates the entire context of a script up to the particular point of the native Rust function call.

The data inside a NativeCallContext can be stored (as a type NativeCallContextStore) for later use, when a new NativeCallContext can be constructed based on these stored data.

A reconstructed NativeCallContext acts almost the same as the original instance, so it is possible to suspend the evaluation of a script, and to continue at a later time with a new NativeCallContext.

Doing so requires the internals feature to access internal API’s.

Step 1: Store NativeCallContext data

// Store context for later use
let context_data = context.store_data();

// ... store 'context_data' somewhere ...
secret_database.push(context_data);

Step 2: Restore NativeCallContext

// ... do something else ...

// Restore the context
let context_data = secret_database.get();

let new_context = context_data.create_context(&engine);

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

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

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

Warning: Functions with one parameter

Functions with only one single parameter is easy to get wrong.

The proper Rust syntax is a tuple with one item:

( arg , )

Notice the comma (,) after the argument. Without it, the expression is a single value (arg) which is the same as arg and not a tuple.

A syntax error with very confusing error message will be generated by the Rust compiler if the comma is omitted.

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, override them with Engine::call_fn_with_options.

FuncArgs Trait

Note

Rhai implements FuncArgs for tuples, arrays and Vec<T>.

Engine::call_fn takes a parameter of any type that implements the FuncArgs trait, which is used to parse a data type into individual argument values for the function call.

Custom types (e.g. structures) can also implement FuncArgs so they can be used for calling Engine::call_fn.

use std::iter::once;
use rhai::FuncArgs;

// A struct containing function arguments
struct Options {
    pub foo: bool,
    pub bar: String,
    pub baz: i64
}

impl FuncArgs for Options {
    fn parse<C: Extend<Dynamic>>(self, container: &mut C) {
        container.extend(once(self.foo.into()));
        container.extend(once(self.bar.into()));
        container.extend(once(self.baz.into()));
    }
}

let options = Options { foo: true, bar: "world", baz: 42 };

// The type 'Options' can now be used as argument to 'call_fn'
// to call a function with three parameters: fn hello(foo, bar, baz)
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", options)?;

Warning: You don’t need this

Implementing FuncArgs is almost never needed because Rhai works directly with any custom type.

It is used only in niche cases where a custom type’s fields need to be split up to pass to functions.

Engine::call_fn_with_options

For more control, use Engine::call_fn_with_options, which takes a type CallFnOptions:

use rhai::{Engine, CallFnOptions};

let options = CallFnOptions::new()
                .eval_ast(false)            // do not evaluate the AST
                .rewind_scope(false)        // do not rewind the scope (i.e. keep new variables)
                .bind_this_ptr(&mut state); // 'this' pointer

let result = engine.call_fn_with_options::<i64>(
                options,                    // options
                &mut scope,                 // scope to use
                &ast,                       // AST containing the functions
                "hello",                    // function entry-point
                ( "abc", 123_i64 )          // arguments
             )?;

CallFnOptions allows control of the following:

FieldTypeDefaultBuild methodDescription
eval_astbooltrueeval_astevaluate the AST before calling the target function (useful to run [import statements])
rewind_scopebooltruerewind_scoperewind the custom Scope at the end of the function call so new local variables are removed
this_ptrOption<&mut Dynamic>Nonebind_this_ptrbind the this pointer to a specific value
tagOption<Dynamic>Nonewith_tagset the custom state for this evaluation (accessed via NativeCallContext::tag)

Skip evaluation of the AST

By default, the AST is evaluated before calling the target function.

This is necessary to make sure that necessary modules imported via import statements are available.

Setting eval_ast to false skips this evaluation.

Keep new variables/constants

By default, the Engine rewinds the custom Scope after each call to the initial size, so any new variable/constant defined are cleared and will not spill into the custom Scope.

This prevents the Scope from being continuously polluted by new variables and is usually the intuitively expected behavior.

Setting rewind_scope to false retains new variables/constants within the custom Scope.

This allows the function to easily pass values back to the caller by leaving them inside the custom Scope.

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 │
└──────┘

let options = CallFnOptions::new().rewind_scope(false);

engine.call_fn_with_options(options, &mut scope, &ast, "initialize", ())?;

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

Bind the this pointer

Note

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

CallFnOptions can also bind a value to the this pointer of a script-defined function.

It is possible, then, to call a function that uses this.

let ast = engine.compile("fn action(x) { this += x; }")?;

let mut value: Dynamic = 1_i64.into();

let options = CallFnOptions::new()
                .eval_ast(false)
                .rewind_scope(false)
                .bind_this_ptr(&mut value);

engine.call_fn_with_options(options, &mut scope, &ast, "action", ( 41_i64, ))?;

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

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| {
    engine.call_fn::<bool>(&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. However, make sure the Fast Operators Mode is disabled; otherwise this will not work.

Must turn off Fast Operators Mode

The Fast Operators Mode, which is enabled by default, causes the Engine to ignore all custom-registered operator functions for built-in operators. This is for performance considerations.

Disable Fast Operators Mode via Engine::set_fast_operators in order for the overloaded operators to be used.

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!

engine.set_fast_operators(false);                   // <- IMPORTANT! must turn off Fast Operators Mode

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.

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.

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.

Register API

For Rhai scripts to interact with the custom type, and API must be registered for it with the Engine.

The API can consist of functions, methods, property getters/setters, indexers, iterators etc.

There are three ways to register an API for a custom type.

1. Auto-Generate API

If you have complete control of the type, then this is the easiest way.

The #[derive(CustomType)] macro can be used to automatically generate an API for a custom type via the CustomType trait.

2. Custom Type Builder

For types in the same crate that you do not control, each function, method, property getter/setter, indexer and iterator can be registered manually, as a single package, via the CustomType trait using the Custom Type Builder.

3. Manual Registration

For external types that cannot implement the CustomType trait due to Rust’s orphan rule, each function, method, property getter/setter, indexer and iterator must be registered manually with the Engine.

Auto-Generate API for Custom Type

Warning

This assumes that you have complete control of the type and can do whatever you want with it (such as putting attributes on fields).

In particular, the type must be defined within the current crate.

To register a type and its API for use with an Engine, the simplest method is via the CustomType trait.

A custom derive macro is provided to auto-implement CustomType on any struct type, which exposes all the type’s fields to an Engine all at once.

It is as simple as adding #[derive(CustomType)] to the type definition.

use rhai::{CustomType, TypeBuilder};    // <- necessary imports

#[derive(Clone, CustomType)]            // <- auto-implement 'CustomType'
pub struct Vec3 {                       //    for normal structs
    pub x: i64,
    pub y: i64,
    pub z: i64,
}

#[derive(Clone, CustomType)]            // <- auto-implement 'CustomType'
pub struct ABC(i64, bool, String);      //    for tuple structs

let mut engine = Engine::new();

// Register the custom types!
engine.build_type::<Vec3>()
      .build_type::<ABC>();

Custom Attribute Options

The rhai_type attribute, with options, can be added to the fields of the type to customize the auto-generated API.

OptionApplies toValueDescription
nametype, fieldstring expressionuse this name instead of the type/field name.
skipfieldnoneskip this field; cannot be used with any other attribute.
readonlyfieldnoneonly auto-generate getter, no setter; cannot be used with set.
getfieldfunction pathuse this getter function (with &self) instead of the auto-generated getter; if get_mut is also set, this is ignored.
get_mutfieldfunction pathuse this getter function (with &mut self) instead of the auto-generated getter.
setfieldfunction pathuse this setter function instead of the auto-generated setter; cannot be used with readonly.
extratypefunction pathcall this function after building the type to add additional API’s

Function signatures

The signature of the function for get is:

Fn(&T) -> V

The signature of the function for get_mut is:

Fn(&mut T) -> V

The signature of the function for set is:

Fn(&mut T, V)

The signature of the function for extra is:

Fn(&mut TypeBuilder<T>)

Example

use rhai::{CustomType, TypeBuilder};    // <- necessary imports

#[derive(Debug, Clone)]
#[derive(CustomType)]                   // <- auto-implement 'CustomType'
pub struct ABC(
    #[rhai_type(skip)]                  // <- 'field0' not included
    i64,

    #[rhai_type(readonly)]              // <- only auto getter, no setter for 'field1'
    i64,
    
    #[rhai_type(name = "flag")]         // <- override property name for 'field2'
    bool,
    
    String                              // <- auto getter/setter for 'field3'
);

#[derive(Default, Clone)]
#[derive(CustomType)]                   // <- auto-implement 'CustomType'
#[rhai_type(name = "MyFoo", extra = Self::build_extra)] // <- call this type 'MyFoo' and use 'build_extra' to add additional API's
pub struct Foo {
    #[rhai_type(skip)]                  // <- field not included
    dummy: i64,

    #[rhai_type(readonly)]              // <- only auto getter, no setter for 'bar'
    bar: i64,

    #[rhai_type(name = "flag")]         // <- override property name
    baz: bool,                          // <- auto getter/setter for 'baz'

    #[rhai_type(get = Self::qux)]       // <- call custom getter (with '&self') for 'qux'
    qux: char,                          // <- auto setter for 'qux'

    #[rhai_type(set = Self::set_hello)] // <- call custom setter for 'hello'
    hello: String                       // <- auto getter for 'hello'
}

impl Foo {
    /// Regular field getter function with `&self`
    pub fn qux(&self) -> char {
        self.qux
    }

    /// Special setter implementation for `hello`
    pub fn set_hello(&mut self, value: String) {
        self.hello = if self.baz {
            let mut s = self.hello.clone();
            s.push_str(&value);
            for _ in 0..self.bar { s.push('!'); }
            s
        } else {
            value
        };
    }

    /// Additional API's
    fn build_extra(builder: &mut TypeBuilder<Self>) {
        // Register constructor function
        builder.with_fn("new_foo", || Self::default());
    }
}

#[derive(Debug, Clone, Eq, PartialEq, CustomType)]
#[rhai_fn(extra = vec3_build_extra)]
pub struct Vec3 {
    #[rhai_type(get = Self::x, set = Self::set_x)]
    x: i64,
    #[rhai_type(get = Self::y, set = Self::set_y)]
    y: i64,
    #[rhai_type(get = Self::z, set = Self::set_z)]
    z: i64,
}

impl Vec3 {
    fn new(x: i64, y: i64, z: i64) -> Self { Self { x, y, z } }
    fn x(&self) -> i64 { self.x }
    fn set_x(&mut self, x: i64) { self.x = x }
    fn y(&self) -> i64 { self.y }
    fn set_y(&mut self, y: i64) { self.y = y }
    fn z(&self) -> i64 { self.z }
    fn set_z(&mut self, z: i64) { self.z = z }
}

fn vec3_build_extra(builder: &mut TypeBuilder<Self>) {
    // Register constructor function
    builder.with_fn("Vec3", Self::new);
}

TL;DR – The above is equivalent to this…

impl CustomType for ABC {
    fn build(mut builder: TypeBuilder<Self>)
    {
        builder.with_name("ABC");
        builder.with_get("field1", |obj: &mut Self| obj.1.clone());
        builder.with_get_set("flag",
            |obj: &mut Self| obj.2.clone(),
            |obj: &mut Self, val| obj.2 = val
        );
        builder.with_get_set("field3",
            |obj: &mut Self| obj.3.clone(),
            |obj: &mut Self, val| obj.3 = val
        );
    }
}

impl CustomType for Foo {
    fn build(mut builder: TypeBuilder<Self>)
    {
        builder.with_name("MyFoo");
        builder.with_get("bar", |obj: &mut Self| obj.bar.clone());
        builder.with_get_set("flag",
            |obj: &mut Self| obj.baz.clone(),
            |obj: &mut Self, val| obj.baz = val
        );
        builder.with_get_set("qux",
            |obj: &Self| Self::qux(&*obj)),
            |obj: &mut Self, val| obj.qux = val
        )
        builder.with_get_set("hello",
            |obj: &mut Self| obj.hello.clone(),
            Self::set_hello
        );
        Self::build_extra(&mut builder);
    }
}

impl CustomType for Vec3 {
    fn build(mut builder: TypeBuilder<Self>)
    {
        builder.with_name("Vec3");
        builder.with_get_set("x", |obj: &mut Self| Self::x(&*obj), Self::set_x);
        builder.with_get_set("y", |obj: &mut Self| Self::y(&*obj), Self::set_y);
        builder.with_get_set("z", |obj: &mut Self| Self::z(&*obj), Self::set_z);
        vec3_build_extra(&mut builder);
    }
}

Register a Custom Type via the Type Builder

Warning

This assumes that the type is defined within the current crate and you can implement traits for it.

However, you may not control the type (it may be auto-generated or maintained by another user), so you cannot put attributes on it.

It is usually convenient to package a custom type’s API (i.e. methods, properties, indexers and type iterators) together such that they can be more easily managed.

This can be achieved by manually implementing the CustomType trait, which contains only a single method:

fn build(builder: TypeBuilder<T>)

The TypeBuilder parameter provides a range of convenient methods to register methods, property getters/setters, indexers and type iterators of a custom type:

MethodDescription
with_nameset a friendly name
on_printregister the to_string function that pretty-prints the custom type
on_debugregister the to_debug function that debug-prints the custom type
with_fnregister a method (or any function really)
with_getregister a property getter
with_setregister a property getter
with_get_setregister property getters/setters
with_indexer_getregister an indexer get function
with_indexer_setregister an indexer set function
with_indexer_get_setregister indexer get/set functions
is_iterableautomatically register a type iterator if the custom type is iterable

Tip: Use plugin module if starting from scratch

The CustomType trait is typically used on external types that are already defined.

To define a custom type and implement its API from scratch, it is more convenient to use a plugin module.

Example

// Custom type
#[derive(Debug, Clone, Eq, PartialEq)]
struct Vec3 {
    x: i64,
    y: i64,
    z: i64,
}

// Custom type API
impl Vec3 {
    fn new(x: i64, y: i64, z: i64) -> Self {
        Self { x, y, z }
    }
    fn get_x(&mut self) -> i64 {
        self.x
    }
    fn set_x(&mut self, x: i64) {
        self.x = x
    }
    fn get_y(&mut self) -> i64 {
        self.y
    }
    fn set_y(&mut self, y: i64) {
        self.y = y
    }
    fn get_z(&mut self) -> i64 {
        self.z
    }
    fn set_z(&mut self, z: i64) {
        self.z = z
    }
}

// The custom type can even be iterated!
impl IntoIterator for Vec3 {
    type Item = i64;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        vec![self.x, self.y, self.z].into_iter()
    }
}

// Use 'CustomType' to register the entire API
impl CustomType for Vec3 {
    fn build(mut builder: TypeBuilder<Self>) {
        builder
            .with_name("Vec3")
            .with_fn("vec3", Self::new)
            .is_iterable()
            .with_get_set("x", Self::get_x, Self::set_x)
            .with_get_set("y", Self::get_y, Self::set_y)
            .with_get_set("z", Self::get_z, Self::set_z)
            // Indexer get/set functions that do not panic on invalid indices
            .with_indexer_get_set(
                |vec: &mut Self, idx: i64) -> Result<i64, Box<EvalAltResult>> {
                    match idx {
                        0 => Ok(vec.x),
                        1 => Ok(vec.y),
                        2 => Ok(vec.z),
                        _ => Err(EvalAltResult::ErrorIndexNotFound(idx.Into(), Position::NONE).into()),
                    }
                },
                |vec: &mut Self, idx: i64, value: i64) -> Result<(), Box<EvalAltResult>> {
                    match idx {
                        0 => vec.x = value,
                        1 => vec.y = value,
                        2 => vec.z = value,
                        _ => Err(EvalAltResult::ErrorIndexNotFound(idx.Into(), Position::NONE).into()),
                    }
                    Ok(())
                }
            );
    }
}

let mut engine = Engine::new();

// Register the custom type in one go!
engine.build_type::<Vec3>();

TL;DR – Why isn’t there is_indexable?

Technically speaking, TypeBuilder can automatically register an indexer get function if the custom type implements Index. Similarly, it can automatically register an indexer set function for IndexMut.

In practice, however, this is usually not desirable because most Index/IndexMut implementations panic on invalid indices.

For Rhai, it is necessary to handle invalid indices properly by returning an error.

Therefore, in the example above, the with_indexer_get_set method properly handles invalid indices by returning errors.

Manually Register Custom Type

Warning

This assumes that the type is defined in an external crate and so the CustomType trait cannot be implemented for it due to Rust’s orphan rule.

Tip: Working with enums

It is also possible to use Rust enums with Rhai.

See the pattern Working with Enums for more details.

The custom type needs to be registered into an Engine via:

Engine APItype_of output
register_type::<T>full Rust path name
register_type_with_name::<T>friendly name
use rhai::{Engine, EvalAltResult};

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

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

let mut engine = Engine::new();

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

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

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

type_of() a Custom Type

Giving types the same name?

It is OK to register several custom types under the same friendly name and type_of() will faithfully return it.

How this might possibly be useful is left to the imagination of the user.

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” friendly 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!("{ts2_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).

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

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: String) {
        self.field = new_val;
    }

    fn new() -> Self {
        Self { field: "hello, world!".to_string() }
    }
}

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

See also

See this section for details on an indexer acting as fallback to properties.

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

Tip: Implement a 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.

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

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;

Tip: Swizzling

Since an indexer can serve as a fallback to property access, it is possible to implement swizzling of properties for use with vector-like custom types.

Such an indexer defined on a custom type (for instance, Float4) can inspect the property name, construct a proper return value based on the swizzle pattern, and return it.

// Assume 'v' is a 'Float4'
let r = v.w;        // -> v.w
let r = v.xx;       // -> Float2::new(v.x, v.x)
let r = v.yxz;      // -> Float3::new(v.y, v.x, v.z)
let r = v.xxzw;     // -> Float4::new(v.x, v.x, v.z, v.w)
let r = v.yyzzxx;   // error: property 'yyzzxx' not found

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(|obj: &mut MyType, prop: &str| -> Result<i64, Box<EvalAltResult>>
          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

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

What About Indexers?

Many users are tempted to register indexers for custom collections. This essentially makes the original Rust type something similar to Vec<MyType>.

Rhai’s standard Array type is Vec<Dynamic> which already holds an ordered, iterable and indexable collection of dynamic items. Since Rhai has built-in support, manipulating arrays is fast.

In most circumstances, it is better to use Array instead of a custom type.

Tip: Convert to Array using .into()

Dynamic implements FromIterator for all iterable types and an Array is created in the process.

So, converting a typed array (i.e. Vec<MyType>) into an array in Rhai is as simple as calling .into().

// Say you have a custom typed array...
let my_custom_array: Vec<MyType> = do_lots_of_calc(42);

// Convert it into a 'Dynamic' that holds an array
let value: Dynamic = my_custom_array.into();

// Use is anywhere in Rhai...
scope.push("my_custom_array", value);

engine
    // Raw function that returns a custom type
    .register_fn("do_lots_of_calc_raw", do_lots_of_calc)
    // Wrap function that return a custom typed array
    .register_fn("do_lots_of_calc", |seed: i64| -> Dynamic {
        let result = do_lots_of_calc(seed);     // Vec<MyType>
        result.into()                           // Array in Dynamic
    });

TL;DR

Why shouldn’t we register Vec<MyType>?

Reason #1: Performance

A main reason why anybody would want to do this is to avoid the overhead of storing Dynamic items.

This is why BLOB’s is a built-in data type in Rhai, even though it is actually defined as Vec<u8>. The overhead of using Dynamic (16 bytes) versus u8 (1 byte) is worth the trouble, although the performance gains may not be as pronounced as expected: benchmarks show a 15% speed improvement inside a tight loop compared with using an array.

Vec<MyType>, however, will be treated as an opaque custom type in Rhai, so performance is not optimized. What you gain from avoiding Dynamic, you pay back in terms of slower access to the Vec as well as MyType (which is treated as yet another opaque custom type).

Reason #2: API

Another reason why it shouldn’t be done is due to the large number of functions and methods that must be registered for each type of this sort. One only has to look at the vast API surface of arrays to see the common methods that a user would expect to be available.

Since Vec<Type> looks, feels and quacks just like a normal array, and the usage syntax is almost equivalent (except for the fact that the data type is restricted), users would be frustrated if they find that certain functions available for arrays are not provided.

This is similar to JavaScript’s Typed Arrays. They are quite awkward to work with, and basically each has a full API definition that must be pre-registered.

Disable Custom Types

The no_object feature disables support for custom types including:

Printing for Custom Types

Provide These Functions

To use custom types for print and debug, or convert a custom type into a string, it is necessary that the following functions, at minimum, 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

Tip: #[rhai_fn(global)]

If these functions are defined via a plugin module, be sure to include the #[rhai_fn(global)] attribute in order to make them available globally.

See this section for more details.

Also Consider These

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, FuncRegistration};

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

// Add new function.
FuncRegistration::new("inc")
    .with_params_info(&["x: i64", "i64"])
    .set_into_module(&mut module, |x: i64| x + 1);

// 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, FuncRegistration};

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

// Add new function.
FuncRegistration::new("inc")
    .with_params_info(&["x: i64", "i64"])
    .set_into_module(&mut module, |x: i64| x + 1);

// 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, FuncRegistration, FnNamespace};

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

// Add new function.
FuncRegistration::new("inc")
    .with_params_info(&["x: i64", "i64"])
    .with_namespace(FnNamespace::Global)    // <- global namespace
    .set_into_module(&mut module, |x: i64| x + 1);

// 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, FuncRegistration};
use rhai::module_resolvers::StaticModuleResolver;

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

module.set_var("answer", 41_i64);           // variable 'answer' under module

FuncRegistration::new("inc")
    .with_params_info(&["x: i64"])
    .set_into_module(&mut module, |x: i64| 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 module 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.

DummyModuleResolver

Default

DummyModuleResolver is the default for no_std or Engine::new_raw.

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

FileModuleResolver

Default

FileModuleResolver is the default for Engine::new.

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 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 functions call functions defined in the calling script (i.e. in the global namespace) if they exist, and only call those defined in the module 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

Useful for no-std

StaticModuleResolver is often used with no_std in 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);

engine.set_module_resolver(resolver);

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.

DylibModuleResolver

Requires external crate rhai-dylib

DylibModuleResolver resides in the rhai-dylib crate which must be specified as a dependency:

[dependencies]
rhai-dylib = { version = "0.1" }

Linux or Windows only

rhai-dylib currently supports only Linux and Windows.

Parallel to how the FileModuleResolver works, DylibModuleResolver loads external native Rust modules from compiled dynamic shared libraries (e.g. .so in Linux and .dll in Windows).

Therefore, FileModuleResolver loads Rhai script files while DylibModuleResolver loads native Rust shared libraries. It is very common to have the two work together.

Example

use rhai::{Engine, Module};
use rhai::module_resolvers::{FileModuleResolver, ModuleResolversCollection};
use rhai_dylib::module_resolvers::DylibModuleResolver;

let mut engine = Engine::new();

// Use a module resolvers collection
let mut resolvers = ModuleResolversCollection::new();

// First search for script files in the file system
resolvers += FileModuleResolver::new();

// Then search for shared-library plugins in the file system
resolvers += DylibModuleResolver::new();

// Set the module resolver into the engine
engine.set_module_resolver(resolvers);


┌─────────────┐
│ Rhai Script │
└─────────────┘

// If there is 'path/to/my_module.rhai', load it.
// Otherwise, check for 'path/to/my_module.so' on Linux
// ('path/to/my_module.dll' on Windows).
import "path/to/my_module" as m;

m::greet();

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

Plugin Modules

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

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

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

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
    }
    // This global function defines a custom operator '@'.
    #[rhai_fn(name = "@", global)]
    pub fn square_add(x: i64, y: i64) -> i64 {
        x * x + y * y
    }

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

    // Define a custom operator '@' with precedence of 160 (i.e. between +|- and *|/).
    engine.register_custom_operator("@", 160).unwrap();
}

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;

x @ x == 3528;      // custom operator

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

    // Define a custom operator '@' with precedence of 160 (i.e. between +|- and *|/).
    engine.register_custom_operator("@", 160).unwrap();
}

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;

x @ x == 3528;      // custom operator

let abc = service::create_abc(x);

type_of(abc) == "ABC";

abc.value == 42;

service::increment(abc);

abc.value == 43;

Use #[rhai_fn(global)]

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

The first parameter of a function can also be of type 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 set_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"methodget_prop_value(x, 0), x.get_prop_value(0)
name = "prop"methodprop(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) {
        // ...
    }
    // The following is a volatile function which returns different values
    // for each call.
    #[rhai_fn(volatile)]
    pub fn get_current_time() -> String {
        // ...
    }
}

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

Volatile Functions

A volatile function is one that does not guarantee the same result for the same input(s).

Most functions are non-volatile, meaning that they always generate the same result when called with the same arguments.

Common examples of volatile functions are:

  • a function that returns the current date and/or time

  • a function that looks up the current value of a variable in the environment

  • a function that reads from a file (which depends on the content of the file at the time of read)

  • a function that reads from a database or a cache (which depends on the content at the time of read)

When using Full Optimization, functions with constant arguments are called eagerly at compile time. However, volatile functions are never called.

Plugin functions are assumed to be non-volatile by default, unless marked with #[rhai_fn(volatile)].

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, #[rhai_mod] 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]
any function or sub-moduledo not export this function/sub-module
global#[rhai_fn]any functionexpose this function to the global namespace
internal#[rhai_fn]any functionkeep this function within the internal module namespace
name = "..."#[rhai_fn]
#[rhai_mod]
any function or sub-moduleregisters function/sub-module under the specified name
get = "..."#[rhai_fn]pub fn (&mut T) -> Vregisters a property getter for the named property
set = "..."#[rhai_fn]pub fn (&mut T, V)registers a property setter for the named property
index_get#[rhai_fn]pub fn (&mut T, X) -> Vregisters an index getter
index_set#[rhai_fn]pub fn (&mut T, X, V)registers an index setter
return_raw#[rhai_fn]pub fn (...) -> Result<V, Box<EvalAltResult>>marks this as a fallible function
pure#[rhai_fn]pub fn (&mut T, ...) -> ...marks this as a pure function
volatile#[rhai_fn]any functionmarks this as a volatile function – i.e. it does not guarantee the same result for the same input(s).

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 be registered 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.register_into_engine(&mut engine);

    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, not available under no_time or no_std)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'.
package.register_into_engine(&mut engine);

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 init code block
                        :
    }

    // Multiple packages can be defined at the same time,
    // possibly with base packages and/or code to setup an Engine.

    /// Package description doc-comment
    pub(crate) name(variable) : base_package_1, base_package_2, ... {
                        :
        // package init code block
                        :
    } |> |engine| {
                        :
        // engine setup code block
                        :
    }

    /// A private package description doc-comment
    name(variable) {
                        :
        // private package init 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
base_packagean external package type that is merged into this package as a dependency
package init code blocka code block that initializes the package
enginea variable name holding a mutable reference to an Engine
engine setup code blocka code block that performs setup tasks on an Engine during registration

Examples

// Import necessary types and traits.
use rhai::def_package;      // 'def_package!' macro
use rhai::packages::{ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage};
use rhai::{FuncRegistration, CustomType, TypeBuilder};

/// This is a custom type.
#[derive(Clone, CustomType)]
struct TestStruct {
    foo: String,
    bar: i64,
    baz: bool
}

def_package! {
    /// My own personal super package
    // Aggregate other base packages (if any) simply by listing them after a colon.
    pub MyPackage(module) : ArithmeticPackage, LogicPackage, BasicArrayPackage, BasicMapPackage
    {
        // Register additional Rust function.
        FuncRegistration::new("get_bar_value")
            .with_params_info(&["s: &mut TestStruct", "i64"])
            .set_into_module(module, |s: &mut TestStruct| s.bar);

        // Register a function for use as a custom operator.
        FuncRegistration::new("@")
            .with_namespace(FnNamespace::Global)    // <- make it available globally.
            .set_into_module(module, |x: i64, y: i64| x * x + y * y);
    } |> |engine| {
        // This optional block performs tasks on an 'Engine' instance,
        // e.g. register custom types and/or custom operators/syntax.

        // Register custom type.
        engine.build_type::<TestStruct>();

        // Define a custom operator '@' with precedence of 160
        // (i.e. between +|- and *|/).
        engine.register_custom_operator("@", 160).unwrap();
    }
}

Tip: Feature gates on base packages

Base packages in the list after the colon (:) can also have attributes (such as feature gates)!

def_package! {
    // 'BasicArrayPackage' is used only under 'arrays' feature.
    pub MyPackage(module) :
            ArithmeticPackage,
            LogicPackage,
            #[cfg(feature = "arrays")]
            BasicArrayPackage
    {
        ...
    }
}

Advanced: Engine setup with |>

A second code block (in the syntax of a closure) following a right-triangle symbol (|>) is run whenever the package is being registered.

It allows performing setup tasks directly on that Engine, e.g. registering custom types, custom operators and/or custom syntax.

def_package! {
    pub MyPackage(module) {
            :
            :
    } |> |engine| {
        // Call methods on 'engine'
    }
}

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 {
    // Custom type.
    pub type ABC = TestStruct;

    // Public constant.
    pub const MY_NUMBER: i64 = 42;

    // Public function.
    pub fn greet(name: &str) -> String {
        format!("hello, {}!", name)
    }

    // Non-public functions are by default not exported.
    fn get_private_num() -> i64 {
        42
    }

    // Public function.
    pub fn get_num() -> i64 {
        get_private_num()
    }

    // Custom operator.
    #[rhai_fn(name = "@")]
    pub fn square_add(x: i64, y: i64) -> i64 {
        x * x + y * y
    }

    // A sub-module.  If using 'combine_with_exported_module!', however,
    // it will be flattened and all functions registered at the top level.
    //
    // Because of this 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 {
        // Only available under 'sub-num-feature'.
        pub fn get_sub_num() -> i64 {
            0
        }
    }
}

def_package! {
    /// My own personal super package
    // Aggregate other base packages (if any) simply by listing them after a colon.
    pub MyPackage(module) : ArithmeticPackage, LogicPackage, BasicArrayPackage, BasicMapPackage
    {
        // 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));
    } |> |engine| {
        // This optional block is used to set up an 'Engine' during registration.

        // Define a custom operator '@' with precedence of 160
        // (i.e. between +|- and *|/).
        engine.register_custom_operator("@", 160).unwrap();
    }
}

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 projects rhai-rand and rhai-sci show simple examples of creating a custom package as an independent crate.

Implementation

Cargo.toml:

[package]
name = "my-package"     # 'my-package' crate

[dependencies]
rhai = "1.18.0"    # assuming 1.18.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-scifunctions for scientific computing
rhai-mlfunctions for AI and machine learning
rhai-fsread/write files in an external filesystem
rhai-urlworking with Urls via the url crate

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

Documentation

See https://docs.rs/rhai-rand for the list of functions.

On crates.io: rhai-rand

On GitHub: rhaiscript/rhai-rand

Package name: RandomPackage

Dependency

Cargo.toml:

[dependencies]
rhai = "1.18.0"
rhai-rand = "0.1"       # use rhai-rand crate

Load Package into Engine

use rhai::Engine;
use rhai::packages::Package;    // needed for 'Package' trait
use rhai_rand::RandomPackage;

let mut engine = Engine::new();

// Create new 'RandomPackage' instance
let random = RandomPackage::new();

// Load the package into the `Engine`
random.register_into_engine(&mut engine);

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.18.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"] }

rhai-sci: Functions for Scientific Computing

rhai-sci is an independent Rhai package that provides functions useful for scientific computing, inspired by languages like MATLAB, Octave, and R.

Documentation

See https://docs.rs/rhai-sci for the list of functions.

On crates.io: rhai-sci

On GitHub: rhaiscript/rhai-sci

Package name: SciPackage

Dependency

Cargo.toml:

[dependencies]
rhai = "1.18.0"
rhai-sci = "0.1"       # use rhai-sci crate

Features

FeatureDescriptionDefault?
metadataenables functions metadata (turns on metadata in Rhai); necessary for running doc-testsno
ioenables the read_matrix function but pulls in several additional dependenciesyes
nalgebraenables the functions regress, inv, mtimes, horzcat, vertcat, and repmat but pulls in nalgebra and linregress.yes
randenables the rand function for generating random values and random matrices, but pulls in rand.yes

Load Package into Engine

use rhai::Engine;
use rhai::packages::Package;    // needed for 'Package' trait
use rhai_sci::SciPackage;

let mut engine = Engine::new();

// Create new 'SciPackage' instance
let sci = SciPackage::new();

// Load the package into the [`Engine`]
sci.register_into_engine(&mut engine);

rhai-ml: Functions for AI and Machine Learning

rhai-ml is an independent Rhai package that provides functions useful for artificial intelligence and machine learning.

Documentation

See https://docs.rs/rhai-ml for the list of functions.

On crates.io: rhai-ml

On GitHub: rhaiscript/rhai-ml

Package name: MLPackage

Dependency

Cargo.toml:

[dependencies]
rhai = "1.18.0"
rhai-ml = "0.1"       # use rhai-ml crate

Features

FeatureDescriptionDefault?
metadataenables functions metadata (turns on metadata in Rhai); necessary for running doc-testsno

Load Package into Engine

use rhai::Engine;
use rhai::packages::Package;    // needed for 'Package' trait
use rhai_ml::MLPackage;

let mut engine = Engine::new();

// Create new 'MLPackage' instance
let ml = MLPackage::new();

// Load the package into the [`Engine`]
ml.register_into_engine(&mut engine);

rhai-fs: Filesystem Access

rhai-fs is an independent Rhai package that enables reading from and writing to files in an external filesystem.

Documentation

See https://docs.rs/rhai-fs for the list of functions.

On crates.io: rhai-fs

On GitHub: rhaiscript/rhai-fs

Package name: FilesystemPackage

Dependency

Cargo.toml:

[dependencies]
rhai = "1.18.0"
rhai-fs = "0.1"       # use rhai-fs crate

Load Package into Engine

use rhai::Engine;
use rhai::packages::Package;    // needed for 'Package' trait
use rhai_fs::FilesystemPackage;

let mut engine = Engine::new();

// Create new 'FilesystemPackage' instance
let fs = FilesystemPackage::new();

// Load the package into the `Engine`
fs.register_into_engine(&mut engine);

Example

// Create a file, or open it if already exists
let file = open_file("example.txt");

// Read the contents of the file (if any) into a BLOB
let blob_buf = file.read_blob();

print(`file contents: ${blob_buf}`);

// Update BLOB data
blob_buf.write_utf8(0..=0x20, "foobar");

print(`new file contents: ${blob_buf}`);

// Seek back to the beginning
file.seek(0);

// Overwrite the original file with new data
blob_buf.write_to_file(file);

Features

FeatureDescriptionDefault?Should be used with Rhai feature
no_arrayremoves support for arrays and BLOB’snono_index
metadataenables functions metadata (turns on metadata in Rhai)no

rhai-url: Working with Urls

rhai-url is an independent Rhai package that enables working with Urls via the url crate.

Documentation

See https://docs.rs/rhai-url for the list of functions.

On crates.io: rhai-url

On GitHub: rhaiscript/rhai-url

Package name: FilesystemPackage

Dependency

Cargo.toml:

[dependencies]
rhai = "1.18.0"
rhai-url = "0.0.1"       # use rhai-url crate

Load Package into Engine

use rhai::Engine;
use rhai::packages::Package;    // needed for 'Package' trait
use rhai_url::UrlPackage;

let mut engine = Engine::new();

// Create new 'UrlPackage' instance
let url = UrlPackage::new();

// Load the package into the `Engine`
url.register_into_engine(&mut engine);

Example

let url = Url("http://example.com/?q=query");

print(url);                 // prints 'http://example.com/?q=query'
print(url.href);            // prints 'http://example.com/?q=query'

print(url.query);           // prints 'q=query'

// fragment and hash are aliases
print(url.fragment);        // prints ''
print(url.hash);            // prints ''

url.query_clear();

print(url.query);           // prints ''

url.query_remove("q");
url.query_append("q", "name");

print(url);                 // prints 'http://example.com/?q=name'

Comments

Comments are C-style, including /**/ pairs for block comments and // for comments to the end of the line.

Block 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:
   /*/*/*/*/**/*/*/*/*/
*/

Module Documentation

Comment lines starting with //! make up the module documentation.

They are used to document the containing module – or for a Rhai script file, to document the file itself.

Requires metadata

Module documentation is only supported under the metadata feature.

If metadata is not active, they are treated as normal comments.

//! Documentation for this script file.
//! This script is used to calculate something and display the result.

fn calculate(x) {
   ...
}

fn display(msg) {
   //! Module documentation can be placed anywhere within the file.
   ...
}

//! All module documentation lines will be collected into a single block.

For the example above, the module documentation block is:

//! Documentation for this script file.
//! This script is used to calculate something and display the result.
//! Module documentation can be placed anywhere within the file.
//! All module documentation lines will be collected into a single block.

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_time or 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 closures, disabled with no_closure)the actual typeactual value
Nothing/void/nil/null/Unit (or whatever it is called)()"()""" (empty string)

No automatic type conversion for integers

The various integer types are treated strictly distinct by Rhai, meaning that i32 and i64 and u32 and u8 are completely different.

They cannot even be added together or compared with each other.

Nor can a smaller integer type be up-casted to a larger integer type.

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.

Warning: 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).

i64 is the default even on 32-bit systems. To use i32 on 32-bit systems requires 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.

Immutable 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)}`)
}

Parse from JSON

Requires metadata

parse_json is defined in the LanguageCorePackage, which is excluded when using a raw Engine.

It also requires the metadata feature; the no_index and no_object features must not be set.

Use parse_json to parse a JSON string into a Dynamic value.

JSON typeRhai type
number (no decimal point)INT
number (with decimal point)FLOAT
stringstring
booleanbool
Arrayarray
Objectobject map
null()

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)}!!!`
}

Standard types

See here for the type_of output of standard types.

Custom types

type_of() a custom type returns:

  • the friendly name, if registered via Engine::register_type_with_name

  • the full Rust type path, if registered via Engine::register_type

struct TestStruct1;
struct TestStruct2;

engine
    // type_of(struct1) == "path::to::module::TestStruct1"
    .register_type::<TestStruct1>()
    // type_of(struct2) == "MyStruct"
    .register_type_with_name::<TestStruct2>("MyStruct");

Interop Dynamic Data with Rust

Create a Dynamic from Rust Type

Rust type
T: Clone,
K: Into<String>
Unavailable underUse API
INT (i64 or i32)value.into()
FLOAT (f64 or f32)no_floatvalue.into()
Decimal (requires decimal)value.into()
boolvalue.into()
()value.into()
String, &str, ImmutableStringvalue.into()
charvalue.into()
Arrayno_indexDynamic::from_array(value)
Blobno_indexDynamic::from_blob(value)
Vec<T>, &[T], Iterator<T>no_indexvalue.into()
Mapno_objectDynamic::from_map(value)
HashMap<K, T>, HashSet<K>,
BTreeMap<K, T>, BTreeSet<K>
no_objectvalue.into()
INT..INT, INT..=INTvalue.into()
Rc<RwLock<T>> or Arc<Mutex<T>>no_closurevalue.into()
Instantno_time or no_stdvalue.into()
All types (including above)Dynamic::from(value)

Type Checking and Casting

Tip: 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.

Getting a Reference to Data

Use Dynamic::read_lock and Dynamic::write_lock to get an immutable/mutable reference to the data inside a Dynamic.

struct TheGreatQuestion {
    answer: i64
}

let question = TheGreatQuestion { answer: 42 };

let mut value: Dynamic = Dynamic::from(question);

let q_ref: &TheGreatQuestion =
        &*value.read_lock::<TheGreatQuestion>().unwrap();
//                       ^^^^^^^^^^^^^^^^^^^^ cast to data type

println!("answer = {}", q_ref.answer);          // prints 42

let q_mut: &mut TheGreatQuestion =
        &mut *value.write_lock::<TheGreatQuestion>().unwrap();
//                            ^^^^^^^^^^^^^^^^^^^^ cast to data type

q_mut.answer = 0;                               // mutate value

let value = value.cast::<TheGreatQuestion>();

println!("new answer = {}", value.answer);      // prints 0

TL;DR – Why read_lock and write_lock?

As the naming shows, something is locked in order to allow accessing the data within a Dynamic, 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.

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
deep_scanrecursively scan for Dynamic values (e.g. items inside an array or object map, or curried arguments in a function pointer)

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_time or no_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)?
is_unitboolis the value ()?
is_intboolis the value an integer?
is_floatno_floatboolis the value a floating-point number?
is_decimalnon-decimalboolis the value a Decimal?
is_boolboolis the value a bool?
is_charboolis the value a character?
is_stringboolis the value a string?
is_arrayno_indexboolis the value an array?
is_blobno_indexboolis the value a BLOB?
is_mapno_objectboolis the value an object map?
is_timestampno_time or no_stdboolis the value a timestamp?

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_time or 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

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 = x;

    // 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
    }

    result
}

let my_result = some_complex_calculation(42);

// 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 (for example, to and from JSON via serde_json).

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

Custom types are serialized as text strings of the value’s type name.

BLOB’s

BLOB’s, or byte-arrays, are serialized and deserialized as simple arrays for some formats such as JSON.

Tip: Lighter alternative for JSON

The serde_json 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.

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.

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-shift (if negative number of bits, shift right instead)numericyesnono
>>, >>=right bit-shift (if negative number of bits, shift left instead)numericyesnono
&, &=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 are defined.

FunctionNot available underPackageDescription
is_odd method and propertyArithmeticPackagereturns true if the value is an odd number, otherwise false
is_even method and propertyArithmeticPackagereturns true if the value is an even number, otherwise false
minLogicPackagereturns the smaller of two numbers
maxLogicPackagereturns the larger of two numbers
to_floatno_floatBasicMathPackageconvert the value into f64 (f32 under f32_float)
to_decimalnon-decimalBasicMathPackageconvert the value into Decimal

Signed Numeric Functions

The following standard functions are defined in the ArithmeticPackage (excluded when using a raw Engine) and 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 are defined in the BasicMathPackage (excluded when using a raw Engine) and 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
Comparisonyesmin, max (also inter-operates with integers)
Testingnois_nan, is_finite, is_infinite methods and properties

Decimal Rounding Functions

The following rounding methods are defined in the BasicMathPackage (excluded when using a raw Engine) and 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 are defined in the BasicMathPackage (excluded when using a raw Engine) to 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 are defined in the BasicStringPackage (excluded when using a raw Engine) to 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

Numerical Functions for Scientific Computing

Check out the rhai-sci crate for more numerical functions.

Value Conversions

Convert Between Integer and Floating-Point

FunctionNot available underFrom typeTo type
to_intINT, FLOAT, DecimalINT
to_floatno_floatINT, FLOAT, DecimalFLOAT
to_decimalnon-decimalINT, FLOAT, DecimalDecimal

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 when 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_empty method and propertyreturns true if the range contains no items
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 when 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 when using a raw Engine).

This is particularly useful when printing output.

Standard Escape Sequences

Tip: Character to_int()

Use the to_int method to convert a Unicode character into its 32-bit Unicode encoding.

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!!!";

No ending quote before the line ends is a syntax error

If the ending double-quote is omitted, it is a syntax error.

let x = "hello
";
//            ^ syntax error: unterminated string literal

Why not go multi-line?

Technically speaking, there is no difficulty in allowing strings to run for multiple lines without the continuation back-slash.

Rhai forces you to manually mark a continuation with a back-slash because the ending quote is easy to omit. Once it happens, the entire remainder of the script would become one giant, multi-line string.

This behavior is different from Rust, where string literals can run for multiple lines.

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 SLOOOOOOOOW

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.

Sub-Strings

Sub-strings, or slices in some programming languages, are parts of strings.

In Rhai, a sub-string can be specified by indexing with a range of characters:

string [ first character (starting from zero) .. last character (exclusive) ]

string [ first character (starting from zero) ..= last character (inclusive) ]

Sub-string ranges always start from zero counting towards the end of the string. Negative ranges are not supported.

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';                               // single character

let slice = record[4..8];               // sub-string slice
slice == " C. D";

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

let text = "hello, world!";
text[0] = 'H';                          // modify a single character
text == "Hello, world!";

text[7..=11] = "Earth";                 // modify a sub-string slice
text == "Hello, Earth!";

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

Multi-Line Literal Strings

A string wrapped by a pair of back-tick (`) characters is interpreted literally.

This means 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

Only literal strings

Interpolation is not supported for normal string or character literals.

What if I want ${ inside?

🤦 Well, you just have to ask for the impossible, don’t you?

Currently there is no way to escape ${. Build the string in three pieces:

`Interpolations start with "`
      + "${"
      + `" and end with }.`

Multi-line literal strings support string interpolation wrapped in ${}.

${} 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]"

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 when 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
is_empty method and propertynonereturns true if the string is empty
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
min
  1. first character/string
  2. second character/string
    returns the smaller of two characters/strings
    max
    1. first character/string
    2. second character/string
      returns the larger of two characters/strings
      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.

      Building Strings

      Strings can be built from segments via the + operator.

      OperatorDescription
      string += itemconvert the item into a string, then append it to the first string
      string + itemconvert the item into a string, then concatenate them as a new string
      item + stringconvert the item into a string, then concatenate them as a new string
      let x = 42;
      
      // Build string with '+'
      let s = "The answer is: " + x + "!!!";
      
      // Prints: "The answer is: 42!!!"
      print(s);

      Standard Operators Between Strings and/or Characters

      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

      Interop with BLOB’s

      For convenience, when a BLOB is appended to a string, or vice versa, it is treated as a UTF-8 encoded byte stream 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
      +, +=append a BLOB (as a UTF-8 encoded byte stream) to the end of the string
      +concatenate a BLOB (as a UTF-8 encoded byte stream) with 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;

      Strings Interner

      Because strings are immutable (i.e. the use the type ImmutableString instead of normal Rust String), each operation on a string actually creates a new ImmutableString instance.

      A strings interner can substantially reduce memory usage by reusing the same ImmutableString instance for the same string content.

      An Engine contains a strings interner which is enabled by default (disabled when using a raw Engine).

      The maximum number of strings to be interned can be set via Engine::set_max_strings_interned (set to zero to disable the strings interner).

      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 ]

      Out-of-Bounds Index

      Trying to read from an index that is out of bounds causes an error.

      Advanced tip: Override standard behavior

      For fine-tuned control on what happens when an out-of-bounds index is accessed, see Out-of-Bounds Index for Arrays.

      Built-in Functions

      The following methods (mostly defined in the BasicArrayPackage but excluded when 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, += operatorelement to append (not an array)appends an element to the end
      append, += operatorarray to appendconcatenates 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 elements to extract, none if ≤ 0, to end if omitted
      extracts a portion of the array into a new array
      extractrange of elements 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
      is_empty method and propertynonereturns true if the array is empty
      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
      for_eachfunction pointer for processing elementsrun through each element in the array in order, binding each to this and calling the processing function taking the following parameters:
      1. this: array element
      2. (optional) index position
      drainfunction pointer to predicate (usually a closure)removes all elements (returning them) that return true when called with the predicate function taking the following parameters (if none, the array element is bound to this):
      1. array element
      2. (optional) index position
      drain
      1. start position, counting from end if < 0, end if ≥ length
      2. number of elements to remove, none if ≤ 0
      removes a portion of the array, returning the removed elements as a new array
      drainrange of elements to remove, from beginning if ≤ 0, to end if ≥ lengthremoves a portion of the array, returning the removed elements as a new array
      retainfunction pointer to predicate (usually a closure)removes all elements (returning them) that do not return true when called with the predicate function taking the following parameters (if none, the array element is bound to this):
      1. array element
      2. (optional) index position
      retain
      1. start position, counting from end if < 0, end if ≥ length
      2. number of elements to retain, none if ≤ 0
      retains a portion of the array, removes all other elements and returning them as a new array
      retainrange of elements 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 elements 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 elements 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)constructs a new array with all elements that return true when called with the predicate function taking the following parameters (if none, the array element is bound to this):
      1. array element
      2. (optional) index 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 element 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)
      2. (optional) start position, counting from end if < 0, end if ≥ length
      returns the position of the first element in the array that returns true when called with the predicate function, or −1 if not found:
      1. array element (if none, the array element is bound to this)
      2. (optional) index position
      find
      1. function pointer to predicate (usually a closure)
      2. (optional) start position, counting from end if < 0, end if ≥ length
      returns the first element in the array that returns true when called with the predicate function, or () if not found:
      1. array element (if none, the array element is bound to this)
      2. (optional) index position
      find_map
      1. function pointer to predicate (usually a closure)
      2. (optional) start position, counting from end if < 0, end if ≥ length
      returns the first non-() value of the first element in the array when called with the predicate function, or () if not found:
      1. array element (if none, the array element is bound to this)
      2. (optional) index position
      dedup(optional) function pointer to predicate (usually a closure); if omitted, the == operator is used, if definedremoves all but the first of consecutive elements in the array that return true when called with the predicate function (non-consecutive duplicates are not removed):
      1st & 2nd parameters: two elements in the array
      mapfunction pointer to conversion function (usually a closure)constructs a new array with all elements mapped to the result of applying the conversion function taking the following parameters (if none, the array element is bound to this):
      1. array element
      2. (optional) index position
      reduce
      1. function pointer to accumulator function (usually a closure)
      2. (optional) the initial value
      reduces the array into a single value via the accumulator function taking the following parameters (if the second parameter is omitted, the array element is bound to this):
      1. accumulated value (() initially)
      2. this: array element
      3. (optional) index position
      reduce_rev
      1. function pointer to accumulator function (usually a closure)
      2. (optional) the initial value
      reduces the array (in reverse order) into a single value via the accumulator function taking the following parameters (if the second parameter is omitted, the array element is bound to this):
      1. accumulated value (() initially)
      2. this: array element
      3. (optional) index position
      zip
      1. array to zip
      2. function pointer to conversion function (usually a closure)
      constructs a new array with all element pairs from two arrays mapped to the result of applying the conversion function taking the following parameters:
      1. first array element
      2. second array element
      3. (optional) index position
      somefunction pointer to predicate (usually a closure)returns true if any element returns true when called with the predicate function taking the following parameters (if none, the array element is bound to this):
      1. array element
      2. (optional) index position
      allfunction pointer to predicate (usually a closure)returns true if all elements return true when called with the predicate function taking the following parameters (if none, the array element is bound to this):
      1. array element
      2. (optional) index position
      sortfunction pointer to a comparison function (usually a closure)sorts the array with a comparison function taking the following parameters:
      1. first element
      2. second element
        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 element 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 element 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 element in y {          // arrays can be iterated with a 'for' statement
          print(element);
      }
      
      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.for_each(|| this *= 2);
      
      a == [84, 246, 198];
      
      a.for_each(|i| this /= 2);
      
      a == [42, 123, 99];
      
      a.map(|v| v + 1);           // returns [43, 124, 100]
      
      a.map(|| this + 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(|| this > 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(|| this > 50);       // returns true
      
      a.some(|v, i| v < i);       // returns false
      
      a.all(|v| v > 50);          // returns false
      
      a.all(|| this > 50);        // returns false
      
      a.all(|v, i| v > i);        // returns true
      
      // Reducing - initial value provided directly
      a.reduce(|sum| sum + this, 0) == 264;
      
      // 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 in reverse - initial value provided directly
      a.reduce_rev(|sum| sum + this, 0) == 264;
      
      // Reducing in reverse - initial value provided directly
      a.reduce_rev(|sum, v| sum + v, 0) == 264;
      
      // Reducing in reverse - initial value is '()'
      a.reduce_rev(
          |sum, v| if sum.type_of() == "()" { v } else { sum + v }
      ) == 264;
      
      // Reducing in reverse - 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 when 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
      as_stringnoneconverts the BLOB into a string (the byte stream is interpreted as UTF-8)
      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, append, += 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. string to append
      creates a new string by concatenating the BLOB (as UTF-8 encoded byte-stream) with the the string
      + operator
      1. string
      2. BLOB to append
      creates a new string by concatenating the BLOB (as UTF-8 encoded byte-stream) to the end of the string
      + 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
      is_empty method and propertynonereturns true if the BLOB is empty
      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)

      Out-of-Bounds Index for Arrays

      Requires internals

      This is an advanced feature that requires the internals feature to be enabled.

      Normally, when an index is out-of-bounds for an array, an error is raised.

      It is possible to completely control this behavior via a special callback function registered into an Engine via on_invalid_array_index.

      Using this callback, for instance, it is simple to instruct Rhai to extend the array to accommodate this new element, or to return a default value instead of raising an error.

      Function Signature

      The function signature passed to Engine::on_invalid_array_index takes the following form.

      Fn(array: &mut Array, index: i64, context: EvalContext) -> Result<Target, Box<EvalAltResult>>

      where:

      ParameterTypeDescription
      array&mut Arraythe array being accessed
      indexi64index value
      contextEvalContextthe current evaluation context

      Return value

      The return value is Result<Target, Box<EvalAltResult>>.

      Target is an advanced type, available only under the internals feature, that represents a reference to a Dynamic value.

      It can be used to point to a particular value within the array or a new temporary value.

      Example

      engine.on_invalid_array_index(|arr, index, _| {
          match index {
              -100 => {
                  // The array can be modified in place
                  arr.push((42_i64).into());
      
                  // Return a mutable reference to an element
                  let value_ref = arr.last_mut().unwrap();
                  Ok(value_ref.into())
              }
              100 => {
                  // Return a temporary value (not a reference)
                  let value = Dynamic::from(100_i64);
                  Ok(value.into())
              }
              // Return the standard out-of-bounds error
              _ => Err(EvalAltResult::ErrorArrayBounds(
                      arr.len(), index, Position::NONE
                   ).into()),
          }
      });

      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.

      Tip: Object maps are FAST

      Normally, when properties are accessed, copies of the data values are made. This is normally slow.

      Object maps have special treatment – properties are accessed via references, meaning that no copies of data values are made.

      This makes object map access fast, especially when deep within a properties chain.

      // 'obj' is a normal custom type
      let x = obj.a.b.c.d;
      
      // The above is equivalent to:
      let a_value = obj.a;        // temp copy of 'a'
      let b_value = a_value.b;    // temp copy of 'b'
      let c_value = b_value.c;    // temp copy of 'c'
      let d_value = c_value.d;    // temp copy of 'd'
      let x = d_value;
      
      // 'map' is an object map
      let x = map.a.b.c.d;        // direct access to 'd'
                                  // 'a', 'b' and 'c' are not copied
      
      map.a.b.c.d = 42;           // directly modifies 'd' in 'a', 'b' and 'c'
                                  // no copy of any property value is made
      
      map.a.b.c.d.calc();         // directly calls 'calc' on 'd'
                                  // no copy of any property value is made

      TL;DR: 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.

      TL;DR: 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 ]

      Handle Non-Existent Properties

      Trying to read a non-existent property returns () instead of causing an error.

      This is similar to JavaScript where accessing a non-existent property returns undefined.

      let map = #{ foo: 42 };
      
      // Regular property access
      let x = map.foo;            // x == 42
      
      // Non-existent property
      let x = map.bar;            // x == ()

      Tip: Force error

      It is possible to force Rhai to return an EvalAltResult:: ErrorPropertyNotFound via Engine:: set_fail_on_invalid_map_property.

      Advanced tip: Override standard behavior

      For fine-tuned control on what happens when a non-existent property is accessed, see Non-Existent Property Handling for Object Maps.

      Check for property existence

      Use the in operator to check whether a property exists in an object-map.

      let map = #{ foo: 42 };
      
      "foo" in map == true;
      
      "bar" in map == false;

      Short-circuit non-existent property access

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

      Default property value

      Using the null-coalescing operator to give non-existent properties default values.

      let map = #{ foo: 42 };
      
      // Regular property access
      let x = map.foo;            // x == 42
      
      // Non-existent property
      let x = map.bar;            // x == ()
      
      // Default value for property
      let x = map.bar ?? 42;      // x == 42

      Built-in Functions

      The following methods (defined in the BasicMapPackage but excluded when 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
      is_emptynonereturns true if the object map is empty
      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-existent 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 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 = do_action;                      // <- de-sugars to 'Fn("do_action")'
      
      obj.action.call(obj, 2);                     // a copy of 'obj' is passed by value
      
      obj.data == 42;                              // 'obj.data' is not changed

      Non-Existent Property Handling for Object Maps

      Requires internals

      This is an advanced feature that requires the internals feature to be enabled.

      Normally, when a property is accessed from an object map that does not exist, () is returned. Via Engine:: set_fail_on_invalid_map_property, it is possible to make this an error instead.

      Other than that, it is possible to completely control this behavior via a special callback function registered into an Engine via on_map_missing_property.

      Using this callback, for instance, it is simple to instruct Rhai to create a new property in the object map on the fly, possibly with a default value, when a non-existent property is accessed.

      Function Signature

      The function signature passed to Engine::on_map_missing_property takes the following form.

      Fn(map: &mut Map, prop: &str, context: EvalContext) -> Result<Target, Box<EvalAltResult>>

      where:

      ParameterTypeDescription
      map&mut Mapthe object map being accessed
      prop&strname of the property being accessed
      contextEvalContextthe current evaluation context

      Return value

      The return value is Result<Target, Box<EvalAltResult>>.

      Target is an advanced type, available only under the internals feature, that represents a reference to a Dynamic value.

      It can be used to point to a particular value within the object map.

      Example

      engine.on_map_missing_property(|map, prop, context| {
          match prop {
              "x" => {
                  // The object-map can be modified in place
                  map.insert("y".into(), (42_i64).into());
      
                  // Return a mutable reference to an element
                  let value_ref = map.get_mut("y").unwrap();
                  Ok(value_ref.into())
              }
              "z" => {
                  // Return a temporary value (not a reference)
                  let value = Dynamic::from(100_i64);
                  Ok(value.into())
              }
              // Return the standard property-not-found error
              _ => Err(EvalAltResult::ErrorPropertyNotFound(
                      prop.to_string(), Position::NONE
                   ).into()),
          }
      });

      Timestamps

      Timestamps are provided by the BasicTimePackage (excluded when using a raw Engine) via the timestamp function.

      Timestamps are not available under no_time or 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 when 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 when 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, elsegotocontrol 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 statements 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

      Statements Block

      Syntax

      Statements 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 statements 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 statements 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 statements 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();

      Valid Assignment Targets

      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 op is not an l-value

      Values are Cloned

      Values assigned are always cloned. So care must be taken when assigning large data types (such as arrays).

      x = y;                  // value of 'y' is cloned
      
      x == y;                 // both 'x' and 'y' hold different copies
                              // of the same value

      Moving Data

      When assigning large data types, sometimes it is desirable to move the data instead of cloning it.

      Use the take function (defined in the LangCorePackage but excluded when using a raw Engine) to move data.

      The original variable is left with ()

      x = take(y);            // value of 'y' is moved to 'x'
      
      y == ();                // 'y' now holds '()'
      
      x != y;                 // 'x' holds the original value of 'y'

      Return large data types from functions

      take is convenient when returning large data types from a function.

      fn get_large_value_naive() {
          let large_result = do_complex_calculation();
      
          large_result.done = true;
      
          // Return a cloned copy of the result, then the
          // local variable 'large_result' is thrown away!
          large_result
      }
      
      fn get_large_value_smart() {
          let large_result = do_complex_calculation();
      
          large_result.done = true;
      
          // Return the result without cloning!
          // Method style call is also OK.
          large_result.take()
      }

      Assigning large data types to object map properties

      take is useful when assigning large data types to object map properties.

      let x = [];
      
      // Build a large array
      for n in 0..1000000 { x += n; }
      
      // The following clones the large array from 'x'.
      // Both 'my_object.my_property' and 'x' now hold exact copies
      // of the same large array!
      my_object.my_property = x;
      
      // Move it to object map property via 'take' without cloning.
      // 'x' now holds '()'.
      my_object.my_property = x.take();
      
      // Without 'take', the following must be done to avoid cloning:
      my_object.my_property = [];
      
      for n in 0..1000000 { my_object.my_property += n; }

      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 when 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 ()

      Tip: Default value for object map property

      Use the null-coalescing operator to implement default values for non-existent object map properties.

      let map = #{ foo: 42 };
      
      // Regular property access
      let x = map.foo;            // x == 42
      
      // Non-existent property
      let x = map.bar;            // x == ()
      
      // Default value for property
      let x = map.bar ?? 42;      // x == 42

      In Operator

      Trivia

      The in operator is simply syntactic sugar for a call to the contains function.

      Similarly, !in is a call to !contains.

      The in operator is used to check for containment – i.e. whether a particular collection data type contains a particular item.

      Similarly, !in is used to check for non-existence – i.e. it is true if a particular collection data type does not contain a particular item.

      42 in array;
      
      array.contains(42);     // <- the above is equivalent to this
      
      123 !in array;
      
      !array.contains(123);   // <- 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
      
      'w' !in "hello, world!" == false;
      
      "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 and !in operators map 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 and !in operators can be easily extended to other types by registering a custom binary function named contains with the correct parameter types.

      Since !in maps to !(... in ...), contains is enough to support both operators.

      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 'ts.contains(42)'
              print("I got 42!");
          } else if 123 !in ts {          // this calls '!ts.contains(123)'
              print("I ain't got 123!");
          }
      
          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 == ();

      Statement before expression

      Beware that, like Rust, if is parsed primarily as a statement where it makes sense. This is to avoid surprises.

      fn index_of(x) {
          // 'if' is parsed primarily as a statement
          if this.contains(x) {
              return this.find_index(x)
          }
      
          -1
      }

      The above will not be parsed as a single expression:

      fn index_of(x) {
          if this.contains(x) { return this.find_index(x) } - 1
          //                          error due to '() - 1' ^
      }
      

      To force parsing as an expression, parentheses are required:

      fn calc_index(b, offset) {
          (if b { 1 } else { 0 }) + offset
      //  ^---------------------^ parentheses
      }

      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 => {
              // A statements block instead of a one-line statement
              print("It's two!");
              print("Again!");
          }
          3 => print("Go!"),
          // A list of alternatives
          4 | 5 | 6 => print("Some small number!"),
          // _ 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
      }
      
      switch wrong_default {
          1 => 2,
          2 => 3,
          3 => 4,
          _ => 8,     // <- syntax error: default case not the last
          _ => 9
      }
      

      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.

      All cases are checked in order, so an earlier case that matches will override all later cases.

      let result = switch calc_secret_value(x) {
          1 if some_external_condition(x, y, z) => 100,
      
          1 | 2 | 3 if x < foo => 200,    // <- all alternatives share the same condition
          
          2 if bar() => 999,
      
          2 => "two",                     // <- fallback value for 2
      
          2 => "dead code",               // <- this case is a duplicate and will never match
                                          //    because the previous case matches first
      
          5 if CONDITION => 123,          // <- value for 5 matching condition
      
          5 => "five",                    // <- fallback value for 5
      
          _ if CONDITION => 8888          // <- syntax error: default case cannot have condition
      };
      

      Tip: Use with type_of()

      Case conditions, together with type_of(), makes it extremely easy to work with values which may be of several different types (like properties in a JSON object).

      switch value.type_of() {
          // if 'value' is a string...
          "string" if value.len() < 5 => ...,
          "string" => ...,
      
          // if 'value' is an array...
          "array" => ...,
      
          // if 'value' is an object map...
          "map" if value.prop == 42 => ...,
          "map" => ...,
      
          // if 'value' is a number...
          "i64" if value > 0 => ...,
          "i64" => ...,
      
          // anything else: probably an error...
          _ => ...
      }
      

      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 a number (including floating-point and Decimal). They never match any other data types.

      Must come after numeric cases

      Range cases must come after all numeric cases.

      let x = 42;
      
      switch x {
          'x' => ...,             // no match: wrong data type
      
          1 => ...,               // <- specific numeric 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!!!
      
          30..100 => ...,         // no match: even though it is within range,
                                  // the previous case matches first
      
          42 => ...,              // <- syntax error: numeric 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
      }

      While Expression

      Like Rust, while statements can also be used as expressions.

      The break statement takes an optional expression that provides the return value.

      The default return value of a while expression is ().

      Tip: Disable all loop expressions

      Loop expressions can be disabled via Engine::set_allow_loop_expressions.

      let x = 0;
      
      // 'while' can be used just like an expression
      let result = while x < 100 {
          if is_magic_number(x) {
              // if the 'while' loop breaks here, return a specific value
              break get_magic_result(x);
          }
      
          x += 1;
      
          // ... if the 'while' loop exits here, the return value is ()
      };
      
      if result == () {
          print("Magic number not found!");
      } else {
          print(`Magic result = ${result}!`);
      }
      

      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;

      Do Expression

      Like Rust, do statements can also be used as expressions.

      The break statement takes an optional expression that provides the return value.

      The default return value of a do expression is ().

      Tip: Disable all loop expressions

      Loop expressions can be disabled via Engine::set_allow_loop_expressions.

      let x = 0;
      
      // 'do' can be used just like an expression
      let result = do {
          if is_magic_number(x) {
              // if the 'do' loop breaks here, return a specific value
              break get_magic_result(x);
          }
      
          x += 1;
      
          // ... if the 'do' loop exits here, the return value is ()
      } until x >= 100;
      
      if result == () {
          print("Magic number not found!");
      } else {
          print(`Magic result = ${result}!`);
      }
      

      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.

      Loop Expression

      Like Rust, loop statements can also be used as expressions.

      The break statement takes an optional expression that provides the return value.

      The default return value of a loop expression is ().

      Tip: Disable all loop expressions

      Loop expressions can be disabled via Engine::set_allow_loop_expressions.

      let x = 0;
      
      // 'loop' can be used just like an expression
      let result = loop {
          if is_magic_number(x) {
              // if the loop breaks here, return a specific value
              break get_magic_result(x);
          }
      
          x += 1;
      
          // ... if the loop exits here, the return value is ()
      };
      
      if result == () {
          print("Magic number not found!");
      } else {
          print(`Magic result = ${result}!`);
      }
      

      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.

      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.

      For Expression

      Unlike Rust, for statements can also be used as expressions.

      The break statement takes an optional expression that provides the return value.

      The default return value of a for expression is ().

      Tip: Disable all loop expressions

      Loop expressions can be disabled via Engine::set_allow_loop_expressions.

      let a = [42, 123, 999, 0, true, "hello", "world!", 987.6543];
      
      // 'for' can be used just like an expression
      let index = for (item, count) in a {
          // if the 'for' loop breaks here, return a specific value
          switch item.type_of() {
              "i64" if item.is_even => break count,
              "f64" if item.to_int().is_even => break count,
          }
      
          // ... if the 'for' loop exits here, the return value is ()
      };
      
      if index == () {
          print("Magic number not found!");
      } else {
          print(`Magic number found at index ${index}!`);
      }
      

      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
      }
      

      Standard Iterable Types

      Certain standard types are iterable via a for statement.

      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>();
      
      // 'TestStruct' is now iterable
      engine.run(
      "
          for value in new_ts() {
              ...
          }
      ")?;

      Tip: Fallible type iterators

      Engine::register_iterator_result allows registration of a fallible type iterator – i.e. an iterator that returns Result<T, Box<EvalAltResult>>.

      On in very rare situations will this be necessary though.

      Return Value

      return

      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.

      exit

      Similar to the return statement, the exit function is used to immediately stop evaluation, but it does so regardless of where it is called from, even deep inside nested function calls.

      fn foo() {
          exit(42);       // exit with result 42
      }
      fn bar() {
          foo();
      }
      fn baz() {
          bar();
      }
      
      let x = baz();      // exits with result 42
      
      print(x);           // <- this is never run

      The exit function is defined in the LanguageCorePackage but excluded when using a raw Engine.

      FunctionParameter(s)Description
      exitresult value (optional)immediately terminate script evaluation (default result value is ())

      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 via the fn keyword, with a syntax that is very similar to Rust without types.

      Valid function names are the same as valid variable names.

      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!

      Rhai functions are pure

      The only possibility for a Rhai script-defined function to modify an external variable is via the this pointer.

      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 (not available under no_object):

      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 ().

      Restrict the Type of this in Function Definitions

      Tip: Automatically global

      Methods defined this way are automatically exposed to the global namespace.

      In many cases it may be desirable to implement methods for different custom types using script-defined functions.

      The Problem

      Doing so is brittle and requires a lot of type checking code because there can only be one function definition for the same name and arity:

      // Really painful way to define a method called 'do_update' on various data types
      fn do_update(x) {
          switch type_of(this) {
              "i64" => this *= x,
              "string" => this.len += x,
              "bool" if this => this *= x,
              "bool" => this *= 42,
              "MyType" => this.update(x),
              "Strange-Type#Name::with_!@#symbols" => this.update(x),
              _ => throw `I don't know how to handle ${type_of(this)}`!`
          }
      }
      

      The Solution

      With a special syntax, it is possible to restrict a function to be callable only when the object pointed to by this is of a certain type:

      fn type name . method ( parameters) {}

      or in quotes if the type name is not a valid identifier itself:

      fn "type name string" . method ( parameters) {}

      Needless to say, this typed method definition style is not available under no_object.

      Type name must be the same as type_of

      The type name specified in front of the function name must match the output of type_of for the required type.

      Tip: int and float

      int can be used in place of the system integer type (usually i64 or i32).

      float can be used in place of the system floating-point type (usually f64 or f32).

      Using these make scripts more portable.

      Examples

      /// This 'do_update' can only be called on objects of type 'MyType' in method style
      fn MyType.do_update(x, y) {
          this.update(x * y);
      }
      
      /// This 'do_update' can only be called on objects of type 'Strange-Type#Name::with_!@#symbols'
      /// (which can be specified via 'Engine::register_type_with_name') in method style
      fn "Strange-Type#Name::with_!@#symbols".do_update(x, y) {
          this.update(x * y);
      }
      
      /// Define a blanket version
      fn do_update(x, y) {
          this = `${this}, ${x}, ${y}`;
      }
      
      /// This 'do_update' can only be called on integers in method style
      fn int.do_update(x, y) {
          this += x * y
      }
      
      let obj = create_my_type();     // 'x' is 'MyType'
      
      obj.type_of() == "MyType";
      
      obj.do_update(42, 123);         // ok!
      
      let x = 42;                     // 'x' is an integer
      
      x.type_of() == "i64";
      
      x.do_update(42, 123);           // ok!
      
      let x = true;                   // 'x' is a boolean
      
      x.type_of() == "bool";
      
      x.do_update(42, 123);           // <- this works because there is a blanket version
      
      // Use 'is_def_fn' with three parameters to test for typed methods
      
      is_def_fn("MyType", "do_update", 2) == true;
      
      is_def_fn("int", "do_update", 2) == true;
      

      Bind to this for Module Functions

      The Problem

      The method-call syntax is not possible for functions imported from modules.

      import "my_module" as foo;
      
      let x = 42;
      
      x.foo::change_value(1);     // <- syntax error
      

      The Solution

      In order to call a module function as a method, it must be defined with a restriction on the type of object pointed to by this:

      ┌────────────────┐
      │ my_module.rhai │
      └────────────────┘
      
      // This is a typed method function requiring 'this' to be an integer.
      // Typed methods are automatically marked global when importing this module.
      fn int.change_value(offset) {
          // 'this' is guaranteed to be an integer
          this += offset;
      }
      
      
      ┌───────────┐
      │ main.rhai │
      └───────────┘
      
      import "my_module";
      
      let x = 42;
      
      x.change_value(1);          // ok!
      
      x == 43;
      

      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.

      Short-Hand Notation

      Not for native

      Native Rust functions cannot use this short-hand notation.

      Having to write Fn("foo") in order to create a function pointer to the function foo is a chore, so there is a short-hand available.

      A function pointer to any script-defined function within the same script can be obtained simply by referring to the function’s name.

      fn foo() { ... }        // function definition
      
      let f = foo;            // function pointer to 'foo'
      
      let f = Fn("foo");      // <- the above is equivalent to this
      
      let g = bar;            // error: variable 'bar' not found

      The short-hand notation is particularly useful when passing functions as closure arguments.

      fn is_even(n) { n % 2 == 0 }
      
      let array = [1, 2, 3, 4, 5];
      
      array.filter(is_even);
      
      array.filter(Fn("is_even"));    // <- the above is equivalent to this
      
      array.filter(|n| n % 2 == 0);   // <- ... or this

      Built-in Functions

      The following standard methods (mostly defined in the BasicFnPackage but excluded when 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
      
      let func = foo;             // <- short-hand: equivalent to 'Fn("foo")'
      
      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)'

      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.

      Global Namespace Only

      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 {
          method1
      } else if x == 0 {
          method2
      } else if x > 0 {
          method3
      };
      
      // Dynamic dispatch
      func.call(42);
      
      // Using functions map
      let map = [ method1, method2, 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 = 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_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

      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 = mul;                 // <- de-sugars to '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(double);
      
      // Pass a function pointer to 'square' using Fn(...) notation
      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: obj_inc,     // use function pointers to
          decrement: obj_dec,     // refer to method functions
          print: 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(anon_fn_0001);
      
      let z = y.map(anon_fn_0002);
      
      let obj = #{
          data: 42,
          increment: anon_fn_0003,
          decrement: anon_fn_0004,
          print: anon_fn_0005
      };

      Closures

      Tip: is_shared

      Use Dynamic::is_shared to check whether a particular Dynamic value is shared.

      Although anonymous functions de-sugar to standard function definitions, they differ from standard functions because they can captures variables that are not defined within the current scope, but are instead defined in an external scope – i.e. where the anonymous function is created.

      All variables that are accessible during the time the anonymous function is created are automatically captured when they are used, as long as they are not shadowed by local variables defined within the function’s.

      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

      Capturing external variables 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 = 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!
      }

      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 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 and Custom Types Metadata

      Requires metadata

      Exporting metadata requires the metadata feature.

      Functions

      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

      Custom Types

      The metadata of a custom type include:

      1. Its full Rust type name

      2. Its pretty-print display name (which can be the same as its Rust type name)

      3. Its purpose, in the form of doc-comments

      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 is defined in the LanguageCorePackage, which is excluded when using a raw Engine.

      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
      this_typestringyesrestrict the type of this if the function is a method
      is_anonymousboolnois this function an anonymous function?
      commentsarray of stringsyesdoc-comments, if any, one per line

      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 custom types and 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 and custom types 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. Custom types registered into the global namespace via the Engine::register_type_with_name API
      4. Public (i.e. non-private) functions (native Rust or Rhai scripted) and custom types in static modules registered via Engine::register_static_module
      5. Native Rust functions and custom types in external packages registered via Engine::register_global_module
      6. Native Rust functions and custom types in built-in packages (optional)

      JSON Schema

      The JSON schema used to hold metadata is very simple, containing a nested structure of modules, a list of customTypes and a list of functions.

      Module Schema

      {
          "doc": "//! Module documentation",
      
          "modules":
          {
              "sub_module_1": /* namespace 'sub_module_1' */
              {
                  "modules":
                  {
                      "sub_sub_module_A": /* namespace 'sub_module_1::sub_sub_module_A' */
                      {
                          "doc": "//! Module documentation can also occur in any sub-module",
      
                          "customTypes": /* custom types exported in 'sub_module_1::sub_sub_module_A' */
                          [
                              { ... custom type metadata ... },
                              { ... custom type metadata ... },
                              { ... custom type metadata ... }
                              ...
                          ],
                          "functions": /* functions exported in 'sub_module_1::sub_sub_module_A' */
                          [
                              { ... function metadata ... },
                              { ... function metadata ... },
                              { ... function metadata ... },
                              { ... function metadata ... }
                              ...
                          ]
                      },
                      "sub_sub_module_B": /* namespace 'sub_module_1::sub_sub_module_B' */
                      {
                          ...
                      }
                  }
              },
              "sub_module_2": /* namespace 'sub_module_2' */
              {
                  ...
              },
              ...
          },
      
          "customTypes": /* custom types registered globally */
          [
              { ... custom type metadata ... },
              { ... custom type metadata ... },
              { ... custom type metadata ... },
              ...
          ],
      
          "functions": /* functions registered globally or in the 'AST' */
          [
              { ... function metadata ... },
              { ... function metadata ... },
              { ... function metadata ... },
              { ... function metadata ... },
              ...
          ]
      }
      

      Custom Type Metadata Schema

      {
          "typeName": "alloc::string::String",    /* name of Rust type */
          "displayName": "MyType",
          "docComments":  /* omitted if none */
          [
              "/// My super-string type.",
              ...
          ]
      }
      

      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",
          "isAnonymous": false,
          "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 */
              ...
          ],
          "thisType": "this_type",    /* omitted if none */
          "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 */",
              ...
          ]
      }
      

      Generate Definition Files for Language Server

      Rhai’s language server works with IDE’s to provide integrated support for the Rhai scripting language.

      Functions and modules registered with an Engine can output their metadata into definition files which are used by the language server.

      Definitions are generated via the Engine::definitions and Engine::definitions_with_scope API.

      This API requires the metadata and internals feature.

      Configurable Options

      The Definitions type supports the following options in a fluent method-chaining style.

      OptionMethodDefault
      Write headers in definition files?with_headersfalse
      Include standard packages in definition files?include_standard_packagestrue
      engine
          .definitions()
          .with_headers(true)                     // write headers in all files
          .include_standard_packages(false)       // skip standard packages
          .write_to_dir("path/to/my/definitions")
          .unwrap();

      Example

      use rhai::{Engine, Scope};
      use rhai::plugin::*;
      
      // Plugin module: 'general_kenobi'
      #[export_module]
      pub mod general_kenobi {
          use std::convert::TryInto;
      
          /// Returns a string where "hello there" is repeated 'n' times.
          pub fn hello_there(n: i64) -> String {
              "hello there ".repeat(n.try_into().unwrap())
          }
      }
      
      // Create scripting engine
      let mut engine = Engine::new();
      
      // Create custom Scope
      let mut scope = Scope::new();
      
      // This variable will also show up in the generated definition file.
      scope.push("hello_there", "hello there");
      
      // Static module namespaces will generate independent definition files.
      engine.register_static_module(
              "general_kenobi",
              exported_module!(general_kenobi).into()
      );
      
      // Custom operators will also show up in the generated definition file.
      engine.register_custom_operator("minus", 100).unwrap();
      engine.register_fn("minus", |a: i64, b: i64| a - b);
      
      engine.run_with_scope(&mut scope,
              "hello_there = general_kenobi::hello_there(4 minus 2);"
      )?;
      
      // Output definition files in the specified directory.
      engine
          .definitions()
          .write_to_dir("path/to/my/definitions")
          .unwrap();
      
      // Output definition files in the specified directory.
      // Variables in the provided 'Scope' are included.
      engine
          .definitions_with_scope(&scope)
          .write_to_dir("path/to/my/definitions")
          .unwrap();
      
      // Output a single definition file with everything merged.
      // Variables in the provided 'Scope' are included.
      engine
          .definitions_with_scope(&scope)
          .write_to_file("path/to/my/definitions/all_in_one.d.rhai")
          .unwrap();
      
      // Output functions metadata to a JSON string.
      // Functions in standard packages are skipped and not included.
      let json = engine
          .definitions()
          .include_standard_packages(false)   // skip standard packages
          .unwrap();

      Definition Files

      The generated definition files will look like the following.

      ┌───────────────────────┐
      │ general_kenobi.d.rhai │
      └───────────────────────┘
      
      module general_kenobi;
      
      /// Returns a string where "hello there" is repeated 'n' times.
      fn hello_there(n: int) -> String;
      
      
      ┌──────────────────┐
      │ __scope__.d.rhai │
      └──────────────────┘
      
      module static;
      
      let hello_there;
      
      
      ┌───────────────────┐
      │ __static__.d.rhai │
      └───────────────────┘
      
      module static;
      
      op minus(int, int) -> int;
      
              :
              :
      
      
      ┌────────────────────┐
      │ __builtin__.d.rhai │
      └────────────────────┘
      
      module static;
      
              :
              :
      
      
      ┌──────────────────────────────┐
      │ __builtin-operators__.d.rhai │
      └──────────────────────────────┘
      
      module static;
      
              :
              :
      

      All-in-One Definition File

      Definitions::write_to_file generates a single definition file with everything merged in, like the following.

      module static;
      
      op minus(int, int) -> int;
      
              :
              :
      
      module general_kenobi {
          /// Returns a string where "hello there" is repeated 'n' times.
          fn hello_there(n: int) -> String;
      }
      
      let hello_there;

      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 {src} at {pos:?}: {s}")
      });
      
      // 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 {src} at {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 statements 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;
      

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

      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.

      These are common vectors for Denial of Service (DOS) attacks.

      Most Important Resources

      Memory

      • Continuously grow a string, an array, a BLOB or object map until all memory is consumed.

      • Continuously create new variables with large data until all memory is consumed.

      • Continuously define new functions all memory is consumed (e.g. a simple closure ||, as short as two characters, is a function – an attractive target for DOS attacks).

      CPU

      • Infinite tight loop that consumes all CPU cycles.

      Time

      • Run indefinitely, thereby blocking the calling system which is waiting for a result.

      Stack

      • Deep recursive call that exhausts the call stack.

      • Large array or object map literal that exhausts the stack during parsing.

      • Degenerated deep expression with so many levels that the parser exhausts the call stack when parsing the expression; or even deeply-nested statements blocks, if nested deep enough.

      • 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 under 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

      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.

      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.

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

      Limiting Run Time

      Track Progress and Force-Termination

      Operations count vs. progress

      Operations count does not indicate the proportion 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.

      Examples

      Periodic Logging

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

      Limit running time

      let mut engine = Engine::new();
      
      let start = get_time();         // get the current system time
      
      engine.on_progress(move |_| {
          let now = get_time();
      
          if now.duration_since(start).as_secs() > 60 {
              // Return a dummy token just to force-terminate the script
              // after running for more than 60 seconds!
              Some(Dynamic::UNIT)
          } else {
              // Continue
              None
          }
      });

      Function Signature of Callback

      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.

      Limiting Memory Usage

      During Evaluation

      To prevent out-of-memory failures, provide a closure to Engine::on_progress to track memory usage and force-terminate a malicious script before it can bring down the host system.

      Most O/S provides system calls to obtain the current memory usage of the process.

      let mut engine = Engine::new();
      
      const MAX_MEMORY: usize = 10 * 1024 * 1024;   // 10MB
      
      engine.on_progress(|_| {
          // Call a system function to obtain the current memory usage
          let memory_usage = get_current_progress_memory_usage();
      
          if memory_usage > MAX_MEMORY {
              // Terminate the script
              Some(Dynamic::UNIT)
          } else {
              // Continue
              None
          }
      });

      During Parsing

      A malicious script can be carefully crafted such that it consumes all available memory during the parsing stage.

      Protect against this by via a closure to Engine::on_parse_token.

      let mut engine = Engine::new();
      
      const MAX_MEMORY: usize = 10 * 1024 * 1024;   // 10MB
      
      engine.on_parse_token(|token, _, _| {
          // Call a system function to obtain the current memory usage
          let memory_usage = get_current_progress_memory_usage();
      
          if memory_usage > MAX_MEMORY {
              // Terminate parsing
              Token::LexError(
                  LexError::Runtime("out of memory".into()).into()
              )
          } else {
              // Continue
              token
          }
      });

      Limiting Stack Usage

      Most O/S differentiates between heap and stack memory.

      Usually the stack (around 1MB) is much smaller than the heap (multiple MB’s or even GB’s).

      Therefore, it is possible for a carefully-crafted script to consume all available stack memory (such as deeply-nested expressions) and crash the host system, even though there is ample heap memory available.

      Calculate Stack Usage

      Some O/S’s provide system calls to get the stack size and/or amount of free stack memory, but these are in the minority.

      In order to determine the amount of stack memory actually used, it is necessary to perform some pointer arithmetic.

      The trick is to get the address of a stack-allocated variable in the very beginning and compare it to the address of another variable.

      // Create a variable on the stack.
      let stack_base_ref = Dynamic::UNIT;
      // Get a pointer to it.
      let stack_base: *const Dynamic = &stack_base_ref;
      
      // ... do a lot of work here ...
      
      // Create another variable on the stack.
      let stack_top = Dynamic::UNIT;
      // Get a pointer to it.
      let stack_top: *const Dynamic = &stack_top;
      
      let usage = unsafe { stack_top.offset_from(stack_base) };

      Negative values

      In many cases, the amount of stack memory used is actually negative (meaning that the base variable is in a higher memory address than the current variable).

      That is because, for many architectures, the stack grows downwards and the heap grows upwards in order to maximize memory usage efficiency.

      During Evaluation

      To prevent stack-overflow failures, provide a closure to Engine::on_progress to track stack usage and force-terminate a malicious script before it can bring down the host system.

      let mut engine = Engine::new();
      
      const MAX_STACK: usize = 100 * 1024;    // 10KB
      
      // Create a variable on the stack.
      let stack_base_ref = Dynamic::UNIT;
      // Get a pointer to it.
      let stack_base: *const Dynamic = &stack_base_ref;
      
      engine.on_progress(move |_| {
          // Create another variable on the stack.
          let stack_top = Dynamic::UNIT;
          // Get a pointer to it.
          let stack_top: *const Dynamic = &stack_top;
      
          let usage = unsafe { stack_base.offset_from(stack_top) };
      
          if usage > MAX_STACK {
              // Terminate the script 
              Some(Dynamic::UNIT)
          } else {
              // Continue
              None
          }
      });

      During Parsing

      A malicious script can be carefully crafted such that it consumes all stack memory during the parsing stage.

      Protect against this by via a closure to Engine::on_parse_token.

      let mut engine = Engine::new();
      
      const MAX_STACK: usize = 100 * 1024;    // 10KB
      
      // Create a variable on the stack.
      let stack_base_ref = Dynamic::UNIT;
      // Get a pointer to it.
      let stack_base: *const Dynamic = &stack_base_ref;
      
      engine.on_parse_token(|token, _, _| {
          // Create another variable on the stack.
          let stack_top = Dynamic::UNIT;
          // Get a pointer to it.
          let stack_top: *const Dynamic = &stack_top;
      
          let usage = unsafe { stack_base.offset_from(stack_top) };
      
          if usage > MAX_STACK {
              // Terminate parsing
              Token::LexError(
                  LexError::Runtime("stack-overflow".into()).into()
              )
          } else {
              // Continue
              token
          }
      });

      Built-In Safety Limits

      Rhai has a number of safety limits built into the Engine.

      All these limits can be disabled, for higher performance (but higher risks as well), via the unchecked feature.

      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.

      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.

      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.

      Maximum Number of Variables

      Rhai by default does not limit how many variables/constants can be defined within a single Scope.

      This can be changed via the Engine::set_max_variables method. Notice that setting the maximum number of variables to zero does not indicate unlimited variables, but disallows defining any variable altogether.

      A script attempting to define more than the maximum number of variables/constants 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_variables(5);      // allow defining only up to 5 variables
      
      engine.set_max_variables(0);      // disallow defining any variable (maximum = zero)
      
      engine.set_max_variables(1000);   // set to a large number for effectively unlimited variables

      Function calls are separate scopes

      Each function call creates a new, empty Scope.

      Therefore, variables/constants defined within functions are counted afresh.

      Care must be taken to avoid deeply-nested (or recursive) function calls from creating too many variables/constants while staying within the limit of each individual Scope.

      Function call arguments count as variables

      The parameters of a function also count as variables within the function’s Scope.

      Thus the maximum number of variables/constants allowed is reduced by the number of parameters of the function.

      Tip: Reusing a variable doesn’t count

      It is possible to reuse a variable such that it is counted only once.

      let x = 42;     // counted as 1 variable
      let y = 123;
      
      let x = 0;      // previous 'x' reused: not counted as new variable

      Maximum Number of Functions

      Rhai by default does not limit how many functions can be defined in a script.

      This can be changed via the Engine::set_max_functions method. Notice that setting the maximum number of functions to zero does not indicate unlimited functions, but disallows defining any scripted function altogether.

      A script attempting to load more than the maximum number of functions will terminate with a parse error.

      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_functions(5);        // allow defining only up to 5 functions
      
      engine.set_max_functions(0);        // disallow defining function (maximum = zero)
      
      engine.set_max_functions(1000);     // set to a large number for effectively unlimited functions

      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 (or a very deeply-nested recursion) such that all stack space is exhausted.

      // This is a function that, when called, recurses forever.
      fn recurse_forever() {
          recurse_forever();
      }

      Main stack size

      The main stack-size of a program is not determined by Rust but is platform-dependent.

      See this on-line Rust docs for more details.

      Because of its intended embedded usage, Rhai, by default, limits function calls to a maximum depth of 64 levels (8 levels in debug build) in order to fit into most platforms’ default stack sizes.

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

      Tip: Getting around the stack size limit

      While the stack size of a program’s main thread is platform-specific, Rust defaults to a stack size of 2MB for spawned threads.

      This default can further be changed such that a spawned thread has as large a stack as needed.

      See the on-line Rust docs for more details.

      Therefore, in order to relax the stack size limit for scripts, run the Engine in a separate spawned thread with a larger stack.

      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

      But 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

      Tip

      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 sign(DECISION) > 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)

      Won’t this be dangerous?

      Yes! Very!

      // Nuclear silo control
      if launch_nukes && president_okeyed {
          print("This is NOT a drill!");
          update_defcon(1);
          start_world_war(3);
          launch_all_nukes();
      } else {
          print("This is a drill.  Thank you for your cooperation.");
      }

      In the script above (well… as if nuclear silos will one day be controlled by Rhai scripts), the functions update_defcon, start_world_war and launch_all_nukes will be evaluated during compilation because they have constant arguments.

      The variables launch_nukes and president_okeyed are never checked, because the script actually has not yet been run! The functions are called during compilation. This is, obviously, not what you want.

      Moral of the story: compile with an Engine that does not have any functions registered. Register functions AFTER compilation.

      Why would I ever want to do this then?

      Good question! There are two reasons:

      • A function call may result in cleaner code than the resultant value. In Rust, this would have been handled via a const function.

      • Evaluating a value to a custom type that has no representation in script.

      // A complex function that returns a unique ID based on the arguments
      let id = make_unique_id(123, "hello", true);
      
      // The above is arguably clearer than:
      //   let id = 835781293546; // generated from 123, "hello" and true
      
      // A custom type that cannot be represented in script
      let complex_obj = make_complex_obj(42);

      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 external environment and does not guarantee the same result for the same inputs.

      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!

      Warning

      Avoid using OptimizationLevel::Full if volatile custom functions are involved.

      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.

      Tip: Mark a function as volatile

      All native functions are assumed to be non-volatile, meaning that they are eagerly called under OptimizationLevel::Full when all arguments are constant (or none).

      It is possible to mark a function defined within a plugin module as volatile to prevent this behavior.

      #[export_module]
      mod my_module {
          // This function is marked 'volatile' and will not be
          // eagerly executed even under OptimizationLevel::Full.
          #[rhai_fn(volatile)]
          pub get_current_time(am_pm: bool) -> String {
              // ...
          }
      }

      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)
      Module documentationVec<SmartString>documentation of the scriptmetadatadoc(&self),
      clear_doc(&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 = args[1].take().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)args[n].take().cast::<ImmutableString>()ImmutableString()
      Others&*args[n].read_lock::<T>().unwrap()&Tuntouched
      Others (consumed)<