What is Rhai

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.0.0 of Rhai.

For the latest development version, see here.

Trivia

Etymology of the name “Rhai” as per Rhai’s author Johnathan Turner

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

One of Rhai’s maintainers, @schungx, was thinking about a logo when he accidentally came across a copy of Catcher in the Rye in a restaurant, and drew the first version of the logo.

Then @semirix refined it to the current version.

On the rhai.rs domain

@yrashk sponsored the domain rhai.rs.

Features

Easy

Fast

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

  • Compile once to AST for repeated evaluations.

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

Dynamic

Safe

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

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

Rugged

Flexible

Supported Targets and Builds

The following targets and builds are support by Rhai:

  • All common CPU targets for Windows, Linux and MacOS.

  • WebAssembly (WASM)

  • no-std

Minimum Rust Version

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

What Rhai Isn’t

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

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

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

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

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

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

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

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

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

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

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

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

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

    Still, the purpose of Rhai is not to be super fast, but to make it as easy and versatile as possible to integrate with native Rust applications.

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

    This lack of formalism allows the tokenizer and parser themselves to be exposed as services in order to support disabling keywords/operators, adding custom operators, and defining custom syntax.

Do Not Write The Next 4D VR Game in Rhai

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

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

Thin Dynamic Wrapper Layer Over Rust Code

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

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

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

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

Licensing

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

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

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

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.

Install the Rhai Crate

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

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.0.0"    # assuming 1.0.0 is the latest version

Or to automatically use the latest released crate version on crates.io:

[dependencies]
rhai = "*"

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.

Most features are here to opt-out of certain functionalities that are not needed. Notice that this deviates from Rust norm where features are additive.

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

Features that Enable Special Functionalities

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

Features that Disable Certain Language Features

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

Features that Disable Certain Engine Features

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

Features that Configure the Engine

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

Features for no-std Builds

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

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

Features for WebAssembly (WASM) Builds

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

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

Example

The Cargo.toml configuration below turns on these six features:

  • sync (everything Send + Sync)
  • unchecked (disable all checking – should not be used with untrusted user scripts)
  • only_i32 (only 32-bit signed integers)
  • no_float (no floating point numbers)
  • no_module (no loading external modules)
  • no_function (no defining functions)
[dependencies]
rhai = { version = "1.0.0", features = [ "sync", "unchecked", "only_i32", "no_float", "no_module", "no_function" ] }

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.

Caveat – 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 remove functionalities.

There is a reason for this design, because the lack of a language feature by itself is a feature.

See here for more details.

Special Builds

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

Performance Build

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

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

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

Use Only One Integer Type

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

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

Use Only 32-Bit Numbers

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

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

Minimize Size of Dynamic

Turning on no_float (or f32_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 Closures

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 closure and capturing support to optimize the hot path because it no longer needs to take locks for shared data.

Unchecked Build

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

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

Disable Position

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

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

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

Avoid Cloning

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

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

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


#![allow(unused)]
fn main() {
let x = create_some_very_big_type();

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

// The above is equivalent to:
let temp_value = x + 1;
x = temp_value;

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

Simple variable references are optimized

Rhai’s script optimizer is usually smart enough to rewrite function calls into method-call style or op-assignment style to take advantage of this. However, there are limits to its intelligence, and only simple variable references are optimized.


#![allow(unused)]
fn main() {
x = x + 1;          // <- this statement...

x += x;             // ... 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
}

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.

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

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

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

Use a Raw Engine

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

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

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

no-std Build

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

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

Nightly Required

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

Implementation

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

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

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

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

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

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

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

// 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() {}

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

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.

Why you would want to is another matter... as there is already a nice, fast, complete scripting language for the the common WASM environment (i.e. a browser) – and it is called JavaScript.

But anyhow, do it because you can!

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

JavaScript Interop

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

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

Size

Also look into minimal builds to reduce generated WASM size.

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

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

Speed

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

Common Features

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

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

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

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

Packaged Utilities

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

ToolDescription
rhai-repla simple REPL, interactively evaluate statements from stdin
rhai-runruns each filename passed to it as a Rhai script

Install Tools

To install these tools (with decimal and metadata support), use the following command:

cargo install --path . --bins  --features decimal,metadata

or specifically:

cargo install --path . --bin rhai-run  --features decimal,metadata

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 before the REPL starts.

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.

Example

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

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

Running a Tool from Cargo

Tools can also be run with the following cargo command:

cargo run --bin {program_name}

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

A number of examples can be found in the examples directory:

ExampleDescription
arrays_and_structsshows how to register a custom Rust type and using arrays on it
custom_types_and_methodsshows how to register a custom Rust type and methods for it
hellosimple example that evaluates an expression and prints the result
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 function
stringsshows different ways to register functions taking string arguments
threadingshows how to communication to an Engine running in a separate thread via an MPSC channel

Running Examples

Examples can be run with the following command:

cargo run --example {example_name}

no-std Samples

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

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

cargo run cannot be used to run a no-std sample. It must first be built:

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
assignment.rhaivariable declarations
comments.rhaijust comments
for1.rhaifor loops
for2.rhaifor loops on arrays
function_decl1.rhaia function without parameters
function_decl2.rhaia function with two parameters
function_decl3.rhaia function with many parameters
function_decl4.rhaia function acting as a method
if1.rhaiif example
if2.rhaiif-expression example
loop.rhaicount-down loop in Rhai, emulating a do .. while 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
switch.rhaiswitch example
strings_map.rhaistring and object map operations
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

Running Example Scripts

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

cargo run --bin rhai-run scripts/any_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.

Hello World in Rhai

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

use rhai::{Engine, EvalAltResult};

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

    let result = engine.eval::<i64>("40 + 2")?;
    //                      ^^^^^^^ cast the result to an 'i64', this is required

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

    Ok(())
}

Evaluate a script file directly:


#![allow(unused)]
fn main() {
// 'eval_file' takes a 'PathBuf'
let result = engine.eval_file::<i64>("hello_world.rhai".into())?;
}

Error Type

rhai::EvalAltResult is the standard Rhai error type, which is a Rust enum containing all errors encountered during the parsing or evaluation process.

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.

Use Dynamic for uncertain return types.


#![allow(unused)]
fn main() {
let result = engine.eval::<i64>("40 + 2")?;     // return type is i64, specified using 'turbofish' notation

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 = engine.eval::<String>("40 + 2")?;  // returns an error because the actual return type is i64, not String
}

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.

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

// This is a Rhai script

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

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 evaluates a pre-compiled AST.


#![allow(unused)]
fn main() {
// 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
}
}

Compiling a script file is also supported with Engine::compile_file (not available under no_std or in WASM builds):


#![allow(unused)]
fn main() {
let ast = engine.compile_file("hello_world.rhai".into())?;
}

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.

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

// This is a Rhai script

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

AST Manipulation API

Advanced users may want to manipulate an AST, especially the functions contained within.

See the section on Manage AST’s for more details.

Evaluate Expressions Only

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


#![allow(unused)]
fn main() {
let result = engine.eval_expression::<i64>("2 + (10 + 10) * 2")?;
}

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

Closures and anonymous functions are also not supported because in the background they compile to functions.


#![allow(unused)]
fn main() {
// The following are all syntax errors because the script is not an expression.

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

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

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

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.

Use Engine::new_raw to create a raw Engine, in which only a minimal set of basic arithmetic and logical operators are supported (see below).

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

Built-in Operators

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

See Built-in Operators for a full list.

Differences with Engine::new

Engine::new is equivalent to:


#![allow(unused)]
fn main() {
use rhai::module_resolvers::FileModuleResolver;
use rhai::packages::StandardPackage;

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

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

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

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

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

engine.register_global_module(package);
}

Built-in Operators

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

OperatorsAssignment operatorsSupported types
(see standard types)
+,+=INT
FLOAT (if not no_float)
Decimal (requires decimal)
char
ImmutableString
-, *, /, %, **,-=, *=, /=, %=, **=INT
FLOAT (if not no_float)
Decimal (requires decimal)
<<, >><<=, >>=INT
&, |, ^&=, |=, ^=INT (bit-wise)
bool (non-short-circuiting)
&&, ||bool (short-circuits)
==, !=INT
FLOAT (if not no_float)
Decimal (requires decimal)
bool
char
ImmutableString
()
>, >=, <, <=INT
FLOAT (if not no_float)
Decimal (requires decimal)
char
ImmutableString
()
inImmutableString
char/ImmutableString
ImmutableString/object map

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

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

Scope – Initializing and Maintaining State

By default, Rhai treats each Engine invocation as a fresh one, persisting only the functions that have been defined 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.

Scope API

MethodDescription
new instance methodcreate a new empty Scope
lennumber of variables/constants currently within the Scope
rewindrewind (i.e. reset) the Scope to a particular number of variables/constants
clearremove all variables/constants from the Scope, making it empty
is_emptyis the Scope empty?
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
containsdoes the particular variable or constant exist in the Scope?
get_value<T>, get_mut<T>, set_value<T>get/set the value of a variable within the Scope (panics if trying to set the value of a constant)
iter, iter_raw, IntoIterator::into_iterget an iterator to the variables/constants within the Scope
Extend::extendadd variables/constants to the Scope

For the complete Scope API, refer to the documentation online.

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.

Example

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


#![allow(unused)]
fn main() {
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 variable when one doesn't exist

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

// Second invocation using the same state
let result = engine.eval_with_scope::<i64>(&mut scope, "x")?;

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

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

Scope Lifetime – Avoid Allocations for Variable Names

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

The reason for such a lifetime parameter is obviously due to something held inside the Scope itself being a reference with a lifetime, and that “something” is the name of each variable (and constant) stored within the Scope.

Names of variables and constants are strings, but they do not need to be owned String types.

In fact, the names can easily be string slices referencing external data. This way, no additional String allocations are needed in order to push a variable or constant into the Scope.

For applications where variables and/or constants are frequently pushed into and removed from a Scope in order to run custom scripts, this has significant performance implications.


#![allow(unused)]
fn main() {
let mut scope = Scope::new();

scope.push("my_var", 42 as i64);                // &'static str

scope.push(String::from("also_var"),            // String
    123 as i64
);

// Read a bunch of configuration values from a database
let items: Vec<_> = script_env.iter()
                              .map(|id| read_from_db(id))
                              .collect();

for item in items {
    // No String allocation for variable name
    // 'scope' now has lifetime of 'items'
    scope.push(&item.name, item.value);         // borrowed &str
}
}

Engine Configuration Options

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

MethodNot available underDescription
set_optimization_levelno_optimizesets the amount of script optimizations performedSee script optimization
set_max_expr_depthsuncheckedsets the maximum nesting levels of an expression/statementSee maximum statement depth
set_max_call_levelsuncheckedsets the maximum number of function call levels (default 50) to avoid infinite recursionSee maximum call stack depth
set_max_operationsuncheckedsets the maximum number of operations that a script is allowed to consumeSee maximum number of operations
set_max_modulesuncheckedsets the maximum number of modules that a script is allowed to loadSee maximum number of modules
set_max_string_sizeuncheckedsets the maximum length (in UTF-8 bytes) for stringsSee maximum length of strings
set_max_array_sizeunchecked, no_indexsets the maximum size for arraysSee maximum size of arrays
set_max_map_sizeunchecked, no_objectsets the maximum number of properties for object mapsSee maximum size of object maps
disable_symboldisables a certain keyword or operatorSee disable keywords and operators

Extend Rhai with Rust

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

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

Traits

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

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

Register a Rust Function

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

To call these functions, they need to be registered with the Engine using Engine::register_fn and Engine::register_result_fn (see fallible functions).


#![allow(unused)]
fn main() {
use rhai::{Dynamic, Engine, EvalAltResult, 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_str(x: i64, s: &str) -> i64 {
    x + s.len()
}
// Function that returns a 'Dynamic' value
fn get_any_value() -> Dynamic {
    Ok(42_i64.into())                   // standard types can use '.into()'
}

let mut engine = Engine::new();

engine.register_fn("add", add_len)
      .register_fn("add_str", add_len_str)
      .register_fn("get_any_value", get_any_value);

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

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

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

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

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

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

To create a Dynamic value, use the Dynamic::from method. Standard types in Rhai can also use .into().


#![allow(unused)]
fn main() {
use rhai::Dynamic;

let x = 42_i64.into();                  // '.into()' works for standard types

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

let x = Dynamic::from(TestStruct::new());
}

Function Overloading

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

New definitions overwrite previous definitions of the same name and same number/types of parameters.

Dynamic Parameters in Rust Functions

It is possible for Rust functions to contain parameters of type Dynamic. Any clonable value can be set into a Dynamic value.

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

For example, the push method of an array is implemented this way (minus code that protects against over-sized arrays), which makes the function applicable for all item types:


#![allow(unused)]
fn main() {
// 'item: Dynamic' matches all data types
fn push(array: &mut Array, item: Dynamic) {
    array.push(item);
}
}

Examples

A Dynamic value has less precedence than a value of a specific type, and parameter matching starts from the left to the right. Candidate functions will be matched in order of parameter types.

Therefore, always leave Dynamic parameters as far to the right as possible.


#![allow(unused)]
fn main() {
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", foo8)
      .register_fn("foo", foo7)
      .register_fn("foo", foo6)
      .register_fn("foo", foo5)
      .register_fn("foo", foo4)
      .register_fn("foo", foo3)
      .register_fn("foo", foo2)
      .register_fn("foo", foo1);
}

Warning: Only the Right-Most 16 Parameters Can Be Dynamic

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

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


#![allow(unused)]
fn main() {
// 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 some unspeakably evil things with all those parameters ...
}
}

String Parameters in Rust Functions

Avoid String

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

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.

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


#![allow(unused)]
fn main() {
fn get_len1(s: String) -> i64 {             // 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' is 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 As Expected

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.


#![allow(unused)]
fn main() {
fn bad(s: &mut String) { ... }              // '&mut String' will not match string values

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

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

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

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

Register a Generic Rust Function

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.


#![allow(unused)]
fn main() {
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>);
}

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

Register a Fallible Rust Function

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

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


#![allow(unused)]
fn main() {
use rhai::{Engine, EvalAltResult, Position};

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

let mut engine = Engine::new();

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

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

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 native call context and exposes the following:

MethodTypeDescription
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
iter_imports()impl Iterator<Item = (&str, &Module)>iterator of the current stack of modules imported via import statements
imports()&Importsreference to the current 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
namespaces()&[&Module]reference to the namespaces (as modules) containing all script-defined functions; requires the internals feature
call_fn_dynamic_raw()Result<Dynamic, Box<EvalAltResult>>call a native Rust function with the supplied arguments; this is an advanced method

Implement Safety Checks with NativeCallContext

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


#![allow(unused)]
fn main() {
use rhai::{Array, NativeCallContext, EvalAltResult, Position};

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

    let array = Array::new();

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

    OK(array)
}
}

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, thereby implementing a callback:


#![allow(unused)]
fn main() {
use rhai::{Dynamic, FnPtr, NativeCallContext, EvalAltResult};

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

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.

Assume this script:


#![allow(unused)]
fn main() {
import "process" as proc;       // this is evaluated every time

// a function with two parameters: string and i64
fn hello(x, y) {
    x.len + y
}

// functions can be overloaded: this one takes only one parameter
fn hello(x) {
    x * 2
}

// this one takes no parameters
fn hello() {
    proc::process_data(42);     // can access imported module
}
}

Functions defined within the script can be called using Engine::call_fn:


#![allow(unused)]
fn main() {
// Compile the script to AST
let ast = engine.compile(script)?;

// A custom scope can also contain any variables/constants available to the functions
let mut scope = Scope::new();

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

let result: i64 = engine.call_fn(&mut scope, &ast, "hello", ( "abc", 123_i64 ) )?;
//          ^^^                                             ^^^^^^^^^^^^^^^^^^
//          return type must be specified                   put arguments in a tuple

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

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

When using Engine::call_fn, the AST is first evaluated before the function is called. This is usually desirable in order to import the necessary external modules that are needed by the function.

If this default behavior is not desirable, use AST::clear_statements to create a copy of the AST without any body script, only function definitions.

FuncArgs trait

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.

Rhai implements FuncArgs for tuples and Vec<T>.

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


#![allow(unused)]
fn main() {
use std::iter::once;
use rhai::FuncArgs;

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

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

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

// The type 'Options' can now be used as arguments to 'call_fn'!
let result: i64 = engine.call_fn(&mut scope, &ast, "hello", options)?;
}

Low-Level API – Engine::call_fn_dynamic

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


#![allow(unused)]
fn main() {
let result = engine.call_fn_dynamic(
                        &mut scope,         // scope to use
                        &ast,               // AST containing the functions
                        false,              // false = do not evaluate the AST
                        "hello",            // function entry-point
                        None,               // 'this' pointer, if any
                        [ "abc".into(), 123_i64.into() ]    // arguments
             )?;
}

Binding the this pointer

Engine::call_fn_dynamic can also bind a value to the this pointer of a script-defined function.


#![allow(unused)]
fn main() {
let ast = engine.compile("fn action(x) { this += x; }")?;

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

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

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

Create a Rust Closure from a Rhai Function

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

Such a closure is very useful as call-back functions.

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


#![allow(unused)]
fn main() {
use rhai::{Engine, Func};       // use 'Func' for 'create_from_script'

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

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

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

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

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

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

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

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?

Search Order of Functions

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

  • Rhai script-defined functions.

  • Native Rust functions registered directly into the Engine via the Engine::register_XXX API.

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

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

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

  • Built-in functions.

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 calls a function named “+“!


#![allow(unused)]
fn main() {
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; as a result, 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 to the Engine via the Engine::register_XXX API.

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


#![allow(unused)]
fn main() {
use rhai::{Engine, EvalAltResult};

let mut engine = Engine::new();

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

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

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

result == 42;

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

result == 1.0;

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

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

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

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

Considerations

Normally, 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 a calculation for + turns into a subtraction, for example.

Operator overloading also impacts script optimization when using OptimizationLevel::Full. See the [script-optimization] for more details.

Register any Rust Type and its Methods

Free Typing

Rhai works seamlessly with any Rust type. The type can be anything; it does not have any prerequisites 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.

The reason why it is termed a custom type throughout this documentation is that Rhai natively supports a number of data types with fast, internal treatment (see the list of standard types). Any type outside of this list is considered custom.

Any type not supported natively by Rhai is stored as a Rust trait object, with no restrictions other than being Clone (plus Send + Sync under the sync feature). It runs slightly slower than natively-supported types as it does not have built-in, optimized implementations for commonly-used functions, but for all other purposes has no difference.

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

Register a Custom Type and its Methods

Any custom type must implement the Clone trait as this allows the Engine to pass by value.

If the sync feature is used, it must also be Send + Sync.

Notice that the custom type needs to be registered using Engine::register_type or Engine::register_type_with_name.

To use native methods on custom types in Rhai scripts, it is common to register an API for the type via the Engine::register_XXX methods.


#![allow(unused)]
fn main() {
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::<TestStruct>()    // register custom type
      .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
}

Rhai follows the convention that 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).

IMPORTANT: Rhai does NOT support normal references (i.e. &T) as parameters.

type_of() a Custom Type

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

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


#![allow(unused)]
fn main() {
engine.register_type::<TestStruct1>()
      .register_fn("new_ts1", TestStruct1::new)
      .register_type_with_name::<TestStruct2>("TestStruct")
      .register_fn("new_ts2", TestStruct2::new);

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

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

Collection Types

Collection types usually contain a push, insert, add, append or += method that adds a particular item to the collection.

If the collection takes a Dynamic value (e.g. like an array), the type of such an add function can take a Dynamic parameter.


#![allow(unused)]
fn main() {
engine.register_fn("push",
    |col: &mut MyCollectionType, item: Dynamic| col.push(col)
);
}

Use the Custom Type With Arrays

In order to use the in operator with a custom type for an array, the == operator must be registered for the custom type:


#![allow(unused)]
fn main() {
// 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 '=='
}

Working With Enums

It is quite easy to use Rust enums with Rhai. See the section on Working with Enums for more details.

Custom Type Property Getters and Setters

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

Properties can be accessed in a JavaScript-like syntax:

object . property

object . property = value ;

Getters and setters each take a &mut reference to the first parameter.

Getters and setters are disabled when the no_object feature is used.

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

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

Cannot Override Object Maps

Property getters and setters are mainly intended for custom types.

Any getter or setter function registered for object maps is simply ignored because the get/set calls will be interpreted as properties on the object maps.

Examples


#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct TestStruct {
    field: String
}

impl TestStruct {
    // Remember &mut must be used even for getters
    fn get_field(&mut self) -> String {
        self.field.clone()
    }

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

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

let mut engine = Engine::new();

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

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

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

IMPORTANT: Rhai does NOT support normal references (i.e. &T) as parameters.

Fallback to Indexer

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.


#![allow(unused)]
fn main() {
a.foo           // if property getter for 'foo' doesn't exist...

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

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 ;

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 feature is used.

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

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

IMPORTANT: Rhai does NOT support normal references (i.e. &T) as parameters.

Cannot Override Arrays, Object Maps, Strings and Integers

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

These types have built-in indexer implementations that are fast and efficient:

TypeIndex typeReturn typeDescription
ArrayINTDynamicaccess a particular element inside the array
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 as a bit-field

Attempting to register indexers for an array, object map, string or INT panics when using the Engine::register_indexer_XXX API. They can, however, be defined in a plugin module, only to be ignored.

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.

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:


#![allow(unused)]
fn main() {
// The following assumes:
//   'index' is 'INT', 'items_len: usize' is the number of elements
let actual_index = if index < 0 {
    index.checked_abs().map_or(0, |n| items_len - (n as usize).min(items_len))
} 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).

Examples


#![allow(unused)]
fn main() {
#[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
}

Indexer as Property Access Fallback

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.


#![allow(unused)]
fn main() {
// You can write this...
let x = foo["hello_world"];

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

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


#![allow(unused)]
fn main() {
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 object = MyType::new();
          object.insert("foo", 1);
          object.insert("bar", 42);
          object.insert("baz", 123);
      })
      // Property 'hello'
      .register_get("hello", |object: &mut MyType| object.len() as i64)
      // Index getter/setter
      .register_indexer_get(|object: &mut MyType, index: &str| *object[index])
      .register_indexer_set(|object: &mut MyType, index: &str, value: i64| object[index] = value);

// Calls a["foo"] because getter for 'foo' does not exist
engine.consume("let a = new_ts(); print(a.foo);");

// Calls a["bar"] because getter for 'bar' does not exist
engine.consume("let a = new_ts(); print(a.bar);");

// Calls a["baz"] = 999 because getter for 'baz' does not exist
engine.consume("let a = new_ts(); a.baz = 999;");

// Error: Property getter is not a fallback for indexer
engine.consume(r#"let a = new_ts(); print(a["hello"]);"#);
}

Call Method as Function

Method-Call Style vs. Function-Call Style

Any registered function with a first argument that is a &mut reference can be used as method because internally they are the same thing: methods on a custom type is implemented as a functions taking a &mut first argument.

This design is similar to Rust.


#![allow(unused)]
fn main() {
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
}

Under no_object, however, the method-call style is no longer supported.


#![allow(unused)]
fn main() {
// Below is a syntax error under 'no_object'.
let result = engine.eval("let x = [1, 2, 3]; x.clear();")?;
                                           // ^ cannot call method-style
}

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.


#![allow(unused)]
fn main() {
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'
}

IMPORTANT: Rhai does NOT support normal references (i.e. &T) as parameters.

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


#![allow(unused)]
fn main() {
// 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

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 used both as the object of a method call (including the this pointer) and also as an argument.


#![allow(unused)]
fn main() {
// A method using the 'this' pointer and an argument
fn foo(x) {
    this + x
}

let value = 42;     // 'value' is not shared by default

let f = || value;   // this closure captures 'value'

// ... at this point, 'value' is shared

value.is_shared() == true;

value.foo(value);   // <- error: data race detected!
}

The reason why it is a data race is because the variable value is shared, and cloning it merely clones a shared reference to it. Using it as the method call object (i.e. the this pointer) takes a mutable reference to the underlying value, which then cause a data race because a non-mutable reference is already outstanding due to the argument (which uses the same variable).

Shared values are typically created from closures which capture external variable, so data races are not possible in Rhai under the no_closure feature.

Disable Custom Types

no_object Feature

The custom types API register_type, register_type_with_name, register_get, register_get_result, register_set, register_set_result and register_get_set are not available under no_object.

no_index Feature

The indexers API register_indexer_get, register_indexer_get_result, register_indexer_set, register_indexer_set_result, and register_indexer_get_set are not available under no_object+no_index.

Printing for Custom Types

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

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

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

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

Modules

Rhai allows organizing functionalities (functions, both Rust-based or script-based, and variables) into independent modules. Modules can be disabled via the no_module feature.

A module is of 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.

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

Usage Patterns

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

Create a Module from Rust

The Easy Way – Create via 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 – Create via Module API

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

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

Create a Module from an AST

Module::eval_ast_as_new

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

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

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:

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.

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


#![allow(unused)]
fn main() {
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 + 1
    }
    fn add_len(x, y) {
        x + y.len
    }

    // Imported modules can 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
        foo,
        hello,
        extra as foobar;    // export sub-module
"#)?;

// 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: 'foobar' (renamed from 'extra')
//   - functions: 'calc', 'add_len'
//   - constants: 'abc' (renamed from 'x'), 'foo', 'hello'
}

Make a Module Available to Scripts

Use Case 1 – Make the Module Globally Available

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

All functions and type iterators can be accessed without namespace qualifiers. Variables and sub-modules are ignored.

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


#![allow(unused)]
fn main() {
use rhai::{Engine, Module};

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

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

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

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

engine.eval::<i64>("inc(41)")? == 42;       // no need to import 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. In fact, the actual implementation of Engine::register_fn etc. simply adds the function to an internal module!


#![allow(unused)]
fn main() {
// 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 the Module a Static Module

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


#![allow(unused)]
fn main() {
use rhai::{Engine, Module};

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

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

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

// 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(41)")? == 42;
}

Expose Functions to the Global Namespace

The Module API can optionally expose functions to the global namespace by setting the namespace parameter to FnNamespace::Global, so getters/setters and indexers for custom types can work as expected.

Type iterators, because of their special nature, are always exposed to the global namespace.


#![allow(unused)]
fn main() {
use rhai::{Engine, Module, FnNamespace};

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

// Expose method 'inc' to the global namespace (default is 'FnNamespace::Internal')
let hash = module.set_native_fn("inc", |x: &mut i64| Ok(x + 1));
module.update_fn_namespace(hash, FnNamespace::Global);

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

// 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(41)")? == 42;

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

Use Case 3 – Make the Module 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.


#![allow(unused)]
fn main() {
use rhai::{Engine, Scope, Module};
use rhai::module_resolvers::StaticModuleResolver;

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

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

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

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

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

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

Module Resolvers

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

See the section on Importing Modules for more details.

Module Resolvers are service types that implement the ModuleResolver trait.

Built-In Module Resolvers

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

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

FileModuleResolver (default)

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

Function namespace

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

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

Base directory

Relative paths are resolved relative to a root directory, which is usually the base directory (if set). The base directory can be set via FileModuleResolver::new_with_path or FileModuleResolver::set_base_path.

If the base directory is not set (e.g. using FileModuleResolver::new), then it is based off the directory holding the loading script. This allows scripts to simply load each other.

Caching

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

Use FileModuleResolver::enable_cache to enable/disable the script file cache.

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

Simulating virtual functions

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

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

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


#![allow(unused)]
fn main() {
+----------------+
| 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

Loads modules that are statically added. This can be used under no_std.

Functions are searched in the global namespace by default.


#![allow(unused)]
fn main() {
use rhai::{Module, module_resolvers::StaticModuleResolver};

let module: Module = create_a_module();

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

ModuleResolversCollection

A collection of module resolvers. Modules will be resolved from each resolver in sequential order.

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

DummyResolversCollection

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

Set into Engine

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


#![allow(unused)]
fn main() {
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());
}

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

  • Upon success, it should return an Rc<Module> (or Arc<Module> under sync).

    The module should call Module::build_index on the target module before returning. 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.

  • 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


#![allow(unused)]
fn main() {
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.consume(
r#"
    import "hello" as foo;  // this 'import' statement will call
                            // 'MyModuleResolver::resolve' with "hello" as 'path'
    foo:bar();
"#)?;
}

Implementing 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

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.


#![allow(unused)]
fn main() {
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.consume(&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.

It does not matter where the import statement occurs (e.g. deep within statement blocks, within function bodies).


#![allow(unused)]
fn main() {
// The following import is pre-resolved.
import "hello" as h;

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

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

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

let module_name = "hello";

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

Plugins

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

Instead of using the large Engine::register_XXX API or the parallel Module API, a plugin simplifies the work of creating and registering new functionality in an Engine.

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

Export a Rust Module to Rhai

Prelude

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


#![allow(unused)]
fn main() {
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 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, all pub constants become module constant variables, and all sub-modules become Rhai sub-modules.

This Rust module can then be registered into an Engine as a normal module. This is done via the exported_module! macro.

The macro combine_with_exported_module! can 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.


#![allow(unused)]
fn main() {
use rhai::plugin::*;        // a "prelude" import for macros

#[export_module]
mod my_module {
    // 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'.
    pub fn greet(name: &str) -> String {
        format!("hello, {}!", name)
    }
    // This function will be registered as 'get_num'.
    pub fn get_num() -> i64 {
        mystic_number()
    }
    // 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(num: &mut i64) {
        *num += 1;
    }
    // This function is not 'pub', so NOT registered.
    fn mystic_number() -> i64 {
        42
    }

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

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

Use Engine::register_global_module

The simplest way to register this into an Engine is to first use the exported_module! macro to turn it into a normal Rhai module, then use the Engine::register_global_module method on it:

fn main() {
    let mut engine = Engine::new();

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

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

The functions contained within the module definition (i.e. greet, get_num and increment) are automatically registered into the Engine when Engine::register_global_module is called.


#![allow(unused)]
fn main() {
let x = greet("world");
x == "hello, world!";

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

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

increment(x);
x == 43;
}

Notice that, 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.

Use Engine::register_static_module

Another simple way to register this into an Engine is, again, to use the exported_module! macro to turn it into a normal Rhai module, then use the Engine::register_static_module method on 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 as a static module namespace.
    engine.register_static_module("service", module.into());
}

The functions contained within the module definition (i.e. greet, get_num and increment), plus the constant MY_NUMBER, are automatically registered under the module namespace service:


#![allow(unused)]
fn main() {
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;

service::increment(x);
x == 43;
}

All functions (usually methods) defined in the module and marked with #[rhai_fn(global)], as well as all type iterators, are automatically exposed to the global namespace, so iteration, getters/setters and indexers for custom types can work as expected.

In fact, the default for all getters/setters and indexers defined in a plugin module is #[rhai_fn(global)] unless specifically overridden by #[rhai_fn(internal)].

Therefore, in the example above, the increment method (defined with #[rhai_fn(global)]) works fine when called in method-call style:


#![allow(unused)]
fn main() {
let x = 42;
x.increment();
x == 43;
}

Use Dynamically

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.

See the module section for more information.

Combine into Custom Package

Finally the plugin module can also be used to develop a custom package, using combine_with_exported_module!:


#![allow(unused)]
fn main() {
def_package!(rhai:MyPackage:"My own personal super package", module, {
    combine_with_exported_module!(module, "my_module_ID", my_module));
});
}

combine_with_exported_module! automatically flattens the module namespace so that all functions in sub-modules are promoted to the top level. This is convenient for custom packages.

Sub-Modules and Feature Gates

Sub-modules in a plugin module definition are turned into valid sub-modules in the resultant Rhai Module.

They are also commonly used to put feature gates or compile-time gates on a group of functions, because currently attributes do not work on individual function definitions due to a limitation of the procedural macros system.

This is especially convenient when using the combine_with_exported_module! macro to develop custom packages because selected groups of functions can easily be included or excluded based on different combinations of feature flags instead of having to manually include/exclude every single function.


#![allow(unused)]
fn main() {
#[export_module]
mod my_module {
    // Always available
    pub fn func0() {}

    // The following sub-module is only available under 'feature1'
    #[cfg(feature = "feature1")]
    pub mod feature1 {
        fn func1() {}
        fn func2() {}
        fn func3() {}
    }

    // The following sub-module is only available under 'feature2'
    #[cfg(feature = "feature2")]
    pub mod feature2 {
        fn func4() {}
        fn func5() {}
        fn func6() {}
    }
}

// Registered functions:
//   func0 - always available
//   func1, func2, func3 - available under 'feature1'
//   func4, func5, func6 - available under 'feature2'
combine_with_exported_module!(module, "my_module_ID", my_module);
}

Function Overloading and Operators

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.

Operators (which require function names that are not valid for Rust) can also be registered this way.

Registering the same function name with the same parameter types will cause a parsing error.


#![allow(unused)]
fn main() {
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)'.
    #[rhai_fn(name = "calc")]
    pub fn calc_with_default(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

Functions can be marked as getters/setters and indexers for custom types via the #[rhai_fn] attribute, which is applied on a function level.


#![allow(unused)]
fn main() {
use rhai::plugin::*;        // a "prelude" import for macros

#[export_module]
mod my_module {
    // This is a normal function 'greet'.
    pub fn greet(name: &str) -> String {
        format!("hello, {}!", name)
    }
    // This is a getter for 'TestStruct::prop'.
    #[rhai_fn(get = "prop", pure)]
    pub fn get_prop(obj: &mut TestStruct) -> i64 {
        obj.prop
    }
    // This is a setter for 'TestStruct::prop'.
    #[rhai_fn(set = "prop")]
    pub fn set_prop(obj: &mut TestStruct, value: i64) {
        obj.prop = value;
    }
    // This is an index getter for 'TestStruct'.
    #[rhai_fn(index_get)]
    pub fn get_index(obj: &mut TestStruct, index: i64) -> bool {
        obj.list[index]
    }
    // This is an index setter for 'TestStruct'.
    #[rhai_fn(index_set)]
    pub fn get_index(obj: &mut TestStruct, index: i64, state: bool) {
        obj.list[index] = state;
    }
}
}

Multiple Registrations

Parameters to the #[rhai_fn(...)] attribute can be applied multiple times.

This is especially useful for the name = "...", get = "..." and set = "..." parameters to give multiple alternative names to the same function.


#![allow(unused)]
fn main() {
use rhai::plugin::*;        // a "prelude" import for macros

#[export_module]
mod my_module {
    // This function can be called in five ways
    #[rhai_fn(name = "get_prop_value", name = "prop", name = "+", set = "prop", index_get)]
    pub fn prop_function(obj: &mut TestStruct, index: i64) -> i64 {
        obj.prop[index]
    }
}
}

The above function can be called in five ways:

Parameter for #[rhai_fn(...)]TypeCall style
name = "get_prop_value"method functionget_prop_value(x, 0), x.get_prop_value(0)
name = "prop"method functionprop(x, 0), x.prop(0)
name = "+"operatorx + 42
set = "prop"setterx.prop = 42
index_getindex getterx[0]

Pure Functions

Apply the #[rhai_fn(pure)] attribute on a method function (i.e. one taking a &mut first parameter) to mark it as pure.

Pure functions MUST NOT modify the value of the &mut parameter.

Therefore, pure functions can be passed a constant value as the first &mut parameter.

Non-pure functions, when passed a constant value as the first &mut parameter, will raise an EvalAltResult::ErrorAssignmentToConstant error.

For example:


#![allow(unused)]
fn main() {
use rhai::plugin::*;        // a "prelude" import for macros

#[export_module]
mod my_module {
    fn internal_calc(array: &mut rhai::Array, x: i64) -> i64 {
        array.iter().map(|v| v.as_int().unwrap()).fold(0, |(r, v)| r += v * x)
    }
    // This function can be passed a constant
    #[rhai_fn(name = "add1", pure)]
    pub fn add_scaled(array: &mut rhai::Array, x: i64) -> i64 {
        internal_calc(array, x)
    }
    // This function CANNOT be passed a constant
    #[rhai_fn(name = "add2")]
    pub fn add_scaled2(array: &mut rhai::Array, x: i64) -> i64 {
        internal_calc(array, x)
    }
    // This getter can be applied to a constant
    #[rhai_fn(get = "first1", pure)]
    pub fn get_first(array: &mut rhai::Array) -> i64 {
        array[0]
    }
    // This getter CANNOT be applied to a constant
    #[rhai_fn(get = "first2")]
    pub fn get_first2(array: &mut rhai::Array) -> i64 {
        array[0]
    }
    // The following is a syntax error because a setter is SUPPOSED to
    // mutate the object.  Therefore the 'pure' attribute cannot be used.
    #[rhai_fn(get = "values", pure)]
    pub fn set_values(array: &mut rhai::Array, value: i64) {
        // ...
    }
}
}

When applied to a Rhai script:


#![allow(unused)]
fn main() {
// Constant
const VECTOR = [1, 2, 3, 4, 5, 6, 7];

let r = VECTOR.add1(2);     // ok!

let r = VECTOR.add2(2);     // runtime error: constant modified

let r = VECTOR.first1;      // ok!

let r = VECTOR.first2;      // runtime error: constant modified
}

Fallible Functions

To register fallible functions (i.e. functions that may return errors), apply the #[rhai_fn(return_raw)] attribute on functions that return Result<T, Box<EvalAltResult>> where T is any clonable type.

A syntax error is generated if the function with #[rhai_fn(return_raw)] does not have the appropriate return type.


#![allow(unused)]
fn main() {
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)
        }
    }
}
}

NativeCallContext Parameter

The first parameter of a function can also be NativeCallContext, which is treated specially by the plugins system.

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

Inner attributes can be applied to the inner items of a module to tweak the export process.

#[rhai_fn] is applied to functions, while #[rhai_mod] is applied to sub-modules.

Parameters should be set on inner attributes to specify the desired behavior.

Attribute ParameterUse withApply toDescription
skip#[rhai_fn], #[rhai_mod]function or sub-moduledo not export this function/sub-module
global#[rhai_fn]functionexpose this function to the global namespace
internal#[rhai_fn]functionkeep this function within the internal module namespace
name = "..."#[rhai_fn], #[rhai_mod]function or sub-moduleregisters function/sub-module under the specified name
get = "..."#[rhai_fn]pub fn (&mut Type) -> Valueregisters a getter for the named property
set = "..."#[rhai_fn]pub fn (&mut Type, Value)registers a setter for the named property
index_get#[rhai_fn]pub fn (&mut Type, INT) -> Valueregisters an index getter
index_set#[rhai_fn]pub fn (&mut Type, INT, Value)registers an index setter
return_raw#[rhai_fn]pub fn (...) -> Result<Type, Box<EvalAltResult>>marks this as a fallible function
pure#[rhai_fn]pub fn (&mut Type, ...) -> ...marks this as a pure function

Export a Rust Function to Rhai

Sometimes only a few ad hoc functions are required and it is simpler to register individual functions instead of a full-blown plugin module.

Macros

MacroSignatureDescription
#[export_fn]apply to rust function defined in a Rust moduleexports the function
register_exported_fn!register_exported_fn!(&mut engine, "name", function)registers the function into an Engine under a specific name
set_exported_fn!set_exported_fn!(&mut module, "name", function)registers the function into a Module under a specific name
set_exported_global_fn!set_exported_global_fn!(&mut module, "name", function)registers the function into a Module under a specific name, exposing it to the global namespace

#[export_fn] and register_exported_fn!

Apply #[export_fn] onto a function defined at module level to convert it into a Rhai plugin function.

The function cannot be nested inside another function – it can only be defined directly under a module.

To register the plugin function, simply call register_exported_fn!. The name of the function can be any text string, so it is possible to register overloaded functions as well as operators.

use rhai::plugin::*;        // import macros

#[export_fn]
fn increment(num: &mut i64) {
    *num += 1;
}

fn main() {
    let mut engine = Engine::new();

    // 'register_exported_fn!' registers the function as 'inc' with the Engine.
    register_exported_fn!(engine, "inc", increment);
}

Pure Functions

Some functions are pure – i.e. they do not mutate any parameter, even though the first parameter may be passed in as &mut (e.g. for a method function).

This is most commonly done to avoid expensive cloning for methods or property getters that return information about a custom type and does not modify it.

Apply the #[export_fn(pure)] attribute on a plugin function to mark it as pure.

Pure functions can be passed a constant value as the first &mut parameter. The condition is that they MUST NOT modify that value.

Non-pure functions, when passed a constant value as the first &mut parameter, will raise an EvalAltResult::ErrorAssignmentToConstant error.


#![allow(unused)]
fn main() {
use rhai::plugin::*;        // a "prelude" import for macros

// This method is pure, so 'len' can be used on a constant 'TestStruct'.
#[export_fn(pure)]
pub fn len(my_type: &mut TestStruct) -> i64 {
    my_type.len()
}

// This method is not pure, so 'clear' will raise an error
// when used on a constant 'TestStruct'.
#[export_fn]
pub fn clear(my_type: &mut TestStruct) {
    my_type.clear();
}
}

Fallible Functions

To register fallible functions (i.e. functions that may return errors), apply the #[export_fn(return_raw)] attribute on plugin functions that return Result<T, Box<EvalAltResult>> where T is any clonable type.

A syntax error is generated if the function with #[export_fn(return_raw)] does not have the appropriate return type.

use rhai::plugin::*;        // a "prelude" import for macros

#[export_fn(return_raw)]
pub fn double_and_divide(x: i64, y: i64) -> Result<i64, Box<EvalAltResult>> {
    if y == 0 {
        Err("Division by zero!".into())
    } else {
        Ok((x * 2) / y)
    }
}

fn main() {
    let mut engine = Engine::new();

    // Overloads the operator '+' with the Engine.
    register_exported_fn!(engine, "+", double_and_divide);
}

NativeCallContext Parameter

The first parameter of a function can also be NativeCallContext, which is treated specially by the plugins system.

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.

Packages are Modules

Internally, a package is a module, with some conveniences to make it easier to define and use as a standard library for an Engine.

Packages typically contain Rust functions that are callable within a Rhai script. All top-level functions in a package are available under the global namespace (i.e. they’re available without namespace qualifiers).

Sub-modules and variables are ignored in packages.

Share a Package Among Multiple Engine‘s

Engine::register_global_module and Engine::register_static_module both require shared modules.

Once a package is created (e.g. via Package::new), it can create shared modules (via Package::as_shared_module) and register into multiple instances of Engine, even across threads (under the sync feature).

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


#![allow(unused)]
fn main() {
use rhai::Engine;
use rhai::packages::Package         // load the 'Package' trait to use packages
use rhai::packages::CorePackage;    // the 'core' package contains basic functionalities (e.g. arithmetic)

// Create a package - can be shared among multiple 'Engine' instances
let package = CorePackage::new();

let mut engines_collection: Vec<Engine> = Vec::new();

// Create 100 'raw' Engines
for _ in 0..100 {
    let mut engine = Engine::new_raw();

    // Register the package into the global namespace.
    // 'Package::as_shared_module' converts the package into a shared module.
    engine.register_global_module(package.as_shared_module());

    engines_collection.push(engine);
}
}

Built-In Packages

Engine::new creates an Engine with the StandardPackage loaded.

Engine::new_raw creates an Engine with no package loaded.

PackageDescriptionIn CoreIn Standard
LanguageCorePackagecore functions for the Rhai languageyesyes
ArithmeticPackagearithmetic operators (e.g. +, -, *, /) for numeric types that are not built in (e.g. u16)yesyes
BasicIteratorPackagenumeric ranges (e.g. range(1, 10))yesyes
LogicPackagelogical and comparison operators (e.g. ==, >) for numeric types that are not built in (e.g. u16)yesyes
BasicStringPackagebasic string functions (e.g. print, debug, len) that are not built inyesyes
BasicTimePackagebasic time functions (e.g. timestamps)yesyes
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
BasicMapPackagebasic object map functions (not available under no_object)noyes
BasicFnPackagebasic methods for function pointers.yesyes
CorePackagebasic essentialsyesyes
StandardPackagestandard library (default for Engine::new)noyes

CorePackage

If only minimal functionalities are required, register the CorePackage instead:


#![allow(unused)]
fn main() {
use rhai::Engine;
use rhai::packages::{Package, CorePackage};

let mut engine = Engine::new_raw();
let package = CorePackage::new();

// Register the package into the 'Engine' by converting it into a shared module.
engine.register_global_module(package.as_shared_module());
}

Create a Custom Package

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.

def_package!

def_package!(root:package_name:description, variable, block)

where:

ParameterDescription
rootroot namespace, usually rhai
package_namename of the package, usually ending in ...Package
descriptiondoc-comment for the package
variablea variable name holding a reference to the module (&mut Module) that is to form the package
blocka code block that initializes the package

Examples


#![allow(unused)]
fn main() {
// Import necessary types and traits.
use rhai::{
    def_package,            // 'def_package!' macro
    packages::Package,      // 'Package' trait
    packages::{             // pre-defined packages
        ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage
    }
};

// Define the package 'MyPackage'.
def_package!(rhai:MyPackage:"My own personal super package", module, {
    // Aggregate other packages simply by calling 'init' on each.
    ArithmeticPackage::init(module);
    LogicPackage::init(module);
    BasicArrayPackage::init(module);
    BasicMapPackage::init(module);

    // Register additional Rust functions using 'Module::set_native_fn'.
    let hash = module.set_native_fn("foo", |s: ImmutableString| {
        Ok(foo(s.into_owned()))
    });

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

Create a Custom Package from a Plugin Module

By far the easiest way to create a custom module is to call plugin::combine_with_exported_module! from within def_package! which simply merges in all the functions defined within a plugin module.

In fact, this exactly is how Rhai’s built-in packages, such as BasicMathPackage, are implemented.

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.


#![allow(unused)]
fn main() {
// Import necessary types and traits.
use rhai::{
    def_package,
    packages::Package,
    packages::{ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage}
};
use rhai::plugin::*;

// Define plugin module.
#[export_module]
mod my_module {
    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'.
def_package!(rhai:MyPackage:"My own personal super package", module, {
    // Aggregate other packages simply by calling 'init' on each.
    ArithmeticPackage::init(module);
    LogicPackage::init(module);
    BasicArrayPackage::init(module);
    BasicMapPackage::init(module);

    // Merge all registered functions and constants from the plugin module into the custom package.
    //
    // The sub-module 'my_sub_module' is flattened and its functions registered at the top level.
    //
    // The text string name in the second parameter can be anything and is reserved for future use;
    // it is recommended to be an ID string that uniquely identifies the plugin module.
    //
    // The constant variable, 'MY_NUMBER', is ignored.
    //
    // This call ends up registering three functions at the top level of the package:
    //   1) greet
    //   2) get_num
    //   3) get_sub_num (pulled up from 'my_sub_module')
    //
    combine_with_exported_module!(module, "my-functions", my_module));
});
}

Rhai Language Reference

This section outlines the Rhai language.

Comments

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

Comments can be nested.


#![allow(unused)]
fn main() {
let /* intruder comment */ name = "Bob";

// This is a very important one-line comment

/* This comment spans
   multiple lines, so it
   only makes sense that
   it is even more important */

/* Fear not, Rhai satisfies all nesting needs with nested comments:
   /*/*/*/*/**/*/*/*/*/
*/
}

Doc-Comments

Similar to Rust, comments starting with /// (three slashes) or /** (two asterisks) are doc-comments.

They are only supported under the metadata feature.

Doc-comments can only appear in front of function definitions, not any other elements. Therefore, doc-comments are not available under no_function.


#![allow(unused)]
fn main() {
/// 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;
}
}

Special Cases

Long streams of //////... and /*****... do NOT form doc-comments. This is consistent with popular comment block styles for C-like languages.


#![allow(unused)]
fn main() {
///////////////////////////////  <- 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()
Integer numberu8, i8, u16, i16,
u32, i32 (default for only_i32),
u64, i64 (default)
"i32", "u64" etc."42", "123" etc.
Floating-point number (disabled with no_float)f32 (default for f32_float), f64 (default)"f32" or "f64""123.4567" etc.
Fixed precision decimal number (requires decimal)Decimal"decimal""42", "123.4567" etc.
Boolean valuebool"bool""true" or "false"
Unicode characterchar"char""A", "x" etc.
Immutable Unicode stringrhai::ImmutableString (implemented as Rc<SmartString> or Arc<SmartString>)"string""hello" etc.
Array (disabled with no_index)rhai::Array"array""[ 1, 2, 3 ]"
Object map (disabled with no_object)rhai::Map"map""#{ "a": 1, "b": 2 }"
Timestamp (disabled with no_std)std::time::Instant (instant::Instant if WASM build)"timestamp""<timestamp>"
Function pointerrhai::FnPtrFn"Fn(foo)"
Dynamic value (i.e. can be anything)rhai::Dynamicthe actual typeactual value
Shared value (a reference-counted, shared Dynamic value, created via automatic currying, disabled with no_closure)the actual typeactual value
System integer (current configuration)rhai::INT (i32 or i64)"i32" or "i64""42", "123" etc.
System floating-point (current configuration, disabled with no_float)rhai::FLOAT (f32 or f64)"f32" or "f64""123.456" etc.
Nothing/void/nil/null/Unit (or whatever it is called)()"()""" (empty string)

All types are treated strictly separate by Rhai, meaning that i32 and i64 and u32 are completely different - they even cannot be added together. This is very similar to Rust.

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.

If no floating-point is needed or supported, use the no_float feature to remove it.

Some applications require fixed-precision decimal numbers, which can be enabled via the decimal feature.

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

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. However, under sync, all types must be Send + Sync.

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) {
    "i64" => print("Hey, I got an integer here!"),
    "f64" => print("Hey, I got a float here!"),
    "decimal" => print("Hey, I got a decimal 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!"),
    "map" => print("Hey, I got an object map here!"),
    "Fn" => print("Hey, I got a function pointer here!"),
    "TestStruct" => print("Hey, I got the TestStruct custom type here!"),
    _ => print(`I don't know what this is: ${type_of(mystery)}`)
}

Functions Returning Dynamic

In Rust, sometimes a Dynamic forms part of a returned value – a good example is an array which contains Dynamic elements, or an object map which contains Dynamic property values.

To get the real values, the actual value types must be known in advance. There is no easy way for Rust to decide, at run-time, what type the Dynamic value is (short of using the type_name function and match against the name).

Type Checking and Casting

A Dynamic value’s actual type can be checked via the is method.

The cast method then converts the value into a specific, known type.

Alternatively, use the try_cast method which does not panic but returns None when the cast fails.

Use clone_cast for on a reference to Dynamic.


#![allow(unused)]
fn main() {
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

The type_name method gets the name of the actual type as a static string slice, which can be match-ed against.


#![allow(unused)]
fn main() {
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" => ...
    "alloc::string::String" => ...
    "bool" => ...
    "crate::path::to::module::TestStruct" => ...
}
}

Note: type_name always returns the full Rust path name of the type, even when the type has been registered with a friendly name via Engine::register_type_with_name. This behavior is different from that of the type_of function in Rhai.

Methods and Traits

The following methods are available when working with Dynamic:

MethodNot available underReturn typeDescription
from<T> (instance method)Dynamiccreate a Dynamic from any value that implements Clone
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

Detection methods

MethodNot available underReturn typeDescription
is<T>boolis the value of type T?
is_variantboolis the value a trait object (i.e. not one of Rhai’s standard types)?
is_read_onlyboolis the value constant? A constant value should not be modified.
is_sharedno_closureboolis the value shared via a closure?
is_lockedno_closureboolis the value shared and locked (i.e. currently being read)?

Casting methods

The following methods cast a Dynamic into a specific type:

MethodNot available underReturn type (error is the actual data type)
cast<T>T (panics on failure)
try_cast<T>Option<T>
clone_cast<T>cloned copy of T (panics on failure)
as_unitResult<(), &str>
as_intResult<i64, &str>
as_int (only_i32)Result<i32, &str>
as_floatno_floatResult<f64, &str>
as_float (f32_float)no_floatResult<f32, &str>
as_decimalnon-decimalResult<Decimal, &str>
as_boolResult<bool, &str>
as_charResult<char, &str>
as_stringResult<String, &str>
as_immutable_stringResult<ImmutableString, &str>

Constructor traits

The following constructor traits are implemented for Dynamic:

TraitNot available underData type
From<i64>i64
From<i32> (only_i32)i32
From<f64>no_floatf64
From<f32> (f32_float)no_floatf32
From<Decimal>non-decimalDecimal
From<bool>bool
From<S: Into<ImmutableString>>
e.g. From<String>, From<&str>
ImmutableString
From<char>char
From<Vec<T>>no_indexarray
From<&[T]>no_indexarray
From<BTreeMap<K: Into<SmartString>, T>>
e.g. From<BTreeMap<String, T>>
no_objectobject map
From<BTreeSet<K: Into<SmartString>>>
e.g. From<BTreeSet<String>>
no_objectobject map
From<HashMap<K: Into<SmartString>, T>>
e.g. From<HashMap<String, T>>
no_object or no_stdobject map
From<HashSet<K: Into<SmartString>>>
e.g. From<HashSet<String>>
no_object or no_stdobject map
From<FnPtr>function pointer
From<Instant>no_stdtimestamp
From<Rc<RefCell<Dynamic>>>sync or no_closureDynamic
From<Arc<RwLock<Dynamic>>> (sync)non-sync or no_closureDynamic

type_of()

The type_of function detects the actual type of a value.

This is useful because all variables are Dynamic in nature.

// Use 'type_of()' to get the actual types of values
type_of('c') == "char";
type_of(42) == "i64";

let x = 123;
x.type_of() == "i64";       // method-call style is also OK
type_of(x) == "i64";

x = 99.999;
type_of(x) == "f64";

x = "hello";
if type_of(x) == "string" {
    do_something_first_with_string(x);
}

switch type_of(x) {
    "string" => do_something_with_string(x),
    "char" => do_something_with_char(x),
    "i64" => do_something_with_int(x),
    "f64" => do_something_with_float(x),
    "bool" => do_something_with_bool(x),
    _ => throw `I cannot work with ${type_of(x)}!!!`
}

Custom Types

type_of() a custom type returns:

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

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


#![allow(unused)]
fn main() {
struct TestStruct1;
struct TestStruct2;

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

Dynamic Value Tag

Each Dynamic value can contain a tag that is i32 and can contain any arbitrary data.

On 32-bit targets, however, the tag is only i16.

The tag defaults to zero.

It is an error to set a tag to a value beyond the bounds of i32 (i16 on 32-bit targets).

Examples


#![allow(unused)]
fn main() {
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

let y = x;

y.tag == 123;           // the tag is copied across assignment

y.tag = 3000000000;     // runtime error: 3000000000 is too large for 'i32'
}

Practical Applications

Attaching arbitrary information together with a value has a lot of practical uses.

Identify code path

For example, it is easy to attach an ID number to a value to indicate how or why that value is originally set.

This is tremendously convenient for debugging purposes where it is necessary to figure out which code path a particular value went through.

After the script is verified, all tag assignment statements can simply be removed.

const ROUTE1 = 1;
const ROUTE2 = 2;
const ROUTE3 = 3;
const ERROR_ROUTE = 9;

fn some_complex_calculation(x) {
    let result;

    if some_complex_condition(x) {
        result = 42;
        result.tag = ROUTE1;        // record route #1
    } else if some_other_very_complex_condition(x) == 1 {
        result = 123;
        result.tag = ROUTE2;        // record route #2
    } else if some_non_understandable_calculation(x) > 0 {
        result = 0;
        result.tag = ROUTE3;        // record route #3
    } else {
        result = -1;
        result.tag = ERROR_ROUTE;   // record error
    }

    result  // this value now contains the tag
}

let my_result = some_complex_calculation(key);

// The code path that 'my_result' went through is now in its tag.

// It is now easy to trace how 'my_result' gets its final value.

print(`Result = ${my_result} and reason = ${my_result.tag}`);

Identify data source

It is convenient to use the tag value to record the source of a piece of data.

let x = [0, 1, 2, 3, 42, 99, 123];

// Store the index number of each value into its tag before
// filtering out all even numbers, leaving only odd numbers
let filtered = x.map(|v, i| { v.tag = i; v }).filter(|v| v.is_odd());

// The tag now contains the original index position

for (data, i) in filtered {
    print(`${i + 1}: Value ${data} from position #${data.tag + 1}`);
}

Identify code conditions

The tag value may also contain a [bit-field of up to 32 (16 under 32-bit targets) individual bits, recording up to 32 (or 16 under 32-bit targets) logic conditions that contributed to the value.

Again, after the script is verified, all tag assignment statements can simply be removed.


fn some_complex_calculation(x) {
    let result = 42;

    // Check first condition
    if some_complex_condition() {
        result += 1;
        result.tag[0] = true;   // Set first bit in bit-field
    }

    // Check second condition
    if some_other_very_complex_condition(x) == 1 {
        result *= 10;
        result.tag[1] = true;   // Set second bit in bit-field
    }

    // Check third condition
    if some_non_understandable_calculation(x) > 0 {
        result -= 42;
        result.tag[2] = true;   // Set third bit in bit-field
    }

    // Check result
    if result > 100 {
        result = 0;
        result.tag[3] = true;   // Set forth bit in bit-field
    }
}

let my_result = some_complex_calculation(key);

// The tag of 'my_result' now contains a bit-field indicating
// the result of each condition.

// It is now easy to trace how 'my_result' gets its final value.
// Use indexing on the tag to get at individual bits.

print(`Result = ${my_result}`);
print(`First condition = ${my_result.tag[0]}`);
print(`Second condition = ${my_result.tag[1]}`);
print(`Third condition = ${my_result.tag[2]}`);
print(`Result check = ${my_result.tag[3]}`);

Poor-man’s tuples

Rust has tuples but Rhai does not (nor does JavaScript in this sense).

Sometimes it is useful to return multiple pieces of data from a function. 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.

The tag value is an ideal container (as a bit-field) for such additional information without resorting to a full-blown object map or array (which may not even be available under no_index or no_object).


#![allow(unused)]
fn main() {
// 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...");
}
}

Serialization and Deserialization of Dynamic with serde

Rhai’s Dynamic type supports serialization and deserialization by serde via the serde feature.

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.


#![allow(unused)]
fn main() {
let value: Dynamic = ...;

// Serialize 'Dynamic' to JSON
let json = serde_json::to_string(&value);

// Deserialize 'Dynamic' from JSON
let result: Dynamic = serde_json::from_str(&json);
}

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

Dynamic as Serialization Format

A Dynamic can be seamlessly converted to and from any type that implements serde::Serialize and/or serde::Deserialize, acting as a serialization format.

Serialize Any Type to Dynamic

The function rhai::serde::to_dynamic automatically converts any Rust type that implements serde::Serialize into a Dynamic.

For primary types, this is usually not necessary because using Dynamic::from is much easier and is essentially the same thing. The only difference is treatment for integer values. Dynamic::from keeps different integer types intact, while rhai::serde::to_dynamic converts them all into INT (i.e. the system integer type which is i64 or i32 depending on the only_i32 feature).

Rust struct‘s (or any type that is marked as a serde map) are converted into object maps while Rust Vec‘s (or any type that is marked as a serde sequence) are converted into arrays.

While it is also simple to serialize a Rust type to JSON via serde, then use Engine::parse_json to convert it into an object map, rhai::serde::to_dynamic serializes it to Dynamic directly via serde without going through the JSON step.


#![allow(unused)]
fn main() {
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).


#![allow(unused)]
fn main() {
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 – i.e. 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.

Lighter Alternative

The serde crate is quite heavy.

If only simple JSON parsing (i.e. only deserialization) of a hash object into a Rhai object map is required, the Engine::parse_json method is available as a cheap alternative, but it does not provide the same level of correctness, nor are there any configurable options.

Numbers

Integers

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.

Integers can also be conveniently manipulated as bit-fields.

Floating-Point Numbers

Floating-point numbers are also supported if not disabled with no_float.

Both decimal and scientific notations can be used.

The default system floating-point type is i64 (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 (.).

SampleFormatTypeno_floatno_float + decimal
_123syntax error (improper separator)
123_345, -42decimalINTINTINT
0o07_76octalINTINTINT
0xab_cd_efhexINTINTINT
0b0101_1001binaryINTINTINT
123._456syntax error (improper separator)
123_456.78_9normal floating-pointFLOATsyntax errorDecimal
-42.ending with decimal pointFLOATsyntax errorDecimal
123_456_.789e-10scientific notationFLOATsyntax errorDecimal
.456syntax error (missing leading 0)
123.456e_10syntax error (improper separator)
123.e-10syntax error (missing 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.

Therefore, care must be taken especially with regards to integer variables pushed inside a custom Scope that they are of the intended type.

It is extremely easy to mess up numeric types since the Rust default integer type is i32 while for Rhai it is i64 (without only_i32).


#![allow(unused)]
fn main() {
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

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.

Use no_float and decimal Together

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.

Numeric Operators

Numeric operators generally follow C styles.

Unary Operators

OperatorDescription
+positive
-negative

#![allow(unused)]
fn main() {
let number = -5;

number = -5 - +5;
}

Binary Operators

OperatorDescriptionIntegerFloating-pointDecimal
+, +=plusyesyes, also with INTyes, also with INT
- -=minusyesyes, also with INTyes, also with INT
*, *=multiplyyesyes, also with INTyes, also with INT
/, /=divide (integer division if acting on integer types)yesyes, also with INTyes, also with INT
%, %=modulo (remainder)yesyes, also with INTyes, also with INT
**, **=power/exponentiationyesyes, also FLOAT**INTno
&, &=bit-wise Andyesnono
|, |=bit-wise Oryesnono
^bit-wise Xoryesnono
<<, <<=left bit-shiftyesnono
>>, >>=right bit-shiftyesnono
==equals toyesyes, also with INTyes, also with INT
!=not equals toyesyes, also with INTyes, also with INT
>greater thanyesyes, also with INTyes, also with INT
>=greater than or equals toyesyes, also with INTyes, also with INT
<less thanyesyes, also with INTyes, also with INT
<=less than or equals toyesyes, also with INTyes, also with INT

Note: when one of the operands to a binary operator is floating-point, it works with INT for the other operand and the result is floating-point.


#![allow(unused)]
fn main() {
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
}

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.


#![allow(unused)]
fn main() {
-2 + 2 == 0;

-2 - 2 == -4;

-2 * 2 == -4;

-2 / 2 == -1;

-2 % 2 == 0;

-2 ** 2 = 4;    // in some languages this means -(2 ** 2) == -4
}

Numeric Functions

Integer Functions

The following standard functions (defined in the ArithmeticPackage but excluded if using a raw Engine) operate on integers only:

FunctionDescription
is_oddreturns true if the value is an odd number, otherwise false
is_evenreturns true if the value is an even number, otherwise false

The following standard functions (defined in the BasicMathPackage but excluded if using a raw Engine) operate on integers only:

FunctionNot available underDescription
to_floatno_floatconvert the value into f64 (f32 under f32_float)
to_decimalnon-decimalconvert the value into Decimal

Signed Numeric Functions

The following standard functions (defined in the ArithmeticPackage but excluded if using a raw Engine) operate on i8, i16, i32, i64, f32, f64 and Decimal (requires decimal) only:

FunctionDescription
absabsolute value
signreturns (INT) −1 if negative, +1 if positive, 0 if zero
is_zeroreturns true if the value is zero, otherwise false

Floating-Point Functions

The following standard functions (defined in the BasicMathPackage but excluded if using a raw Engine) operate on f64 (f32 under f32_float) and Decimal (requires decimal) only:

CategorySupports DecimalFunctions
Trigonometrynosin, cos, tan, sinh, 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)
Logarithmicnolog(x) in base 10, log(x,base)
Roundingyesfloor, ceiling, round, int, fraction methods and properties
Conversionyesto_int, to_decimal (requires decimal), to_float (not under no_float)
Conversionnoto_degrees, to_radians
Testingnois_nan, is_finite, is_infinite methods and properties

Decimal Rounding Functions

The following rounding methods (defined in the BasicMathPackage but excluded if using a raw Engine) operate on Decimal only, which requires the decimal feature:

Rounding typeBehaviorMethods
Nonefloor, ceiling, int, fraction methods and properties
Banker’s roundinground to integerround method and property
Banker’s roundinground to specified number of decimal pointsround(decimal points)
Round upaway from zeroround_up(decimal points)
Round downtowards zeroround_down(decimal points)
Round half-upmid-point away from zeroround_half_up(decimal points)
Round half-downmid-point towards zeroround_half_down(decimal points)

Parsing Functions

The following standard functions (defined in the BasicMathPackage but excluded if using a raw Engine) parse numbers:

FunctionNo available underDescription
parse_intconverts a string to INT with an optional radix
parse_floatno_floatconverts a string to FLOAT
parse_decimalnon-decimalconverts a string to Decimal

Formatting Functions

The following standard functions (defined in the BasicStringPackage but excluded if using a raw Engine) convert integer numbers into a string of hex, octal or binary representations:

FunctionDescription
to_binaryconverts an integer number to binary
to_octalconverts an integer number to octal
to_hexconverts an integer number to hex

These formatting functions are defined for all available integer numbers – i.e. INT, u8, i8, u16, i16, u32, i32, u64, i64, u128 and i128 unless disabled by feature flags.

Constants

The following functions return standard mathematical constants:

FunctionDescription
PIreturns the value of π
Ereturns the value of e

Value Conversions

Convert Between Integer and Floating-Point

The to_float function converts a supported number to FLOAT (defaults to f64).

The to_int function converts a supported number to INT (i32 or i64 depending on only_i32).

The to_decimal function converts a supported number to Decimal (requires decimal).

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

The parse_float function converts a string into a FLOAT (defaults to f64).

The parse_int function converts a string into an INT (i32 or i64 depending on only_i32). An optional radix (2-36) can be provided to parse the string into a number of the specified radix.

The parse_decimal function converts a string into a Decimal (requires decimal).


#![allow(unused)]
fn main() {
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";
}

Formatting Numbers

The to_binary function converts an integer number to a string in binary (i.e. only 1 and 0).

The to_octal function converts an integer number to a string in octal (i.e. from 0 to 7).

The to_hex function converts an integer number to a string in hex.


#![allow(unused)]
fn main() {
let x = 0x1234abcd;

x == 305441741;

x.to_string() == "305441741";

x.to_binary() == "10010001101001010101111001101";

x.to_octal() == "2215125715";

x.to_hex() == "1234abcd";
}

Integer as Bit-Fields

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.

There is nothing here that cannot be done via standard bit-manipulation (i.e. shifting and masking) operations. However, this is an extremely useful, and performant, short-hand since it usually replaces a sequence of multiple steps.

Indexing an integer as a bit-field is disabled via the no_index feature.

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 ] = new value ;

The maximum bit number that can be accessed is 63 (or 31 under only_i32).

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 ] = new value ;

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 LogicPackage but excluded if using a raw Engine) operate on INT bit-fields:

FunctionParameter(s)Description
get_bitbit number, counting from MSB if < 0returns the state of a bit: true if 1, false if 0
set_bit1) bit number, counting from MSB if < 0
2) new state: true if 1, false if 0
sets the state of a bit
get_bits1) 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
set_bits1) 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
bits1) (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

Example

// Assume the following bits fields in a single 16-bit word:
//
// +---------+-------+-----------------+------------+
// |   0-2   |   3   |      4-11       |    12-15   |
// +---------+-------+-----------------+------------+
// | Command | Flag  |    0-255 data   |   0 0 0 0  |
// +---------+-------+-----------------+------------+

let value = read_from_hw_register(42);

let command = value.get_bits(0, 3);         // Command = bits 0-2
let flag = value[3];                        // Flag = bit 3
let data = value.get_bits(4, 8);            // Data = bits 4-11
let reserved = value.get_bits(-4);          // Reserved = last 4 bits

if reserved != 0 {
    throw reserved;
}

switch command {
    0 => print(`Data = ${data}`),
    1 => value.set_bits(4, 8, data / 2),
    2 => value[3] = !flag,
    _ => print(`Unknown: ${command}`)
}

Strings and Characters

String in Rhai contain any text sequence of valid Unicode characters. Internally strings are stored in UTF-8 encoding.

Strings can be built up from other strings and types via the + operator (provided by the MoreStringPackage but excluded if using a raw Engine). This is particularly useful when printing output.

type_of() a string returns "string".

The maximum allowed length of a string can be controlled via Engine::set_max_string_size (see maximum length of strings).

String and Character Literals

String and character literals follow JavaScript-style syntax:

TypeQuotesEscapes?Continuation?Interpolation?
Normal string"..."yesyes (with \)no
Multi-line literal string`...`nonoyes (${...})
Character'...'yesnono

Standard Escape Sequences

There is built-in support for Unicode (\uxxxx or \Uxxxxxxxx) and hex (\xxx) escape sequences for normal strings and characters.

Hex sequences map to ASCII characters, while \u maps to 16-bit common Unicode code points and \U maps the full, 32-bit extended Unicode code points.

Escape sequences are not supported for multi-line literal strings wrapped by back-ticks (`).

Escape sequenceMeaning
\\back-slash (\)
\ttab
\rcarriage-return (CR)
\nline-feed (LF)
\"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.


#![allow(unused)]
fn main() {
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:


#![allow(unused)]
fn main() {
let x = "hello, world!\n\
         hello world again!\n\
         this is the last time!!!";

// The above is the same as:
let x = "hello, world!\nhello world again!\nthis is the last time!!!";
}

Multi-Line Literal Strings

A string wrapped by a pair of back-tick (`) characters is interpreted literally, meaning that every single character that lies between the two back-ticks is taken verbatim. This include new-lines, whitespaces, escape characters etc.

let x = `hello, world! "\t\x42"
  hello world again! 'x'
     this is the last time!!! `;

// The above is the same as:
let x = "hello, world! \"\\t\\x42\"\n  hello world again! 'x'\n     this is the last time!!! ";

If a back-tick (`) appears at the end of a line, then it is understood that the entire text block starts from the next line; the starting new-line character is stripped.

let x = `
        hello, world! "\t\x42"
  hello world again! 'x'
     this is the last time!!!
`;

// The above is the same as:
let x = "        hello, world! \"\\t\\x42\"\n  hello world again! 'x'\n     this is the last time!!!\n";

To actually put a back-tick (`) character inside a multi-line literal string requires post-processing.

String Interpolation

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

Interpolation is not supported for normal string or character literals.

${ ... } acts as a statements block and can contain anything that is allowed within a statements block, including another interpolated string! The last result of the block is taken as the value for interpolation.

Rhai uses to_string() to convert any value into a string, then physically joins all the sub-strings together.

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

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

Actual implementation

Internally, a Rhai string is still stored compactly as a Rust UTF-8 string in order to save memory.

Therefore, getting the character at a particular index involves walking through the entire UTF-8 encoded bytes stream to extract individual Unicode characters, counting them on the way.

Because of this, indexing can be a slow procedure, especially for long strings. Along the same lines, getting the length of a string (which returns the number of characters, not bytes) can also be slow.

Examples

let name = "Bob";
let middle_initial = 'C';
let last = "Davis";

let full_name = `${name} ${middle_initial}. ${last}`;
full_name == "Bob C. Davis";

// String building with different types
let age = 42;
let record = `${full_name}: age ${age}`;
record == "Bob C. Davis: age 42";

// Unlike Rust, Rhai strings can be indexed to get a character
// (disabled with 'no_index')
let c = record[4];
c == 'C';

ts.s = record;                          // custom type properties can take strings

let c = ts.s[4];
c == 'C';

let c = ts.s[-4];                       // negative index counts from the end
c == 'e';

let c = "foo"[0];                       // indexing also works on string literals...
c == 'f';

let c = ("foo" + "bar")[5];             // ... and expressions returning strings
c == 'r';

// Escape sequences in strings
record += " \u2764\n";                  // escape sequence of '❤' in Unicode
record == "Bob C. Davis: age 42 ❤\n";   // '\n' = new-line

// Unlike Rust, Rhai strings can be directly modified character-by-character
// (disabled with 'no_index')
record[4] = '\x58'; // 0x58 = 'X'
record == "Bob X. Davis: age 42 ❤\n";

// Use 'in' to test if a substring (or character) exists in a string
"Davis" in record == true;
'X' in record == true;
'C' in record == false;

// Strings can be iterated with a 'for' statement, yielding characters
for ch in record {
    print(ch);
}

The ImmutableString Type

All strings in Rhai are implemented as ImmutableString, which is an alias to Rc<SmartString> (or Arc<SmartString> under sync).

SmartString is used because many strings in scripts are short (fewer than 24 ASCII characters).

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.


#![allow(unused)]
fn main() {
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 of Character Indexing

Although Rhai exposes a string as a simple array of char which can be directly indexed to get at a particular character, internally the string is still stored as UTF-8 (native Rust Strings).

All indexing operations 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.

Built-in String Functions

Standard Functions

The following standard methods (mostly defined in the MoreStringPackage but excluded if using a raw Engine) operate on strings (and possibly characters):

FunctionNot available underParameter(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
pad1) target length
2) character/string to pad
pads the string with a character or a string to at least a specified length
appendcharacter/string to appendadds a character or a string to the end of another string
removecharacter/string to removeremoves a character or a string from the 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
containscharacter/sub-string to search forchecks if a certain character or sub-string occurs in the string
index_of1) character/sub-string to search for
2) (optional) start index, counting from end if < 0, end if ≥ length
returns the index that a certain character or sub-string occurs in the string, or −1 if not found
sub_string1) start index, counting from end if < 0
2) (optional) number of characters to extract, none if < 0
extracts a sub-string (to the end of the string if length is not specified)
splitno_indexnonesplits the string by individual characters, returning an array of characters
splitno_indexPosition 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
splitno_index1) 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_revno_index1) 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
crop1) start index, counting from end if < 0
2) (optional) number of characters to retain, none if < 0
retains only a portion of the string
replace1) target character/sub-string
2) replacement character/string
replaces a sub-string with another
trimnonetrims the string of whitespace at the beginning and end
chars1) (optional) start index, counting from end if < 0
2) (optional) number of characters to iterate, none if < 0
allows iteration of the characters inside the string

Beware that functions that involve indexing into a string to get at individual characters, e.g. sub_string, require walking through the entire UTF-8 encoded bytes stream to extract individual Unicode characters and counting them, which can be slow for long strings.

Standard Operators

The following standard operators inter-operate between strings and/or characters.

When one (or both) of the operands is a character, it is first converted into a one-character string before running the operator.

OperatorDescription
+, +=character/string concatenation
-, -=remove character/sub-string from string
==equals to
!=not equals to
>greater than
>=greater than or equals to
<less than
<=less than or equals to

Examples


#![allow(unused)]
fn main() {
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.truncate(6);
full_name.len == 6;
full_name == "Bob C.";

full_name.replace("Bob", "John");
full_name.len == 7;
full_name == "John C.";

full_name.contains('C') == true;
full_name.contains("John") == true;

full_name.crop(5);
full_name == "C.";

full_name.crop(0, 1);
full_name == "C";

full_name.clear();
full_name.len == 0;
}

Arrays

Arrays are first-class citizens in Rhai.

Array literals are built within square brackets [ ... ] and separated by commas ,:

[ value , value , ... , value ]

[ value , value , ... , value , ] // trailing comma is OK

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.

type_of() an array returns "array".

Arrays are disabled via the no_index feature.

The maximum allowed size of an array can be controlled via Engine::set_max_array_size (see maximum size of arrays.

Element Access

From beginning

Like C, arrays are accessed with zero-based, non-negative integer indices:

array [ index from 0 to (length−1) ]

From end

A negative index accesses an element in the array counting from the end, with −1 being the last element.

array [ index from −1 to −length ]

Built-in Functions

The following methods (mostly defined in the BasicArrayPackage but excluded if using a raw Engine) operate on arrays:

FunctionParameter(s)Description
pushelement to insertinserts an element at the end
appendarray to appendconcatenates the second array to the end of the first
+= operator1) array
2) element to insert (not another array)
inserts an element at the end
+= operator1) array
2) array to append
concatenates the second array to the end of the first
+ operator1) first array
2) second array
concatenates the first array with the second
== operator1) first array
2) second array
are the two arrays the same (elements compared with the == operator, if defined)?
!= operator1) first array
2) second array
are the two arrays different (elements compared with the == operator, if defined)?
insert1) position, counting from end if < 0, end if ≥ length
2) element to insert
inserts an element at a certain index
popnoneremoves the last element and returns it (() if empty)
shiftnoneremoves the first element and returns it (() if empty)
extract1) start position, counting from end if < 0, end if ≥ length
2) (optional) number of items to extract, none if < 0
extracts a portion of the array into a new array
removeindexremoves an element at a particular index and returns it (() if the index is not valid)
reversenonereverses the array
len method and propertynonereturns the number of elements
pad1) 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
split1) 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
drain1) function pointer to predicate (usually a closure)removes all items (returning them) that return true when called with the predicate function:
1st parameter: array item
2nd parameter: (optional) offset index
drain1) start position, counting from end if < 0, end if ≥ length
2) number of items to remove, none if < 0
removes a portion of the array, returning the removed items (not in original order)
retain1) function pointer to predicate (usually a closure)removes all items (returning them) that do not return true when called with the predicate function:
1st parameter: array item
2nd parameter: (optional) offset index
retain1) start position, counting from end if < 0, end if ≥ length
2) number of items to retain, none if < 0
retains a portion of the array, removes all other items and returning them (not in original order)
splice1) start position, counting from end if < 0, end if ≥ length
2) number of items to remove, none if < 0
3) array to insert
replaces a portion of the array with another (not necessarily of the same length as the replaced portion)
filterfunction pointer to predicate (usually a closure)constructs a new array with all items that return true when called with the predicate function:
1st parameter: array item
2nd parameter: (optional) offset index
containselement to finddoes the array contain an element? The == operator (if defined) is used to compare custom types
index_of1) element to find (not a function pointers)
2) (optional) start index, counting from end if < 0, end if ≥ length
returns the index of the first item in the array that equals the supplied element (using the == operator, if defined), or −1 if not found
index_of1) function pointer to predicate (usually a closure)
2) (optional) start index, counting from end if < 0, end if ≥ length
returns the index of the first item in the array that returns true when called with the predicate function, or −1 if not found:
1st parameter: array item
2nd parameter: (optional) offset index
mapfunction pointer to conversion function (usually a closure)constructs a new array with all items mapped to the result of applying the conversion function:
1st parameter: array item
2nd parameter: (optional) offset index
reduce1) function pointer to accumulator function (usually a closure)
2) (optional) the initial value
reduces the array into a single value via the accumulator function:
1st parameter: accumulated value (() initially)
2nd parameter: array item
3rd parameter: (optional) offset index
reduce_rev1) 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:
1st parameter: accumulated value (() initially)
2nd parameter: array item
3rd parameter: (optional) offset index
somefunction pointer to predicate (usually a closure)returns true if any item returns true when called with the predicate function:
1st parameter: array item
2nd parameter: (optional) offset index
allfunction pointer to predicate (usually a closure)returns true if all items return true when called with the predicate function:
1st parameter: array item
2nd parameter: (optional) offset index
sortfunction pointer to a comparison function (usually a closure)sorts the array with a comparison function:
1st parameter: first item
2nd parameter: second item
return value: INT < 0 if first < second, > 0 if first > second, 0 if first == second

Use Custom Types With Arrays

To use a custom type with arrays, a number of array 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


#![allow(unused)]
fn main() {
let y = [2, 3];             // y == [2, 3]

let y = [2, 3,];            // y == [2, 3]

y.insert(0, 1);             // y == [1, 2, 3]

y.insert(999, 4);           // y == [1, 2, 3, 4]

y.len == 4;

y[0] == 1;
y[1] == 2;
y[2] == 3;
y[3] == 4;

(1 in y) == true;           // use 'in' to test if an item exists in the array

(42 in y) == false;         // 'in' uses the 'contains' function, which uses the
                            // '==' operator (that users can override)
                            // to check if the target item exists in the array

y.contains(1) == true;      // the above de-sugars to this

y[1] = 42;                  // y == [1, 42, 3, 4]

(42 in y) == true;

y.remove(2) == 3;           // y == [1, 42, 4]

y.len == 3;

y[2] == 4;                  // elements after the removed element are shifted

ts.list = y;                // arrays can be assigned completely (by value copy)

ts.list[1] == 42;

[1, 2, 3][0] == 1;          // indexing on array literal

[1, 2, 3][-1] == 3;         // negative index counts from the end

fn abc() {
    [42, 43, 44]            // a function returning an array
}

abc()[0] == 42;

y.push(4);                  // y == [1, 42, 4, 4]

y += 5;                     // y == [1, 42, 4, 4, 5]

y.len == 5;

y.shift() == 1;             // y == [42, 4, 4, 5]

y.chop(3);                  // y == [4, 4, 5]

y.len == 3;

y.pop() == 5;               // y == [4, 4]

y.len == 2;

for item in y {             // arrays can be iterated with a 'for' statement
    print(item);
}

y.pad(6, "hello");          // y == [4, 4, "hello", "hello", "hello", "hello"]

y.len == 6;

y.truncate(4);              // y == [4, 4, "hello", "hello"]

y.len == 4;

y.clear();                  // y == []

y.len == 0;

let a = [42, 123, 99];

a.map(|v| v + 1);           // returns [43, 124, 100]

a.map(|v, i| v + i);        // returns [42, 124, 101]

a.filter(|v| v > 50);       // returns [123, 99]

a.filter(|v, i| i == 1);    // returns [123]

// Provide an initial value directly
a.reduce(|sum, v| sum + v, 0) == 264;

// Detect the initial value of '()'
a.reduce(
    |sum, v| if sum.type_of() == "()" { v } else { sum + v }
) == 264;

// Detect the initial value via index
a.reduce(|sum, v, i|
    if i == 0 { v } else { sum + v }
) == 264;

// Provide an initial value directly
a.reduce_rev(|sum, v| sum + v, 0) == 264;

// Detect the initial value of '()'
a.reduce_rev(
    |sum, v| if sum.type_of() == "()" { v } else { sum + v }
) == 264;

// Detect the initial value via index
a.reduce_rev(|sum, v, i|
    if i == 2 { v } else { sum + v }
) == 264;

a.some(|v| v > 50);         // returns true

a.some(|v, i| v < i);       // returns false

a.none(|v| v != 0);         // returns false

a.none(|v, i| v == i);      // returns true

a.all(|v| v > 50);          // returns false

a.all(|v, i| v > i);        // returns true

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| x - y);       // 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 == []
}

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

type_of() an object map returns "map".

Object maps are disabled via the no_object feature.

The maximum allowed size of an object map can be controlled via Engine::set_max_map_size (see maximum size of object maps).

Object Map Literals

Object map literals are built within braces #{ ... } (name : value syntax similar to Rust) and separated by commas ,:

#{ property : value , ... , property : value }

#{ property : value , ... , property : value , } // trailing comma is OK

The property name can be a simple variable name following the same naming rules as variables, or a string literal without interpolation.

Access Properties

Dot Notation

The dot notation allows only property names that follow the same naming rules as variables.

object . property

Index Notation

The index notation allows setting/getting properties of arbitrary names (even the empty string).

object [ property ]

Non-Existence

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

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

Built-in Functions

The following methods (defined in the BasicMapPackage but excluded if using a raw Engine) operate on object maps:

FunctionParameter(s)Description
contains operatorproperty namedoes the object map contain a property of a particular name?
lennonereturns the number of properties
clearnoneempties the object map
removeproperty nameremoves a certain property and returns it (() if the property does not exist)
+= operator, mixinsecond object mapmixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values)
+ operator1) first object map
2) second object map
merges the first object map with the second
== operator1) first object map
2) second object map
are the two object map the same (elements compared with the == operator, if defined)?
!= operator1) first object map
2) second object map
are the two object map 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
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

Examples


#![allow(unused)]
fn main() {
let y = #{              // object map literal with 3 properties
    a: 1,
    bar: "hello",
    "baz!$@": 123.456,  // like JavaScript, you can use any string as property names...
    "": false,          // even the empty string!

    `hello`: 999,       // literal strings are also OK

    a: 42,              // <- syntax error: duplicated property name

    `a${2}`: 42,        // <- syntax error: property name cannot have string interpolation
};

y.a = 42;               // access via dot notation
y.a == 42;

y.baz!$@ = 42;          // <- syntax error: only proper variable names allowed in dot notation
y."baz!$@" = 42;        // <- syntax error: strings not allowed in dot notation
y["baz!$@"] = 42;       // access via index notation is OK

"baz!$@" in y == true;  // use 'in' to test if a property exists in the object map
("z" in y) == false;

ts.obj = y;             // object maps can be assigned completely (by value copy)
let foo = ts.list.a;
foo == 42;

let foo = #{ a:1, };    // trailing comma is OK

let foo = #{ a:1, b:2, c:3 }["a"];
let foo = #{ a:1, b:2, c:3 }.a;
foo == 1;

fn abc() {
    { a:1, b:2, c:3 }  // a function returning an object map
}

let foo = abc().b;
foo == 2;

let foo = y["a"];
foo == 42;

y.contains("a") == true;
y.contains("xyz") == false;

y.xyz == ();            // a non-existing property returns '()'
y["xyz"] == ();

y.len == ();            // an object map has no property getter function
y.len() == 3;           // method calls are OK

y.remove("a") == 1;     // remove property

y.len() == 2;
y.contains("a") == false;

for name in y.keys() {  // get an array of all the property names via 'keys'
    print(name);
}

for val in y.values() { // get an array of all the property values via 'values'
    print(val);
}

y.clear();              // empty the object map

y.len() == 0;
}

No Support for Property Getters

In order not to affect the speed of accessing properties in an object map, new property getters cannot be registered because they conflict with the syntax of property access.

A property getter function registered via Engine::register_get, for example, for a Map will never be found – instead, the property will be looked up in the object map.

Properties should be registered as methods instead:


#![allow(unused)]
fn main() {
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

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

A valid JSON string does not start with a hash character # while a Rhai object map does – that’s the major difference!

Use the Engine::parse_json method to parse a piece of JSON into an object map. The JSON text must represent a single object hash (i.e. must be wrapped within “{ .. }“) otherwise it returns a syntax error.


#![allow(unused)]
fn main() {
// 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::<INT>(r#"map["^^^!!!"].len()"#)?;

result == 3;          // the object map is successfully used in the script
}

Representation of Numbers

JSON numbers are all floating-point while Rhai supports integers (INT) and floating-point (FLOAT) if the no_float feature is not used.

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.

Parse JSON with Sub-Objects

Engine::parse_json depends on the fact that the object map literal syntax in Rhai is almost the same as a JSON object. However, it is almost because the syntax for a sub-object in JSON (i.e. “{ ... }“) is different from a Rhai object map literal (i.e. “#{ ... }“).

When Engine::parse_json encounters JSON with sub-objects, it fails with a syntax error.

If it is certain that no text string in the JSON will ever contain the character {, then it is possible to parse it by first replacing all occupance of { with #{.

A JSON object hash starting with #{ is handled transparently by Engine::parse_json.


#![allow(unused)]
fn main() {
// JSON with sub-object 'b'.
let json = r#"{"a":1, "b":{"x":true, "y":false}}"#;

// Our JSON text does not contain the '{' character, so off we go!
let new_json = json.replace("{", "#{");

// The leading '{' will also be replaced to '#{', but 'parse_json' handles this just fine.
let map = engine.parse_json(&new_json, false)?;

map.len() == 2;       // 'map' contains two properties: 'a' and 'b'
}

Use serde to Serialize/Deserialize to/from JSON

Remember, Engine::parse_json is nothing more than a cheap alternative to true JSON parsing.

If correctness is needed, or for more configuration possibilities, turn on the serde feature to pull in the serde crate which enables serialization and deserialization to/from multiple formats, including JSON.

Beware, though... the serde crate is quite heavy.

See Serialization/Deserialization of Dynamic with serde for more details.

Special Support for OOP via Object Maps

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:


#![allow(unused)]
fn main() {
let obj = #{
                data: 40,
                action: || this.data += x    // 'action' holds a closure
           };

obj.action(2);                               // calls the function pointer with `this` bound to 'obj'

obj.call(obj.action, 2);                     // <- the above de-sugars to this

obj.data == 42;

// To achieve the above with normal function pointer call will fail.

fn do_action(map, x) { map.data += x; }      // 'map' is a copy

obj.action = Fn("do_action");

obj.action.call(obj, 2);                     // a copy of 'obj' is passed by value

obj.data == 42;                              // 'obj.data' is not changed
}

timestamp

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

Timestamps are not available under no_std.

The Rust type of a timestamp is std::time::Instant (instant::Instant in WASM builds).

type_of() a timestamp returns "timestamp".

Built-in Functions

The following methods (defined in the BasicTimePackage but excluded if using a raw Engine) operate on timestamps:

FunctionParameter(s)Description
elapsed method and propertynonereturns the number of seconds since the timestamp
- operator1) later timestamp
2) earlier timestamp
returns the number of seconds between the two timestamps
+ operatornumber of seconds to addreturns a new timestamp
- operatornumber of seconds to subtractreturns a new timestamp

Examples


#![allow(unused)]
fn main() {
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, const, globalvar, staticvariables
is_sharedshared valuesno_closure
if, elsegoto, exitcontrol flow
switchmatch, caseswitching and matching
do, while, loop, until, for, in, continue, breaklooping
fn, privatepublic, protected, newfunctionsno_function
returnreturn values
throw, try, catchthrow/catch exceptions
import, export, asuse, with, module, package, supermodulesno_module
Fn, call, curryfunction pointers
spawn, thread, go, sync, async, await, yieldthreading/async
type_of, print, debug, evalspecial functions
default, void, null, nilspecial values

Keywords cannot become the name of a function or variable, even when they are disabled.

Statements

Terminated by ;

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 and loop statements.


#![allow(unused)]
fn main() {
let a = 42;             // normal assignment statement
let a = foo(42);        // normal function call statement
foo < 42;               // normal expression as statement

let a = { 40 + 2 };     // 'a' is set to the value of the statement block, which is the value of the last statement
//              ^ the last statement does not require a terminating semicolon (but also works with it)
//                ^ semicolon required here to terminate the 'let' statement
//                  it is a syntax error without it, even though it ends with '}'
//                  that is because the 'let' statement doesn't end in a block

if foo { a = 42 }
//               ^ no need to terminate an if-statement with a semicolon
//                 that is because the 'if' statement ends in a block

4 * 10 + 2              // a statement which is just one expression - no ending semicolon is OK
                        // because it is the last statement of the whole block
}

Statement Expression

A statement can be used anywhere where an expression is expected. These are called, for lack of a more creative name, “statement expressions.”

The last statement of a statement block is always the block’s return value when used as a statement, regardless of whether it is terminated by a semicolon or not. This is different from Rust where, if the last statement is terminated by a semicolon, the block’s return value is taken to be ().

If the last statement has no return value (e.g. variable definitions, assignments) then it is assumed to be ().

Variables

Valid Names

Variables in Rhai follow normal C naming rules – must contain only ASCII letters, digits and underscores _, and cannot start with a digit.

For example: _c3po and r2d2 are valid variable names, but 3abc is not.

However, unlike Rust, a variable name must also contain at least one ASCII letter, and an ASCII letter must come before any digit. In other words, the first character that is not an underscore _ must be an ASCII letter and not a digit.

Therefore, some names acceptable to Rust, like _, _42foo, _1 etc., are not valid in Rhai. This restriction is to reduce confusion because, for instance, _1 can easily be misread (or mis-typed) as -1.

Variable names are case sensitive.

Variable names also cannot be the same as a keyword.

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.

Declare a Variable

Variables are declared using the let keyword.

Variables do not have to be given an initial value. If none is provided, it defaults to ().

A variable defined within a statement block is local to that block.

Use is_def_var to detect if a variable is defined.


#![allow(unused)]
fn main() {
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
x == 42;
X == 123;

{
    let x = 999;    // local variable 'x' shadows the 'x' in parent block
    x == 999;       // access to local 'x'
}
x == 42;            // the parent block's 'x' is not changed

is_def_var("x") == true;

is_def_var("_x") == true;

is_def_var("y") == false;
}

Constants

Constants can be defined using the const keyword and are immutable.

Constants follow the same naming rules as variables, but as a convention are often named with all-capital letters.


#![allow(unused)]
fn main() {
const X = 42;

print(X * 2);       // prints 84

X = 123;            // <- syntax error: constant modified
}

#![allow(unused)]
fn main() {
const X;            // 'X' is a constant '()'

const X = 40 + 2;   // 'X' is a constant 42
}

Manually Add Constant into Custom Scope

It is possible to add a constant into a custom Scope so it’ll be available to scripts running with that Scope.

It is very useful to have a constant value hold a custom type, which essentially acts as a singleton.


#![allow(unused)]
fn main() {
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.consume_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.consume_with_scope(&mut scope, script)?;
}

Caveat – Constants Can be Modified via Rust

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.


#![allow(unused)]
fn main() {
// 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
}

Pure plugin functions

When functions are registered as part of a plugin, &mut parameters are protected from being passed constant values.

By default, plugin functions that take a first &mut parameter disallow passing constants as that parameter.

However, if a plugin function is marked with the #[export_fn(pure)] or #[rhai_fn(pure)] attribute, it is considered pure (i.e. assumed to not modify its arguments) and so constants are allowed.

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.


#![allow(unused)]
fn main() {
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'
}
}

Overriding global

It is possible to override the automatic global module by importing another module under the name global.


#![allow(unused)]
fn main() {
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'
}
}

Logic Operators

Comparison Operators

OperatorDescription
(x operator y)
x, y same type, or 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 between most values of the same type are built in for all standard types.

Floating-point numbers can inter-operate with integers

Comparing a floating-point number (FLOAT or Decimal) with an integer is also supported.


#![allow(unused)]
fn main() {
42 == 42.0;         // true

42.0 == 42;         // true

42.0 > 42;          // false

42 >= 42.0;         // true

42.0 < 42;          // false
}

Strings can inter-operate with characters

Comparing a string with a character is also supported, with the character first turned into a string before performing the comparison.


#![allow(unused)]
fn main() {
'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.


#![allow(unused)]
fn main() {
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 and for subtle errors to creep in.


#![allow(unused)]
fn main() {
// 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

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.


#![allow(unused)]
fn main() {
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
}

It is strongly recommended that, when defining operators for custom types, always define the full set of six operators (or at least the == and != pair) together.

Boolean operators

OperatorDescriptionArityShort-Circuits?
! (prefix)boolean NOTunaryno
&&boolean ANDbinaryyes
&boolean ANDbinaryno
||boolean ORbinaryyes
|boolean 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.


#![allow(unused)]
fn main() {
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
}

All boolean operators are built in for the bool data type.

Compound Assignment Operators


#![allow(unused)]
fn main() {
let number = 9;

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.

For example, it is used to build strings:


#![allow(unused)]
fn main() {
let my_str = "abc";
my_str += "ABC";
my_str += 12345;

my_str == "abcABC12345"
}

to concatenate arrays:


#![allow(unused)]
fn main() {
let my_array = [1, 2, 3];
my_array += [4, 5];

my_array == [1, 2, 3, 4, 5];
}

and mix two object maps together:


#![allow(unused)]
fn main() {
let my_obj = #{a:1, b:2};
my_obj += #{c:3, d:4, e:5};

my_obj.len() == 5;
}

if Statement

if statements follow C syntax:


#![allow(unused)]
fn main() {
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.


#![allow(unused)]
fn main() {
// Rhai is not C!
if (decision) print("I've decided!");
//            ^ syntax error, expecting '{' in statement block
}

if-Expressions

Like Rust, if statements can also be used as expressions, replacing the ? : conditional operators in other C-like languages.


#![allow(unused)]
fn main() {
// 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 == ();
}

in Operator

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

Standard data types with built-in support for the in operator are:


#![allow(unused)]
fn main() {
42 in [1, "abc", 42, ()] == true;   // check array for item

"foo" in #{                         // check object map for property name
    foo: 42,
    bar: true,
    baz: "hello"
} == true;

'w' in "hello, world!" == true;     // check string for character

"wor" in "hello, world" == true;    // check string for sub-string
}

Array Items Comparison

The default implementation of the in operator for arrays uses the == operator (if defined) to compare items.

Beware that, 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.


#![allow(unused)]
fn main() {
let ts = new_ts();                  // assume 'new_ts' returns a custom type

let a = [1, 2, 3, ts, 42, 999];     // array contains custom type

42 in a == true;                    // 42 cannot be compared with 'ts'
                                    // so it defaults to 'false'
                                    // because == operator is not defined
}

Custom Implementation of contains

The in operator maps directly to a call to a function contains with the two operands switched.

For example:


#![allow(unused)]
fn main() {
item in container
}

maps to the following function call:


#![allow(unused)]
fn main() {
contains(container, item)
}

Support for the in operator can be easily extended to other types by registering a custom binary function named contains with the correct parameter types.

For example:


#![allow(unused)]
fn main() {
engine.register_type::<TestStruct>()
      .register_fn("new_ts", || TestStruct::new())
      .register_fn("contains", |container: &mut TestStruct, item: i64| -> bool {
          // Remember the parameters are switched from the 'in' expression
          container.contains(item)
      });
}

Now the in operator can be used for TestStruct:


#![allow(unused)]
fn main() {
let ts = new_ts();

if 42 in ts {                       // this calls the 'contains' function
    print("I got 42!");
}

let err = "hello" in ts;            // <- runtime error: 'contains' not found
}

switch Expression

The switch expression allows matching on literal values, and it mostly follows Rust’s match syntax.

switch calc_secret_value(x) {
    1 => print("It's one!"),
    2 => {
        print("It's two!");
        print("Again!");
    }
    3 => print("Go!"),
    // _ is the default when no cases match. It must be the last case.
    _ => print(`Oops! Something's wrong: ${x}`)
}

The default case (i.e. when no other cases match), however, must be the last case in the statement.

switch wrong_default {
    1 => 2,
    _ => 9,     // <- syntax error: default case not the last
    2 => 3,
    3 => 4,     // <- ending with extra comma is OK
}

Expression, Not Statement

switch is not a statement, but an expression. This means that a switch expression can appear anywhere a regular expression can, e.g. as function call arguments.

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

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" } => { ... }
    _ => { ... }
}

Switching on arrays is very useful when working with Rust enums (see this chapter 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.

let result = switch calc_secret_value(x) {
    1 if some_external_condition(x, y, z) => 100,

    2 if x < foo => 200,
    2 if bar() => 999,      // <- syntax error: still cannot have duplicated cases

    3 => if CONDITION {     // <- put condition inside statement block for
        123                 //    duplicated cases
    } else {
        0
    }

    _ if CONDITION => 8888  // <- syntax error: default case cannot have condition
};

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.

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.


#![allow(unused)]
fn main() {
let x = 10;

while x > 0 {
    x -= 1;
    if x < 6 { continue; }  // skip to the next iteration
    print(x);
    if x == 5 { break; }    // break out of while loop
}
}

do Loop

do loops have two opposite variants: do ... while and do ... until.

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.


#![allow(unused)]
fn main() {
let x = 10;

do {
    x -= 1;
    if x < 6 { continue; }  // skip to the next iteration
    print(x);
    if x == 5 { break; }    // break out of do loop
} while x > 0;


do {
    x -= 1;
    if x < 6 { continue; }  // skip to the next iteration
    print(x);
    if x == 5 { break; }    // break out of do loop
} until x == 0;
}

Infinite loop

Infinite loops follow Rust syntax.

Like Rust, continue can be used to skip to the next iteration, by-passing all following statements; break can be used to break out of the loop unconditionally.


#![allow(unused)]
fn main() {
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
}
}

Beware: a loop statement without a break statement inside its loop block is infinite - there is no way for the loop to stop iterating.

for Loop

Iterating through a range or an array, or any type with a registered type iterator, is provided by the for ... in loop.

There are two alternative syntaxes, one including a counter variable:

for variable-name in expression { ... }

for ( variable-name , counter-variable-name ) in expression { ... }

Counter Variable

The counter variable, if specified, starts from zero, incrementing upwards.

let a = [42, 123, 999, 0, true, "hello", "world!", 987.6543];

// Loop through the array
for (item, count) in a {
    if x.type_of() == "string" {
        continue;                   // skip to the next iteration
    }

    // 'item' contains a copy of each element during each iteration
    // 'count' increments (starting from zero) for each iteration
    print(`Item #${count + 1} = ${item}`);

    if x == 42 { break; }           // break out of for loop
}

Break or Continue

Like C, continue can be used to skip to the next iteration, by-passing all following statements; break can be used to break out of the loop unconditionally.

Iterate Through Arrays

Iterating through an array yields cloned copies of each element.


#![allow(unused)]
fn main() {
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

The chars method allows iterating through a string, yielding characters.

chars optionally accepts the character to start from (counting from the end if negative), as well as the number of characters to iterate (defaults all).


#![allow(unused)]
fn main() {
let s = "hello, world!";

// Iterate through all the characters.
for ch in s.chars() {
    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 Through Numeric Ranges

The range function allows iterating through a range of numbers (not including the last number).


#![allow(unused)]
fn main() {
// Iterate starting from 0 and stopping at 49
// The step is assumed to be 1 when omitted for integers
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).

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


#![allow(unused)]
fn main() {
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);
}
}

Iterators for Custom Types

If a custom type is iterable, the for loop can be used to iterate through its items in sequence.

In order to use a for statement, a type iterator must be registered for the custom type in question.

Engine::register_iterator<T> allows registration of a type iterator for any type that implements IntoIterator:


#![allow(unused)]
fn main() {
// 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()
    }
}

engine.register_type_with_name::<TestStruct>("TestStruct")
      .register_fn("new_ts", || TestStruct { fields: vec![1, 2, 3, 42] })
      .register_iterator::<TestStruct>();         // register type iterator
}

With a type iterator registered, the custom type can be iterated through:


#![allow(unused)]
fn main() {
let ts = new_ts();

// Use 'for' statement to loop through items in 'ts'
for value in ts {
    ...
}
}

Return Values

The return statement is used to immediately stop evaluation and exist the current context (typically a function call) yielding a return value.


#![allow(unused)]
fn main() {
return;             // equivalent to return ();

return 123 + 456;   // returns 579
}

A return statement at global level stop the entire script evaluation, the return value is taken as the result of the script evaluation.

Throw Exception on Error

All of Engine‘s evaluation/consuming methods return Result<T, Box<rhai::EvalAltResult>> with EvalAltResult holding error information.

To deliberately return an error during an evaluation, use the throw keyword.


#![allow(unused)]
fn main() {
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 Err(Box<EvalAltResult::ErrorRuntime(value, position)>) with the exception value captured by value.


#![allow(unused)]
fn main() {
let result = engine.eval::<i64>(
r#"
    let x = 42;

    if x > 0 {
        throw x;
    }
"#);

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 try ... catch statement common to many C-like languages.


#![allow(unused)]
fn main() {
try
{
    throw 42;
}
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 Err(Box<EvalAltResult::ErrorRuntime>) containing the exception value that has been thrown.

It is possible, via the try ... catch statement, to catch exceptions, optionally with an error variable.


#![allow(unused)]
fn main() {
// 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
}
}

Re-Throw Exception

Like the try ... catch syntax in most languages, it is possible to re-throw an exception within the catch block simply by another throw statement without a value.


#![allow(unused)]
fn main() {
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 try ... catch:

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 [this]object map
Data type mismatchobject map
Assignment to a calculated/constant valueobject map
Array/string indexing out-of-boundsobject map
Indexing with an inappropriate data typeobject map
Error in a dot expressionobject map
for statement without a type iteratorobject map
Error in an in expressionobject 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:

  • Syntax error during parsing
  • System error – e.g. script file not found
  • Script evaluation metrics over safety limits
  • Function calls nesting exceeding maximum call stack depth
  • Script evaluation manually terminated

Functions

Rhai supports defining functions in script (unless disabled with no_function):


#![allow(unused)]
fn main() {
fn add(x, y) {
    return x + y;
}

fn sub(x, y,) {     // trailing comma in parameters list is OK
    return x - y;
}

add(2, 3) == 5;

sub(2, 3,) == -1;   // trailing comma in arguments list is OK
}

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.


#![allow(unused)]
fn main() {
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.


#![allow(unused)]
fn main() {
// 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: functions cannot be defined 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.


#![allow(unused)]
fn main() {
let x = 42;

fn foo() { x }          // <- syntax error: variable 'x' doesn't exist
}

But Can Call Other Functions and Access Modules

All functions in the same AST can call each other.


#![allow(unused)]
fn main() {
fn foo(x) { x + 1 }     // function defined in the global namespace

fn bar(x) { foo(x) }    // ok! function 'foo' can be called
}

In addition, modules imported at global level can be accessed.


#![allow(unused)]
fn main() {
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.


#![allow(unused)]
fn main() {
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.


#![allow(unused)]
fn main() {
let x = foo(41);            // <- I can do this!

fn foo(x) { x + 1 }         // <- define 'foo' after use
}

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.


#![allow(unused)]
fn main() {
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!
}

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.


#![allow(unused)]
fn main() {
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;
}

this – Simulating an Object Method

Arguments passed to script-defined functions are always by value because functions are pure.

However, script-defined functions can also be called in method-call style:

object . function ( parameters .. )

When a function is called this way, the keyword this binds to the object in the method call and can be changed.

The only way for a script-defined function to change an external value is via this.


#![allow(unused)]
fn main() {
fn change() {       // not that the object 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
}

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)     { print(`HA! NEW ONE! ${x}`); }   // overwrites previous definition

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:

NamespaceHow ManySourceLookupSub-modules?Variables?
GlobalOne1) AST being evaluated
2) Engine::register_XXX API
3) global modules registered via Engine::register_global_module
4) functions in static modules registered via Engine::register_static_module and marked global
simple nameignoredignored
ModuleMany1) 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.


#![allow(unused)]
fn main() {
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 Engine::register_global_module.

  • Functions defined in modules registered via Engine::register_static_module 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 and there is no way to lock down the function being called. This aspect is very similar to JavaScript before ES6 modules.


#![allow(unused)]
fn main() {
// 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.consume_ast(&ast1)?;     // prints 'Boo!'
}

Therefore, care must be taken when cross-calling functions to make sure that the correct functions are 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:


#![allow(unused)]
fn main() {
+--------------+
| message.rhai |
+--------------+

fn get_message() { "Hello!" }


+-------------+
| script.rhai |
+-------------+

import "message" as msg;

fn say_hello() {
    print(msg::get_message());
}
say_hello();
}

Function Pointers

It is possible to store a function pointer in a variable just like a normal value. In fact, internally a function pointer simply stores the name of the function as a string.

A function pointer is created via the Fn function, which takes a string parameter.

Call a function pointer using the call method.

Built-in Functions

The following standard methods (mostly defined in the BasicFnPackage but excluded if using a raw Engine) operate on function pointers:

FunctionParameter(s)Description
name method and propertynonereturns the name of the function encapsulated by the function pointer
is_anonymous method and propertynonedoes the function pointer refer to an anonymous function? Not available under no_function.
callargumentscalls the function matching the function pointer’s name with the arguments

Examples


#![allow(unused)]
fn main() {
fn foo(x) { 41 + x }

let func = Fn("foo");       // use the 'Fn' function to create a function pointer

print(func);                // prints 'Fn(foo)'

let func = fn_name.Fn();    // <- error: 'Fn' cannot be called in method-call style

func.type_of() == "Fn";     // type_of() as function pointer is 'Fn'

func.name == "foo";

func.call(1) == 42;         // call a function pointer with the 'call' method

foo(1) == 42;               // <- the above de-sugars to this

call(func, 1);              // normal function call style also works for 'call'

let len = Fn("len");        // 'Fn' also works with registered native Rust functions

len.call("hello") == 5;

let fn_name = "hello";      // the function name does not have to exist yet

let hello = Fn(fn_name + "_world");

hello.call(0);              // error: function not found - 'hello_world (i64)'
}

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. See Function Namespaces for more details.


#![allow(unused)]
fn main() {
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.


#![allow(unused)]
fn main() {
let x = some_calculation();

// These are the functions to call depending on the value of 'x'
fn method1(x) { ... }
fn method2(x) { ... }
fn method3(x) { ... }

// Traditional - using decision variable
let func = sign(x);

// Dispatch with if-statement
if func == -1 {
    method1(42);
} else if func == 0 {
    method2(42);
} else if func == 1 {
    method3(42);
}

// Using pure function pointer
let func = if x < 0 {
    Fn("method1")
} else if x == 0 {
    Fn("method2")
} else if x > 0 {
    Fn("method3")
}

// Dynamic dispatch
func.call(42);

// Using functions map
let map = [ Fn("method1"), Fn("method2"), Fn("method3") ];

let func = sign(x) + 1;

// Dynamic dispatch
map[func].call(42);
}

Bind the this Pointer

When call is called as a method but not on a function pointer, it is possible to dynamically dispatch to a function call while binding the object in the method call to the this pointer of the function.

To achieve this, pass the function pointer as the first argument to call:


#![allow(unused)]
fn main() {
fn add(x) {                 // define function which uses 'this'
    this += x;
}

let func = Fn("add");       // function pointer to 'add'

func.call(1);               // error: 'this' pointer is not bound

let x = 41;

func.call(x, 1);            // error: function 'add (i64, i64)' not found

call(func, x, 1);           // error: function 'add (i64, i64)' not found

x.call(func, 1);            // 'this' is bound to 'x', dispatched to 'func'

x == 42;
}

Beware that this only works for method-call style. Normal function-call style cannot bind the this pointer (for syntactic reasons).

Therefore, obviously, binding the this pointer is unsupported under no_object.

Call a Function Pointer in Rust

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 AST is required to call a function pointer, as well as the entire execution context that the script is running in.

For a rust function taking a function pointer as parameter, the Low-Level API must be used to register the function.

Essentially, use the low-level Engine::register_raw_fn method to register the function. FnPtr::call_dynamic is used to actually call the function pointer, passing to it the current native call context, the this pointer, and other necessary arguments.


#![allow(unused)]
fn main() {
use rhai::{Engine, Module, Dynamic, FnPtr, NativeCallContext};

let mut engine = Engine::new();

// Define Rust function in required low-level API signature
fn call_fn_ptr_with_value(context: NativeCallContext, args: &mut [&mut Dynamic])
    -> Result<Dynamic, Box<EvalAltResult>>
{
    // 'args' is guaranteed to contain enough arguments of the correct types
    let fp = std::mem::take(args[1]).cast::<FnPtr>();   // 2nd argument - function pointer
    let value = std::mem::take(args[2]);                // 3rd argument - function argument
    let this_ptr = args.get_mut(0).unwrap();            // 1st argument - 'this' pointer

    // Use 'FnPtr::call_dynamic' to call the function pointer.
    fp.call_dynamic(&context, Some(this_ptr), [value])
}

// Register a Rust function using the low-level API
engine.register_raw_fn("super_call",
    &[ // parameter types
        std::any::TypeId::of::<i64>(),
        std::any::TypeId::of::<FnPtr>(),
        std::any::TypeId::of::<i64>()
    ],
    call_fn_ptr_with_value
);
}

NativeCallContext

FnPtr::call_dynamic takes a parameter of type NativeCallContext which holds the native call context of the particular call to a registered Rust function. It is a type that exposes the following:

MethodTypeDescription
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
iter_imports()impl Iterator<Item = (&str, &Module)>iterator of the current stack of modules imported via import statements
imports()&Importsreference to the current 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
namespaces()&[&Module]reference to the namespaces (as modules) containing all script-defined functions; requires the internals feature
call_fn_dynamic_raw()Result<Dynamic, Box<EvalAltResult>>call a native Rust function with the supplied arguments; this is an advanced method

This type is normally provided by the Engine (e.g. when using Engine::register_fn_raw). However, it may also be manually constructed from a tuple:


#![allow(unused)]
fn main() {
use rhai::{Engine, FnPtr, NativeCallContext};

let engine = Engine::new();

// Compile script to AST
let mut 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)?;

// Get rid of the script, retaining only functions
ast.retain_functions(|_, _, _| true);

// Create function namespace from the 'AST'
let lib = [ast.as_ref()];

// Create native call context
let fn_name = fn_ptr.fn_name().to_string();
let context = NativeCallContext::new(&engine, &fn_name, &lib);

// 'f' captures: the engine, the AST, and the closure
let f = move |x: i64| fn_ptr.call_dynamic(&context, None, [x.into()]);

// 'f' can be called like a normal function
let result = f(42)?;
}

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.


#![allow(unused)]
fn main() {
fn mul(x, y) {                  // function with two parameters
    x * y
}

let func = Fn("mul");

func.call(21, 2) == 42;         // two arguments are required for 'mul'

let curried = func.curry(21);   // currying produces a new function pointer which
                                // carries 21 as the first argument

let curried = curry(func, 21);  // function-call style also works

curried.call(2) == 42;          // <- de-sugars to 'func.call(21, 2)'
                                //    only one argument is now required
}

Automatic Currying

Anonymous functions defined via a closure syntax capture external variables that are not shadowed inside the function’s scope.

This is accomplished via automatic currying.

Anonymous Functions

Sometimes it gets tedious to define separate functions only to dispatch them via single function pointers. This scenario is especially common when simulating object-oriented programming (OOP).


#![allow(unused)]
fn main() {
// Define object
let obj = #{
    data: 42,
    increment: Fn("inc_obj"),           // use function pointers to
    decrement: Fn("dec_obj"),           // refer to method functions
    print: Fn("print_obj")
};

// Define method functions one-by-one
fn inc_obj(x) { this.data += x; }
fn dec_obj(x) { this.data -= x; }
fn print_obj() { print(this.data); }
}

The above can be replaced by using anonymous functions which have the same syntax as Rust’s closures (but they are NOT real closures, merely syntactic sugar):


#![allow(unused)]
fn main() {
let obj = #{
    data: 42,
    increment: |x| this.data += x,      // one-liner
    decrement: |x| this.data -= x,
    print_obj: || {
        print(this.data);               // full function body
    }
};
}

The anonymous functions will be hoisted into separate functions in the global namespace. The above is equivalent to:


#![allow(unused)]
fn main() {
let obj = #{
    data: 42,
    increment: Fn("anon_fn_1000"),
    decrement: Fn("anon_fn_1001"),
    print: Fn("anon_fn_1002")
};

fn anon_fn_1000(x) {
    this.data += x;                     // when called, 'this' maps to the object map
}
fn anon_fn_1001(x) {
    this.data -= x;                     // when called, 'this' maps to the object map
}
fn anon_fn_1002() {
    print(this.data);                   // when called, 'this' maps to the object map
}
}

WARNING – NOT Real Closures

Remember: anonymous functions, though having the same syntax as Rust closures, are themselves not real closures.

In particular, they capture their execution environment via automatic currying (disabled via no_closure).

Simulating Closures

Capture External Variables via Automatic Currying

Since anonymous functions de-sugar to standard function definitions, they retain all the behaviors of Rhai functions, including being pure, having no access to external variables.

The anonymous function syntax, however, automatically captures variables that are not defined within the current scope, but are defined in the external scope – i.e. the scope where the anonymous function is created.

Variables that are accessible during the time the anonymous function is created can be captured, as long as they are not shadowed by local variables defined within the function’s scope.

The captured variables are automatically converted into reference-counted shared values (Rc<RefCell<Dynamic>> in normal builds, Arc<RwLock<Dynamic>> in sync builds).

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.

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

Automatic currying can be turned off via the no_closure feature.

Examples


#![allow(unused)]
fn main() {
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 this:
fn anon$1001(x, y) { x + y }        // parameter 'x' is inserted

$make_shared(x);                    // convert variable 'x' into a shared value

let f = Fn("anon$1001").curry(x);   // shared 'x' is curried

f.call(2) == 42;
}

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


#![allow(unused)]
fn main() {
let funcs = [];

for i in range(0, 10) {
    funcs.push(|| print(i));        // the for loop variable 'i' is captured
}

funcs.len() == 10;                  // 10 closures stored in the array

funcs[0].type_of() == "Fn";         // make sure these are closures

for f in funcs {
    f.call();                       // all references to 'i' are the same variable!
}
}

Therefore – Be Careful to Prevent Data Races

Rust does not have data races, but that doesn’t mean Rhai doesn’t.

Avoid performing a method call on a captured shared variable (which essentially takes a mutable reference to the shared object) while using that same variable as a parameter in the method call – this is a sure-fire way to generate a data race error.

If a shared value is used as the this pointer in a method call to a closure function, then the same shared value must not be captured inside that function, or a data race will occur and the script will terminate with an error.


#![allow(unused)]
fn main() {
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.


#![allow(unused)]
fn main() {
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

Q: How is it actually implemented?

The actual implementation of closures de-sugars to:

  1. Keeping track of what variables are accessed inside the anonymous function,

  2. If a variable is not defined within the anonymous function’s scope, it is looked up outside the function and in the current execution scope – where the anonymous function is created.

  3. The variable is added to the parameters list of the anonymous function, at the front.

  4. The variable is then converted into a reference-counted shared value.

    An anonymous function which captures an external variable is the only way to create a reference-counted shared value in Rhai.

  5. The shared value is then curried into the function pointer itself, essentially carrying a reference to that shared value and inserting it into future calls of the function.

    This process is called Automatic Currying, and is the mechanism through which Rhai simulates normal closures.

Q: Why are closures implemented as automatic currying?

In concept, a closure closes over captured variables from the outer scope – that’s why they are called closures. When this happen, a typical language implementation hoists those variables that are captured away from the stack frame and into heap-allocated storage. This is because those variables may be needed after the stack frame goes away.

These heap-allocated captured variables only go away when all the closures that need them are finished with them. A garbage collector makes this trivial to implement – they are automatically collected as soon as all closures needing them are destroyed.

In Rust, this can be done by reference counting instead, with the potential pitfall of creating reference loops that will prevent those variables from being deallocated forever. Rhai avoids this by clone-copying most data values, so reference loops are hard to create.

Rhai does the hoisting of captured variables into the heap by converting those values into reference-counted locked values, also allocated on the heap. The process is identical.

Closures are usually implemented as a data structure containing two items:

  1. A function pointer to the function body of the closure,
  2. A data structure containing references to the captured shared variables on the heap.

Usually a language implementation passes the structure containing references to captured shared variables into the function pointer, the function body taking this data structure as an additional parameter.

This is essentially what Rhai does, except that Rhai passes each variable individually as separate parameters to the function, instead of creating a structure and passing that structure as a single parameter. This is the only difference.

Therefore, in most languages, essentially all closures are implemented as automatic currying of shared variables hoisted into the heap, automatically passing those variables as parameters into the function. Rhai just brings this directly up to the front.

Functions Metadata

The function get_fn_metadata_list returns an array of object maps, each containing the metadata of one script-defined function in scope.

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 imported via the import statement (latest imports first),
  4. 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 not available under no_index or no_object), containing the following fields:

FieldTypeOptional?Description
namespacestringyesthe module namespace if the function is defined within a module
accessstringno"public" if the function is public,
"private" if it is private
namestringnofunction name
paramsarray of stringsnoparameter names
is_anonymousboolnois this function an anonymous function?

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:


#![allow(unused)]
fn main() {
// 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' and a 'Position' argument can be used to
// override 'debug'.
engine.on_debug(|x, src, pos| println!("DEBUG of {} at {:?}: {}", src.unwrap_or("unknown"), pos, x));

// Example: quick-'n-dirty logging
let logbook = Arc::new(RwLock::new(Vec::<String>::new()));

// Redirect print/debug output to 'log'
let log = logbook.clone();
engine.on_print(move |s| log.write().unwrap().push(format!("entry: {}", s)));

let log = logbook.clone();
engine.on_debug(move |s, src, pos| log.write().unwrap().push(
                        format!("DEBUG of {} at {:?}: {}", src.unwrap_or("unknown"), pos, s)
               ));

// Evaluate script
engine.eval::<()>(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) + 'static

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 the AST::set_source method.

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 is of 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 in Module

The easiest way to expose a collection of functions as a self-contained module is to do it via a Rhai script itself.

See the section on Creating a Module from AST for more details.

The script text is evaluated, variables are then selectively exposed via the export statement. Functions defined by the script are automatically exported.

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 selected variables as members 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).


#![allow(unused)]
fn main() {
// This is a module script.

let hidden = 123;       // variable not exported - default hidden
let x = 42;             // this will be exported below

export x;               // the variable 'x' is exported under its own name

export const x = 42;    // convenient short-hand to declare a constant and export it
                        // under its own name

export let x = 123;     // variables can be exported as well, though it'll still be constant

export x as answer;     // the variable 'x' is exported under the alias 'answer'
                        // another script can load this module and access 'x' as 'module::answer'

{
    let inner = 0;      // local variable - it disappears when the statement block ends,
                        //                  therefore it is not 'global' and cannot be exported

    export inner;       // <- syntax error: cannot export a local variable
}
}

Multiple Exports

One export statement can export multiple variables, even under multiple names.


#![allow(unused)]
fn main() {
// The following exports three variables:
//   - 'x' (as 'x' and 'hello')
//   - 'y' (as 'foo' and 'bar')
//   - 'z' (as 'z')
export x, x as hello, x as world, y as foo, y as bar, z;
}

Export Functions

All functions are automatically exported, unless it is explicitly opt-out with the private prefix.

Functions declared private are hidden to the outside.


#![allow(unused)]
fn main() {
// This is a module script.

fn inc(x) { x + 1 }     // script-defined function - default public

private fn foo() {}     // private function - hidden
}

private functions are commonly called to initialize the module. They cannot be called apart from this.

Sub-Modules

All loaded modules are automatically exported as sub-modules.

To prevent a module from being exported, load it inside a block statement so that it goes away at the end of the block.


#![allow(unused)]
fn main() {
// This is a module script.

import "hello" as foo;      // exported as sub-module 'foo'

{
    import "world" as bar;  // not exported - the module disappears at the end
                            //                of the statement block and is not 'global'
}
}

Import a Module

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.

See the section on Module Resolvers for more details.

import Statement

A module can be imported via the import statement, and be given a name. Its members can be accessed via :: similar to C++.

A module that is only import-ed but not under any module name is commonly used for initialization purposes, where the module script contains initialization statements that puts the functions registered with the Engine into a particular state.

import "crypto_init";           // run the script file 'crypto_init.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

Scoped Imports

import statements are scoped, meaning that they are only accessible inside the scope that they’re imported.

They can appear anywhere a normal statement can be, but in the vast majority of cases import statements are usually grouped at the beginning of a script so they have global 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!


#![allow(unused)]
fn main() {
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 range(0, 1000) {
    import "crypto" as c;       // <- importing a module inside a loop is a Very Bad Idea™

    c.encrypt(something);
}
}

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:


#![allow(unused)]
fn main() {
+------------+
| hello.rhai |
+------------+

import "hello" as foo;          // import itself - infinite recursion!

foo::do_something();
}

Modules cross-referencing also cause infinite recursion:


#![allow(unused)]
fn main() {
+------------+
| 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!


#![allow(unused)]
fn main() {
let x = 10;

fn foo(x) { x += 12; x }

let script = "let y = x;";      // build a script
script +=    "y += foo(y);";
script +=    "x + y";

let result = eval(script);      // <- look, JavaScript, we can also do this!

result == 42;

x == 10;                        // prints 10: functions call arguments are passed by value
y == 32;                        // prints 32: variables defined in 'eval' persist!

eval("{ let z = y }");          // to keep a variable local, use a statement block

print(z);                       // <- error: variable 'z' not found

"print(42)".eval();             // <- nope... method-call style doesn't work with 'eval'
}

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! It is almost as if the script segments were 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, not inside another function call!


#![allow(unused)]
fn main() {
let script = "x += 32";
let x = 10;
eval(script);                   // variable 'x' in the current scope is visible!
print(x);                       // prints 42

// The above is equivalent to:
let script = "x += 32";
let x = 10;
x += 32;
print(x);
}

eval is Evil

For those who subscribe to the (very sensible) motto of eval is evil”, disable eval using Engine::disable_symbol:


#![allow(unused)]
fn main() {
engine.disable_symbol("eval");  // disable usage of 'eval'
}

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.

The most important resources to watch out for are:

  • Memory: A malicious script may continuously grow a string, an array or object map until all memory is consumed.

    It may also create a large array or object map literal that exhausts all memory during parsing.

  • CPU: A malicious script may run an infinite tight loop that consumes all CPU cycles.

  • Time: A malicious script may run indefinitely, thereby blocking the calling system which is waiting for a result.

  • Stack: A malicious script may attempt an infinite recursive call that exhausts the call stack.

    Alternatively, it may create a degenerated deep expression with so many levels that the parser exhausts the call stack when parsing the expression; or even deeply-nested statement blocks, if nested deep enough.

    Another way to cause a stack overflow is to load a self-referencing module.

  • Overflows: A malicious script may deliberately cause numeric over-flows and/or under-flows, divide by zero, and/or create bad floating-point representations, in order to crash the system.

  • Files: A malicious script may continuously import an external module within an infinite loop, thereby 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.

  • Data: A malicious script may attempt to read from and/or write to data that it does not own. If this happens, it is a severe security breach and may put the entire system at risk.

Don’t Panic Guarantee – Any Panic is a Bug

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

All the above safe-guards can be turned off via the unchecked feature, which disables all safety checks (even fatal ones such as stack overflow, arithmetic overflow and division-by-zero).

This increases script evaluation performance somewhat, but at the expense of breaking the no-panic guarantee.

Under unchecked, it is very possible for a malicious script to panic and bring down the host system.

Safety Checks

Checked Arithmetic

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.

Scripts under normal builds of Rhai never crash the host system – any panic is a bug.

This checking can be turned off via the unchecked feature for higher performance (but higher risks as well).


#![allow(unused)]
fn main() {
let x = 1_000_000_000_000;

x * x;      // Normal build: runtime error: multiplication overflow

x * x;      // 'unchecked' build: panic!

x / 0;      // Normal build: runtime error: division by zero

x / 0;      // 'unchecked' build: panic!
}

Other Safety Checks

In addition to arithmetic overflows etc., there are many other safety checks performed by Rhai at runtime. unchecked turns them all off as well, such as...

Infinite loops


#![allow(unused)]
fn main() {
// Normal build: runtime error: exceeds maximum number of operations
loop { foo(); }

// 'unchecked' build: never terminates!
loop { foo(); }
}

Infinite recursion


#![allow(unused)]
fn main() {
fn foo() { foo(); }

foo();      // Normal build: runtime error: exceeds maximum stack depth

foo();      // 'unchecked' build: panic due to stack overflow!
}

Gigantic data structures


#![allow(unused)]
fn main() {
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


#![allow(unused)]
fn main() {
// 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) { ... }
}

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.


#![allow(unused)]
fn main() {
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...
}

Using Rhai to Control External Environment

How does a sand-boxed, immutable Engine control the external environment? This is necessary in order to use Rhai as a dynamic control layer over a Rust core system.

There are two general patterns, both involving wrapping the external system in a shared, interior-mutated object (e.g. Rc<RefCell<T>>):

Maximum Length of Strings

Limit How Long Strings Can Grow

Rhai by default does not limit how long a string can be.

This can be changed via the Engine::set_max_string_size method, with zero being unlimited (the default).

A script attempting to create a string literal longer than the maximum length will terminate with a parse error.

Any script operation that produces a string longer than the maximum also terminates the script with an error result.

This check can be disabled via the unchecked feature for higher performance (but higher risks as well).


#![allow(unused)]
fn main() {
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
}

Setting 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

Limit How Large Arrays Can Grow

Rhai by default does not limit how large an array 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 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).


#![allow(unused)]
fn main() {
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
}

Setting Maximum Size

Be conservative when setting a maximum limit and always consider the fact that a registered function may grow an array’s size without Rhai noticing until the very end.

For instance, the built-in + operator for arrays concatenates two arrays together to form one larger array; if both arrays are slightly below the maximum size limit, the resultant array may be almost twice the maximum size.

As a malicious script may 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 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).

Maximum Size of Object Maps

Limit How Large Object Maps Can Grow

Rhai by default does not limit how large (i.e. the number of properties) an object map can be.

This can be changed via the Engine::set_max_map_size method, with zero being unlimited (the default).

A script attempting to create an object map literal with more properties than the maximum will terminate with a parse error.

Any script operation that produces an object map with more properties than the maximum also terminates the script with an error result.

This check can be disabled via the unchecked feature for higher performance (but higher risks as well).


#![allow(unused)]
fn main() {
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
}

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

Maximum Number of Operations

In Rhai, it is trivial to construct infinite loops, or scripts that run for a very long time.


#![allow(unused)]
fn main() {
loop { ... }                        // infinite loop

while 1 < 2 { ... }                 // loop with always-true condition
}

Limit How Long a Script Can Run

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


#![allow(unused)]
fn main() {
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
}

What Does One Operation Mean

The concept of one single operation in Rhai is volatile – it roughly equals one expression node, loading one variable/constant, one operator call, one iteration of a loop, or one function call etc. with sub-expressions, statements and function calls executed inside these contexts accumulated on top.

A good rule-of-thumb is that one simple non-trivial expression consumes on average 5-10 operations.

One operation can take an unspecified amount of time and real CPU cycles, depending on the particulars. For example, loading a constant consumes very few CPU cycles, while calling an external Rust function, though also counted as only one operation, may consume much more computing resources.

To help visualize, think of an operation as roughly equals to one instruction of a hypothetical CPU which includes specialized instructions, such as function call, load module etc., each taking up one CPU cycle to execute.

Track Progress and Force-Termination

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 the Engine::on_progress method:


#![allow(unused)]
fn main() {
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
});
}

The closure passed to Engine::on_progress will be called once for every operation. Return Some(token) to terminate the script immediately, with the provided value (any Dynamic) acting as a termination token.

Progress tracking is disabled with the unchecked feature.

Termination Token

The Dynamic value returned by the closure for Engine::on_progress is a termination token. A script that is manually terminated returns with Err(EvalAltResult::ErrorTerminated(token, position)) wrapping this value.

The termination token is commonly used to provide information on the reason or source behind the termination decision.

If the termination token is not needed, simply return Some(Dynamic::UNIT) to terminate the script run with () as the token.

Operations Count vs. Progress Percentage

Notice that the operations count value passed into the closure does not indicate the percentage of work already done by the script (and thus it is not real progress tracking), because it is impossible to determine how long a script may run.

It is possible, however, to calculate this percentage based on an estimated total number of operations for a typical run.

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


#![allow(unused)]
fn main() {
let mut engine = Engine::new();

engine.set_max_modules(5);      // allow loading only up to 5 modules

engine.set_max_modules(0);      // disallow loading any module (maximum = zero)

engine.set_max_modules(1000);   // set to a large number for effectively unlimited modules
}

Maximum Call Stack Depth

In Rhai, it is trivial for a function call to perform infinite recursion such that all stack space is exhausted.


#![allow(unused)]
fn main() {
// This is a function that, when called, recurse forever.
fn recurse_forever() {
    recurse_forever();
}
}

Limit How Stack Usage by Scripts

Rhai by default limits function calls to a maximum depth of 64 levels (8 levels in debug build).

This limit may be changed via the Engine::set_max_call_levels method.

A script exceeding the maximum call stack depth will terminate with an error result.

This check can be disabled via the unchecked feature for higher performance (but higher risks as well).


#![allow(unused)]
fn main() {
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)
}

Setting Maximum Stack Depth

When setting this limit, care must be also taken to the evaluation depth of each statement within a function. It is entirely possible for a malicious script to embed a recursive call deep inside a nested expression or statement block (see maximum statement depth).

Maximum Statement Depth

Limit How Deeply-Nested a Statement Can Be

Rhai by default limits statements and expressions 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.


#![allow(unused)]
fn main() {
// 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 the Engine::set_max_expr_depths method.

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


#![allow(unused)]
fn main() {
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
}

Beware that there may be multiple layers 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 script 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.

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.

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.

Dead Code Removal

Rhai attempts to eliminate dead code (i.e. code that does nothing, for example an expression by itself as a statement, which is allowed in Rhai).


#![allow(unused)]
fn main() {
{
    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:


#![allow(unused)]
fn main() {
{
    let x = 999;
    foo(42);
    666
}
}

Normally, nobody deliberately writes scripts with dead code, but it is extremely common for template-based machine-generated scripts, especially where constants are involved.

Constants Propagation

Constants propagation is used to remove dead code:


#![allow(unused)]
fn main() {
const ABC = true;

if ABC || some_work() { print("done!"); }   // 'ABC' is constant so it is replaced by 'true'...

if true || some_work() { print("done!"); }  // since '||' short-circuits, 'some_work' 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
}

These are quite effective for template-based machine-generated scripts where certain constant values are spliced into the script text in order to turn on/off certain sections.

For fixed script texts, the constant values can be provided in a user-defined Scope object to the Engine for use in compilation and evaluation.

Caveat – beware 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).


#![allow(unused)]
fn main() {
let mut scope = Scope::new();

// Push a large constant into the scope...
scope.push_constant("MY_BIG_TYPE", AVeryLargeType::take_long_time_to_create());

// Causes each usage of 'MY_BIG_TYPE' in the script below to be replaced
// by cloned copies of 'AVeryLargeType'.
let result = engine.consume_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::consume_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.


#![allow(unused)]
fn main() {
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.

Op-Assignment Rewrite

Usually, an op-assignment operator (e.g. += for append) takes a mutable first parameter (i.e. &mut) while the corresponding simple operator (i.e. +) does not.

This has huge performance implications because arguments passed as reference are always cloned.


#![allow(unused)]
fn main() {
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
}

The script optimizer rewrites normal expressions into op-assignment style wherever possible.

However, and only those involving simple variable references are optimized. In other words, no common sub-expression elimination is performed by Rhai.


#![allow(unused)]
fn main() {
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
}

Eager Operator Evaluation

Beware, however, that most operators are actually function calls, and those functions can be overridden, so whether they are optimized away depends on the situation:

  • If the operands are not constant values, it is not optimized.

  • If the operator is overloaded, it is not optimized because the overloading function may not be pure (i.e. may cause side-effects when called).

  • If the operator is not built-in (see list of built-in operators), it is not optimized.

  • If the operator is a built-in operator for a standard type, it is called and replaced by a constant result.

Rhai guarantees that no external function will be run (in order not to trigger side-effects) during the optimization process (unless the optimization level is set to 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
}

Because of the eager evaluation of operators for standard types, many constant expressions will be evaluated and replaced by the result.


#![allow(unused)]
fn main() {
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:


#![allow(unused)]
fn main() {
// 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.

Optimization Levels

There are three levels of optimization: None, Simple and Full.

  • None is obvious – no optimization on the AST is performed.

  • Simple (default) 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).

    However, it is important to bear the caveat in mind that, when constants propagation is performed, and if the constants are modified later on (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.

    Furthermore, overriding a built-in operator in the Engine afterwards has no effect after the optimizer replaces an expression with its calculated value.

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

Set Optimization Level

An Engine‘s optimization level is set via a call to Engine::set_optimization_level:


#![allow(unused)]
fn main() {
// Turn on aggressive optimizations
engine.set_optimization_level(rhai::OptimizationLevel::Full);
}

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 the Engine::optimize_ast method, effectively pruning out unused code sections.

The final, optimized AST is then used for evaluations.


#![allow(unused)]
fn main() {
// Compile master script to AST
let master_ast = engine.compile(
"
    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.consume_ast(&new_ast)?;
}
}

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

For instance, the same example above:


#![allow(unused)]
fn main() {
// When compiling the following with OptimizationLevel::Full...

const DECISION = 1;
                            // this condition is now eliminated because 'sign(DECISION) > 0'
if DECISION.sign() > 0 {    // is a call to the 'sign' and '>' functions, and they return 'true'
    print("hello!");        // this block is promoted to the parent level
} else {
    print("boo!");          // this block is eliminated because it is never reached
}

print("hello!");            // <- the above is equivalent to this
                            //    ('print' and 'debug' are handled specially)
}

Side-Effect Considerations for Full Optimization Level

All of Rhai’s built-in functions (and operators which are implemented as functions) are pure (i.e. they do not mutate state nor cause any side-effects, with the exception of print and debug which are handled specially) so using OptimizationLevel::Full is usually quite safe unless custom types and functions are registered.

If custom functions are registered, they may be called (or maybe not, if the calls happen to lie within a pruned code block).

If custom functions are registered to overload built-in operators, they will also be called when the operators are used (in an if statement, for example), potentially causing side-effects.

Therefore, the rule-of-thumb is:

  • Always register custom types and functions after compiling scripts if OptimizationLevel::Full is used.

  • DO NOT depend on knowledge that the functions have no side-effects, because those functions can change later on and, when that happens, existing scripts may break in subtle ways.

Volatility Considerations for Full Optimization Level

Even if a custom function does not mutate state nor cause side-effects, it may still be volatile, i.e. it depends on the external environment and is not pure.

A perfect example is a function that gets the current time – obviously each run will return a different value!

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.

Therefore, avoid using OptimizationLevel::Full if volatile custom functions are involved.

Subtle Semantic Changes After Optimization

Some optimizations can alter subtle semantics of the script.

For example:


#![allow(unused)]
fn main() {
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 an error return.

In fact, any errors inside a statement that has been eliminated will silently disappear:


#![allow(unused)]
fn main() {
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.

It is usually a Very Bad Idea™ to depend on a script failing or such kind of subtleties, but if it turns out to be necessary (why? I would never guess), turn script optimization off by setting the optimization level to OptimizationLevel::None.

Advanced Topics

This section covers advanced features of the Rhai Engine.

Manage AST‘s

When compiling a Rhai script to an AST, the following data are packaged together as a single unit:

DataTypeDescriptionAccess API
Source nameImmutableStringoptional text name to identify the source of the scriptsource(&self), clone_source(&self), set_source(&mut self, source), clear_source(&mut self)
StatementsVec<Stmt>list of script statements at global levelstatements(&self), statements_mut(&mut self) (only available under internals)
Functions (not available under no_function)Modulefunctions defined in the scriptshared_lib(&self), lib(&self) (only available under internals)
Embedded module resolver (not available under no_module)StaticModuleResolverembedded module resolver for self-contained ASTresolver(&self) (only available under internals)

Use the source name to identify the source script in errors – useful when multiple modules are imported recursively.

Most of the AST API is available only under the internals feature.

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

Merge and Combine AST’s

The following methods merge one AST with another:

MethodDescription
merge(&self, &second), + operatorappend 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, &second, 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, second), += operatorappend 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, second, 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.


#![allow(unused)]
fn main() {
// First script
let ast1 = engine.compile(
"
     fn foo(x) { 42 + x }
     foo(1)
")?;

// Second script
let ast2 = engine.compile(
r#"
     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 function signature of the callback function is:

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 statementcondition expression, then statements, else statements (if any)
switch statementmatch element, each of the case conditions and statements, default statements (if any)
while, do, loop statementcondition expression, statements body
for statementcollection expression, statements body
try ... catch statementtry statements body, catch statements body
return statementreturn value expression
import statementpath expression
Array literaleach of the element expressions
Object map literaleach of the element expressions
Interpolated stringeach of the string/expression segments
IndexingLHS, RHS
Field access/method callLHS, RHS
&&, ||LHS, RHS
Function call, operator expressioneach of the argument expressions
let, const statementvalue expression
Assignment statementl-value expression, value expression
Statements blockeach of the statements
Custom syntax expressioneach of the $expr$, $stmt$, $ident$, $bool$, $int$, $float$, $string$ blocks
All otherssingle child, or none

Capture The Calling Scope for Function Call

Peeking Out of The Pure Box

Rhai functions are pure, meaning that they depend on on their arguments and have no access to the calling environment.

When a function accesses a variable that is not defined within that function’s scope, it raises an evaluation error.

It is possible, through a special syntax, to capture the calling scope – i.e. the scope that makes the function call – and access variables defined there.


#![allow(unused)]
fn main() {
fn foo(y) {             // function accesses 'x' and 'y', but 'x' is not defined
    x += y;             // 'x' is modified in this function
    x
}

let x = 1;

foo(41);                // error: variable 'x' not found

// Calling a function with a '!' causes it to capture the calling scope

foo!(41) == 42;         // the function can access the value of 'x', but cannot change it

x == 1;                 // 'x' is still the original value

x.method!();            // <- syntax error: capturing is not allowed in method-call style

// Capturing also works for function pointers

let f = Fn("foo");

call!(f, 41) == 42;     // must use function-call style

f.call!(41);            // <- syntax error: capturing is not allowed in method-call style

// Capturing is not available for module functions

import "hello" as h;

h::greet!();            // <- syntax error: capturing is not allowed in namespace-qualified calls
}

No Mutations

Variables in the calling scope are captured as cloned copies. Changes to them do not reflect back to the calling scope.

Rhai functions remain pure in the sense that they can never mutate their environment.

Caveat Emptor

Functions relying on the calling scope is often a Very Bad Idea™ because it makes code almost impossible to reason and maintain, as their behaviors are volatile and unpredictable.

They behave more like macros that are expanded inline than actual function calls, thus the syntax is also similar to Rust’s macro invocations.

This usage should be at the last resort. YOU HAVE BEEN WARNED.

Use the Low-Level API to Register a Rust Function

When a native Rust function is registered with an Engine using the Engine::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.

The function signature includes the current native call context which exposes the current Engine, among others, so the Rust function can recursively call methods on the same Engine.


#![allow(unused)]
fn main() {
engine.register_raw_fn(
    "increment_by",                                         // function name
    &[                                                      // a slice containing parameter types
        std::any::TypeId::of::<i64>(),                      // type of first parameter
        std::any::TypeId::of::<i64>()                       // type of second parameter
    ],
    |context, args| {                                       // fixed function signature
        // Arguments are guaranteed to be correct in number and of the correct types.

        // But remember this is Rust, so you can keep only one mutable reference at any one time!
        // Therefore, get a '&mut' reference to the first argument _last_.
        // Alternatively, use `args.split_first_mut()` etc. to split the slice first.

        let y = *args[1].read_lock::<i64>().unwrap();       // get a reference to the second argument
                                                            // then copy it because it is a primary type

        let y = std::mem::take(args[1]).cast::<i64>();      // alternatively, directly 'consume' it

        let y = args[1].as_int().unwrap();                  // alternatively, use 'as_xxx()'

        let x = args[0].write_lock::<i64>().unwrap();       // get a '&mut' reference to the first argument

        *x += y;                                            // perform the action

        Ok(Dynamic::UNIT)                                   // must be 'Result<Dynamic, Box<EvalAltResult>>'
    }
);

// The above is the same as (in fact, internally they are equivalent):

engine.register_fn("increment_by", |x: &mut i64, y: i64| *x += y);
}

Function Signature

The function signature passed to Engine::register_raw_fn takes the following form:

Fn(context: NativeCallContext, args: &mut [&mut Dynamic])
-> Result<T, Box<EvalAltResult>> + 'static

where:

ParameterTypeDescription
Timpl Clonereturn type of the function
contextNativeCallContextthe current native call context
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:


#![allow(unused)]
fn main() {
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:

Argument typeAccess (n = argument position)ResultOriginal value
INTargs[n].as_int().unwrap()INTuntouched
FLOATargs[n].as_float().unwrap()FLOATuntouched
Decimalargs[n].as_decimal().unwrap()Decimaluntouched
boolargs[n].as_bool().unwrap()booluntouched
charargs[n].as_char().unwrap()charuntouched
()args[n].as_unit().unwrap()()untouched
String&*args[n].read_lock::<ImmutableString>().unwrap()&ImmutableStringuntouched
String (consumed)std::mem::take(args[n]).cast::<ImmutableString>()ImmutableString()
Custom type&*args[n].read_lock::<T>().unwrap()&Tuntouched
Custom type (consumed)std::mem::take(args[n]).cast::<T>()T()

Example – Passing a Callback to a Rust Function

The low-level API is useful when there is a need to interact with the scripting Engine within a function.

The following example registers a function that takes a function pointer as an argument, then calls it within the same Engine. This way, a callback function can be provided to a native Rust function.


#![allow(unused)]
fn main() {
use rhai::{Engine, FnPtr};

let mut engine = Engine::new();

// Register a Rust function
engine.register_raw_fn(
    "bar",
    &[
        std::any::TypeId::of::<i64>(),                      // parameter types
        std::any::TypeId::of::<FnPtr>(),
        std::any::TypeId::of::<i64>(),
    ],
    |context, args| {
        // 'args' is guaranteed to contain enough arguments of the correct types

        let fp = std::mem::take(args[1]).cast::<FnPtr>();   // 2nd argument - function pointer
        let value = std::mem::take(args[2]);                // 3rd argument - function argument
        let this_ptr = args.get_mut(0).unwrap();            // 1st argument - this pointer

        // Use 'FnPtr::call_dynamic' to call the function pointer.
        // Beware, private script-defined functions will not be found.
        fp.call_dynamic(&context, Some(this_ptr), [value])
    },
);

let result = engine.eval::<i64>(
r#"
    fn foo(x) { this += x; }        // script-defined function 'foo'

    let x = 41;                     // object
    x.bar(Fn("foo"), 1);            // pass 'foo' as function pointer
    x
"#)?;
}

TL;DR – Why read_lock and write_lock

The Dynamic API that casts it to a reference to a particular data type is read_lock (for an immutable reference) and write_lock (for a mutable reference).

As the naming shows, something is locked in order to allow this access, and that something is a shared value created by capturing variables from closures.

Shared values are implemented as Rc<RefCell<Dynamic>> (Arc<RwLock<Dynamic>> under sync).

If the value is not a shared value, or if running under no_closure where there is no capturing, this API de-sugars to a simple reference cast.

In other words, there is no locking and reference counting overhead for the vast majority of non-shared values.

If the value is a shared value, then it is first locked and the returned lock guard allows access to the underlying value in the specified type.

Hold Multiple References

In order to access a value argument that is expensive to clone while holding a mutable reference to the first argument, use one of the following tactics:

  1. if it is a primary type other than string, use as_xxx() as above

  2. directly consume that argument via std::mem::take as above

  3. use split_first_mut to partition the slice:


#![allow(unused)]
fn main() {
// Partition the slice
let (first, rest) = args.split_first_mut().unwrap();

// Mutable reference to the first parameter, of type '&mut A'
let this_ptr = &mut *first.write_lock::<A>().unwrap();

// Immutable reference to the second value parameter, of type '&B'
// This can be mutable but there is no point because the parameter is passed by value
let value_ref = &*rest[0].read_lock::<B>().unwrap();
}

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 the Engine::on_var method:


#![allow(unused)]
fn main() {
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(Box::new(
            EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::NONE)
        )),
        // Silently maps 'chameleon' into 'innocent'.
        "chameleon" => context.scope().get_value("innocent").map(Some).ok_or_else(|| Box::new(
            EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::NONE)
        )),
        // Return Ok(None) to continue with the normal variable resolution process.
        _ => Ok(None)
    }
});
}

Returned Values are Constants

Variable values, if any returned, are treated as constants by the script and cannot be assigned to. This is to avoid needing a mutable reference to the underlying data provider which may not be possible to obtain.

In order to change these variables, it is best to push them into a custom Scope instead of using a variable resolver. Then these variables can be assigned to and their updated values read back after the script is evaluated.

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.

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>> + 'static

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.
context&EvalContextreference to the current evaluation context
scope()&Scopereference to the current Scope
engine()&Enginereference to the current Engine
source()Option<&str>reference to the current source, if any
iter_imports()impl Iterator<Item = (&str, &Module)>iterator of the current stack of modules imported via import statements
imports()&Importsreference to the current 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
namespaces()&[&Module]reference to the namespaces (as modules) containing all script-defined functions; requires the internals feature
this_ptr()Option<&Dynamic>reference to the current bound [this] pointer, if any
call_level()usizethe current nesting level of function calls

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(Dynamic))value of the variable, treated as a constant
Err(Box<EvalAltResult>)error that is reflected back to the Engine.
Normally this is EvalAltResult::ErrorVariableNotFound(var_name, Position::NONE) to indicate that the variable does not exist, but it can be any EvalAltResult.

Use Rhai as a Domain-Specific Language (DSL)

Rhai can be successfully used as a domain-specific language (DSL).

Expressions Only

In many DSL scenarios, only evaluation of expressions is needed.

The Engine::eval_expression_XXX API can be used to restrict a script to expressions only.

Unicode Standard Annex #31 Identifiers

Variable names and other identifiers do not necessarily need to be ASCII-only.

The unicode-xid-ident feature, when turned on, causes Rhai to allow variable names and identifiers that follow Unicode Standard Annex #31.

This is sometimes useful in a non-English DSL.

Disable Keywords and/or Operators

In some DSL scenarios, it is necessary to further restrict the language to exclude certain language features that are not necessary or dangerous to the application.

For example, a DSL may disable the while loop while keeping all other statement types intact.

It is possible, in Rhai, to surgically disable keywords and operators.

Custom Operators

On the other hand, some DSL scenarios require special operators that make sense only for that specific environment. In such cases, it is possible to define custom operators in Rhai.

For example:


#![allow(unused)]
fn main() {
let animal = "rabbit";
let food = "carrot";

animal eats food            // custom operator 'eats'

eats(animal, food)          // <- the above really de-sugars to this
}

Although a custom operator always de-sugars to a simple function call, nevertheless it makes the DSL syntax much simpler and expressive.

Custom Syntax

For advanced DSL scenarios, it is possible to define entire expression syntax – essentially custom statement types.

For example, the following is a SQL-like syntax for some obscure DSL operation:


#![allow(unused)]
fn main() {
let table = [..., ..., ..., ...];

// Syntax = calculate $ident$ ( $expr$ -> $ident$ ) => $ident$ : $expr$
let total = calculate sum(table->price) => row : row.weight > 50;

// Note: There is nothing special about those symbols; to make it look exactly like SQL:
// Syntax = SELECT $ident$ ( $ident$ ) AS $ident$ FROM $expr$ WHERE $expr$
let total = SELECT sum(price) AS row FROM table WHERE row.weight > 50;
}

After registering this custom syntax with Rhai, it can be used anywhere inside a script as a normal expression.

For its evaluation, the callback function will receive the following list of inputs:

  • inputs[0] = "sum" - math operator
  • inputs[1] = "price" - field name
  • inputs[2] = "row" - loop variable name
  • inputs[3] = Expression(table) - data source
  • inputs[4] = Expression(row.weight > 50) - filter predicate

Other identifiers, such as "calculate", "FROM", as well as symbols such as -> and : etc., are parsed in the order defined within the custom syntax.

Disable Certain Keywords and/or Operators

For certain embedded usage, it is sometimes necessary to restrict the language to a strict subset of Rhai to prevent usage of certain language features.

Rhai supports surgically disabling a keyword or operator via the Engine::disable_symbol method.


#![allow(unused)]
fn main() {
use rhai::Engine;

let mut engine = Engine::new();

engine
    .disable_symbol("if")       // disable the 'if' keyword
    .disable_symbol("+=");      // disable the '+=' operator

// The following all return parse errors.

engine.compile("let x = if true { 42 } else { 0 };")?;
//                      ^ 'if' is rejected as a reserved keyword

engine.compile("let x = 40 + 2; x += 1;")?;
//                                ^ '+=' is not recognized as an operator
//                         ^ other operators are not affected
}

Custom Operators

For use as a DSL (Domain-Specific Languages), it is sometimes more convenient to augment Rhai with customized operators performing specific logic.

Engine::register_custom_operator registers a keyword as a custom operator, giving it a particular precedence (which cannot be zero).

Example


#![allow(unused)]
fn main() {
use rhai::Engine;

let mut engine = Engine::new();

// Register a custom operator named 'foo' and give it a precedence of 160
// (i.e. between +|- and *|/)
// Also register the implementation of the customer operator as a function
engine.register_custom_operator("foo", 160)?
      .register_fn("foo", |x: i64, y: i64| (x * y) - (x + y));

// The custom operator can be used in expressions
let result = engine.eval_expression::<i64>("1 + 2 * 3 foo 4 - 5 / 6")?;
//                                                    ^ custom operator

// The above is equivalent to: 1 + ((2 * 3) foo 4) - (5 / 6)
result == 15;
}

Alternatives to a Custom Operator

Custom operators are merely syntactic sugar. They map directly to registered functions.

Therefore, the following are equivalent (assuming foo has been registered as a custom operator):


#![allow(unused)]
fn main() {
1 + 2 * 3 foo 4 - 5 / 6     // use custom operator

1 + foo(2 * 3, 4) - 5 / 6   // use function call
}

A script using custom operators can always be pre-processed, via a pre-processor application, into a syntax that uses the corresponding function calls.

Using Engine::register_custom_operator merely enables a convenient shortcut.

Must be a Valid Identifier or Reserved Symbol

All custom operators must be identifiers that follow the same naming rules as variables.

Alternatively, they can also be reserved symbols, disabled operators or keywords.


#![allow(unused)]
fn main() {
engine.register_custom_operator("foo", 20);     // 'foo' is a valid custom operator

engine.register_custom_operator("#", 20);       // the reserved symbol '#' is also
                                                // a valid custom operator

engine.register_custom_operator("+", 30);       // <- error: '+' is an active operator

engine.register_custom_operator("=>", 30);      // <- error: '=>' is an active symbol
}

Binary Operators Only

All custom operators must be binary (i.e. they take two operands). Unary custom operators are not supported.


#![allow(unused)]
fn main() {
engine.register_custom_operator("foo", 160)?
      .register_fn("foo", |x: i64| x * x);

engine.eval::<i64>("1 + 2 * 3 foo 4 - 5 / 6")?; // error: function 'foo (i64, i64)' not found
}

Operator Precedence

All operators in Rhai has a precedence indicating how tightly they bind.

A higher precedence binds more tightly than a lower precedence, so * and / binds before + and - etc.

When registering a custom operator, the operator’s precedence must also be provided.

The following precedence table shows the built-in precedence of standard Rhai operators:

CategoryOperatorsPrecedence (0-255)
Assignments=, +=, -=, *=, /=, **=, %=,
<<=, >>=, &=, |=, ^=
0
Logic and bit masks||, |, ^30
Logic and bit masks&&, &60
Comparisons==, !=90
Containmentin110
Comparisons>, >=, <, <=130
Arithmetic+, -150
Arithmetic*, /, %180
Arithmetic** (binds to right)190
Bit-shifts<<, >>210
Unary operatorsunary +, -, ! (binds to right)second highest
Object field access. (binds to right)highest

Extend Rhai with Custom Syntax

For the ultimate adventurous, there is a built-in facility to extend the Rhai language with custom-defined syntax.

But before going off to define the next weird statement type, heed this warning:

Don’t Do It™

Stick with standard language syntax as much as possible.

Having to learn Rhai is bad enough, no sane user would ever want to learn yet another obscure language syntax just to do something.

Try to use custom operators first. Defining a custom syntax should be considered a last resort.

Where This Might Be Useful

  • Where an operation is used a LOT and a custom syntax saves a lot of typing.

  • Where a custom syntax significantly simplifies the code and significantly enhances understanding of the code’s intent.

  • Where certain logic cannot be easily encapsulated inside a function.

  • Where you just want to confuse your user and make their lives miserable, because you can.

Step One – Design The Syntax

A custom syntax is simply a list of symbols.

These symbol types can be used:

  • Standard keywords
  • Standard operators.
  • Reserved symbols.
  • Identifiers following the variable naming rules.
  • $expr$ – any valid expression, statement or statement block.
  • $block$ – any valid statement block (i.e. must be enclosed by { .. }).
  • $ident$ – any variable name.
  • $bool$ – a boolean value.
  • $int$ – an integer number.
  • $float$ – a floating-point number (if not no_float).
  • $string$ – a string literal.

The First Symbol Must be an Identifier

There is no specific limit on the combination and sequencing of each symbol type, except the first symbol which must be a custom keyword that follows the naming rules of variables.

The first symbol also cannot be a normal or reserved keyword. In other words, any valid identifier that is not a keyword will work fine.

The First Symbol Must be Unique

Rhai uses the first symbol as a clue to parse custom syntax.

Therefore, at any one time, there can only be one custom syntax starting with each unique symbol.

Any new custom syntax definition using the same first symbol simply overwrites the previous one.

Example


#![allow(unused)]
fn main() {
exec [ $ident$ ; $int$ ] <- $expr$ : $block$
}

The above syntax is made up of a stream of symbols:

PositionInput slotSymbolDescription
1execcustom keyword
2[the left bracket symbol
20$ident$a variable name
3;the semicolon symbol
41$int$an integer number
5]the right bracket symbol
6<-the left-arrow symbol (which is a reserved symbol in Rhai).
72$expr$an expression, which may be enclosed with { .. }, or not.
8:the colon symbol
93$block$a statement block, which must be enclosed with { .. }.

This syntax matches the following sample code and generates three inputs (one for each non-keyword):


#![allow(unused)]
fn main() {
// Assuming the 'exec' custom syntax implementation declares the variable 'hello':
let x = exec [hello;42] <- foo(1, 2) : {
            hello += bar(hello);
            baz(hello);
        };

print(x);       // variable 'x'  has a value returned by the custom syntax

print(hello);   // variable declared by a custom syntax persists!
}

Step Two – Implementation

Any custom syntax must include an implementation of it.

Function Signature

The function signature of an implementation is:

Fn(context: &mut EvalContext, inputs: &[Expression]) -> Result<Dynamic, Box<EvalAltResult>>

where:

ParameterTypeDescription
context&mut EvalContextmutable reference to the current evaluation context
scope()&Scopereference to the current Scope
scope_mut()&mut &mut Scopemutable reference to the current Scope; variables can be added to/removed from it
engine()&Enginereference to the current Engine
source()Option<&str>reference to the current source, if any
iter_imports()impl Iterator<Item = (&str, &Module)>iterator of the current stack of modules imported via import statements
imports()&Importsreference to the current 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
namespaces()&[&Module]reference to the namespaces (as modules) containing all script-defined functions; requires the internals feature
this_ptr()Option<&Dynamic>reference to the current bound [this] pointer, if any
call_level()usizethe current nesting level of function calls
inputs&[Expression]a list of input expression trees

Return Value

Return value is the result of evaluating the custom syntax expression.

Access Arguments

The most important argument is inputs where the matched identifiers ($ident$), expressions/statements ($expr$) and statement blocks ($block$) are provided.

To access a particular argument, use the following patterns:

Argument typePattern (n = slot in inputs)Result typeDescription
$ident$inputs[n].get_variable_name().unwrap()&strname of a variable
$expr$&inputs[n]&Expressionan expression tree
$block$&inputs[n]&Expressionan expression tree
$bool$inputs[n].get_literal_value::<bool>().unwrap()boolboolean value
$int$inputs[n].get_literal_value::<INT>().unwrap()INTinteger number
$float$inputs[n].get_literal_value::<FLOAT>().unwrap()FLOATfloating-point number
$string$inputs[n].get_literal_value::<ImmutableString>().unwrap()ImmutableStringstring literal

Get literal constants

Several argument types represent literal constants that can be obtained directly via Expression::get_literal_value<T>.


#![allow(unused)]
fn main() {
let expression = &inputs[0];

// Use 'get_literal_value' with a turbo-fish type to extract the value
let string_value = expression.get_literal_value::<ImmutableString>().unwrap();
let float_value = expression.get_literal_value::<FLOAT>().unwrap();

// Or assign directly to a variable with type...
let int_value: INT = expression.get_literal_value().unwrap();

// Or use type inference!
let bool_value = expression.get_literal_value().unwrap();

if bool_value { ... }       // 'bool_value' inferred to be 'bool'
}

Evaluate an Expression Tree

Use the EvalContext::eval_expression_tree method to evaluate an arbitrary expression tree within the current evaluation context.


#![allow(unused)]
fn main() {
let expression = &inputs[0];
let result = context.eval_expression_tree(expression)?;
}

Declare Variables

New variables maybe declared (usually with a variable name that is passed in via `$ident$).

It can simply be pushed into the Scope.

However, beware that all new variables must be declared prior to evaluating any expression tree. In other words, any Scope calls that change the list of must come before any EvalContext::eval_expression_tree calls.


#![allow(unused)]
fn main() {
let var_name = inputs[0].get_variable_name().unwrap();
let expression = inputs.get(1).unwrap();

context.scope_mut().push(var_name, 0_i64);      // do this BEFORE 'context.eval_expression_tree'!

let result = context.eval_expression_tree(expression)?;
}

Step Three – Register the Custom Syntax

Use Engine::register_custom_syntax to register a custom syntax.

Again, beware that the first symbol must be unique. If there already exists a custom syntax starting with that symbol, the previous syntax will be overwritten.

The syntax is passed simply as a slice of &str.


#![allow(unused)]
fn main() {
// Custom syntax implementation
fn implementation_func(
    context: &mut EvalContext,
    inputs: &[Expression]
) -> Result<Dynamic, Box<EvalAltResult>> {
    let var_name = inputs[0].get_variable_name().unwrap().to_string();
    let stmt = inputs.get(1).unwrap();
    let condition = inputs.get(2).unwrap();

    // Push new variable into the scope BEFORE 'context.eval_expression_tree'
    context.scope_mut().push(var_name.clone(), 0_i64);

    let mut count = 0_i64;

    loop {
        // Evaluate the statement block
        context.eval_expression_tree(stmt)?;

        count += 1;

        // Declare a new variable every three turns...
        if count % 3 == 0 {
            context.scope_mut().push(format!("{}{}", var_name, count), count);
        }

        // Evaluate the condition expression
        let expr_result = !context.eval_expression_tree(condition)?;

        match expr_result.as_bool() {
            Ok(true) => (),
            Ok(false) => break,
            Err(err) => return Err(EvalAltResult::ErrorMismatchDataType(
                            "bool".to_string(),
                            err.to_string(),
                            condition.position(),
                        ).into()),
        }
    }

    Ok(Dynamic::UNIT)
}

// Register the custom syntax (sample): exec<x> -> { x += 1 } while x < 0
engine.register_custom_syntax(
    &[ "exec", "<", "$ident$", ">", "->", "$block$", "while", "$expr$" ], // the custom syntax
    true,  // variables declared within this custom syntax
    implementation_func
)?;
}

Remember that a custom syntax acts as an expression, so it can show up practically anywhere:


#![allow(unused)]
fn main() {
// Use as an expression:
let foo = (exec<x> -> { x += 1 } while x < 42) * 100;

// New variables are successfully declared...
x == 42;
x3 == 3;
x6 == 6;

// Use as a function call argument:
do_something(exec<x> -> { x += 1 } while x < 42, 24, true);

// Use as a statement:
exec<x> -> { x += 1 } while x < 0;
//                               ^ terminate statement with ';'
}

Step Four – Disable Unneeded Statement Types

When a DSL needs a custom syntax, most likely than not it is extremely specialized. Therefore, many statement types actually may not make sense under the same usage scenario.

So, while at it, better disable those built-in keywords and operators that should not be used by the user. The would leave only the bare minimum language surface exposed, together with the custom syntax that is tailor-designed for the scenario.

A keyword or operator that is disabled can still be used in a custom syntax.

In an extreme case, it is possible to disable every keyword in the language, leaving only custom syntax (plus possibly expressions). But again, Don’t Do It™ – unless you are certain of what you’re doing.

Step Five – Document

For custom syntax, documentation is crucial.

Make sure there are lots of examples for users to follow.

Step Six – Profit!

Really Advanced – Custom Parsers

Sometimes it is desirable to have multiple custom syntax starting with the same symbol. This is especially common for command-style syntax where the second symbol calls a particular command:


#![allow(unused)]
fn main() {
// The following simulates a command-style syntax, all starting with 'perform'.
perform hello world;        // A fixed sequence of symbols
perform action 42;          // Perform a system action with a parameter
perform update system;      // Update the system
perform check all;          // Check all system settings
perform cleanup;            // Clean up the system
perform add something;      // Add something to the system
perform remove something;   // Delete something from the system
}

Alternatively, a custom syntax may have variable length, with a termination symbol:


#![allow(unused)]
fn main() {
// The following is a variable-length list terminated by '>'  
tags < "foo", "bar", 123, ... , x+y, true >
}

For even more flexibility in order to handle these advanced use cases, there is a low level API for custom syntax that allows the registration of an entire mini-parser.

Use Engine::register_custom_syntax_raw to register a custom syntax parser together with the implementation function.

How Custom Parsers Work

A custom parser takes as input parameters two pieces of information:

  • The symbols parsed so far; $ident$ is replaced with the actual identifier parsed, while $expr$ and $block$ stay as they were.

    The custom parser can inspect this symbols stream to determine the next symbol to parse.

  • The look-ahead symbol, which is the symbol that will be parsed next.

    If the look-ahead is an expected symbol, the customer parser just returns it to continue parsing, or it can return $ident$ to parse it as an identifier, or even $expr$ to start parsing an expression.

    If the look-ahead is {, then the custom parser may also return $block$ to start parsing a statements block.

    If the look-ahead is unexpected, the custom parser should then return the symbol expected and Rhai will fail with a parse error containing information about the expected symbol.

A custom parser always returns the next symbol expected, which can also be $ident$, $expr$ or $block$, or None if parsing should terminate (without reading the look-ahead symbol).

Example


#![allow(unused)]
fn main() {
engine.register_custom_syntax_raw(
    "perform",
    // The custom parser implementation - always returns the next symbol expected
    // 'look_ahead' is the next symbol about to be read
    |symbols, look_ahead| match symbols.len() {
        // perform ...
        1 => Ok(Some("$ident$".to_string())),
        // perform command ...
        2 => match symbols[1].as_str() {
            "action" => Ok(Some("$expr$".into())),
            "hello" => Ok(Some("world".into())),
            "update" | "check" | "add" | "remove" => Ok(Some("$ident$".into())),
            "cleanup" => Ok(None),
            cmd => Err(ParseError(Box::new(ParseErrorType::BadInput(
                LexError::ImproperSymbol(format!("Improper command: {}", cmd))
            )), Position::NONE)),
        },
        // perform command arg ...
        3 => match (symbols[1].as_str(), symbols[2].as_str()) {
            ("action", _) => Ok(None),
            ("hello", "world") => Ok(None),
            ("update", arg) if arg == "system" => Ok(None),
            ("update", arg) if arg == "client" => Ok(None),
            ("check", arg) => Ok(None),
            ("add", arg) => Ok(None),
            ("remove", arg) => Ok(None),
            (cmd, arg) => Err(ParseError(Box::new(ParseErrorType::BadInput(
                LexError::ImproperSymbol(
                    format!("Invalid argument for command {}: {}", cmd, arg)
                )
            )), Position::NONE)),
        },
        _ => unreachable!(),
    },
    // No variables declared/removed by this custom syntax
    false,
    // Implementation function
    implementation_func
);
}

Function Signature

The custom syntax parser has the following signature:

Fn(symbols: &[ImmutableString], look_ahead: &str) -> Result<Option<ImmutableString>, ParseError>

where:

ParameterTypeDescription
symbols&[ImmutableString]a slice of symbols that have been parsed so far, possibly containing $expr$ and/or $block$; $ident$ is replaced by the actual identifier
look_ahead&stra string slice containing the next symbol that is about to be read

Most strings are ImmutableString‘s so it is usually more efficient to just clone the appropriate one (if any matches, or keep an internal cache for commonly-used symbols) as the return value.

Return Value

The return value is Result<Option<ImmutableString>, ParseError> where:

ValueDescription
Ok(None)parsing complete and there are no more symbols to match
Ok(Some(symbol))the next symbol to match, which can also be $expr$, $ident$ or $block$
Err(ParseError)error that is reflected back to the Engine – normally ParseError(ParseErrorType::BadInput(LexError::ImproperSymbol(message)), Position::NONE) to indicate that there is a syntax error, but it can be any ParseError.

Multiple Instantiation

Background

Rhai’s features are not strictly additive. This is easily deduced from the no_std feature which prepares the crate for no-std builds. Obviously, turning on this feature has a material impact on how Rhai behaves.

Many crates resolve this by going the opposite direction: build for no-std in default, but add a std feature, included by default, which builds for the stdlib.

Rhai Language Features Are Not Additive

Rhai, however, is more complex. Language features cannot be easily made additive.

That is because the lack of a language feature is a feature by itself.

For example, by including no_float, a project sets the Rhai language to ignore floating-point math. Floating-point numbers do not even parse under this case and will generate syntax errors. Assume that the project expects this behavior (why? perhaps integers are all that make sense within the project domain).

Now, assume that a dependent crate also depends on Rhai. Under such circumstances, unless exact versioning is used and the dependent crate depends on a different version of Rhai, Cargo automatically merges both dependencies, with the no_float feature turned on because Cargo features are additive.

This will break the dependent crate, which does not by itself specify no_float and expects floating-point numbers and math to work normally.

There is no way out of this dilemma. Reversing the features set with a float feature causes the project to break because floating-point numbers are not rejected as expected.

Multiple Instantiations of Rhai Within The Same Project

The trick is to differentiate between multiple identical copies of Rhai, each having a different features set, by their sources:

Use the following configuration in Cargo.toml to pull in multiple copies of Rhai within the same project:

[dependencies]
rhai = { version = "1.0.0", features = [ "no_float" ] }
rhai_github = { git = "https://github.com/rhaiscript/rhai", features = [ "unchecked" ] }
rhai_my_github = { git = "https://github.com/my_github/rhai", branch = "variation1", features = [ "serde", "no_closure" ] }
rhai_local = { path = "../rhai_copy" }

The example above creates four different modules: rhai, rhai_github, rhai_my_github and rhai_local, each referring to a different Rhai copy with the appropriate features set.

Only one crate of any particular version can be used from each source, because Cargo merges all candidate cases within the same source, adding all features together.

If more than four different instantiations of Rhai is necessary (why?), create more local repositories or GitHub forks or branches.

Caveat – No Way To Avoid Dependency Conflicts

Unfortunately, pulling in Rhai from different sources do not resolve the problem of features conflict between dependencies. Even overriding crates.io via the [patch] manifest section doesn’t work – all dependencies will eventually find the only one copy.

What is necessary – multiple copies of Rhai, one for each dependent crate that requires it, together with their unique features set intact. In other words, turning off Cargo’s crate merging feature just for Rhai.

Unfortunately, as of this writing, there is no known method to achieve it.

Therefore, moral of the story: avoid pulling in multiple crates that depend on Rhai.

Functions Metadata

The metadata of a function means all relevant information related to a function’s definition including:

  1. Its callable name

  2. Its access mode (public or private)

  3. Its parameters and types (if any)

  4. Its return value and type (if any)

  5. Its nature (i.e. native Rust-based or Rhai script-based)

  6. Its namespace (module or global)

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

  8. Usage notes, warnings, 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] fn_name ( param_1: type_1, param_2: type_2, ... , param_n : type_n ) -> return_type

Exporting metadata requires the metadata feature.

Generate 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 function available to that Engine instance.

fn_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 global modules registered via Engine::register_global_module (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 and their actual types because Rust simply does not make such metadata available natively. The return type is also undetermined.

A function registered under the name foo with three parameters and unknown return type:

foo(_, _, _)

An operator function – again, unknown parameters and return type. Notice that function names do not need to be valid identifiers.

+(_, _)

A property setter – again, unknown parameters and return type. 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(_, _, _)

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.

A script-defined function always takes dynamic arguments, and the return type is also dynamic, so no type information is needed:

foo(x, y, z)

probably defined as:


#![allow(unused)]
fn main() {
fn foo(x, y, z) {
    ...
}
}

is 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 the metadata describing the functions.

For example, a plugin function merge:

merge(list: &mut MyStruct<i64>, num: usize, name: &str) -> Option<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>, num: usize, name: &str) -> Result<bool, Box<EvalAltResult>>

For example, a property getter defined in a plugin module:

get$prop(obj: &mut MyStruct<i64>) -> String

Export Functions Metadata to JSON

Engine::gen_fn_metadata_to_json
Engine::gen_fn_metadata_with_ast_to_json

As part of a reflections API, Engine::gen_fn_metadata_to_json and the corresponding Engine::gen_fn_metadata_with_ast_to_json export the full list of functions metadata in JSON format.

The metadata feature must be used to turn on this API, which requires the serde_json crate.

Sources

Functions from the following sources are included:

  1. Script-defined functions in an AST (for Engine::gen_fn_metadata_with_ast_to_json)
  2. Native Rust functions registered into the global namespace via the Engine::register_XXX API
  3. Native Rust functions in global modules registered via Engine::register_global_module (optional)
  4. Public (i.e. non-private) functions (native Rust or Rhai scripted) in static modules registered via Engine::register_static_module

Notice that if a function has been overloaded, only the overriding function’s metadata is included.

JSON Schema

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

Modules Schema

{
  "modules":
  {
    "sub_module_1":
    {
      "modules":
      {
        "sub_sub_module_A":
        {
          "functions":
          [
            { ... function metadata ... },
            { ... function metadata ... },
            { ... function metadata ... },
            { ... function metadata ... },
            ...
          ]
        },
        "sub_sub_module_B":
        {
            ...
        }
      }
    },
    "sub_module_2":
    {
      ...
    },
    ...
  },
  "functions":
  [
    { ... function metadata ... },
    { ... function metadata ... },
    { ... function metadata ... },
    { ... function metadata ... },
    ...
  ]
}

Function Metadata Schema

{
  "namespace": "internal" | "global",
  "access": "public" | "private",
  "name": "fn_name",
  "type": "native" | "script",
  "numParams": 42,  /* number of parameters */
  "params":  /* omitted if no parameters */
  [
    { "name": "param_1", "type": "type_1" },
    { "name": "param_2" },  /* no type info */
    { "name": "_", "type": "type_3" },
    ...
  ],
  "returnType": "ret_type",  /* omitted if 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 */",
    ...
  ]
}

Usage Patterns

Leverage the full power and flexibility of Rhai in different scenarios.

Object-Oriented Programming (OOP)

Rhai does not have objects per se, but it is possible to simulate object-oriented programming.

Use Object Maps to Simulate OOP

Rhai’s object maps has special support for OOP.

Rhai conceptMaps to OOP
Object mapsobjects
Object map properties holding valuesproperties
Object map properties that hold function pointersmethods

When a property of an object map is called like a method function, and if it happens to hold a valid function pointer (perhaps defined via an anonymous function or more commonly as a closure), then the call will be dispatched to the actual function with this binding to the object map itself.

Use Closures to Define Methods

Anonymous functions or closures defined as values for object map properties take on a syntactic shape which resembles very closely that of class methods in an OOP language.

Closures also capture variables from the defining environment, which is a very common language feature. Capturing is accomplished via a feature called automatic currying and can be turned off via the no_closure feature.


#![allow(unused)]
fn main() {
let factor = 1;

// Define the object
let obj = #{
        data: 0,                            // object field
        increment: |x| this.data += x,      // 'this' binds to 'obj'
        update: |x| this.data = x * factor, // 'this' binds to 'obj', 'factor' is captured
        action: || print(this.data)         // 'this' binds to 'obj'
    };

// Use the object
obj.increment(1);
obj.action();                               // prints 1

obj.update(42);
obj.action();                               // prints 42

factor = 2;

obj.update(42);
obj.action();                               // prints 84
}

Simulating Inheritance with Polyfills

The fill_with method of object maps can be conveniently used to polyfill default method implementations from a base class, as per OOP lingo.

Do not use the mixin method because it overwrites existing fields.


#![allow(unused)]
fn main() {
// Define base class
let BaseClass = #{
    factor: 1,
    data: 42,

    get_data: || this.data * 2,
    update: |x| this.data += x * this.factor
};

let obj = #{
    // Override base class field
    factor: 100,

    // Override base class method
    // Notice that the base class can also be accessed, if in scope
    get_data: || this.call(BaseClass.get_data) * 999,
}

// Polyfill missing fields/methods
obj.fill_with(BaseClass);

// By this point, 'obj' has the following:
//
// #{
//      factor: 100
//      data: 42,
//      get_data: || this.call(BaseClass.get_data) * 999,
//      update: |x| this.data += x * this.factor
// }

// obj.get_data() => (this.data (42) * 2) * 999
obj.get_data() == 83916;

obj.update(1);

obj.data == 142
}

Prototypical Inheritance via Mixin

Some languages like JavaScript has prototypical inheritance, which bases inheritance on a prototype object.

It is possible to simulate this form of inheritance using object maps, leveraging the fact that, in Rhai, all values are cloned and there are no pointers. This significantly simplifies coding logic.


#![allow(unused)]
fn main() {
// Define prototype 'class'

const PrototypeClass = #{
    field: 42,

    get_field: || this.field,
    set_field: |x| this.field = x
};

// Create instances of the 'class'

let obj1 = PrototypeClass;                  // a copy of 'PrototypeClass'

obj1.get_field() == 42;

let obj2 = PrototypeClass;                  // a copy of 'PrototypeClass'

obj2.mixin(#{                               // override fields and methods
    field: 1,
    get_field: || this.field * 2
};

obj2.get_field() == 2;

let obj2 = PrototypeClass + #{              // compact syntax with '+'
    field: 1,
    get_field: || this.field * 2
};

obj2.get_field() == 2;

// Inheritance chain

const ParentClass = #{
    field: 123,
    new_field: 0,
    action: || print(this.new_field * this.field)
};

const ChildClass = #{
    action: || {
        this.field = this.new_field;
        this.new_field = ();
    }
}

let obj3 = PrototypeClass + ParentClass + ChildClass;

// Alternate formulation

const ParentClass = PrototypeClass + #{
    field: 123,
    new_field: 0,
    action: || print(this.new_field * this.field)
};

const ChildClass = ParentClass + #{
    action: || {
        this.field = this.new_field;
        this.new_field = ();
    }
}

let obj3 = ChildClass;                      // a copy of 'ChildClass'
}

Working With Rust Enums

Enums in Rust are typically used with pattern matching.

Rhai is dynamic, so although it integrates with Rust enum variants just fine (treated transparently as custom types), it is impossible (short of registering a complete API) to distinguish between individual enum variants or to extract internal data from them.

Simulate an Enum API

A plugin module is extremely handy in creating an entire API for a custom enum type.


#![allow(unused)]
fn main() {
use rhai::plugin::*;
use rhai::{Dynamic, Engine, EvalAltResult};

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum MyEnum {
    Foo,
    Bar(i64),
    Baz(String, bool),
}

// Create a plugin module with functions constructing the 'MyEnum' variants
#[export_module]
mod MyEnumModule {
    // Constructors for 'MyEnum' variants
    pub const Foo: &MyEnum = MyEnum::Foo;
    pub fn Bar(value: i64) -> MyEnum { MyEnum::Bar(value) }
    pub fn Baz(val1: String, val2: bool) -> MyEnum { MyEnum::Baz(val1, val2) }
    // Access to fields
    #[rhai_fn(global, get = "enum_type", pure)]
    pub fn get_type(my_enum: &mut MyEnum) -> String {
        match my_enum {
            MyEnum::Foo => "Foo".to_string(),
            MyEnum::Bar(_) => "Bar".to_string(),
            MyEnum::Baz(_, _) => "Baz".to_string(),
        }
    }
    #[rhai_fn(global, get = "field_0", pure)]
    pub fn get_field_0(my_enum: &mut MyEnum) -> Dynamic {
        match my_enum {
            MyEnum::Foo => Dynamic::UNIT,
            MyEnum::Bar(x) => Dynamic::from(x),
            MyEnum::Baz(x, _) => Dynamic::from(x),
        }
    }
    #[rhai_fn(global, get = "field_1", pure)]
    pub fn get_field_1(my_enum: &mut MyEnum) -> Dynamic {
        match my_enum {
            MyEnum::Foo | MyEnum::Bar(_) => Dynamic::UNIT,
            MyEnum::Baz(_, x) => Dynamic::from(x),
        }
    }
    // Printing
    #[rhai_fn(global, name = "to_string", name = "to_debug", pure)]
    pub fn to_string(my_enum: &mut MyEnum) -> String {
        format!("{:?}", my_enum)
    }
    // '==' and '!=' operators
    #[rhai_fn(global, name = "==", pure)]
    pub fn eq(my_enum: &mut MyEnum, my_enum2: MyEnum) -> bool {
        my_enum == &my_enum2
    }
    #[rhai_fn(global, name = "!=", pure)]
    pub fn neq(my_enum: &mut MyEnum, my_enum2: MyEnum) -> bool {
        my_enum != &my_enum2
    }
}

let mut engine = Engine::new();

// Load the module as the module namespace "MyEnum"
engine.register_type_with_name::<MyEnum>("MyEnum")
      .register_static_module("MyEnum", exported_module!(MyEnumModule).into());
}

With this API in place, working with enums feels almost the same as in Rust:


#![allow(unused)]
fn main() {
let x = MyEnum::Foo;

let y = MyEnum::Bar(42);

let z = MyEnum::Baz("hello", true);

x == MyEnum::Foo;

y != MyEnum::Bar(0);

// Detect enum types

x.enum_type == "Foo";

y.enum_type == "Bar";

z.enum_type == "Baz";

// Extract enum fields

y.field_0 == 42;

y.field_1 == ();

z.field_0 == "hello";

z.field_1 == true;
}

Since enums are internally treated as custom types, they are not literals and cannot be used as a match case in switch expressions. This is quite a limitation because the equivalent match statement is commonly used in Rust to work with enums and bind variables to variant-internal data.

It is possible, however, to switch through enum variants based on their types:

switch my_enum.enum_type {
  "Foo" => ...,
  "Bar" => {
    let value = foo.field_0;
    ...
  }
  "Baz" => {
    let val1 = foo.field_0;
    let val2 = foo.field_1;
    ...
  }
}

Use switch Through Arrays

Another way to work with Rust enums in a switch expression is through exposing the internal data (or at least those that act as effective discriminants) of each enum variant as a variable-length array, usually with the name of the variant as the first item for convenience:


#![allow(unused)]
fn main() {
use rhai::Array;

engine.register_get("enum_data", |my_enum: &mut MyEnum| {
    match my_enum {
        MyEnum::Foo => vec![ "Foo".into() ] as Array,

        // Say, skip the data field because it is not
        // used as a discriminant
        MyEnum::Bar(value) => vec![ "Bar".into() ] as Array,

        // Say, all fields act as discriminants
        MyEnum::Baz(val1, val2) => vec![
            "Baz".into(), val1.clone().into(), (*val2).into()
        ] as Array
    }
});
}

Then it is a simple matter to match an enum via the switch expression:

// Assume 'value' = 'MyEnum::Baz("hello", true)'
// 'enum_data' creates a variable-length array with 'MyEnum' data
let x = switch value.enum_data {
    ["Foo"] => 1,
    ["Bar"] => value.field_1,
    ["Baz", "hello", false] => 4,
    ["Baz", "hello", true] => 5,
    _ => 9
};

x == 5;

// Which is essentially the same as:
let x = switch [value.type, value.field_0, value.field_1] {
    ["Foo", (), ()] => 1,
    ["Bar", 42, ()] => 42,
    ["Bar", 123, ()] => 123,
            :
    ["Baz", "hello", false] => 4,
    ["Baz", "hello", true] => 5,
    _ => 9
}

Usually, a helper method returns an array of values that can uniquely determine the switch case based on actual usage requirements – which means that it probably skips fields that contain data instead of discriminants.

Then switch is used to very quickly match through a large number of array shapes and jump to the appropriate case implementation.

Data fields can then be extracted from the enum independently.

Loadable Configuration

Usage Scenario

  • A system where settings and configurations are complex and logic-driven.

  • Where said system is too complex to configure via standard configuration file formats such as JSON, TOML or YAML.

  • The system is complex enough to require a full programming language to configure. Essentially configuration by code.

  • Yet the configuration must be flexible, late-bound and dynamically loadable, just like a configuration file.

Key Concepts

  • Leverage the loadable modules of Rhai. The no_module feature must not be on.

  • Expose the configuration API. Use separate scripts to configure that API. Dynamically load scripts via the import statement.

  • Leverage function overloading to simplify the API design.

  • Since Rhai is sand-boxed, it cannot mutate the environment. To modify the external configuration object via an API, it must be wrapped in a RefCell (or RwLock/Mutex for sync) and shared to the Engine.

Implementation

Configuration Type


#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Default)]
struct Config {
    pub id: String;
    pub some_field: i64;
    pub some_list: Vec<String>;
    pub some_map: HashMap<String, bool>;
}
}

Make Shared Object


#![allow(unused)]
fn main() {
pub type SharedConfig = Rc<RefCell<Config>>;
}

or in multi-threaded environments with the sync feature, use one of the following:


#![allow(unused)]
fn main() {
let config: SharedConfig = Arc<RwLock<Config>>;

let config: SharedConfig = Arc<Mutex<Config>>;
}

Register Config API

The trick to building a Config API is to clone the shared configuration object and move it into each function registration via a closure.

Therefore, it is not possible to use a plugin module to achieve this, and each function must be registered one after another.


#![allow(unused)]
fn main() {
// Notice 'move' is used to move the shared configuration object into the closure.
let cfg = config.clone();
engine.register_fn("config_set_id", move |id: String| *cfg.borrow_mut().id = id);

let cfg = config.clone();
engine.register_fn("config_get_id", move || cfg.borrow().id.clone());

let cfg = config.clone();
engine.register_fn("config_set", move |value: i64| *cfg.borrow_mut().some_field = value);

// Remember Rhai functions can be overloaded when designing the API.

let cfg = config.clone();
engine.register_fn("config_add", move |value: String|
    cfg.borrow_mut().some_list.push(value)
);

let cfg = config.clone();
engine.register_fn("config_add", move |values: &mut Array|
    cfg.borrow_mut().some_list.extend(values.into_iter().map(|v| v.to_string()))
);

let cfg = config.clone();
engine.register_fn("config_add", move |key: String, value: bool|
    cfg.borrow_mut().some_map.insert(key, value)
);

let cfg = config.clone();
engine.register_fn("config_contains", move |value: String|
    cfg.borrow().some_list.contains(&value)
);

let cfg = config.clone();
engine.register_fn("config_is_set", move |value: String|
    cfg.borrow().some_map.get(&value).cloned().unwrap_or(false)
);
}

Configuration Script


#![allow(unused)]
fn main() {
+----------------+
| my_config.rhai |
+----------------+

config_set_id("hello");

config_add("foo");          // add to list
config_add("bar", true);    // add to map

if config_contains("hey") || config_is_set("hey") {
    config_add("baz", false);   // add to map
}
}

Load the Configuration


#![allow(unused)]
fn main() {
import "my_config";         // run configuration script without creating a module

let id = config_get_id();

id == "hello";
}

Consider a Custom Syntax

This is probably one of the few scenarios where a custom syntax can be recommended.

A properly-designed custom syntax can make the configuration file clean, simple to write, easy to understand and quick to modify.

For example, the above configuration example may be expressed by this custom syntax:


#![allow(unused)]
fn main() {
+----------------+
| my_config.rhai |
+----------------+

// Configure ID
id "hello";

// Add to list
list + "foo";

// Add to map
map "bar" => true;

if config contains "hey" || config is_set "hey" {
    map "baz" => false;
}
}

Notice that contains and is_set may be implemented as a custom operator.

Hot Reloading

Usage Scenario

  • A system where scripts are used for behavioral control.

  • All or parts of the control scripts need to be modified dynamically without re-initializing the host system.

  • New scripts must be active as soon as possible after modifications are detected.

Key Concepts

  • The Rhai Engine is re-entrant, meaning that it is decoupled from scripts.

  • A new script only needs to be recompiled and the new AST replaces the old for new behaviors to be active.

  • Surgically patch scripts when only parts of the scripts are modified.

Implementation

Embed scripting engine and script into system

Say, a system has a Rhai Engine plus a compiled script (in AST form)...


#![allow(unused)]
fn main() {
// Main system object
struct System {
    engine: Engine,
    script: AST,
      :
}

let mut system = System::new();

// Embed Rhai 'Engine' and control script
system.engine = Engine::new();
system.ast = system.engine.compile_file("config.rhai")?;

// Handle events with script functions
system.on_event(|sys: &System, event: &str, data: Map| {
    let mut scope = Scope::new();

    // Call script function which is the same name as the event
    sys.engine.call_fn(&mut scope, &sys.ast, event, (data,)).unwrap();

    result
});
}

Hot reload entire script upon change

If the control scripts are small enough and changes are infrequent, it is much simpler just to recompile the whole set of script and replace the original AST with the new one.


#![allow(unused)]
fn main() {
// Watch for script file change
system.watch(|sys: &mut System, file: &str| {
    // Compile the new script
    let ast = sys.engine.compile_file(file.into())?;

    // Hot reload - just replace the old script!
    sys.ast = ast;
});
}

Hot load specific functions via patching

If the control scripts are large and complicated, and if the system can detect changes to specific functions, it is also possible to patch just the changed functions.


#![allow(unused)]
fn main() {
// Watch for changes in the script
system.watch_for_script_change(|sys: &mut System, fn_name: &str| {
    // Get the script file that contains the function
    let script = get_script_file_path(fn_name);

    // Compile the new script
    let mut patch_ast = sys.engine.compile_file(script)?;

    // Remove everything other than the specified function
    patch_ast.clear_statements();
    patch_ast.retain_functions(|_, _, name, _| name == fn_name);

    // Hot reload (via +=) only those functions in the script!
    sys.ast += patch_ast;
});
}

Scriptable Control Layer Over Rust Backend

Usage Scenario

  • A system provides core functionalities, but no driving logic.

  • The driving logic must be dynamic and hot-loadable.

  • A script is used to drive the system and provide control intelligence.

Key Concepts

  • Expose a Control API.

  • Leverage function overloading to simplify the API design.

  • Since Rhai is sand-boxed, it cannot mutate the environment. To perform external actions via an API, the actual system must be wrapped in a RefCell (or RwLock/Mutex for sync) and shared to the Engine.

Implementation

There are two broad ways for Rhai to control an external system, both of which involve wrapping the system in a shared, interior-mutated object.

This is one way which does not involve exposing the data structures of the external system, but only through exposing an abstract API primarily made up of functions.

Use this when the API is relatively simple and clean, and the number of functions is small enough.

For a complex API involving lots of functions, or an API that has a clear object structure, use the Singleton Command Object pattern instead.

Functional API

Assume that a system provides the following functional API:


#![allow(unused)]
fn main() {
struct EnergizerBunny;

impl EnergizerBunny {
    pub fn new () -> Self { ... }
    pub fn go (&mut self) { ... }
    pub fn stop (&mut self) { ... }
    pub fn is_going (&self) { ... }
    pub fn get_speed (&self) -> i64 { ... }
    pub fn set_speed (&mut self, speed: i64) { ... }
}
}

Wrap API in Shared Object


#![allow(unused)]
fn main() {
pub type SharedBunny = Rc<RefCell<EnergizerBunny>>;
}

or in multi-threaded environments with the sync feature, use one of the following:


#![allow(unused)]
fn main() {
pub type SharedBunny = Arc<RwLock<EnergizerBunny>>;

pub type SharedBunny = Arc<Mutex<EnergizerBunny>>;
}

Register Control API

The trick to building a Control API is to clone the shared API object and move it into each function registration via a closure.

Therefore, it is not possible to use a plugin module to achieve this, and each function must be registered one after another.


#![allow(unused)]
fn main() {
// Notice 'move' is used to move the shared API object into the closure.
let b = bunny.clone();
engine.register_fn("bunny_power", move |on: bool| {
    if on {
        if b.borrow().is_going() {
            println!("Still going...");
        } else {
            b.borrow_mut().go();
        }
    } else {
        if b.borrow().is_going() {
            b.borrow_mut().stop();
        } else {
            println!("Already out of battery!");
        }
    }
});

let b = bunny.clone();
engine.register_fn("bunny_is_going", move || b.borrow().is_going());

let b = bunny.clone();
engine.register_fn("bunny_get_speed", move ||
    if b.borrow().is_going() { b.borrow().get_speed() } else { 0 }
);

let b = bunny.clone();
engine.register_result_fn("bunny_set_speed", move |speed: i64|
    if speed <= 0 {
        return Err("Speed must be positive!".into());
    } else if speed > 100 {
        return Err("Bunny will be going too fast!".into());
    }

    if b.borrow().is_going() {
        b.borrow_mut().set_speed(speed)
    } else {
        return Err("Bunny is not yet going!".into());
    }

    Ok(())
);
}

Use the API


#![allow(unused)]
fn main() {
if !bunny_is_going() { bunny_power(true); }

if bunny_get_speed() > 50 { bunny_set_speed(50); }
}

Caveat

Although this usage pattern appears a perfect fit for game logic, avoid writing the entire game in Rhai. Performance will not be acceptable.

Implement as much functionalities of the game engine in Rust as possible. Rhai integrates well with Rust so this is usually not a hinderance.

Lift as much out of Rhai as possible. Use Rhai only for the logic that must be dynamic or hot-loadable.

Singleton Command Object

Usage Scenario

  • A system provides core functionalities, but no driving logic.

  • The driving logic must be dynamic and hot-loadable.

  • A script is used to drive the system and provide control intelligence.

  • The API is multiplexed, meaning that it can act on multiple system-provided entities, or

  • The API lends itself readily to an object-oriented (OO) representation.

Key Concepts

  • Expose a Command type with an API. The no_object feature must not be on.

  • Leverage function overloading to simplify the API design.

  • Since Rhai is sand-boxed, it cannot mutate the environment. To perform external actions via an API, the command object type must be wrapped in a RefCell (or RwLock/Mutex for sync) and shared to the Engine.

  • Load each command object into a custom Scope as constant variables.

  • Control each command object in script via the constants.

Implementation

There are two broad ways for Rhai to control an external system, both of which involve wrapping the system in a shared, interior-mutated object.

This is the other way which involves directly exposing the data structures of the external system as a name singleton object in the scripting space.

Use this when the API is complex but has a clear object structure.

For a relatively simple API that is action-based and not object-based, use the Control Layer pattern instead.

Functional API

Assume the following command object type:


#![allow(unused)]
fn main() {
struct EnergizerBunny { ... }

impl EnergizerBunny {
    pub fn new () -> Self { ... }
    pub fn go (&mut self) { ... }
    pub fn stop (&mut self) { ... }
    pub fn is_going (&self) -> bol { ... }
    pub fn get_speed (&self) -> i64 { ... }
    pub fn set_speed (&mut self, speed: i64) { ... }
    pub fn turn (&mut self, left_turn: bool) { ... }
}
}

Wrap Command Object Type as Shared


#![allow(unused)]
fn main() {
pub type SharedBunny = Rc<RefCell<EnergizerBunny>>;
}

or in multi-threaded environments with the sync feature, use one of the following:


#![allow(unused)]
fn main() {
pub type SharedBunny = Arc<RwLock<EnergizerBunny>>;

pub type SharedBunny = Arc<Mutex<EnergizerBunny>>;
}

Register the Custom Type


#![allow(unused)]
fn main() {
engine.register_type_with_name::<SharedBunny>("EnergizerBunny");
}

Develop a Plugin with Methods and Getters/Setters

The easiest way to develop a complete set of API for a custom type is via a plugin module.

Notice that putting pure in #[rhai_fn(...)] allows a getter/setter to operate on a constant without raising an error.


#![allow(unused)]
fn main() {
use rhai::plugin::*;

#[export_module]
pub mod bunny_api {
    pub const MAX_SPEED: i64 = 100;

    #[rhai_fn(get = "power", pure)]
    pub fn get_power(bunny: &mut SharedBunny) -> bool {
        bunny.borrow().is_going()
    }
    #[rhai_fn(set = "power", pure)]
    pub fn set_power(bunny: &mut SharedBunny, on: bool) {
        if on {
            if bunny.borrow().is_going() {
                println!("Still going...");
            } else {
                bunny.borrow_mut().go();
            }
        } else {
            if bunny.borrow().is_going() {
                bunny.borrow_mut().stop();
            } else {
                println!("Already out of battery!");
            }
        }
    }
    #[rhai_fn(get = "speed", pure)]
    pub fn get_speed(bunny: &mut SharedBunny) -> i64 {
        if bunny.borrow().is_going() {
            bunny.borrow().get_speed()
        } else {
            0
        }
    }
    #[rhai_fn(set = "speed", pure, return_raw)]
    pub fn set_speed(bunny: &mut SharedBunny, speed: i64)
            -> Result<(), Box<EvalAltResult>>
    {
        if speed <= 0 {
            Err("Speed must be positive!".into())
        } else if speed > MAX_SPEED {
            Err("Bunny will be going too fast!".into())
        } else if !bunny.borrow().is_going() {
            Err("Bunny is not yet going!".into())
        } else {
            b.borrow_mut().set_speed(speed);
            Ok(())
        }
    }
    pub fn turn_left(bunny: &mut SharedBunny) {
        if bunny.borrow().is_going() {
            bunny.borrow_mut().turn(true);
        }
    }
    pub fn turn_right(bunny: &mut SharedBunny) {
        if bunny.borrow().is_going() {
            bunny.borrow_mut().turn(false);
        }
    }
}

engine.register_global_module(exported_module!(bunny_api).into());
}

Compile script into AST


#![allow(unused)]
fn main() {
let ast = engine.compile(script)?;
}

Push Constant Command Object into Custom Scope and Run AST


#![allow(unused)]
fn main() {
let bunny: SharedBunny = Rc::new(RefCell::new(EnergizerBunny::new()));

let mut scope = Scope::new();

// Add the command object into a custom Scope.
// Constants, as a convention, are named with all-capital letters.
scope.push_constant("BUNNY", bunny.clone());

// Run the compiled AST
engine.consume_ast_with_scope(&mut scope, &ast)?;

// Running the script directly, as below, is less desirable because
// the constant 'BUNNY' will be propagated and copied into each usage
// during the script optimization step
engine.consume_with_scope(&mut scope, script)?;
}

Use the Command API in Script


#![allow(unused)]
fn main() {
// Access the command object via constant variable 'BUNNY'.

if !BUNNY.power { BUNNY.power = true; }

if BUNNY.speed > 50 { BUNNY.speed = 50; }

BUNNY.turn_left();
}

Multi-Layer Functions

Usage Scenario

  • A system is divided into separate layers, each providing logic in terms of scripted functions.

  • A lower layer provides default implementations of certain functions.

  • Higher layers each provide progressively more specific implementations of the same functions.

  • A more specific function, if defined in a higher layer, always overrides the implementation in a lower layer.

  • This is akin to object-oriented programming but with functions.

  • This type of system is extremely convenient for dynamic business rules configuration, setting corporate-wide policies, granting permissions for specific roles etc. where specific, local rules need to override corporate-wide defaults.

Key Concepts

  • Each layer is a separate script.

  • The lowest layer script is compiled into a base AST.

  • Higher layer scripts are also compiled into AST and combined into the base using AST::combine (or the += operator), overriding any existing functions.

Examples

Assume the following four scripts:


#![allow(unused)]
fn main() {
+--------------+
| default.rhai |
+--------------+

// Default implementation of 'foo'.
fn foo(x) { x + 1 }

// Default implementation of 'bar'.
fn bar(x, y) { x + y }

// Default implementation of 'no_touch'.
fn no_touch() { throw "do not touch me!"; }


+-------------+
| lowest.rhai |
+-------------+

// Specific implementation of 'foo'.
fn foo(x) { x * 2 }

// New implementation for this layer.
fn baz() { print("hello!"); }


+-------------+
| middle.rhai |
+-------------+

// Specific implementation of 'bar'.
fn bar(x, y) { x - y }

// Specific implementation of 'baz'.
fn baz() { print("hey!"); }


+--------------+
| highest.rhai |
+--------------+

// Specific implementation of 'foo'.
fn foo(x) { x + 42 }
}

Load and combine them sequentially:


#![allow(unused)]
fn main() {
let engine = Engine::new();

// Compile the baseline default implementations.
let mut ast = engine.compile_file("default.rhai".into())?;

// Combine the first layer.
let lowest = engine.compile_file("lowest.rhai".into())?;
ast += lowest;

// Combine the second layer.
let middle = engine.compile_file("middle.rhai".into())?;
ast += lowest;

// Combine the third layer.
let highest = engine.compile_file("highest.rhai".into())?;
ast += lowest;

// Now, 'ast' contains the following functions:
//
// fn no_touch() {              // from 'default.rhai'
//     throw "do not touch me!";
// }
// fn foo(x) { x + 42 }         // from 'highest.rhai'
// fn bar(x, y) { x - y }       // from 'middle.rhai'
// fn baz() { print("hey!"); }  // from 'middle.rhai'
}

Unfortunately, there is no super call that calls the base implementation (i.e. no way for a higher-layer function to call an equivalent lower-layer function).

One Engine Instance Per Call

Usage Scenario

  • A system where scripts are called a lot, in tight loops or in parallel.

  • Keeping a global Engine instance is sub-optimal due to contention and locking.

  • Scripts need to be executed independently from each other, perhaps concurrently.

  • Scripts are used to create Rust closures that are stored and may be called at any time, perhaps concurrently. In this case, the Engine instance is usually moved into the closure itself.

Key Concepts

  • Rhai’s AST structure is sharable – meaning that one copy of the AST can be run on multiple instances of Engine simultaneously.

  • Rhai’s packages and modules are also sharable.

  • This means that Engine instances can be decoupled from the base system (packages and modules) as well as the scripts (AST) so they can be created very cheaply.

Procedure

Examples


#![allow(unused)]
fn main() {
use rhai::def_package;
use rhai::packages::{Package, StandardPackage};

// Define the custom package 'MyCustomPackage'.

def_package!(rhai:MyCustomPackage:"My own personal super-duper custom package", module, {
    // Aggregate other packages simply by calling 'init' on each.
    StandardPackage::init(module);

    // Register additional Rust functions using 'Module::set_native_fn'.
    let hash = module.set_native_fn("foo", |s: ImmutableString| {
        Ok(foo(s.into_owned()))
    });

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

let ast = /* ... some AST ... */;

let custom_pkg = MyCustomPackage::new();

// The following loop creates 10,000 Engine instances!

for x in 0..10_000 {
    // Create a raw Engine - extremely cheap
    let mut engine = Engine::new_raw();

    // Register custom package - cheap
    engine.register_global_module(custom_pkg.as_shared_module());

    // Evaluate script
    engine.consume_ast(&ast)?;
}
}

Multi-Threaded Synchronization

Usage Scenarios

  • A system needs to communicate with an Engine running in a separate thread.

  • Multiple Engines running in separate threads need to coordinate/synchronize with each other.

Key Concepts

  • An MPSC channel (or any other appropriate synchronization primitive) is used to send/receive messages to/from an Engine running in a separate thread.

  • An API is registered with the Engine that is essentially blocking until synchronization is achieved.

Example

use rhai::{Engine};

fn main() {
    // Channel: Script -> Master
    let (tx_script, rx_master) = std::sync::mpsc::channel();
    // Channel: Master -> Script
    let (tx_master, rx_script) = std::sync::mpsc::channel();

    // Spawn thread with Engine
    std::thread::spawn(move || {
        // Create Engine
        let mut engine = Engine::new();

        // Register API
        // Notice that the API functions are blocking
        engine.register_fn("get", move || rx_script.recv().unwrap())
              .register_fn("put", move |v: i64| tx_script.send(v).unwrap());

        // Run script
        engine.consume(
        r#"
            print("Starting script loop...");

            loop {
                // The following call blocks until there is data
                // in the channel
                let x = get();
                print(`Script Read: ${x}`);

                x += 1;

                print(`Script Write: ${x}`);

                // The following call blocks until the data
                // is successfully sent to the channel
                put(x);
            }
        "#).unwrap();
    });

    // This is the main processing thread

    println!("Starting main loop...");

    let mut value = 0_i64;

    while value < 10 {
        println!("Value: {}", value);
        // Send value to script
        tx_master.send(value).unwrap();
        // Receive value from script
        value = rx_master.recv().unwrap();
    }
}

Considerations for sync

std::mpsc::Sender and std::mpsc::Receiver are not Sync, therefore they cannot be used in registered functions if the sync feature is enabled.

In that situation, it is possible to wrap the Sender and Receiver each in a Mutex, which makes them Sync. This, however, incurs the additional overhead of locking and unlocking the Mutex during every function call, which is technically not necessary because there are no other references to them.

Regarding Async Functions

The example above highlights the fact that Rhai scripts can call any Rust function, including ones that are blocking.

However, Rhai is essentially a blocking, single-threaded engine. Therefore it does not provide an async API.

That means, although it is simple to use Rhai within a multi-threading environment where blocking a thread is acceptable or even expected, it is currently not possible to call async functions within Rhai scripts because there is no mechanism in Engine to wrap the state of the call stack inside a future.

Fortunately an Engine is re-entrant so it can be shared among many async tasks. It is usually possible to split a script into multiple parts to avoid having to call async functions.

Creating an Engine is also relatively cheap (extremely cheap if creating a raw Engine), so it is also a valid pattern to spawn a new Engine instance for each task.

Scriptable Event Handler with State

Usage Scenario

  • A system sends events that must be handled.

  • Flexibility in event handling must be provided, through user-side scripting.

  • State must be kept between invocations of event handlers.

  • Default implementations of event handlers can be provided.

Key Concepts

  • An event handler object is declared that holds the following items:

    • Engine with registered functions serving as an API,
    • AST of the user script,
    • a Scope containing state.
  • Upon an event, the appropriate event handler function in the script is called via Engine::call_fn.

  • Optionally, trap the EvalAltResult::ErrorFunctionNotFound error to provide a default implementation.

Implementation

Declare Handler Object

In most cases, it would be simpler to store an Engine instance together with the handler object because it only requires registering all API functions only once.

In rare cases where handlers are created and destroyed in a tight loop, a new Engine instance can be created for each event. See One Engine Instance Per Call for more details.


#![allow(unused)]
fn main() {
use rhai::{Engine, Scope, AST, EvalAltResult};

// Event handler
struct Handler {
    // Scripting engine
    pub engine: Engine,
    // Use a custom 'Scope' to keep stored state
    pub scope: Scope,
    // Program script
    pub ast: AST
}
}

Register API for Any Custom Type

Custom types are often used to hold state. The easiest way to register an entire API is via a plugin module.


#![allow(unused)]
fn main() {
use rhai::plugin::*;

// A custom type to a hold state value.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct SomeType {
    data: i64;
}

#[export_module]
mod SomeTypeAPI {
    #[rhai_fn(global)]
    pub func1(obj: &mut SomeType) -> bool { ... }
    #[rhai_fn(global)]
    pub func2(obj: &mut SomeType) -> bool { ... }
    pub process(data: i64) -> i64 { ... }
    #[rhai_fn(get = "value", pure)]
    pub get_value(obj: &mut SomeType) -> i64 { obj.data }
    #[rhai_fn(set = "value")]
    pub set_value(obj: &mut SomeType, value: i64) { obj.data = value; }
}
}

Initialize Handler Object

Steps to initialize the event handler:

  1. Register an API with the Engine,
  2. Create a custom Scope to serve as the stored state,
  3. Add default state variables into the custom Scope,
  4. Get the handler script and compile it,
  5. Store the compiled AST for future evaluations,
  6. Run the AST to initialize event handler state variables.

#![allow(unused)]
fn main() {
impl Handler {
    pub new(path: impl Into<PathBuf>) -> Self {
        let mut engine = Engine::new();

        // Register custom types and API's
        engine.register_type_with_name::<SomeType>("SomeType")
              .register_global_module(exported_module!(SomeTypeAPI));

        // Create a custom 'Scope' to hold state
        let mut scope = Scope::new();

        // Add initialized state into the custom 'Scope'
        scope.push("state1", false);
        scope.push("state2", SomeType::new(42));

        // Compile the handler script.
        // In a real application you'd be handling errors...
        let ast = engine.compile_file(path).unwrap();

        // Evaluate the script to initialize it and other state variables.
        // In a real application you'd again be handling errors...
        engine.consume_ast_with_scope(&mut scope, &ast).unwrap();

        // The event handler is essentially these three items:
        Handler { engine, scope, ast }
    }
}
}

Hook up events

There is usually an interface or trait that gets called when an event comes from the system.

Mapping an event from the system into a scripted handler is straight-forward:


#![allow(unused)]
fn main() {
impl Handler {
    // Say there are three events: 'start', 'end', 'update'.
    // In a real application you'd be handling errors...
    pub fn on_event(&mut self, event_name: &str, event_data: i64) -> Result<(), Error> {
        let engine = &self.engine;
        let scope = &mut self.scope;
        let ast = &self.ast;

        match event_name {
            // The 'start' event maps to function 'start'.
            // In a real application you'd be handling errors...
            "start" => engine.call_fn(scope, ast, "start", (event_data,))?,

            // The 'end' event maps to function 'end'.
            // In a real application you'd be handling errors...
            "end" => engine.call_fn(scope, ast, "end", (event_data,))?,

            // The 'update' event maps to function 'update'.
            // This event provides a default implementation when the scripted function
            // is not found.
            "update" =>
                engine.call_fn(scope, ast, "update", (event_data,))
                      .or_else(|err| match *err {
                         EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "update" => {
                            // Default implementation of 'update' event handler
                            self.scope.set_value("state2", SomeType::new(42));
                            // Turn function-not-found into a success
                            Ok(Dynamic::UNIT)
                         }
                         _ => Err(err.into())
                      })?
        }
    }
}
}

Sample Handler Script

Because the stored state is kept in a custom Scope, it is possible for all functions defined in the handler script to access and modify these state variables.

The API registered with the Engine can be also used throughout the script.


#![allow(unused)]
fn main() {
fn start(data) {
    if state1 {
        throw "Already started!";
    }
    if state2.func1() || state2.func2() {
        throw "Conditions not yet ready to start!";
    }
    state1 = true;
    state2.value = data;
}

fn end(data) {
    if !state1 {
        throw "Not yet started!";
    }
    if state2.func1() || state2.func2() {
        throw "Conditions not yet ready to start!";
    }
    state1 = false;
    state2.value = data;
}

fn update(data) {
    state2.value += process(data);
}
}

Dynamic Constants Provider

Usage Scenario

  • A system has a large number of constants, but only a minor set will be used by any script.

  • The system constants are expensive to load.

  • The system constants set is too massive to push into a custom Scope.

  • The values of system constants are volatile and call-dependent.

Key Concepts

  • Use a variable resolver to intercept variable access.

  • Only load a variable when it is being used.

  • Perform a lookup based on variable name, and provide the correct data value.

  • May even perform back-end network access or look up the latest value from a database.

Implementation


#![allow(unused)]
fn main() {
let mut engine = Engine::new();

// Create shared data provider.
// Assume that SystemValuesProvider::get(&str) -> Option<value> gets a value.
let provider = Arc::new(SystemValuesProvider::new());

// Clone the shared provider
let db = provider.clone();

// Register a variable resolver.
// Move the shared provider into the closure.
engine.on_var(move |name, _, _, _| Ok(db.get(name).map(Dynamic::from)));
}

Values are Constants

All values provided by a variable resolver are constants due to their dynamic nature. They cannot be assigned to.

In order to change values in an external system, register a dedicated API for that purpose.

External Tools

External tools available to work with Rhai.

Online Playground

The Online Playground runs off a WASM build of Rhai and allows evaluating Rhai scripts directly within a browser editor window.

Author : @alvinhochun

Repo : on GitHub

URL : link to Online Playground

Rhai Script Documentation Tool

The Rhai Script Documentation Tool, rhai-doc, takes a source directory and scans for Rhai script files (recursively), building a web-based documentation site for all functions defined. Documentation is taken from MarkDown doc-comments on the functions.

Author: @semirix

Repo: on GitHub

Binary: on crates.io

Example: on rhai.rs

Install

cargo install rhai-doc

Flags and Options

Flag/OptionParameterDefaultDescription
-h, --helpprint help
-V, --versionprint version
-a, --allgenerate documentation for all functions, including private ones (default false)
-vuse multiple to set verbosity: 1=silent, 2,3 (default)=full
-c, --config<file>rhai.tomlset configuration file
-D, --dest<directory>distset destination directory for documentation output
-d, --dir<directory>current directoryset source directory for Rhai scripts
-p, --pages<directory>pagesset source directory for additional MarkDown page files to include

Commands

CommandDescriptionExample
nonegenerate documentationrhai-doc
newcreate a skeleton rhai.toml in the source directoryrhai-doc new

Configuration file

A configuration file, which is usually named rhai.toml, contains configuration options for rhai-doc and must be placed in the source directory.

A skeleton rhai.toml can be generated inside the source directory via the new command.

An alternate configuration file can be specified via the --config option.

Example

name = "My Rhai Project"                # project name
color = [246, 119, 2]                   # theme color
root = "/docs/"                         # root URL for generated site
index = "home.md"                       # this file becomes 'index.html'
icon = "logo.svg"                       # project icon
stylesheet = "my_stylesheet.css"        # custom stylesheet
code_theme = "atom-one-light"           # 'highlight.js' theme
code_lang = "ts"                        # default language for code blocks
extension = "rhai"                      # script extension
google_analytics = "G-ABCDEF1234"       # Google Analytics ID

[[links]]                               # external link for 'Blog'
name = "Blog"
link = "https://example.com/blog"

[[links]]                               # external link for 'Tools'
name = "Tools"
link = "https://example.com/tools"

Configuration Options

OptionValue typeDefaultDescription
namestringnonename of project – used as titles on documentation pages
colorRGB values (0-255) array[246, 119, 2]theme color for generated documentation
rootURL stringnoneroot URL generated as part of documentation
indexfile pathnonemain MarkDown file – becomes index.html
iconfile pathRhai iconproject icon
stylesheetfile pathnonecustom stylesheet
code_themetheme stringdefaulthighlight.js theme for syntax highlighting in code blocks
code_langlanguage stringtsdefault language for code blocks
extensionextension string.rhaiscript files extension (default .rhai)
google_analyticsID stringnoneGoogle Analytics ID
[[links]]tablenoneexternal links
namestringnone• title of external link
linkURL stringnone• URL of external link

MarkDown Pages

By default, rhai-doc will generate documentation pages from a pages sub-directory under the scripts directory.

Alternatively, you can specify another location via the --pages option.

Appendix

This section contains miscellaneous reference materials.

Keywords List

KeywordDescriptionInactive underIs function?
trueboolean true literalno
falseboolean false literalno
letvariable declarationno
constconstant declarationno
globalautomatic global moduleno_functionno
ifif statementno
elseelse block of if statementno
switchmatchingno
doloopingno
while1) while loop
2) condition for do loop
no
untildo loopno
loopinfinite loopno
forfor loopno
in1) containment test
2) part of for loop
no
continuecontinue a loop at the next iterationno
breakbreak out of loop iterationno
returnreturn valueno
throwthrow exceptionno
trytrap exceptionno
catchcatch exceptionno
importimport moduleno_moduleno
exportexport variableno_moduleno
asalias for variable exportno_moduleno
privatemark function privateno_functionno
fn (lower-case f)function definitionno_functionno
Fn (capital F)create a function pointeryes
callcall a function pointeryes
currycurry a function pointeryes
is_def_fnis function defined?no_functionyes
is_def_varis variable defined?yes
thisreference to base object for method callno_functionno
type_ofget type name of valueyes
printprint valueyes
debugprint value in debug formatyes
evalevaluate scriptyes

Reserved Keywords

KeywordPotential usage
varvariable declaration
staticvariable declaration
sharedshare value
gotocontrol flow
exitcontrol flow
matchmatching
casematching
publicfunction/field access
protectedfunction/field access
newconstructor
useimport namespace
withscope
modulemodule
packagepackage
superbase class/module
threadthreading
spawnthreading
gothreading
awaitasync
asyncasync
syncasync
yieldasync
defaultspecial value
voidspecial value
nullspecial value
nilspecial value

Operators and Symbols

Operators

OperatorDescriptionBinary?Binding direction
+addyesleft
-1) subtract
2) negative (prefix)
yes
no
left
right
*multiplyyesleft
/divideyesleft
%moduloyesleft
**power/exponentiationyesright
>>right bit-shiftyesleft
<<left bit-shiftyesleft
&1) bit-wise AND
2) boolean AND
yesleft
|1) bit-wise OR
2) boolean OR
yesleft
^1) bit-wise XOR
2) boolean XOR
yesleft
=, +=, -=, *=, /=,
**=, %=, <<=, >>=, &=,
|=, ^=
assignmentsyesn/a
==equals toyesleft
!=not equals toyesleft
>greater thanyesleft
>=greater than or equals toyesleft
<less thanyesleft
<=less than or equals toyesleft
&&boolean AND (short-circuits)yesleft
||boolean OR (short-circuits)yesleft
!boolean NOTnoright
[ .. ]indexingyesleft
.1) property access
2) method call
yesleft

Symbols and Patterns

SymbolNameDescription
_underscoredefault switch case
;semicolonstatement separator
,commalist separator
:colonobject map property value separator
::pathmodule path separator
#{ .. }hash mapobject map literal
" .. "double quotestring
` .. `back-tickmulti-line literal string
' .. 'single quotecharacter
\1) escape
2) line continuation
escape character literal
( .. )parenthesesexpression grouping
{ .. }bracesblock statement
| .. |pipesclosure
[ .. ]bracketsarray literal
!bangfunction call in calling scope
=>double arrowswitch expression case separator
//commentline comment
///doc-commentline [doc-comment]
/* .. */commentblock comment
/** .. */doc-commentblock [doc-comment]
(* .. *)commentreserved
#!shebangreserved
< .. >tagreserved
++incrementreserved
--decrementreserved
..range/restreserved
...restreserved
~tildereserved
#hashreserved
@atreserved
$dollarreserved
->arrowreserved
<-left arrowreserved
===strict equals toreserved
!==strict not equals toreserved
:=assignmentreserved
::< .. >turbofishreserved

Literals Syntax

TypeLiteral syntax
INTdecimal: 42, -123, 0
hex: 0x????..
binary: 0b????..
octal: 0o????..
FLOAT,
Decimal (requires no_float+decimal)
42.0, -123.456, 123., 123.456e-10
Normal string"... \x?? \u???? \U???????? ..."
String with continuation"this is the first line\
second line\
the third line"
Multi-line literal string `this is the first line
second line
the last line`
Multi-line literal string with interpolation `this is the first field: ${obj.field1}
second field: {obj.field2}
the last field: ${obj.field3}`
Charactersingle: '?'
ASCII hex: '\x??'
Unicode: '\u????', '\U????????'
Array[ ???, ???, ??? ]
Object map#{ a: ???, b: ???, c: ???, "def": ??? }
Boolean truetrue
Boolean falsefalse
Nothing/null/nil/void/Unit()