The Rhai Book
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.20.0 of Rhai.
Introduction to Rhai
Trivia
In the beginning there was ChaiScript, which is an embedded scripting language for C++. Originally it was intended to be a scripting language similar to JavaScript.
With java being a kind of hot beverage, the new language was named after another hot beverage – Chai, which is the word for “tea” in many world languages and, in particular, a popular kind of spicy milk tea consumed in India.
Later, when the novel implementation technique behind ChaiScript was ported from C++ to Rust,
logically the C
was changed to an R
to make it “RhaiScript”, or just “Rhai”.
– Rhai author Johnathan Turner
Features of Rhai
-
Simple language similar to JavaScript+Rust with dynamic typing.
-
Tight integration with native Rust functions and types including getters/setters, methods and indexers.
-
Freely pass Rust values into a script as variables/constants via an external
Scope
– all clonable Rust types are supported seamlessly without the need to implement any special trait. -
Easily call a script-defined function from Rust.
-
Very few additional dependencies – right now only
smallvec
,thin-vec
,num-traits
,ahash
,bitflags
andsmartstring
; forno-std
and WASM builds, a number of additional dependencies are pulled in to provide for missing functionalities. -
Plugins system powered by procedural macros simplifies custom API development.
-
Fairly efficient evaluation – 1 million iterations in 0.14 sec on a single-core, 2.6 GHz Linux VM (see benchmarks).
-
Compile once to AST for repeated evaluations.
-
Scripts are optimized – useful for template-based machine-generated scripts.
-
Organize code base with dynamically-loadable modules, optionally overriding the resolution process.
-
Dynamic dispatch via function pointers with additional support for currying.
-
Closures that can capture shared variables.
-
Some support for object-oriented programming (OOP).
-
Hook into variables access via a variable resolver, or control definition of variables via a variable definition filter.
-
Relatively little
unsafe
code – yes there are some for performance reasons. -
Sand-boxed – the scripting
Engine
, if declared immutable, cannot mutate the containing environment unless explicitly permitted. -
Passes Miri.
-
Don’t Panic guarantee – Any panic is a bug. It never panics the host system.
-
Protected against malicious attacks – such as stack-overflow, over-sized data, and runaway scripts etc. – that may come from untrusted third-party user-land scripts.
-
Track script evaluation progress and manually terminate a script run.
-
Re-entrant scripting
Engine
can be madeSend + Sync
(via thesync
feature). -
Support for
Decimal
numbers. -
Serialization/deserialization support via
serde
. -
Support for minimal builds by excluding unneeded language features.
-
Supports most build targets including
no-std
and WASM. -
Surgically disable keywords and operators to restrict the language.
-
Use as a DSL by defining custom operators and/or extending the language with custom syntax.
-
A debugging interface provides powerful debugging support.
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 a built-in object map type which is adequate for most uses.
It is also possible to simulate object-oriented programming (OOP) by storing function pointers or closures in object map properties, turning them into methods.
- No first-class functions – Code your functions in Rust instead, and register them with Rhai.
There is 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 support for simulated closures via currying a function pointer with captured shared variables.
- No formal language grammar – Rhai uses a hand-coded lexer, a hand-coded top-down recursive-descent parser for statements, and a hand-coded Pratt parser for expressions.
This lack of formalism allows the tokenizer and parser themselves to be exposed as services in order to support a wide range of user customizations, such as:
- disabling keywords and operators,
- dynamically changing tokens during parsing,
- adding custom operators,
- defining custom syntax,
- filtering variables definition.
- No bytecodes/JIT – Rhai uses a heavily-optimized AST-walking interpreter which is fast enough for most real-life scenarios.
See Rhai performance benchmarks.
Benchmarking Rhai
The purpose of Rhai is not to be blazing fast, but to make it as easy and versatile as possible to integrate with native Rust applications.
What you lose from running an AST walker, you gain back from increased flexibility.
The following benchmarks were run on a 2.6GHz Linux VM comparing performance-optimized and full builds of Rhai with Python 3 and V8 (Node.js).
Benchmark | Rhai (Perf) | Rhai(Full) | Python 3 (bytecodes) | V8 (JIT) | Description |
---|---|---|---|---|---|
Fibonacci | 2.25s | 3.2s | 0.6s | 0.07s | stresses recursive function calls |
1M loop | 0.13s | 0.2s | 0.08s | 0.05s | a simple counting loop (1 million iterations) that must run as fast as possible |
Prime numbers | 0.85s | 1.2s | 0.4s | 0.09s | a closer-to-real-life calculation workload |
In general, Rhai is roughly 2x slower than Python 3, which is a bytecodes interpreter, for typical real-life workloads.
Small data structures
Essential AST and runtime data structures are packed small and kept together to maximize cache friendliness.
Pre-calculations
Functions are dispatched based on pre-calculated hashes.
Variables are mostly accessed through pre-calculated offsets to the variables file (a Scope
).
It is seldom necessary to look something up by name.
Caching
Function resolutions are cached so they do not incur lookup costs after a couple of calls.
No scope-chain
Maintaining a scope chain is deliberately avoided by design so function scopes do not pay any speed penalty. This allows variables data to be kept together in a contiguous block, avoiding allocations and fragmentation while being cache-friendly.
Immutable strings
Rhai uses immutable strings to bypass cloning issues.
No sharing
In a typical script evaluation run, no data is shared and nothing is locked (other than variables captured by closures).
Rhai deliberately keeps the language small and lean by omitting advanced language features such as classes, inheritance, interfaces, generics, first-class functions/closures, pattern matching, monads (whatever), concurrency, async etc.
Focus is on flexibility and ease of use instead of a powerful, expressive language.
Avoid the temptation to write full-fledge application logic entirely in Rhai – that use case is best fulfilled by more complete scripting languages such as JavaScript or Lua.
In actual practice, it is usually best to expose a Rust API into Rhai for scripts to call.
All the core functionalities should be written in Rust, with Rhai being the dynamic control layer.
This is similar to some dynamic languages where most of the core functionalities reside in a C/C++ standard library (e.g. Python 3).
Another similar scenario is a web front-end driving back-end services written in a systems language. In this case, JavaScript takes the role of Rhai while the back-end language, well… it can actually also be Rust. Except that Rhai integrates with Rust much more tightly, removing the need for interfaces such as XHR calls and payload encoding such as JSON.
Supported Targets and Builds
Rhai supports all CPU and O/S targets supported by Rust, including:
Minimum Rust Version
The minimum version of Rust required to compile Rhai is 1.66.0
.
Dependencies
Rhai takes care to pull in as few dependencies as possible in order to avoid bloat when using the library.
Main Dependencies
Crate | Description | Why use it? |
---|---|---|
smallvec | Vec variant that stores a number of items inline | most functions have very few parameters, and avoiding allocations result in significant performance improvement |
num-traits | numeric traits | for use with macros defining arithmetic functions and operators |
ahash | fast hashing for data | not cryptographically secure, thus faster than standard Rust hashing; Rhai does a lot of hashing so this matters |
once_cell | global static data | fixed hashing keys for static hashing |
bitflags | bit fields | store flags in AST nodes to minimize memory usage |
smartstring | String variant that stores short strings inline | most strings in scripts (e.g. keywords, properties, symbols, variables, function names etc.) are short, and avoiding allocations result in significant performance improvement |
no-std
Dependencies
Crate | Description |
---|---|
no-std-compat | create std imports under no-std |
libm | no-std math library |
core-error | Error trait for no-std |
hashbrown | HashMap and HashSet for no-std |
Feature Dependencies
Crate | Pulled in by feature |
---|---|
rust_decimal | decimal |
unicode-xid | unicode-xid-ident |
serde | serde , metadata |
serde_json | metadata |
rustyline | bin-features |
WASM Dependencies
Crate | Pulled in by feature |
---|---|
wasm-bindgen | wasm-bindgen |
stdweb | stdweb |
instant | wasm-bindgen , stdweb |
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
-
GitHub – Rhai organization
-
rhai.rs
– Home website -
crates.io
– Rhai crate -
DOCS.RS
– Rhai API documentation -
LIB.RS
– Rhai library info -
Discord Chat – Rhai channel
-
Zulip Chat – Rhai organization
-
Reddit – Rhai community
-
Online Playground – Run Rhai scripts directly from an editor in the browser
-
Language Server – Language Server Protocol (LSP) server for Rhai
-
rhai-doc
– Rhai script documentation tool
-
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 thefn
keyword) are styled properly. Elements not highlighted include:- strings interpolation
- the
switch
,import
andexport
statements - the
this
andprivate
keywords - built-in functions such as
Fn
,call
,type_of
,is_shared
,is_def_var
,is_def_fn
Use
js
(JavaScript) when there is strings interpolation. Elements not highlighted include:
-
ChaiScript – A strong inspiration for Rhai. An embedded scripting language for C++.
-
Check out the list of scripting languages for Rust on awesome-rust
Getting Started
This section shows how to install the Rhai crate into a Rust application.
Online Playground
For more details, see the section here.
Rhai provides an online playground to try out its language and engine features without having to install anything.
The playground provides a syntax-highlighting script editor with example snippets.
Scripts can be evaluated directly from the editor.
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.20.0" # assuming 1.20.0 is the latest version
Automatically use the latest released crate version on crates.io
:
[dependencies]
rhai = "*"
Optional Features
By default, Rhai includes all the standard functionalities in a small, tight package.
Most Rhai features are not strictly additive, i.e. they do not only add optional functionalities.
In fact, most features are subtractive, i.e. they opt-out of unneeded functionalities. Notice that this deviates from Rust norm where features are additive.
Excluding functionalities result in smaller, faster builds as well as more control over what scripts can (or cannot) do.
There is a reason for this design, because the lack of a language feature by itself is a feature (that’s deep…).
See here for more details.
Features that Enable Special Functionalities
Feature | Additive? | Description |
---|---|---|
std | no | standard features |
sync | no | restricts all values types to those that are Send + Sync ; under this feature, all Rhai types, including Engine , Scope and AST , are all Send + Sync |
decimal | no | enables the Decimal number type (pulls in the rust_decimal crate) |
unicode-xid-ident | no | allows Unicode Standard Annex #31 as identifiers (pulls in the unicode-xid crate) |
serde | yes | enables serialization/deserialization via serde (pulls in the serde crate) |
metadata | yes | enables exporting functions metadata; implies serde and additionally pulls in serde_json |
internals | yes | exposes internal data structures (e.g. AST nodes);Safety Warnings
|
debugging | yes | enables the debugging interface; implies internals |
Features that Disable Certain Language Features
Feature | Additive? | Description |
---|---|---|
no_float | no | disables floating-point numbers and math |
no_index | no | disables arrays and indexing features |
no_object | no | disables support for custom types and object maps |
no_time | no | disables timestamps |
no_function | no | disables script-defined functions; implies no_closure |
no_module | no | disables loading external modules |
no_closure | no | disables capturing external variables in closures |
no_custom_syntax | no | disables custom syntax and custom operators |
Features that Disable Certain Engine Features
Feature | Additive? | Description |
---|---|---|
unchecked | no | disables 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_optimize | no | disables script optimization |
no_position | no | disables position tracking during parsing |
Features that Configure the Engine
Feature | Additive? | Description |
---|---|---|
f32_float | no | sets the system floating-point type (FLOAT ) to f32 instead of f64 ; no effect under no_float |
only_i32 | no | sets the system integer type (INT ) to i32 and disable all other integer types |
only_i64 | no | sets the system integer type (INT ) to i64 and disable all other integer types |
Features for no-std
Builds
The following features are provided exclusively for no-std
targets.
Do not use them when not compiling for no-std
.
Specify default-features = false
when compiling for no-std
, which will remove the default
std
feature.
Feature | Additive? | Description |
---|---|---|
no_std | no | builds for no-std ; notice that additional dependencies will be pulled in to replace missing std features |
Features for WebAssembly (WASM) Builds
The following features are provided exclusively for WASM targets. Do not use them for non-WASM targets.
Feature | Additive? | Description |
---|---|---|
wasm-bindgen | no | uses wasm-bindgen to compile for WASM |
stdweb | no | uses stdweb to compile for WASM |
Features for Building Bin Tools
The feature bin-features
include all the features necessary for building the bin tools.
By default, it includes: decimal
, metadata
, serde
, debugging
and rustyline
.
Example
The Cargo.toml
configuration below:
[dependencies]
rhai = { version = "1.20.0", features = [ "sync", "unchecked", "only_i32", "no_float", "no_module", "no_function" ] }
turns on these six features:
Feature | Description |
---|---|
sync | everything is Send + Sync |
unchecked | disable all safety checks (should not be used with untrusted user scripts) |
only_i32 | use only 32-bit signed integers and no others |
no_float | no floating point numbers |
no_module | no loading external modules |
no_function | no defining functions |
The resulting scripting engine supports only the i32
integer numeral type (and no others like
u32
, i16
or i64
), no floating-point, is Send + Sync
(so it can be safely used across
threads), and does not support defining functions nor loading external modules.
This configuration is perfect for an expression parser in a 32-bit embedded system without floating-point hardware.
Packaged Utilities
A number of Rhai-driven tools can be found in the src/bin
directory:
It is possible to turn these tools into Domain-Specific Tools.
Tool | Required feature(s) | Description |
---|---|---|
rhai-run | runs Rhai script files | |
rhai-repl | rustyline | a simple REPL tool |
rhai-dbg | debugging | the Rhai Debugger |
Some bin tools require certain features and will not be built by default without those features set.
For convenience, a feature named bin-features
is available which is a combination of
the following:
Feature | Description |
---|---|
decimal | support for decimal numbers |
metadata | access functions metadata |
serde | export functions metadata to JSON |
debugging | required by rhai-dbg |
rustyline | required by rhai-repl |
Install Tools
To install all these tools (with full features), use the following command:
cargo install --path . --bins --features bin-features
or specifically:
cargo install --path . --bin sample_app_to_run --features bin-features
Run a Tool from Cargo
Tools can also be run with the following cargo
command:
cargo run --features bin-features --bin sample_app_to_run
Tools List
rhai-repl
is a particularly useful tool – it allows one to interactively try out
Rhai’s language features in a standard REPL (Read-Eval-Print Loop).
Filenames passed to it as command line arguments are run and loaded as Rhai scripts before the REPL starts.
Test functions
The following test functions are pre-registered, via Engine::register_fn
, into rhai-repl
.
They are intended for testing purposes.
Function | Description |
---|---|
test(x: i64, y: i64) | returns a string with both numbers |
test(x: &mut i64, y: i64, z: &str) | displays the parameters and add y to x |
Example
The following command first runs three scripts – init1.rhai
, init2.rhai
and init3.rhai
–
loading the functions defined in each script into the global namespace.
Then it enters an REPL, which can call the above functions freely.
rhai-repl init1.rhai init2.rhai init3.rhai
Use rhai-run
to run Rhai scripts.
Filenames passed to it as command line arguments are run in sequence as Rhai scripts.
Example
The following command runs the scripts script1.rhai
, script2.rhai
and script3.rhai
in order.
rhai-run script1.rhai script2.rhai script3.rhai
Use rhai-dbg
to debug a Rhai script.
Filename passed to it will be loaded as a Rhai script for debugging.
Example
The following command debugs the script my_script.rhai
.
rhai-dbg my_script.rhai
Using the Engine
Rhai’s interpreter resides in the Engine
type under the master rhai
namespace.
This section shows how to set up, configure and use this scripting engine.
Your First Script in Rhai
Run a Script
To get going with Rhai is as simple as creating an instance of the scripting engine rhai::Engine
via Engine::new
, then calling Engine::run
.
use rhai::{Engine, EvalAltResult};
pub fn main() -> Result<(), Box<EvalAltResult>>
// ^^^^^^^^^^^^^^^^^^
// Rhai API error type
{
// Create an 'Engine'
let engine = Engine::new();
// Your first Rhai Script
let script = "print(40 + 2);";
// Run the script - prints "42"
engine.run(script)?;
// Done!
Ok(())
}
Get a Return Value
To return a value from the script, use Engine::eval
instead.
use rhai::{Engine, EvalAltResult};
pub fn main() -> Result<(), Box<EvalAltResult>>
{
let engine = Engine::new();
let result = engine.eval::<i64>("40 + 2")?;
// ^^^^^^^ required: cast the result to a type
println!("Answer: {result}"); // prints 42
Ok(())
}
Use Script Files
Or evaluate a script file directly with Engine::run_file
or Engine::eval_file
.
Loading and running script files is not available for no_std
or WASM builds.
let result = engine.eval_file::<i64>("hello_world.rhai".into())?;
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// a 'PathBuf' is needed
// Running a script file also works in a similar manner
engine.run_file("hello_world.rhai".into())?;
On Unix-like systems, the shebang (#!
) is used at the very beginning of a script file to mark a
script with an interpreter (for Rhai this would be rhai-run
).
If a script file starts with #!
, the entire first line is skipped by Engine::compile_file
and
Engine::eval_file
. Because of this, Rhai scripts with shebangs at the beginning need no special processing.
This behavior is also present for non-Unix (e.g. Windows) environments so scripts are portable.
#!/home/to/me/bin/rhai-run
// This is a Rhai script
let answer = 42;
print(`The answer is: ${answer}`);
Specify the Return Type
Use Dynamic
if you’re uncertain of the return type.
The type parameter for Engine::eval
is used to specify the type of the return value, which must
match the actual type or an error is returned. Rhai is very strict here.
There are two ways to specify the return type: turbofish notation, or type inference.
Turbofish
let result = engine.eval::<i64>("40 + 2")?; // return type is i64
result.is::<i64>() == true;
let result = engine.eval::<Dynamic>("boo()")?; // use 'Dynamic' if you're not sure what type it'll be!
let result = engine.eval::<String>("40 + 2")?; // returns an error because the actual return type is i64, not String
Type inference
let result: i64 = engine.eval("40 + 2")?; // return type is inferred to be i64
result.is::<i64>() == true;
let result: Dynamic = engine.eval("boo()")?; // use 'Dynamic' if you're not sure what type it'll be!
let result: String = engine.eval("40 + 2")?; // returns an error because the actual return type is i64, not String
Compile a Script (to AST)
To repeatedly evaluate a script, compile it first with Engine::compile
into an AST
(Abstract Syntax Tree) form.
Engine::eval_ast_XXX
and Engine::run_ast_XXX
evaluate a pre-compiled AST
.
// Compile to an AST and store it for later evaluations
let ast = engine.compile("40 + 2")?;
for _ in 0..42 {
let result: i64 = engine.eval_ast(&ast)?;
println!("Answer #{i}: {result}"); // prints 42
}
Advanced users who may want to manipulate an AST
, especially the functions contained within,
should see the section on Manage AST’s for more details.
Practical Use – Header Template Scripts
Sometimes it is desirable to include a standardized header template in a script that contains pre-defined functions, constants and imported modules.
// START OF THE HEADER TEMPLATE
// The following should run before every script...
import "hello" as h;
import "world" as w;
// Standard constants
const GLOBAL_CONSTANT = 42;
const SCALE_FACTOR = 1.2;
// Standard functions
fn foo(x, y) { ... }
fn bar() { ... }
fn baz() { ... }
// END OF THE HEADER TEMPLATE
// Everything below changes from run to run
foo(bar() + GLOBAL_CONSTANT, baz() * SCALE_FACTOR)
Option 1 – The easy way
Prepend the script header template onto independent scripts and run them as a whole.
Pros: Easy!
Cons: If the header template is long, work is duplicated every time to parse it.
let header_template = "..... // scripts... .....";
for index in 0..10000 {
let user_script = db.get_script(index);
// Just merge the two scripts...
let combined_script = format!("{header_template}\n{user_script}\n");
// Run away!
let result = engine.eval::<i64>(combined_script)?;
println!("{result}");
}
Option 2 – The hard way
Option 1 requires the script header template to be recompiled every time. This can be expensive if the header is very long.
This option compiles both the script header template and independent scripts as separate AST
’s
which are then joined together to form a combined AST
.
Pros: No need to recompile the header template!
Cons: More work…
let header_template = "..... // scripts... .....";
let mut template_ast = engine.compile(header_template)?;
// If you don't want to run the template, only keep the functions
// defined inside (e.g. closures), clear out the statements.
template_ast.clear_statements();
for index in 0..10000 {
let user_script = db.get_script(index);
let user_ast = engine.compile(user_script)?;
// Merge the two AST's
let combined_ast = template_ast + user_ast;
// Run away!
let result = engine.eval_ast::<i64>(combined_ast)?;
println!("{result}");
Option 3 – The not-so-hard way
Option 1 does repeated work, option 2 requires manipulating AST
’s…
This option makes the scripted functions (not imported modules nor constants
however) available globally by first making it a module (via Module::eval_ast_as_new
)
and then loading it into the Engine
via Engine::register_global_module
.
Pros: No need to recompile the header template!
Cons: No imported modules nor constants; if the header template is changed, a new
Engine
must be created.
let header_template = "..... // scripts... .....";
let template_ast = engine.compile(header_template)?;
let template_module = Module::eval_ast_as_new(Scope::new(), &template_ast, &engine)?;
engine.register_global_module(template_module.into());
for index in 0..10000 {
let user_script = db.get_script(index);
// Run away!
let result = engine.eval::<i64>(user_script)?;
println!("{result}");
}
Raw Engine
Engine::new
creates a scripting Engine
with common functionalities (e.g. printing to stdout
via print
or debug
).
In many controlled embedded environments, however, these may not be needed and unnecessarily occupy application code storage space.
Even with a raw Engine
, some operators are built-in and always available.
See Built-in Operators for a full list.
Use Engine::new_raw
to create a raw Engine
, in which only a minimal set of
built-in basic arithmetic and logical operators are supported.
To add more functionalities to a raw Engine
, load packages into it.
Since packages can be shared, this is an extremely efficient way to create multiple instances of
the same Engine
with the same set of functions.
Engine::new | Engine::new_raw | |
---|---|---|
Built-in operators | yes | yes |
Package loaded | StandardPackage | none |
Module resolver | FileModuleResolver | none |
Strings interner | yes | no |
on_print | yes | none |
on_debug | yes | none |
A raw Engine
disables the strings interner by default.
This may lead to a significant increase in memory usage if many strings are created in scripts.
Turn the strings interner back on via Engine::set_max_strings_interned
.
use rhai::module_resolvers::FileModuleResolver;
use rhai::packages::StandardPackage;
// Create a raw scripting Engine
let mut engine = Engine::new_raw();
// Use the file-based module resolver
engine.set_module_resolver(FileModuleResolver::new());
// Enable the strings interner
engine.set_max_strings_interned(1024);
// Default print/debug implementations
engine.on_print(|text| println!("{text}"));
engine.on_debug(|text, source, pos| match (source, pos) {
(Some(source), Position::NONE) => println!("{source} | {text}"),
(Some(source), pos) => println!("{source} @ {pos:?} | {text}"),
(None, Position::NONE) => println!("{text}"),
(None, pos) => println!("{pos:?} | {text}"),
});
// Register the Standard Package
let package = StandardPackage::new();
// Load the package into the [`Engine`]
package.register_into_engine(&mut engine);
Built-in Operators
The following operators are built-in, meaning that they are always available, even when using a raw Engine
.
All built-in operators are binary, and are supported for both operands of the same type.
Operators | Assignment operators | Supported types (see standard types) |
---|---|---|
+ , | += | |
- , * , / , % , ** , | -= , *= , /= , %= , **= | |
<< , >> | <<= , >>= |
|
& , | , ^ | &= , |= , ^= |
|
&& , || |
| |
== , != | ||
> , >= , < , <= |
FLOAT
and Decimal
also inter-operate with INT
, while strings inter-operate
with characters for certain operators (e.g. +
).
Scope
– Maintaining State
By default, Rhai treats each Engine
invocation as a fresh one, persisting only the functions
that have been registered but no global state.
This gives each evaluation a clean starting slate.
In order to continue using the same global state from one invocation to the next, such a state
(a Scope
) must be manually created and passed in.
All Scope
variables and constants have values that are Dynamic
, meaning they can store
values of any type.
Under sync
, however, only types that are Send + Sync
are supported, and the entire Scope
itself will also be Send + Sync
. This is extremely useful in multi-threaded applications.
Scope
has a lifetime parameter, in the vast majority of cases it can be omitted and
automatically inferred to be 'static
.
Currently, that lifetime parameter is not used. It is there to maintain backwards compatibility
as well as for possible future expansion when references can also be put into the Scope
.
The lifetime parameter is not guaranteed to remain unused for future versions.
In order to put a Scope
into a struct
, use Scope<'static>
.
Scope
also has a const
generic parameter, which is a number that defaults to 8.
It indicates the number of entries that the Scope
can keep inline without allocations.
The larger this number, the larger the Scope
type gets, but allocations will happen far
less frequently.
A smaller number makes Scope
smaller, but allocation costs will be incurred when the
number of entries exceed the inline capacity.
Scope
API
Method | Description |
---|---|
new instance method | create a new empty Scope |
with_capacity instance method | create a new empty Scope with a specified initial capacity |
len | number of variables/constants currently within the Scope |
rewind | rewind (i.e. reset) the Scope to a particular number of variables/constants |
clear | remove all variables/constants from the Scope , making it empty |
is_empty | is the Scope empty? |
is_constant | is the particular variable/constant in the Scope a constant? |
push , push_constant | add a new variable/constant into the Scope with a specified value |
push_dynamic , push_constant_dynamic | add a new variable/constant into the Scope with a Dynamic value |
set_or_push<T> | set the value of the last variable within the Scope by name if it exists and is not constant; add a new variable into the Scope otherwise |
contains | does the particular variable or constant exist in the Scope ? |
get_value<T> | get the value of the last variable/constant within the Scope by name |
set_value<T> | set the value of the last variable within the Scope by name, panics if it is constant |
remove<T> | remove the last variable/constant from the Scope by name, returning its value |
get | get a reference to the value of the last variable/constant within the Scope by name |
get_mut | get a reference to the value of the last variable within the Scope by name, None if it is constant |
set_alias | exported the last variable/constant within the Scope by name |
iter , iter_raw , IntoIterator::into_iter | get an iterator to the variables/constants within the Scope |
Extend::extend | add variables/constants to the Scope |
For details on the Scope
API, refer to the
documentation online.
Serializing/Deserializing
With the serde
feature, Scope
is serializable and deserializable via
serde
.
Custom types stored in the Scope
, however, are serialized as full type-name strings.
Data in custom types are not serialized.
Example
In the following example, a Scope
is created with a few initialized variables, then it is threaded
through multiple evaluations.
use rhai::{Engine, Scope, EvalAltResult};
let engine = Engine::new();
// First create the state
let mut scope = Scope::new();
// Then push (i.e. add) some initialized variables into the state.
// Remember the system number types in Rhai are i64 (i32 if 'only_i32')
// and f64 (f32 if 'f32_float').
// Better stick to them or it gets hard working with the script.
scope.push("y", 42_i64)
.push("z", 999_i64)
.push_constant("MY_NUMBER", 123_i64) // constants can also be added
.set_value("s", "hello, world!"); // 'set_value' adds a new variable when one doesn't exist
// First invocation
engine.run_with_scope(&mut scope,
"
let x = 4 + 5 - y + z + MY_NUMBER + s.len;
y = 1;
")?;
// Second invocation using the same state.
// Notice that the new variable 'x', defined previously, is still here.
let result = engine.eval_with_scope::<i64>(&mut scope, "x + y")?;
println!("result: {result}"); // prints 1103
// Variable y is changed in the script - read it with 'get_value'
assert_eq!(scope.get_value::<i64>("y").expect("variable y should exist"), 1);
// We can modify scope variables directly with 'set_value'
scope.set_value("y", 42_i64);
assert_eq!(scope.get_value::<i64>("y").expect("variable y should exist"), 42);
Engine
API Using Scope
Engine
API methods that accept a Scope
parameter all end in _with_scope
, making that
Scope
(and everything inside it) available to the script:
Engine API | Not available under |
---|---|
Engine::eval_with_scope | |
Engine::eval_ast_with_scope | |
Engine::eval_file_with_scope | no_std |
Engine::eval_expression_with_scope | |
Engine::run_with_scope | |
Engine::run_ast_with_scope | |
Engine::run_file_with_scope | no_std |
Engine::compile_file_with_scope | no_std |
Engine::compile_expression_with_scope |
Variables or constants defined at the global level of a script persist inside the custom Scope
even after the script ends.
let mut scope = Scope::new();
engine.run_with_scope(&mut scope, "let x = 42;")?;
// Variable 'x' stays inside the custom scope!
engine.run_with_scope(&mut scope, "print(x);")?; // prints 42
Due to variable shadowing, new variables/constants are simply added on top of
existing ones (even when they already exist), so care must be taken that new variables/constants
inside the custom Scope
do not grow without bounds.
let mut scope = Scope::new();
// Don't do this - this creates 1 million variables named 'x'
// inside 'scope'!!!
for _ in 0..1_000_000 {
engine.run_with_scope(&mut scope, "let x = 42;")?;
}
// The 'scope' contains a LOT of variables...
assert_eq!(scope.len(), 1_000_000);
// Variable 'x' stays inside the custom scope!
engine.run_with_scope(&mut scope, "print(x);")?; // prints 42
In order to remove variables or constants introduced by a script, use the rewind
method.
// Run a million times
for _ in 0..1_000_000 {
// Save the current size of the 'scope'
let orig_scope_size = scope.len();
engine.run_with_scope(&mut scope, "let x = 42;")?;
// Rewind the 'scope' to the original size
scope.rewind(orig_scope_size);
}
// The 'scope' is empty
assert_eq!(scope.len(), 0);
// Variable 'x' is no longer inside 'scope'!
engine.run_with_scope(&mut scope, "print(x);")?; // error: variable 'x' not found
Evaluate Expressions Only
Use Dynamic
if you’re uncertain of the return type.
Very often, a use case does not require a full-blown scripting language, but only needs to evaluate expressions.
In these cases, use the Engine::compile_expression
and Engine::eval_expression
methods or their
_with_scope
variants.
let result: i64 = engine.eval_expression("2 + (10 + 10) * 2")?;
let result: Dynamic = engine.eval_expression("get_value(42)")?;
// Usually this is done together with a custom scope with variables...
let mut scope = Scope::new();
scope.push("x", 42_i64);
scope.push_constant("SCALE", 10_i64);
let result: i64 = engine.eval_expression_with_scope(&mut scope,
"(x + 1) * SCALE"
)?;
When evaluating expressions, no full-blown statement (e.g. while
, for
, fn
) –
not even variable assignment – is supported and will be considered syntax errors.
This is true also for statement expressions and closures.
// The following are all syntax errors because the script
// is not a strict expression.
engine.eval_expression::<()>("x = 42")?;
let ast = engine.compile_expression("let x = 42")?;
let result = engine.eval_expression_with_scope::<i64>(&mut scope,
"{ let y = calc(x); x + y }"
)?;
let fp: FnPtr = engine.eval_expression("|x| x + 1")?;
if
expressions are allowed if both statement blocks
contain only a single expression each.
switch
expressions are allowed if all match
actions are expressions and not statements.
loop expressions are not allowed.
// The following are allowed.
let result = engine.eval_expression_with_scope::<i64>(&mut scope,
"if x { 42 } else { 123 }"
)?;
let result = engine.eval_expression_with_scope::<i64>(&mut scope, "
switch x {
0 => x * 42,
1..=9 => foo(123) + bar(1),
10 => 0,
}
")?;
Engine Configuration Options
A number of other configuration options are available from the Engine
to fine-tune behavior and safeguards.
Compile-Time Language Features
Method | Description | Default |
---|---|---|
set_optimization_level (not available under no_optimize ) | sets the amount of script optimizations performed (see script optimization) | Simple |
set_allow_if_expression | allows/disallows if -expressions | allow |
set_allow_switch_expression | allows/disallows switch expressions | allow |
set_allow_loop_expressions | allows/disallows loop expressions | allow |
set_allow_statement_expression | allows/disallows statement expressions | allow |
set_allow_anonymous_fn (not available under no_function ) | allows/disallows anonymous functions | allow |
set_allow_looping | allows/disallows looping (i.e. while , loop , do and for statements) | allow |
set_allow_shadowing | allows/disallows shadowing of variables | allow |
set_strict_variables | enables/disables Strict Variables mode | disabled |
set_fast_operators | enables/disables Fast Operators mode | enabled |
disable_symbol | disables a certain keyword or operator (see disable keywords and operators) |
Beware that these options activate during compile-time only. If an AST
is compiled on an
Engine
but then evaluated on a different Engine
with different configuration, disallowed
features contained inside the AST
will still run as normal.
Runtime Behavior
Method | Description |
---|---|
set_fail_on_invalid_map_property (not available under no_object ) | sets whether to raise errors (instead of returning () ) when invalid properties are accessed on object maps |
set_default_tag | sets the default value of the custom state (which can be obtained via NativeCallContext::tag ) for each evaluation run |
Safety Limits
Method | Not available under | Description |
---|---|---|
set_max_expr_depths | unchecked | sets the maximum nesting levels of an expression/statement (see maximum statement depth) |
set_max_call_levels | unchecked , no_function | sets the maximum number of function call levels (default 50) to avoid infinite recursion (see maximum call stack depth) |
set_max_operations | unchecked | sets the maximum number of operations that a script is allowed to consume (see maximum number of operations) |
set_max_variables | unchecked | sets the maximum number of variables that a script is allowed to define within a single Scope (see maximum number of variables) |
set_max_functions | unchecked , no_function | sets the maximum number of functions that a script is allowed to define (see maximum number of functions) |
set_max_modules | unchecked , [no_modules ] | sets the maximum number of modules that a script is allowed to load (see maximum number of modules) |
set_max_string_size | unchecked | sets the maximum length (in UTF-8 bytes) for strings (see maximum length of strings) |
set_max_array_size | unchecked , no_index | sets the maximum size for arrays (see maximum size of arrays) |
set_max_map_size | unchecked , no_object | sets the maximum number of properties for object maps (see maximum size of object maps) |
set_max_strings_interned | sets the maximum number of strings to be interned (if zero, the strings interner is disabled) |
Examples
Rhai comes with a number of examples showing how to integrate the scripting Engine
within a Rust
application, as well as a number of sample scripts that showcase different Rhai language features.
Rust Examples
Standard Examples
A number of examples can be found under examples
.
Example | Description |
---|---|
arrays_and_structs | shows how to register a Rust type and using it with arrays |
callback | shows how to store a Rhai closure and call it later within Rust |
custom_types_and_methods | shows how to register a Rust type and methods/getters/setters for it |
custom_types | shows how to register a Rust type and methods/getters/setters using the CustomType trait. |
definitions | shows how to generate definition files for use with the Rhai Language Server (requires the metadata feature) |
hello | simple example that evaluates an expression and prints the result |
pause_and_resume | shows how to pause/resume/stop an Engine running in a separate thread via an MPSC channel |
reuse_scope | evaluates two pieces of code in separate runs, but using a common Scope |
serde | example to serialize and deserialize Rust types with serde (requires the serde feature) |
simple_fn | shows how to register a simple Rust function |
strings | shows different ways to register Rust functions taking string arguments |
threading | shows how to communicate in duplex with an Engine running in a separate thread via a pair of MPSC channels |
Scriptable Event Handler With State Examples
Because of its popularity, the pattern Scriptable Event Handler With State has sample implementations for different styles.
Running Examples
Examples can be run with the following command:
cargo run --example {example_name}
no-std
Examples
To illustrate no-std
builds, a number of example applications are available under the no_std
directory:
Example | Description | Optimization | Allocator | Panics |
---|---|---|---|---|
no_std_test | bare-bones test application that evaluates a Rhai expression and sets the result as the return value | size | wee_alloc | abort |
Building the no-std
examples
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:
Script | Description |
---|---|
array.rhai | arrays example |
assignment.rhai | variable declarations |
comments.rhai | just regular comments |
doc-comments.rhai | doc-comments example |
for1.rhai | for loops |
for2.rhai | for loops with array iterations |
for3.rhai | for loops with closures |
function_decl1.rhai | a function without parameters |
function_decl2.rhai | a function with two parameters |
function_decl3.rhai | a function with many parameters |
function_decl4.rhai | a function acting as a method |
function_decl5.rhai | multiple functions as methods for different data types |
if1.rhai | if example |
if2.rhai | if -expression example |
loop.rhai | count-down loop in Rhai, emulating a do … while loop |
module.rhai | import a script file as a module |
oop.rhai | simulate object-oriented programming (OOP) with closures |
op1.rhai | just simple addition |
op2.rhai | simple addition and multiplication |
op3.rhai | change evaluation order with parenthesis |
string.rhai | string operations, including interpolation |
strings_map.rhai | string and object map operations |
switch.rhai | switch example |
while.rhai | while loop |
Benchmark Scripts
The following scripts are for benchmarking the speed of Rhai:
Scripts | Description |
---|---|
speed_test.rhai | a simple application to measure the speed of Rhai’s interpreter (1 million iterations) |
primes.rhai | use Sieve of Eratosthenes to find all primes smaller than a limit |
fibonacci.rhai | calculate the n-th Fibonacci number using a really dumb algorithm |
mat_mul.rhai | matrix multiplication test to measure the speed of multi-dimensional array access |
Run Example Scripts
The rhai-run
utility can be used to run Rhai scripts:
cargo run --bin rhai-run scripts/any_script.rhai
Special Builds
It is possible to mix-and-match various features of the Rhai crate to make specialized builds with specific characteristics and behaviors.
Performance Build
Some features are for performance. In order to squeeze out the maximum performance from Rhai, the following features should be considered:
Feature | Description | Rationale |
---|---|---|
only_i32 | support only a single i32 integer type | reduce data size |
no_float | remove support for floating-point numbers | reduce code size |
f32_float | set floating-point numbers (if not disabled) to 32-bit | reduce data size |
no_closure | remove support for variables sharing | no need for data locking |
unchecked | disable all safety checks | remove checking code |
no_module | disable loading external modules | reduce code size |
no_position | disable position tracking during parsing | reduce data size |
no_custom_syntax | disable custom syntax | reduce code size |
When the above feature flags are used, performance may increase by around 15-20%.
See Rhai performance benchmarks.
Unchecked Build
By default, Rhai provides a Don’t Panic guarantee and prevents malicious scripts from bringing down the host. Any panic can be considered a bug.
For maximum performance, however, these safety checks can be turned off via the unchecked
feature.
Fast Operators Mode
Make sure that Fast Operators Mode, which is enabled by default, is on. It ignores any user overloading of built-in operators.
For operator-heavy scripts, this may provide a substantial speed-up.
Use Only One Integer Type
If only a single integer type is needed in scripts – most of the time this is the case –
it is best to avoid registering lots of functions related to other integer types that will never be used.
As a result, Engine
creation will be faster because fewer functions need to be loaded.
The only_i32
and only_i64
features disable all integer types except i32
or i64
respectively.
Use Only 32-Bit Numbers
If only 32-bit integers are needed – again, most of the time this is the case – turn on only_i32
.
Under this feature, only i32
is supported as a built-in integer type and no others.
On 64-bit targets this may not gain much, but on certain 32-bit targets this improves performance due to 64-bit arithmetic requiring more CPU cycles to complete.
Minimize Size of Dynamic
Turning on f32_float
(or no_float
) and only_i32
on 32-bit targets makes the critical
Dynamic
data type only 8 bytes long for 32-bit targets.
Normally Dynamic
needs to be up 12-16 bytes long in order to hold an i64
or f64
.
A smaller Dynamic
helps performance due to better cache efficiency.
Use ImmutableString
Internally, Rhai uses immutable strings instead of the Rust String
type.
This is mainly to avoid excessive cloning when passing function arguments.
Rhai’s internal string type is ImmutableString
(basically Rc<SmartString>
or
Arc<SmartString>
depending on the sync
feature). It is cheap to clone, but expensive to modify
(a new copy of the string must be made in order to change it).
Therefore, functions taking String
parameters should use ImmutableString
or &str
(maps to ImmutableString
) for the best performance with Rhai.
Disable Capturing in Closures
Anonymous functions continue to work even under no_closure
.
Only capturing of external shared variables is disabled.
Support for closures that capture shared variables adds material overhead to script evaluation.
This is because every data access must be checked whether it is a shared value and, if so, take a read lock before reading it.
As the vast majority of variables are not shared, needless to say this is a non-trivial performance overhead.
Use no_closure
to disable support for closures to optimize the hot path because it no longer
needs to take locks for shared data.
Disable Position
For embedded scripts that are not expected to cause errors, the no_position
feature can be used
to disable position tracking during parsing.
No line number/character position information is kept for error reporting purposes.
This may result in a slightly fast build due to elimination of code related to position tracking.
Avoid Cloning
Use &mut
functions
Rhai values are typically cloned when passed around, especially into function calls. Large data structures may incur material cloning overhead.
Some functions accept the first parameter as a mutable reference (i.e. &mut
), for example
methods for custom types, and may avoid potentially-costly cloning.
Compound assignment
For example, the +=
(append) compound assignment takes a mutable reference to the variable while
the corresponding +
(add) assignment usually doesn’t. The difference in performance can be huge:
let x = create_some_very_big_and_expensive_type();
x = x + 1;
// ^ 'x' is cloned here
// The above is equivalent to:
let temp_value = x.clone() + 1;
x = temp_value;
x += 1; // <- 'x' is NOT cloned
Use take
Another example: use the take
function to extract a value out of a variable (replacing it with
()
) without cloning.
let x = create_some_very_big_and_expensive_type();
let y = x; // <- 'x' is cloned here
let y = x.take(); // <- 'x' is NOT cloned
Rhai’s script optimizer is usually smart enough to rewrite function calls into method-call style or compound assignment style to take advantage of this.
However, there are limits to its intelligence, and only simple variable references are optimized.
x = x + 1; // <- this statement...
x += 1; // ... is rewritten as this
x[y] = x[y] + 1; // <- but this is not, so this is MUCH slower...
x[y] += 1; // ... than this
some_func(x, 1); // <- this statement...
x.some_func(1); // ... is rewritten as this
some_func(x[y], 1); // <- but this is not, so 'x[y]` is cloned
Short Variable Names for 32-Bit Systems
On 32-bit systems, variable and constant names longer than 11 ASCII characters incur additional allocation overhead.
This is particularly true for local variables inside a hot loop, where they are created and destroyed in rapid succession.
Therefore, avoid long variable and constant names that are over this limit.
On 64-bit systems, this limit is raised to 23 ASCII characters, which is almost always adequate.
Minimal Build
Configuration
In order to compile a minimal build – i.e. a build optimized for size – perhaps for
no-std
embedded targets or for compiling to WASM, it is essential that the correct linker flags
are used in Cargo.toml
:
[profile.release]
lto = "fat" # turn on Link-Time Optimizations
codegen-units = 1 # trade compile time with maximum optimization
opt-level = "z" # optimize for size
Use i32
Only
For embedded systems that must optimize for code size, the architecture is commonly 32-bit.
Use only_i32
to prune away large sections of code implementing functions for other numeric types
(including i64
).
If, for some reason, 64-bit long integers must be supported, use only_i64
instead of only_i32
.
Opt-Out of Features
Opt out of as many features as possible, if they are not needed, to reduce code size because, remember, by default all code is compiled into the final binary since what a script requires cannot be predicted. If a language feature will never be needed, omitting it is a prudent strategy to optimize the build for size.
Removing the script optimizer (no_optimize
) yields a sizable code saving,
at the expense of a less efficient script.
Omitting arrays (no_index
) yields the most code-size savings, followed by floating-point support
(no_float
), safety checks (unchecked
) and finally object maps and custom types (no_object
).
Where the usage scenario does not call for loading externally-defined modules, use no_module
to
save some bytes. Disable script-defined functions (no_function
) and possibly closures
(no_closure
) when the features are not needed. Both of these have some code size savings but not much.
For embedded scripts that are not expected to cause errors, the no_position
feature can be used
to disable position tracking during parsing. No line number/character position information is kept
for error reporting purposes. This may result in a slightly smaller build due to elimination of code
related to position tracking.
Use a Raw Engine
Engine::new_raw
creates a raw engine. A raw engine supports, out of the box,
only a very restricted set of basic arithmetic and logical operators.
Selectively include other necessary functionalities by picking specific packages to minimize the footprint.
Packages are shared (even across threads via the sync
feature), so they only have to be created once.
In addition, a Engine::new_raw
disables the strings interner, which might
actually increase memory usage if many strings are created in scripts. Therefore, selectively turn
on the strings interner via Engine::set_max_strings_interned
.
no-std
Build
The feature no_std
automatically converts the scripting engine into a no-std
build.
Usually, a no-std
build goes hand-in-hand with minimal builds because typical embedded
hardware (the primary target for no-std
) has limited storage.
Currently, no_std
requires the nightly compiler due to the crates that it uses.
Implementation
Rhai allocates, so the first thing that must be included in any no-std
project is
an allocator crate, such as wee_alloc
.
Then there is the need to set up proper error/panic handlers.
The following example uses panic = "abort"
and wee_alloc
as the allocator.
// Set up for no-std.
#![no_std]
// The following no-std features are usually needed.
#![feature(alloc_error_handler, start, core_intrinsics, lang_items, link_cfg)]
// Set up the global allocator.
extern crate alloc;
extern crate wee_alloc;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
// Rust needs a CRT runtime on Windows when compiled with MSVC.
#[cfg(all(windows, target_env = "msvc"))]
#[link(name = "msvcrt")]
#[link(name = "libcmt")]
extern "C" {}
// Set up panic and error handlers
#[alloc_error_handler]
fn err_handler(_: core::alloc::Layout) -> ! {
core::intrinsics::abort();
}
#[panic_handler]
#[lang = "panic_impl"]
extern "C" fn rust_begin_panic(_: &core::panic::PanicInfo) -> ! {
core::intrinsics::abort();
}
#[lang = "eh_personality"]
extern "C" fn eh_personality() {}
#[no_mangle]
extern "C" fn rust_eh_register_frames() {}
#[no_mangle]
extern "C" fn rust_eh_unregister_frames() {}
#[no_mangle]
extern "C" fn _Unwind_Resume() {}
#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
// ... main program ...
}
Check out the no-std
sample applications
for different operating environments.
WebAssembly (WASM) Build
There is already a fast and powerful scripting language that integrates nicely with WASM – JavaScript.
Anyhow, do it because you can!
It is possible to use Rhai when compiling to WebAssembly (WASM).
This yields a scripting engine (and language) that can be run in a standard web browser, among other places.
Check out the Online Playground project which is driven
by a Rhai Engine
compiled into WASM.
JavaScript Interop
Specify either of the wasm-bindgen
or stdweb
features when building for WASM that requires
interop with JavaScript. This selects the appropriate JavaScript interop layer to use.
It is still possible to compile for WASM without either wasm-bindgen
or stdweb
,
but then the interop code must then be explicitly provided.
Target Environments
Rhai requires a system-provided source of random numbers (for hashing).
Such random number source is available from JavaScript (implied by wasm-bindgen
or stdweb
).
The js
feature on the getrandom
crate is
enabled automatically to provide the random number source.
See also: https://docs.rs/getrandom/latest/getrandom/#webassembly-support for details.
Rhai requires a system-provided source of random numbers (for hashing).
Non-JavaScript/non-browser environments may not have random numbers available, so it is necessary to
opt out of default-features
in order to enable static hashing which uses fixed (non-random) keys.
[dependencies]
rhai = { version = "1.20.0", default-features = false, features = [ "std" ] }
Size
Also look into minimal builds to reduce generated WASM size.
A typical, full-featured Rhai scripting engine compiles to a single WASM32 file that is less than 400KB (non-gzipped).
When excluding features that are marginal in WASM environment, the gzipped payload can be shrunk further.
Standard packages can also be excluded to yield additional size savings.
Speed
In benchmark tests, a WASM build runs scripts roughly 30% slower than a native optimized release build.
Common Features
Some Rhai functionalities are not necessary in a WASM environment, so the following features are typically used for a WASM build:
Feature | Description |
---|---|
wasm-bindgen or stdweb | use wasm-bindgen or stdweb as the JavaScript interop layer, omit if using custom interop code |
unchecked | when 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_i32 | WASM supports 32-bit and 64-bit integers, but most scripts will only need 32-bit |
f32_float | WASM supports 32-bit single-precision and 64-bit double-precision floating-point numbers, but single-precision is usually fine for most uses |
no_module | a WASM module cannot load modules from the file system, so usually this is not needed, but the savings are minimal; alternatively, a custom module resolver can be provided that loads other Rhai scripts |
no_custom_syntax | if custom syntax is not used, this results in a small size saving |
The following features are typically not used because they don’t make sense in a WASM build:
Feature | Why unnecessary |
---|---|
sync | WASM is single-threaded |
no_std | std lib works fine with WASM |
metadata | WASM usually doesn’t need access to Rhai functions metadata |
internals | WASM usually doesn’t need to access Rhai internal data structures, unless you are walking the AST |
debugging | unless debugging is needed |
Extend Rhai with Rust
Most features and functionalities required by a Rhai script should actually be coded in Rust, which leverages the superior native run-time speed.
This section discusses how to extend Rhai with functionalities written in Rust.
Traits
A number of traits, under the rhai::
module namespace, provide additional functionalities.
Trait | Description | Methods |
---|---|---|
CustomType | trait to build a custom type for use with an Engine | build |
Func | trait for creating Rust closures from script | create_from_ast , create_from_script |
FuncArgs | trait for parsing function call arguments | parse |
ModuleResolver | trait implemented by module resolution services | resolve , resolve_ast , resolve_raw |
packages::Package | trait implemented by packages | init , init_engine , register_into_engine , register_into_engine_as , as_shared_module |
plugin::PluginFunction | trait implemented by plugin functions | call , is_method_call , has_context , is_pure |
Register a Rust Function for Use in Rhai Scripts
Rhai’s scripting engine is very lightweight. It gets most of its abilities from functions.
To call these functions, they need to be registered via Engine::register_fn
.
use rhai::{Dynamic, Engine, ImmutableString};
// Normal function that returns a standard type
// Remember to use 'ImmutableString' and not 'String'
fn add_len(x: i64, s: ImmutableString) -> i64 {
x + s.len()
}
// Alternatively, '&str' maps directly to 'ImmutableString'
fn add_len_count(x: i64, s: &str, c: i64) -> i64 {
x + s.len() * c
}
// Function that returns a 'Dynamic' value
fn get_any_value() -> Dynamic {
42_i64.into() // standard types can use '.into()'
}
let mut engine = Engine::new();
// Notice that all three functions are overloaded into the same name with
// different number of parameters and/or parameter types.
engine.register_fn("add", add_len)
.register_fn("add", add_len_count)
.register_fn("add", get_any_value)
.register_fn("inc", |x: i64| { // closure is also OK!
x + 1
})
.register_fn("log", |label: &str, x: i64| {
println!("{label} = {x}");
});
let result = engine.eval::<i64>(r#"add(40, "xx")"#)?;
println!("Answer: {result}"); // prints 42
let result = engine.eval::<i64>(r#"add(40, "x", 2)"#)?;
println!("Answer: {result}"); // prints 42
let result = engine.eval::<i64>("add()")?;
println!("Answer: {result}"); // prints 42
let result = engine.eval::<i64>("inc(41)")?;
println!("Answer: {result}"); // prints 42
engine.run(r#"log("value", 42)"#)?; // prints "value = 42"
It is common for short functions to be registered via a closure.
┌──────┐
│ Rust │
└──────┘
engine.register_fn("foo", |x: i64, y: i64| x * 2 + y * 3);
// ^^^ ^^^
// Usually parameter types need to be specified
engine.register_fn("bar", |x: i64| -> Result<_, Box<EvalAltResult>> { x * 2 });
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// For fallible closures, the return ERROR type may need to be specified
┌─────────────┐
│ Rhai script │
└─────────────┘
foo(42, 100); // <- 42 * 2 + 100 * 3
Interact with external environment
An additional benefit to using closures is that they can capture external variables.
For example, capturing a type wrapped in shared mutability (e.g. Rc<RefCell<T>>
)
allows a script to interact with the external environment through that shared type.
See also: Control Layer.
┌──────┐
│ Rust │
└──────┘
/// A type that encapsulates some behavior.
#[derive(Clone)]
struct TestStruct { ... }
impl TestSTruct {
/// Some action defined on that type.
pub fn do_foo(&self, x: i64, y: bool) {
// ... do something drastic with x and y
}
}
/// Wrapped in shared mutability: Rc<RefCell<TestStruct>>.
let shared_obj = Rc::new(RefCell::new(TestStruct::new()));
/// Clone the shared reference and move it into the closure.
let embedded_obj = shared.clone();
engine.register_fn("foo", move |x: i64, y: bool| {
// ^^^^ 'embedded_obj' is captured into the closure
embedded_obj.borrow().do_foo(x, y);
});
┌─────────────┐
│ Rhai script │
└─────────────┘
foo(42, true); // <- equivalent to: shared_obj.borrow().do_foo(42, true);
Rhai uses an intricate system of traits (in particular RhaiNativeFunc
) with many generic parameters to ensure
an intuitive and smooth developer experience that just works.
Because of this, it is not recommended to touch those generic parameters directly. These generic parameters may change liberally in future versions of Rhai. In most situations they are automatically inferred by the compiler.
In the cases where the compiler fail to infer types when registering a closure, (usually with the error type of a fallible function), manually declare the parameter and/or return types.
// The following fails to compile because the compiler does not know
// the return _error_ type of the closure.
// It knows the return type, which is 'Result<i64, E>', but 'E' is not known.
engine.register_fn("foo", |x: i64| Ok(x));
// Don't do this...
engine.register_fn::<_, 1, false, i64, Box<EvalAltResult>>("foo", |x: i64| Ok(x));
// Do this...
engine.register_fn("foo", |x: i64| -> Result<i64, Box<EvalAltResult>> { Ok(x) });
Function Overloading
Functions registered with the Engine
can be overloaded as long as the signature is unique,
i.e. different functions can have the same name as long as their parameters are of different numbers
(i.e. arity) or different types.
New definitions overwrite previous definitions of the same name, same arity and same parameter types.
Rhai does not support default values for function parameters.
However it is extremely easy to simulate default parameter values via multiple overloaded registrations of the same function name.
// The following definition of 'foo' is equivalent to the pseudo-code:
// fn foo(x = 42_i64, y = "hello", z = true) -> i64 { ... }
fn foo3(x: i64, y: &str, z: bool) -> i64 { ... }
fn foo2(x: i64, y: &str) -> i64 { foo3(x, y, true) }
fn foo1(x: i64) -> i64 { foo2(x, "hello") }
fn foo0() -> i64 { foo1(42) }
engine.register_fn("foo", foo0) // no parameters
.register_fn("foo", foo1) // 1 parameter
.register_fn("foo", foo2) // 2 parameters
.register_fn("foo", foo3); // 3 parameters
Register a Generic Rust Function
Due to its dynamic nature, Rhai cannot monomorphize generic functions automatically.
Monomorphization of generic functions must be performed manually.
Rust generic functions can be used in Rhai, but separate instances for each concrete type must be registered separately.
This essentially overloads the function with different parameter types as Rhai does not natively support generics but Rhai does support function overloading.
The example below shows how to register multiple functions (or, in this case, multiple overloaded versions of the same function) under the same name.
use std::fmt::Display;
use rhai::Engine;
fn show_it<T: Display>(x: &mut T) {
println!("put up a good show: {x}!");
}
let mut engine = Engine::new();
engine.register_fn("print", show_it::<i64>)
.register_fn("print", show_it::<bool>)
.register_fn("print", show_it::<ImmutableString>);
String
Parameters in Rust Functions
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
A common mistake made by novice Rhai users is to register functions with String
parameters.
Rust functions accepting parameters of String
should use &str
instead because it maps directly
to ImmutableString
which is the type that Rhai uses to represent strings internally.
The parameter type String
involves always converting an ImmutableString
into a String
which mandates cloning it.
Using ImmutableString
or &str
is much more efficient.
fn get_len1(s: String) -> i64 { // BAD!!! Very inefficient!!!
s.len() as i64
}
fn get_len2(s: &str) -> i64 { // this is better
s.len() as i64
}
fn get_len3(s: ImmutableString) -> i64 { // the above is equivalent to this
s.len() as i64
}
engine.register_fn("len1", get_len1)
.register_fn("len2", get_len2)
.register_fn("len3", get_len3);
let len = engine.eval::<i64>("len1(x)")?; // 'x' cloned, very inefficient!!!
let len = engine.eval::<i64>("len2(x)")?; // 'x' is shared
let len = engine.eval::<i64>("len3(x)")?; // 'x' is shared
A function with the first parameter being &mut String
does not match a string argument passed to it,
which has type ImmutableString
.
In fact, &mut String
is treated as an opaque custom type.
fn bad(s: &mut String) { ... } // '&mut String' will not match string values
fn good(s: &mut ImmutableString) { ... }
engine.register_fn("bad", bad)
.register_fn("good", good);
engine.eval(r#"bad("hello")"#)?; // <- error: function 'bad (string)' not found
engine.eval(r#"good("hello")"#)?; // <- this one works
Dynamic
Parameters in Rust Functions
It is possible for Rust functions to contain parameters of type Dynamic
.
A Dynamic
value can hold any clonable type.
The push
method of an array is implemented as follows (minus code for safety protection
against over-sized arrays), allowing the function to be called with
all item types.
// 'item: Dynamic' matches all data types
fn push(array: &mut Array, item: Dynamic) {
array.push(item);
}
Precedence
Any parameter in a registered Rust function with a specific type has higher precedence over
Dynamic
, so it is important to understand which version of a function will be used.
Parameter matching starts from the left to the right. Candidate functions will be matched in order of parameter types.
Therefore, always leave Dynamic
parameters (up to 16, see below) as far to the right as possible.
use rhai::{Engine, Dynamic};
// Different versions of the same function 'foo'
// will be matched in the following order.
fn foo1(x: i64, y: &str, z: bool) { }
fn foo2(x: i64, y: &str, z: Dynamic) { }
fn foo3(x: i64, y: Dynamic, z: bool) { }
fn foo4(x: i64, y: Dynamic, z: Dynamic) { }
fn foo5(x: Dynamic, y: &str, z: bool) { }
fn foo6(x: Dynamic, y: &str, z: Dynamic) { }
fn foo7(x: Dynamic, y: Dynamic, z: bool) { }
fn foo8(x: Dynamic, y: Dynamic, z: Dynamic) { }
let mut engine = Engine::new();
// Register all functions under the same name (order does not matter)
engine.register_fn("foo", foo5)
.register_fn("foo", foo7)
.register_fn("foo", foo2)
.register_fn("foo", foo8)
.register_fn("foo", foo1)
.register_fn("foo", foo3)
.register_fn("foo", foo6)
.register_fn("foo", foo4);
The number of parameter permutations goes up exponentially, and therefore there is a realistic limit
of 16 parameters allowed to be Dynamic
, counting from the right-most side.
For example, Rhai will not find the following function – Oh! and those 16 parameters to the right certainly have nothing to do with it!
// The 'd' parameter counts 17th from the right!
fn weird(a: i64, d: Dynamic, x1: i64, x2: i64, x3: i64, x4: i64,
x5: i64, x6: i64, x7: i64, x8: i64,
x9: i64, x10: i64, x11: i64, x12: i64,
x13: i64, x14: i64, x15: i64, x16: i64) {
// ... do something unspeakably evil with all those parameters ...
}
TL;DR
Hash lookup
Since functions in Rhai can be overloaded, Rhai uses a single hash number to quickly lookup the actual function, based on argument types.
For each function call, a hash is calculated from:
- the function’s namespace, if any,
- the function’s name,
- number of arguments (its arity),
- unique ID of the type of each argument, if any.
The correct function is then obtained via a simple hash lookup.
Limitations
This method is fast, but at the expense of flexibility (such as multiple argument types that must map to a single version). That is because each type has a different ID, and thus they calculate to different hash numbers.
This is the reason why generic functions must be expanded into concrete types.
The type ID of Dynamic
is different from any other type, but it must match all types seamlessly.
Needless to say, this creates a slight problem.
Trying combinations
If the combined hash calculated from the actual argument type ID’s is not found, then the Engine
calculates hashes for different combinations of argument types and Dynamic
, systematically
replacing different arguments with Dynamic
starting from the right-most parameter.
Thus, assuming a three-argument function call:
foo(42, "hello", true);
The following hashes will be calculated, in order. They will be all different.
Order | Hash calculation method |
---|---|
1 | foo + 3 + i64 + string + bool |
2 | foo + 3 + i64 + string + Dynamic |
3 | foo + 3 + i64 + Dynamic + bool |
4 | foo + 3 + i64 + Dynamic + Dynamic |
5 | foo + 3 + Dynamic + string + bool |
6 | foo + 3 + Dynamic + string + Dynamic |
7 | foo + 3 + Dynamic + Dynamic + bool |
8 | foo + 3 + Dynamic + Dynamic + Dynamic |
Therefore, the version with all the correct parameter types will always be found first if it exists.
At soon as a hash is found, the process stops.
Otherwise, it goes on for up to 16 arguments, or at most 65,536 tries. That’s where the 16 parameters limit comes from.
Of course not. Don’t be silly.
Not every function has 16 parameters
Studies have repeatedly shown that most functions accept few parameters, with the mean between
2-3 parameters per function. Functions with more than 5 parameters are rare in normal code bases.
If at all, they are usually closures that capture lots of external variables, bumping up the
parameter count; but closures are always script-defined and thus all parameters are already
Dynamic
.
In fact, you have a bigger problem if you write such a function that you need to call regularly. It would be far more efficient to group those parameters into object maps.
Caching to the rescue
Function hashes are cached, so this process only happens once, and only up to the number of rounds for the correct function to be found.
If not, then yes, it will calculate up to 2n hashes where n is the number of arguments (up to 16). But again, this will only be done once for that particular combination of argument types.
The functions resolution cache resides only in the global namespace. This is a limitation.
Therefore, calls to functions in an import
ed module (i.e. qualified with
a namespace path) do not have the benefit of a cache.
Thus, up to 2n hashes are calculated during every function call. This is unlikely to cause a performance issue since most functions accept only a few parameters.
Dynamic
Return Value
Rhai supports registering functions that return Dynamic
.
A Dynamic
value can hold any clonable type.
use rhai::{Engine, Dynamic};
// The 'Dynamic' return type allows this function to
// return values of any supported type!
fn get_info(bag: &mut PropertyBag, key: &str) -> Dynamic {
if let Some(prop_type) = bag.get_type(key) {
match prop_type {
// Use '.into()' for standard types
"string" => bag.get::<&str>(key).into(),
"int" => bag.get::<i64>(key).into(),
"bool" => bag.get::<bool>(key).into(),
:
:
// Use 'Dynamic::from' for custom types
"bag" => Dynamic::from(bag.get::<PropertyBag>(key))
}
} else {
// Return () upon error
Dynamic::UNIT
}
}
let mut engine = Engine::new();
engine.register_fn("get_info", get_info);
To create a Dynamic
value, use Dynamic::from
.
Standard types in Rhai can also use .into()
.
use rhai::Dynamic;
let obj = TestStruct::new();
let x = Dynamic::from(obj);
// '.into()' works for standard types
let x = 42_i64.into();
let y = "hello!".into();
Instead of registering a fallible function, it is usually more idiomatic to leverage the dynamic
nature of Rhai and simply return ()
upon error.
use rhai::{Engine, Dynamic};
// Function that may fail - return () upon failure
fn safe_divide(x: i64, y: i64) -> Dynamic {
if y == 0 {
// Return () to indicate an error if y is zero
Dynamic::UNIT
} else {
// Use '.into()' to convert standard types to 'Dynamic'
(x / y).into()
}
}
let mut engine = Engine::new();
engine.register_fn("divide", safe_divide);
// The following prints 'error!'
engine.run(r#"
let result = divide(40, 0);
if result == () {
print("error!");
} else {
print(result);
}
"#)?;
Register a Fallible Rust Function
If a function is fallible (i.e. it returns a Result<_, _>
), it can also be registered with via
Engine::register_fn
.
The function must return Result<T, Box<EvalAltResult>>
where T
is any clonable type.
In other words, the error type must be Box<EvalAltResult>
. It is Box
ed in order to reduce
the size of the Result
type since the error path is rarely hit.
use rhai::{Engine, EvalAltResult};
// Function that may fail - the error type must be 'Box<EvalAltResult>'
fn safe_divide(x: i64, y: i64) -> Result<i64, Box<EvalAltResult>> {
if y == 0 {
// Return an error if y is zero
Err("Division by zero!".into()) // shortcut to create Box<EvalAltResult::ErrorRuntime>
} else {
Ok(x / y)
}
}
let mut engine = Engine::new();
engine.register_fn("divide", safe_divide);
if let Err(error) = engine.eval::<i64>("divide(40, 0)") {
println!("Error: {error:?}"); // prints ErrorRuntime("Division by zero detected!", (1, 1)")
}
Box<EvalAltResult>
implements From<&str>
and From<String>
etc.
and the error text gets converted into Box<EvalAltResult::ErrorRuntime>
.
The error values are Box
-ed in order to reduce memory footprint of the error path,
which should be hit rarely.
NativeCallContext
If the first parameter of a function is of type rhai::NativeCallContext
, then it is treated
specially by the Engine
.
NativeCallContext
is a type that encapsulates the current call context of a Rust function call
and exposes the following.
Method | Return type | Description |
---|---|---|
engine() | &Engine | the 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() | &str | name of the function called (useful when the same Rust function is mapped to multiple Rhai-callable function names) |
source() | Option<&str> | reference to the current source, if any |
position() | Position | position of the function call |
call_level() | usize | the current nesting level of function calls |
tag() | &Dynamic | reference to the custom state that is persistent during the current run |
iter_imports() | impl Iterator<Item = (&str, &Module )> | iterator of the current stack of modules imported via import statements, in reverse order (i.e. later modules come first) |
global_runtime_state() | &GlobalRuntimeState | reference to the current global runtime state (including the stack of modules imported via import statements); requires the internals feature |
iter_namespaces() | impl Iterator<Item = &Module > | iterator of the namespaces (as modules) containing all script-defined functions, in reverse order (i.e. later modules come first) |
namespaces() | &[&Module] | reference to the namespaces (as modules) containing all script-defined functions; requires the internals feature |
call_fn(...) | Result<T, Box<EvalAltResult>> | call a function with the supplied arguments, casting the result into the required type |
call_native_fn(...) | Result<T, Box<EvalAltResult>> | call a registered native Rust function with the supplied arguments, casting the result into the required type |
call_fn_raw(...) | Result< Dynamic , Box<EvalAltResult>> | call a function with the supplied arguments; this is an advanced method |
call_native_fn_raw(...) | Result< Dynamic , Box<EvalAltResult>> | call a registered native Rust function with the supplied arguments; this is an advanced method |
Example Implementations
The native call context is useful for protecting a function from malicious scripts.
use rhai::{Array, NativeCallContext, EvalAltResult, Position};
// This function builds an array of arbitrary size, but is protected against attacks
// by first checking with the allowed limit set into the 'Engine'.
pub fn new_array(context: NativeCallContext, size: i64) -> Result<Array, Box<EvalAltResult>>
{
let array = Array::new();
if size <= 0 {
return array;
}
let size = size as usize;
let max_size = context.engine().max_array_size();
// Make sure the function does not generate a data structure larger than
// the allowed limit for the Engine!
if max_size > 0 && size > max_size {
return Err(EvalAltResult::ErrorDataTooLarge(
"Size to grow".to_string(),
max_size, size,
context.position(),
).into());
}
for x in 0..size {
array.push(x.into());
}
OK(array)
}
The native call context can be used to call a function within the current evaluation
via call_fn
(or more commonly call_native_fn
).
use rhai::{Engine, NativeCallContext};
let mut engine = Engine::new();
// A function expecting a callback in form of a function pointer.
engine.register_fn("super_call", |context: NativeCallContext, value: i64| {
// Call a function within the current evaluation!
// 'call_native_fn' ensures that only registered native Rust functions
// are called, so a scripted function named 'double' cannot hijack
// the process.
// To also include scripted functions, use 'call_fn' instead.
context.call_native_fn::<i64>("double", (value,))
// ^^^^^^^^ arguments passed in tuple
});
The native call context can be used to call a function pointer or closure that has been passed
as a parameter to the function (via FnPtr::call_within_context
), thereby implementing a callback.
use rhai::{Dynamic, FnPtr, NativeCallContext, EvalAltResult};
pub fn greet(context: NativeCallContext, callback: FnPtr) -> Result<String, Box<EvalAltResult>>
{
// Call the callback closure with the current evaluation context!
let name = callback.call_within_context(&context, ())?;
Ok(format!("hello, {}!", name))
}
Advanced Usage – Restore NativeCallContext
The NativeCallContext
type encapsulates the entire context of a script up to the
particular point of the native Rust function call.
The data inside a NativeCallContext
can be stored (as a type NativeCallContextStore
) for later
use, when a new NativeCallContext
can be constructed based on these stored data.
A reconstructed NativeCallContext
acts almost the same as the original instance, so it is possible
to suspend the evaluation of a script, and to continue at a later time with a new
NativeCallContext
.
Doing so requires the internals
feature to access internal API’s.
Step 1: Store NativeCallContext
data
// Store context for later use
let context_data = context.store_data();
// ... store 'context_data' somewhere ...
secret_database.push(context_data);
Step 2: Restore NativeCallContext
// ... do something else ...
// Restore the context
let context_data = secret_database.get();
let new_context = context_data.create_context(&engine);
Override a Built-in Function
Any similarly-named function defined in a script overrides any built-in or registered native Rust function of the same name and number of parameters.
// Override the built-in function 'to_float' when called as a method
fn to_float() {
print(`Ha! Gotcha! ${this}`);
42.0
}
let x = 123.to_float();
print(x); // what happens?
Most of Rhai’s built-in functionality resides in registered functions.
If you dislike any built-in function, simply provide your own implementation to override the built-in version.
The ability to modify the operating environment dynamically at runtime is called “monkey patching.” It is rarely recommended, but if you need it, you need it bad.
In other words, do it only when all else fails. Do not monkey patch Rhai simply because you can.
Rhai searches for the correct implementation of a function in the following order:
-
Rhai script-defined functions,
-
Native Rust functions registered directly 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 to the global namespace (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 to the global namespace (e.g. via the#[rhai_fn(global)]
attribute in a plugin module), -
Built-in functions.
Call Rhai Functions from Rust
Rhai also allows working backwards from the other direction – i.e. calling a Rhai-scripted
function from Rust via Engine::call_fn
.
┌─────────────┐
│ Rhai script │
└─────────────┘
import "process" as proc; // this is evaluated every time
fn hello(x, y) {
// hopefully 'my_var' is in scope when this is called
x.len + y + my_var
}
fn hello(x) {
// hopefully 'my_string' is in scope when this is called
x * my_string.len()
}
fn hello() {
// hopefully 'MY_CONST' is in scope when this is called
if MY_CONST {
proc::process_data(42); // can access imported module
}
}
┌──────┐
│ Rust │
└──────┘
// Compile the script to AST
let ast = engine.compile(script)?;
// Create a custom 'Scope'
let mut scope = Scope::new();
// A custom 'Scope' can also contain any variables/constants available to
// the functions
scope.push("my_var", 42_i64);
scope.push("my_string", "hello, world!");
scope.push_constant("MY_CONST", true);
// Evaluate a function defined in the script, passing arguments into the
// script as a tuple.
//
// Beware, arguments must be of the correct types because Rhai does not
// have built-in type conversions. If arguments of the wrong types are passed,
// the Engine will not find the function.
//
// Variables/constants pushed into the custom 'Scope'
// (i.e. 'my_var', 'my_string', 'MY_CONST') are visible to the function.
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ( "abc", 123_i64 ) )?;
// ^^^ ^^^^^^^^^^^^^^^^^^
// return type must be specified put arguments in a tuple
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ( 123_i64, ) )?;
// ^^^^^^^^^^^^ tuple of one
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", () )?;
// ^^ unit = tuple of zero
Functions with only one single parameter is easy to get wrong.
The proper Rust syntax is a tuple with one item:
( arg , )
Notice the comma (,
) after the argument. Without it, the expression is a single value
(arg)
which is the same as arg
and not a tuple.
A syntax error with very confusing error message will be generated by the Rust compiler if the comma is omitted.
When using Engine::call_fn
, the AST
is always evaluated before the function is called.
This is usually desirable in order to import the necessary external modules that are needed by the function.
All new variables/constants introduced are, by default, not retained inside the Scope
.
In other words, the Scope
is rewound before each call.
If these default behaviors are not desirable, override them with Engine::call_fn_with_options
.
FuncArgs
Trait
Rhai implements FuncArgs
for tuples, arrays and Vec<T>
.
Engine::call_fn
takes a parameter of any type that implements the FuncArgs
trait,
which is used to parse a data type into individual argument values for the function call.
Custom types (e.g. structures) can also implement FuncArgs
so they can be used for
calling Engine::call_fn
.
use std::iter::once;
use rhai::FuncArgs;
// A struct containing function arguments
struct Options {
pub foo: bool,
pub bar: String,
pub baz: i64
}
impl FuncArgs for Options {
fn parse<C: Extend<Dynamic>>(self, container: &mut C) {
container.extend(once(self.foo.into()));
container.extend(once(self.bar.into()));
container.extend(once(self.baz.into()));
}
}
let options = Options { foo: true, bar: "world", baz: 42 };
// The type 'Options' can now be used as argument to 'call_fn'
// to call a function with three parameters: fn hello(foo, bar, baz)
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", options)?;
Implementing FuncArgs
is almost never needed because Rhai works directly with
any custom type.
It is used only in niche cases where a custom type’s fields need to be split up to pass to functions.
Engine::call_fn_with_options
For more control, use Engine::call_fn_with_options
, which takes a type CallFnOptions
:
use rhai::{Engine, CallFnOptions};
let options = CallFnOptions::new()
.eval_ast(false) // do not evaluate the AST
.rewind_scope(false) // do not rewind the scope (i.e. keep new variables)
.bind_this_ptr(&mut state); // 'this' pointer
let result = engine.call_fn_with_options::<i64>(
options, // options
&mut scope, // scope to use
&ast, // AST containing the functions
"hello", // function entry-point
( "abc", 123_i64 ) // arguments
)?;
CallFnOptions
allows control of the following:
Field | Type | Default | Build method | Description |
---|---|---|---|---|
eval_ast | bool | true | eval_ast | evaluate the AST before calling the target function (useful to run [import statements]) |
rewind_scope | bool | true | rewind_scope | rewind the custom Scope at the end of the function call so new local variables are removed |
this_ptr | Option<&mut Dynamic> | None | bind_this_ptr | bind the this pointer to a specific value |
tag | Option<Dynamic> | None | with_tag | set the custom state for this evaluation (accessed via NativeCallContext::tag ) |
Skip evaluation of the AST
By default, the AST
is evaluated before calling the target function.
This is necessary to make sure that necessary modules imported via import
statements are available.
Setting eval_ast
to false
skips this evaluation.
Keep new variables/constants
By default, the Engine
rewinds the custom Scope
after each call to the initial size,
so any new variable/constant defined are cleared and will not spill into the custom Scope
.
This prevents the Scope
from being continuously polluted by new variables and is usually the
intuitively expected behavior.
Setting rewind_scope
to false
retains new variables/constants within the custom Scope
.
This allows the function to easily pass values back to the caller by leaving them inside the
custom Scope
.
┌─────────────┐
│ Rhai script │
└─────────────┘
fn initialize() {
let x = 42; // 'x' is retained
let y = x * 2; // 'y' is retained
// Use a new statements block to define temp variables
{
let temp = x + y; // 'temp' is NOT retained
foo = temp * temp; // 'foo' is visible in the scope
}
}
let foo = 123; // 'foo' is retained
// Use a new statements block to define temp variables
{
let bar = foo / 2; // 'bar' is NOT retained
foo = bar * bar;
}
┌──────┐
│ Rust │
└──────┘
let options = CallFnOptions::new().rewind_scope(false);
engine.call_fn_with_options(options, &mut scope, &ast, "initialize", ())?;
// At this point, 'scope' contains these variables: 'foo', 'x', 'y'
Bind the this
pointer
CallFnOptions
can also bind a value to the this
pointer of a script-defined function.
It is possible, then, to call a function that uses this
.
let ast = engine.compile("fn action(x) { this += x; }")?;
let mut value: Dynamic = 1_i64.into();
let options = CallFnOptions::new()
.eval_ast(false)
.rewind_scope(false)
.bind_this_ptr(&mut value);
engine.call_fn_with_options(options, &mut scope, &ast, "action", ( 41_i64, ))?;
assert_eq!(value.as_int()?, 42);
Create a Rust Closure from a Rhai Function
It is possible to further encapsulate a script in Rust such that it becomes a normal Rust closure.
Creating them is accomplished via the Func
trait which contains create_from_script
(as well as its companion method create_from_ast
).
use rhai::{Engine, Func}; // use 'Func' for 'create_from_script'
let engine = Engine::new(); // create a new 'Engine' just for this
let script = "fn calc(x, y) { x + y.len < 42 }";
// Func takes two type parameters:
// 1) a tuple made up of the types of the script function's parameters
// 2) the return type of the script function
//
// 'func' will have type Box<dyn Fn(i64, &str) -> Result<bool, Box<EvalAltResult>>> and is callable!
let func = Func::<(i64, &str), bool>::create_from_script(
// ^^^^^^^^^^^ function parameter types in tuple
engine, // the 'Engine' is consumed into the closure
script, // the script, notice number of parameters must match
"calc" // the entry-point function name
)?;
func(123, "hello")? == false; // call the closure
schedule_callback(func); // pass it as a callback to another function
// Although there is nothing you can't do by manually writing out the closure yourself...
let engine = Engine::new();
let ast = engine.compile(script)?;
schedule_callback(Box::new(move |x: i64, y: String| {
engine.call_fn::<bool>(&mut Scope::new(), &ast, "calc", (x, y))
}));
Operator Overloading
In Rhai, a lot of functionalities are actually implemented as functions, including basic operations such as arithmetic calculations.
For example, in the expression “a + b
”, the +
operator actually calls a function named “+
”!
let x = a + b;
let x = +(a, b); // <- the above is equivalent to this function call
Similarly, comparison operators including ==
, !=
etc. are all implemented as functions,
with the stark exception of &&
, ||
and ??
.
Because they short-circuit, &&
, ||
and ??
are
handled specially and not via a function.
Overriding them has no effect at all.
Overload Operator via Rust Function
Operator functions cannot be defined in script because operators are usually not valid function names.
However, operator functions can be registered via Engine::register_fn
.
When a custom operator function is registered with the same name as an operator, it overrides the built-in version. However, make sure the Fast Operators Mode is disabled; otherwise this will not work.
The Fast Operators Mode, which is enabled by default, causes the Engine
to ignore all custom-registered operator functions for built-in operators. This is for
performance considerations.
Disable Fast Operators Mode via Engine::set_fast_operators
in order for the overloaded operators to be used.
use rhai::{Engine, EvalAltResult};
let mut engine = Engine::new();
fn strange_add(a: i64, b: i64) -> i64 {
(a + b) * 42
}
engine.register_fn("+", strange_add); // overload '+' operator for two integers!
engine.set_fast_operators(false); // <- IMPORTANT! must turn off Fast Operators Mode
let result: i64 = engine.eval("1 + 0"); // the overloading version is used
result == 42;
let result: f64 = engine.eval("1.0 + 0.0"); // '+' operator for two floats not overloaded
result == 1.0;
fn mixed_add(a: i64, b: bool) -> f64 { a + if b { 42 } else { 99 } }
engine.register_fn("+", mixed_add); // register '+' operator for an integer and a bool
let result: i64 = engine.eval("1 + true"); // <- normally an error...
result == 43; // ... but not now
Use operator overloading for custom types only.
Be very careful when overriding built-in operators because users expect standard operators to
behave in a consistent and predictable manner, and will be annoyed if an expression involving +
turns into a subtraction, for example. You may think it is amusing, but users who need to get
things done won’t.
Operator overloading also impacts script optimization when using OptimizationLevel::Full
.
See the section on script optimization for more details.
Working with Any Rust Type
If the sync
feature is used, a custom type must also be Send + Sync
.
Rhai works seamlessly with any Rust type, as long as it implements Clone
as this allows the
Engine
to pass by value.
A type that is not one of the standard types is termed a “custom type”.
Custom types can have the following:
-
a custom (friendly) display name
Free Typing
Rhai internally supports a number of standard data types (see this list).
Any type outside of the list is considered custom.
Custom types run slower than built-in types due to an additional level of indirection, but for all other purposes there is no difference.
Rhai works seamlessly with any Rust type.
A custom type is stored in Rhai as a Rust trait object (specifically, a dyn rhai::Variant
),
with no restrictions other than being Clone
(plus Send + Sync
under the sync
feature).
The type literally does not have any prerequisite other than being Clone
.
It does not need to implement any other trait or use any custom #[derive]
.
This allows Rhai to be integrated into an existing Rust code base with as little plumbing as possible, usually silently and seamlessly.
External types that are not defined within the same crate (and thus cannot implement special Rhai
traits or use special #[derive]
) can also be used easily with Rhai.
Support for custom types can be turned off via the no_object
feature.
Register API
For Rhai scripts to interact with the custom type, and API must be registered for it with the Engine
.
The API can consist of functions, methods, property getters/setters, indexers, iterators etc.
There are three ways to register an API for a custom type.
1. Auto-Generate API
If you have complete control of the type, then this is the easiest way.
The #[derive(CustomType)]
macro can be used to automatically generate an
API for a custom type via the CustomType
trait.
2. Custom Type Builder
For types in the same crate that you do not control, each function, method, property getter/setter,
indexer and iterator can be registered manually, as a single package, via the CustomType
trait
using the Custom Type Builder.
3. Manual Registration
For external types that cannot implement the CustomType
trait due to Rust’s orphan rule,
each function, method, property getter/setter, indexer and iterator
must be registered manually with the Engine
.
Auto-Generate API for Custom Type
This assumes that you have complete control of the type and can do whatever you want with it (such as putting attributes on fields).
In particular, the type must be defined within the current crate.
To register a type and its API for use with an Engine
, the simplest method is via the CustomType
trait.
A custom derive macro is provided to auto-implement CustomType
on any struct
type,
which exposes all the type’s fields to an Engine
all at once.
It is as simple as adding #[derive(CustomType)]
to the type definition.
use rhai::{CustomType, TypeBuilder}; // <- necessary imports
#[derive(Clone, CustomType)] // <- auto-implement 'CustomType'
pub struct Vec3 { // for normal structs
pub x: i64,
pub y: i64,
pub z: i64,
}
#[derive(Clone, CustomType)] // <- auto-implement 'CustomType'
pub struct ABC(i64, bool, String); // for tuple structs
let mut engine = Engine::new();
// Register the custom types!
engine.build_type::<Vec3>()
.build_type::<ABC>();
Custom Attribute Options
The rhai_type
attribute, with options, can be added to the fields of the type to customize the auto-generated API.
Option | Applies to | Value | Description |
---|---|---|---|
name | type, field | string expression | use this name instead of the type/field name. |
skip | field | none | skip this field; cannot be used with any other attribute. |
readonly | field | none | only auto-generate getter, no setter; cannot be used with set . |
get | field | function path | use this getter function (with &self ) instead of the auto-generated getter; if get_mut is also set, this is ignored. |
get_mut | field | function path | use this getter function (with &mut self ) instead of the auto-generated getter. |
set | field | function path | use this setter function instead of the auto-generated setter; cannot be used with readonly . |
extra | type | function path | call this function after building the type to add additional API’s |
Function signatures
The signature of the function for get
is:
Fn(&T) -> V
The signature of the function for get_mut
is:
Fn(&mut T) -> V
The signature of the function for set
is:
Fn(&mut T, V)
The signature of the function for extra
is:
Fn(&mut TypeBuilder<T>)
Example
use rhai::{CustomType, TypeBuilder}; // <- necessary imports
#[derive(Debug, Clone)]
#[derive(CustomType)] // <- auto-implement 'CustomType'
pub struct ABC(
#[rhai_type(skip)] // <- 'field0' not included
i64,
#[rhai_type(readonly)] // <- only auto getter, no setter for 'field1'
i64,
#[rhai_type(name = "flag")] // <- override property name for 'field2'
bool,
String // <- auto getter/setter for 'field3'
);
#[derive(Default, Clone)]
#[derive(CustomType)] // <- auto-implement 'CustomType'
#[rhai_type(name = "MyFoo", extra = Self::build_extra)] // <- call this type 'MyFoo' and use 'build_extra' to add additional API's
pub struct Foo {
#[rhai_type(skip)] // <- field not included
dummy: i64,
#[rhai_type(readonly)] // <- only auto getter, no setter for 'bar'
bar: i64,
#[rhai_type(name = "flag")] // <- override property name
baz: bool, // <- auto getter/setter for 'baz'
#[rhai_type(get = Self::qux)] // <- call custom getter (with '&self') for 'qux'
qux: char, // <- auto setter for 'qux'
#[rhai_type(set = Self::set_hello)] // <- call custom setter for 'hello'
hello: String // <- auto getter for 'hello'
}
impl Foo {
/// Regular field getter function with `&self`
pub fn qux(&self) -> char {
self.qux
}
/// Special setter implementation for `hello`
pub fn set_hello(&mut self, value: String) {
self.hello = if self.baz {
let mut s = self.hello.clone();
s.push_str(&value);
for _ in 0..self.bar { s.push('!'); }
s
} else {
value
};
}
/// Additional API's
fn build_extra(builder: &mut TypeBuilder<Self>) {
// Register constructor function
builder.with_fn("new_foo", || Self::default());
}
}
#[derive(Debug, Clone, Eq, PartialEq, CustomType)]
#[rhai_fn(extra = vec3_build_extra)]
pub struct Vec3 {
#[rhai_type(get = Self::x, set = Self::set_x)]
x: i64,
#[rhai_type(get = Self::y, set = Self::set_y)]
y: i64,
#[rhai_type(get = Self::z, set = Self::set_z)]
z: i64,
}
impl Vec3 {
fn new(x: i64, y: i64, z: i64) -> Self { Self { x, y, z } }
fn x(&self) -> i64 { self.x }
fn set_x(&mut self, x: i64) { self.x = x }
fn y(&self) -> i64 { self.y }
fn set_y(&mut self, y: i64) { self.y = y }
fn z(&self) -> i64 { self.z }
fn set_z(&mut self, z: i64) { self.z = z }
}
fn vec3_build_extra(builder: &mut TypeBuilder<Self>) {
// Register constructor function
builder.with_fn("Vec3", Self::new);
}
impl CustomType for ABC {
fn build(mut builder: TypeBuilder<Self>)
{
builder.with_name("ABC");
builder.with_get("field1", |obj: &mut Self| obj.1.clone());
builder.with_get_set("flag",
|obj: &mut Self| obj.2.clone(),
|obj: &mut Self, val| obj.2 = val
);
builder.with_get_set("field3",
|obj: &mut Self| obj.3.clone(),
|obj: &mut Self, val| obj.3 = val
);
}
}
impl CustomType for Foo {
fn build(mut builder: TypeBuilder<Self>)
{
builder.with_name("MyFoo");
builder.with_get("bar", |obj: &mut Self| obj.bar.clone());
builder.with_get_set("flag",
|obj: &mut Self| obj.baz.clone(),
|obj: &mut Self, val| obj.baz = val
);
builder.with_get_set("qux",
|obj: &Self| Self::qux(&*obj)),
|obj: &mut Self, val| obj.qux = val
)
builder.with_get_set("hello",
|obj: &mut Self| obj.hello.clone(),
Self::set_hello
);
Self::build_extra(&mut builder);
}
}
impl CustomType for Vec3 {
fn build(mut builder: TypeBuilder<Self>)
{
builder.with_name("Vec3");
builder.with_get_set("x", |obj: &mut Self| Self::x(&*obj), Self::set_x);
builder.with_get_set("y", |obj: &mut Self| Self::y(&*obj), Self::set_y);
builder.with_get_set("z", |obj: &mut Self| Self::z(&*obj), Self::set_z);
vec3_build_extra(&mut builder);
}
}
Register a Custom Type via the Type Builder
This assumes that the type is defined within the current crate and you can implement traits for it.
However, you may not control the type (it may be auto-generated or maintained by another user), so you cannot put attributes on it.
It is usually convenient to package a custom type’s API (i.e. methods, properties, indexers and type iterators) together such that they can be more easily managed.
This can be achieved by manually implementing the CustomType
trait, which contains only a single method:
fn build(builder: TypeBuilder<T>)
The TypeBuilder
parameter provides a range of convenient methods to register methods, property
getters/setters, indexers and type iterators of a custom type:
Method | Description |
---|---|
with_name | set a friendly name |
on_print | register the to_string function that pretty-prints the custom type |
on_debug | register the to_debug function that debug-prints the custom type |
with_fn | register a method (or any function really) |
with_get | register a property getter |
with_set | register a property getter |
with_get_set | register property getters/setters |
with_indexer_get | register an indexer get function |
with_indexer_set | register an indexer set function |
with_indexer_get_set | register indexer get/set functions |
is_iterable | automatically register a type iterator if the custom type is iterable |
The CustomType
trait is typically used on external types that are already defined.
To define a custom type and implement its API from scratch, it is more convenient to use a plugin module.
Example
// Custom type
#[derive(Debug, Clone, Eq, PartialEq)]
struct Vec3 {
x: i64,
y: i64,
z: i64,
}
// Custom type API
impl Vec3 {
fn new(x: i64, y: i64, z: i64) -> Self {
Self { x, y, z }
}
fn get_x(&mut self) -> i64 {
self.x
}
fn set_x(&mut self, x: i64) {
self.x = x
}
fn get_y(&mut self) -> i64 {
self.y
}
fn set_y(&mut self, y: i64) {
self.y = y
}
fn get_z(&mut self) -> i64 {
self.z
}
fn set_z(&mut self, z: i64) {
self.z = z
}
}
// The custom type can even be iterated!
impl IntoIterator for Vec3 {
type Item = i64;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
vec![self.x, self.y, self.z].into_iter()
}
}
// Use 'CustomType' to register the entire API
impl CustomType for Vec3 {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("Vec3")
.with_fn("vec3", Self::new)
.is_iterable()
.with_get_set("x", Self::get_x, Self::set_x)
.with_get_set("y", Self::get_y, Self::set_y)
.with_get_set("z", Self::get_z, Self::set_z)
// Indexer get/set functions that do not panic on invalid indices
.with_indexer_get_set(
|vec: &mut Self, idx: i64) -> Result<i64, Box<EvalAltResult>> {
match idx {
0 => Ok(vec.x),
1 => Ok(vec.y),
2 => Ok(vec.z),
_ => Err(EvalAltResult::ErrorIndexNotFound(idx.Into(), Position::NONE).into()),
}
},
|vec: &mut Self, idx: i64, value: i64) -> Result<(), Box<EvalAltResult>> {
match idx {
0 => vec.x = value,
1 => vec.y = value,
2 => vec.z = value,
_ => Err(EvalAltResult::ErrorIndexNotFound(idx.Into(), Position::NONE).into()),
}
Ok(())
}
);
}
}
let mut engine = Engine::new();
// Register the custom type in one go!
engine.build_type::<Vec3>();
Technically speaking, TypeBuilder
can automatically register an indexer get function if the custom type implements Index
.
Similarly, it can automatically register an indexer set function for IndexMut
.
In practice, however, this is usually not desirable because most Index
/IndexMut
implementations panic on invalid indices.
For Rhai, it is necessary to handle invalid indices properly by returning an error.
Therefore, in the example above, the with_indexer_get_set
method properly handles invalid indices by returning errors.
Manually Register Custom Type
This assumes that the type is defined in an external crate and so the CustomType
trait
cannot be implemented for it due to Rust’s orphan rule.
It is also possible to use Rust enums with Rhai.
See the pattern Working with Enums for more details.
The custom type needs to be registered into an Engine
via:
Engine API | type_of output |
---|---|
register_type::<T> | full Rust path name |
register_type_with_name::<T> | friendly name |
use rhai::{Engine, EvalAltResult};
#[derive(Debug, Clone)]
struct TestStruct {
field: i64
}
impl TestStruct {
fn new() -> Self {
Self { field: 1 }
}
}
let mut engine = Engine::new();
// Register custom type with friendly name
engine.register_type_with_name::<TestStruct>("TestStruct")
.register_fn("new_ts", TestStruct::new);
// Cast result back to custom type.
let result = engine.eval::<TestStruct>(
"
new_ts() // calls 'TestStruct::new'
")?;
println!("result: {}", result.field); // prints 1
type_of()
a Custom Type
It is OK to register several custom types under the same friendly name
and type_of()
will faithfully return it.
How this might possibly be useful is left to the imagination of the user.
type_of()
works fine with custom types and returns the name of the type.
If Engine::register_type_with_name
is used to register the custom type with a special
“pretty-print” friendly name, type_of()
will return that name instead.
engine.register_type::<TestStruct1>()
.register_fn("new_ts1", TestStruct1::new)
.register_type_with_name::<TestStruct2>("TestStruct")
.register_fn("new_ts2", TestStruct2::new);
let ts1_type = engine.eval::<String>("let x = new_ts1(); x.type_of()")?;
let ts2_type = engine.eval::<String>("let x = new_ts2(); x.type_of()")?;
println!("{ts1_type}"); // prints 'path::to::TestStruct'
println!("{ts2_type}"); // prints 'TestStruct'
==
Operator
Many standard functions (e.g. filtering, searching and sorting) expect a custom type to be
comparable, meaning that the ==
operator must be registered for the custom type.
For example, in order to use the in
operator with a custom type for an array,
the ==
operator is used to check whether two values are the same.
// Assume 'TestStruct' implements `PartialEq`
engine.register_fn("==",
|item1: &mut TestStruct, item2: TestStruct| item1 == &item2
);
// Then this works in Rhai:
let item = new_ts(); // construct a new 'TestStruct'
item in array; // 'in' operator uses '=='
Methods
Methods of custom types are registered via Engine::register_fn
.
use rhai::{Engine, EvalAltResult};
#[derive(Debug, Clone)]
struct TestStruct {
field: i64
}
impl TestStruct {
fn new() -> Self {
Self { field: 1 }
}
fn update(&mut self, x: i64) { // methods take &mut as first parameter
self.field += x;
}
}
let mut engine = Engine::new();
// Most Engine API's can be chained up.
engine.register_type_with_name::<TestStruct>("TestStruct")
.register_fn("new_ts", TestStruct::new)
.register_fn("update", TestStruct::update);
// Cast result back to custom type.
let result = engine.eval::<TestStruct>(
"
let x = new_ts(); // calls 'TestStruct::new'
x.update(41); // calls 'TestStruct::update'
x // 'x' holds a 'TestStruct'
")?;
println!("result: {}", result.field); // prints 42
First Parameter Must be &mut
Methods of custom types take a &mut
first parameter to that type, so that invoking methods can
always update it.
All other parameters in Rhai are passed by value (i.e. clones).
Rhai does NOT support normal references (i.e. &T
) as parameters.
All references must be mutable (i.e. &mut T
).
Call Method as Function
Method-Call Style vs. Function-Call Style
Method-call syntax
object
.
function(
parameter,
…,
parameter)
Method-call style not supported under no_object
// Below is a syntax error under 'no_object'.
engine.run("let x = [42]; x.clear();")?;
// ^ cannot call method-style
Function-call syntax
function
(
object,
parameter,
…,
parameter)
Equivalence
Internally, methods on a custom type is the same as a function taking a &mut
first argument of
the object’s type.
Therefore, methods and functions can be called interchangeably.
impl TestStruct {
fn foo(&mut self) -> i64 {
self.field
}
}
engine.register_fn("foo", TestStruct::foo);
let result = engine.eval::<i64>(
"
let x = new_ts();
foo(x); // normal call to 'foo'
x.foo() // 'foo' can also be called like a method on 'x'
")?;
println!("result: {result}"); // prints 1
First &mut
Parameter
The opposite direction also works — methods in a Rust custom type registered with the
Engine
can be called just like a regular function. In fact, like Rust, object methods are
registered as regular functions in Rhai that take a first &mut
parameter.
Unlike functions defined in script (for which all arguments are passed by value),
native Rust functions may mutate the first &mut
argument.
Sometimes, however, there are more subtle differences. Methods called in normal function-call style may end up not muting the object afterall — see the example below.
Custom types, properties, indexers and methods are disabled under the
no_object
feature.
let a = new_ts(); // constructor function
a.field = 500; // property setter
a.update(); // method call, 'a' can be modified
update(a); // <- this de-sugars to 'a.update()'
// 'a' can be modified and is not a copy
let array = [ a ];
update(array[0]); // <- 'array[0]' is an expression returning a calculated value,
// a transient (i.e. a copy), so this statement has no effect
// except waste time cloning 'a'
array[0].update(); // <- call in method-call style will update 'a'
Rhai does NOT support normal references (i.e. &T
) as parameters.
All references must be mutable (i.e. &mut T
).
Number of Parameters in Methods
Native Rust methods registered with an Engine
take one additional parameter more than
an equivalent method coded in script, where the object is accessed via the this
pointer instead.
The following table illustrates the differences:
Function type | No. of parameters | Object reference | Function signature |
---|---|---|---|
Native Rust | N + 1 | first &mut T parameter | Fn(obj: &mut T, x: U, y: V) |
Rhai script | N | this | Fn(x: U, y: V) |
&mut
is Efficient, Except for &mut ImmutableString
Using a &mut
first parameter is highly encouraged when using types that are expensive to clone,
even when the intention is not to mutate that argument, because it avoids cloning that argument value.
Even when a function is never intended to be a method – for example an operator,
it is still sometimes beneficial to make it method-like (i.e. with a first &mut
parameter)
if the first parameter is not modified.
For types that are expensive to clone (remember, all function calls are passed cloned copies of argument values), this may result in a significant performance boost.
For primary types that are cheap to clone (e.g. those that implement Copy
), including ImmutableString
,
this is not necessary.
// This is a type that is very expensive to clone.
#[derive(Debug, Clone)]
struct VeryComplexType { ... }
// Calculate some value by adding 'VeryComplexType' with an integer number.
fn do_add(obj: &VeryComplexType, offset: i64) -> i64 {
...
}
engine.register_type::<VeryComplexType>()
.register_fn("+", add_pure /* or add_method*/);
// Very expensive to call, as the 'VeryComplexType' is cloned before each call.
fn add_pure(obj: VeryComplexType, offset: i64) -> i64 {
do_add(obj, offset)
}
// Efficient to call, as only a reference to the 'VeryComplexType' is passed.
fn add_method(obj: &mut VeryComplexType, offset: i64) -> i64 {
do_add(obj, offset)
}
Data Race Considerations
Data races are not possible in Rhai under the no_closure
feature because no sharing ever occurs.
Because methods always take a mutable reference as the first argument, even it the value is never changed, care must be taken when using shared values with methods.
Usually data races are not possible in Rhai because, for each function call, there is ever only one value that is mutable – the first argument of a method. All other arguments are cloned.
It is possible, however, to create a data race with a shared value, when the same value is captured in a closure and then used again as the object of calling that closure!
let x = 20;
x.is_shared() == false; // 'x' is not shared, so no data race is possible
let f = |a| this += x + a; // 'x' is captured in this closure
x.is_shared() == true; // now 'x' is shared
x.call(f, 2); // <- error: data race detected on 'x'
Custom Type Property Getters and Setters
Property getters and setters are intended for custom types.
Any getter or setter function registered for object maps is simply ignored.
Get/set syntax on object maps is interpreted as access to properties.
A custom type can also expose properties by registering get
and/or set
functions.
Properties can be accessed in a Rust-like syntax:
object
.
propertyobject
.
property=
value;
The Elvis operator can be used to short-circuit
processing if the object itself is ()
:
// returns () if object is ()
object?.
property
// no action if object is ()
object?.
property=
value;
Property getter and setter functions are called behind the scene.
They each take a &mut
reference to the first parameter.
Getters and setters are disabled under the no_object
feature.
Engine API | Function signature(s) ( T: Clone = custom type,V: Clone = data type) | Can mutate T ? |
---|---|---|
register_get | Fn(&mut T) -> V | yes, but not advised |
register_set | Fn(&mut T, V) | yes |
register_get_set | getter: Fn(&mut T) -> V setter: Fn(&mut T, V) | yes, but not advised in getter |
Rhai does NOT support normal references (i.e. &T
) as parameters.
All references must be mutable (i.e. &mut T
).
By convention, property getters are assumed to be pure, meaning that they are not supposed to mutate the custom type, although there is nothing that prevents this mutation in Rust.
Even though a property getter function also takes &mut
as the first parameter, Rhai assumes that
no data is changed when the function is called.
Examples
#[derive(Debug, Clone)]
struct TestStruct {
field: String
}
impl TestStruct {
// Remember &mut must be used even for getters.
fn get_field(&mut self) -> String {
// Property getters are assumed to be PURE, meaning they are
// not supposed to mutate any data.
self.field.clone()
}
fn set_field(&mut self, new_val: String) {
self.field = new_val;
}
fn new() -> Self {
Self { field: "hello, world!".to_string() }
}
}
let mut engine = Engine::new();
engine.register_type::<TestStruct>()
.register_get_set("xyz", TestStruct::get_field, TestStruct::set_field)
.register_fn("new_ts", TestStruct::new);
let result = engine.eval::<String>(
r#"
let a = new_ts();
a.xyz = "42";
a.xyz
"#)?;
println!("Answer: {result}"); // prints 42
Fallback to Indexer
See this section for details on an indexer acting as fallback to properties.
If the getter/setter of a particular property is not defined, but an indexer is defined on the custom type with string index, then the corresponding indexer will be called with the name of the property as the index value.
In other words, indexers act as a fallback to property getters/setters.
a.foo // if property getter for 'foo' doesn't exist...
a["foo"] // an indexer (if any) is tried
This feature makes it very easy for custom types to act as property bags (similar to an object map) which can add/remove properties at will.
Chaining Updates
It is possible to chain property accesses and/or indexing (via indexers) together to modify a particular property value at the end of the chain.
Rhai detects such modifications and updates the changed values all the way back up the chain.
In the end, the syntax works as expected by intuition, automatically and without special attention.
// Assume a deeply-nested object...
let root = get_new_container_object();
root.prop1.sub["hello"].list[0].value = 42;
// The above is equivalent to:
// First getting all the intermediate values...
let prop1_value = root.prop1; // via property getter
let sub_value = prop1_value.sub; // via property getter
let sub_value_item = sub_value["hello"]; // via index getter
let list_value = sub_value_item.list; // via property getter
let list_item = list_value[0]; // via index getter
list_item.value = 42; // modify property value deep down the chain
// Propagate the changes back up the chain...
list_value[0] = list_item; // via index setter
sub_value_item.list = list_value; // via property setter
sub_value["hello"] = sub_value_item; // via index setter
prop1_value.sub = sub_value; // via property setter
root.prop1 = prop1_value; // via property setter
// The below prints 42...
print(root.prop1.sub["hello"].list[0].value);
Custom Type Indexers
A custom type can also expose an indexer by registering an indexer function.
A custom type with an indexer function defined can use the bracket notation to get/set a property value at a particular index:
object
[
index]
object
[
index]
=
value;
The Elvis notation is similar except that it returns ()
if the object itself is ()
.
// returns () if object is ()
object?[
index]
// no action if object is ()
object?[
index]
=
value;
Like property getters/setters, indexers take a &mut
reference to the first parameter.
They also take an additional parameter of any type that serves as the index within brackets.
Indexers are disabled when the no_index
and no_object
features are used together.
Engine API | Function signature(s) ( T: Clone = custom type,X: Clone = index type,V: Clone = data type) | Can mutate T ? |
---|---|---|
register_indexer_get | Fn(&mut T, X) -> V | yes, but not advised |
register_indexer_set | Fn(&mut T, X, V) | yes |
register_indexer_get_set | getter: Fn(&mut T, X) -> V setter: Fn(&mut T, X, V) | yes, but not advised in getter |
Rhai does NOT support normal references (i.e. &T
) as parameters.
All references must be mutable (i.e. &mut T
).
By convention, index getters are not supposed to mutate the custom type, although there is nothing that prevents this mutation.
For fallible indexers, it is customary to return
EvalAltResult::ErrorIndexNotFound
when called with an invalid index value.
Cannot Override Arrays, BLOB’s, Object Maps, Strings and Integers
They can be defined in a plugin module, but will be ignored.
For efficiency reasons, indexers cannot be used to overload (i.e. override) built-in indexing operations for arrays, object maps, strings and integers (acting as bit-field operation).
The following types have built-in indexer implementations that are fast and efficient.
Type | Index type | Return type | Description |
---|---|---|---|
Array | INT | Dynamic | access a particular element inside the array |
Blob | INT | INT | access a particular byte value inside the BLOB |
Map | ImmutableString ,String , &str | Dynamic | access a particular property inside the object map |
ImmutableString ,String , &str | INT | character | access a particular character inside the string |
INT | INT | boolean | access a particular bit inside the integer number as a bit-field |
INT | range | INT | access a particular range of bits inside the integer number as a bit-field |
In general, it is a bad idea to overload indexers for any of the standard types supported internally by Rhai, since built-in indexers may be added in future versions.
Examples
#[derive(Debug, Clone)]
struct TestStruct {
fields: Vec<i64>
}
impl TestStruct {
// Remember &mut must be used even for getters
fn get_field(&mut self, index: String) -> i64 {
self.fields[index.len()]
}
fn set_field(&mut self, index: String, value: i64) {
self.fields[index.len()] = value
}
fn new() -> Self {
Self { fields: vec![1, 2, 3, 4, 5] }
}
}
let mut engine = Engine::new();
engine.register_type::<TestStruct>()
.register_fn("new_ts", TestStruct::new)
// Short-hand: .register_indexer_get_set(TestStruct::get_field, TestStruct::set_field);
.register_indexer_get(TestStruct::get_field)
.register_indexer_set(TestStruct::set_field);
let result = engine.eval::<i64>(
r#"
let a = new_ts();
a["xyz"] = 42; // these indexers use strings
a["xyz"] // as the index type
"#)?;
println!("Answer: {result}"); // prints 42
Convention for Negative Index
If the indexer takes a signed integer as an index (e.g. the standard INT
type), care should be
taken to handle negative values passed as the index.
It is a standard API convention for Rhai to assume that an index position counts backwards from the end if it is negative.
-1
as an index usually refers to the last item, -2
the second to last item, and so on.
Therefore, negative index values go from -1
(last item) to -length
(first item).
A typical implementation for negative index values is:
// The following assumes:
// 'index' is 'INT', 'items: usize' is the number of elements
let actual_index = if index < 0 {
index.checked_abs().map_or(0, |n| items - (n as usize).min(items))
} else {
index as usize
};
The end of a data type can be interpreted creatively. For example, in an integer used as a
bit-field, the start is the least-significant-bit (LSB) while the end
is the
most-significant-bit (MSB).
Convention for Range Index
By convention, negative values are not interpreted specially in indexers for ranges.
It is very common for ranges to be used as indexer parameters via the types
std::ops::Range<INT>
(exclusive) and std::ops::RangeInclusive<INT>
(inclusive).
One complication is that two versions of the same indexer must be defined to support exclusive and inclusive ranges respectively.
use std::ops::{Range, RangeInclusive};
let mut engine = Engine::new();
engine
/// Version of indexer that accepts an exclusive range
.register_indexer_get_set(
|obj: &mut TestStruct, range: Range<i64>| -> bool { ... },
|obj: &mut TestStruct, range: Range<i64>, value: bool| { ... },
)
/// Version of indexer that accepts an inclusive range
.register_indexer_get_set(
|obj: &mut TestStruct, range: RangeInclusive<i64>| -> bool { ... },
|obj: &mut TestStruct, range: RangeInclusive<i64>, value: bool| { ... },
);
engine.run(
"
let obj = new_ts();
let x = obj[0..12]; // use exclusive range
obj[0..=11] = !x; // use inclusive range
")?;
Indexer as Property Access Fallback
Such an indexer allows easy creation of property bags (similar to object maps) which can dynamically add/remove properties.
An indexer taking a string index is a special case – it acts as a fallback to property getters/setters.
During a property access, if the appropriate property getter/setter is not defined, an indexer is called and passed the string name of the property.
This is also extremely useful as a short-hand for indexers, when the string keys conform to property name syntax.
// Assume 'obj' has an indexer defined with string parameters...
// Let's create a new key...
obj.hello_world = 42;
// The above is equivalent to this:
obj["hello_world"] = 42;
// You can write this...
let x = obj["hello_world"];
// but it is easier with this...
let x = obj.hello_world;
Since an indexer can serve as a fallback to property access, it is possible to implement swizzling of properties for use with vector-like custom types.
Such an indexer defined on a custom type (for instance, Float4
) can inspect the property name,
construct a proper return value based on the swizzle pattern, and return it.
// Assume 'v' is a 'Float4'
let r = v.w; // -> v.w
let r = v.xx; // -> Float2::new(v.x, v.x)
let r = v.yxz; // -> Float3::new(v.y, v.x, v.z)
let r = v.xxzw; // -> Float4::new(v.x, v.x, v.z, v.w)
let r = v.yyzzxx; // error: property 'yyzzxx' not found
Caveat – Reverse is NOT True
The reverse, however, is not true – when an indexer fails or doesn’t exist, the corresponding property getter/setter, if any, is not called.
type MyType = HashMap<String, i64>;
let mut engine = Engine::new();
// Define custom type, property getter and string indexers
engine.register_type::<MyType>()
.register_fn("new_ts", || {
let mut obj = MyType::new();
obj.insert("foo".to_string(), 1);
obj.insert("bar".to_string(), 42);
obj.insert("baz".to_string(), 123);
obj
})
// Property 'hello'
.register_get("hello", |obj: &mut MyType| obj.len() as i64)
// Index getter/setter
.register_indexer_get(|obj: &mut MyType, prop: &str| -> Result<i64, Box<EvalAltResult>>
obj.get(index).cloned().ok_or_else(|| "not found".into())
).register_indexer_set(|obj: &mut MyType, prop: &str, value: i64|
obj.insert(prop.to_string(), value)
);
engine.run("let ts = new_ts(); print(ts.foo);");
// ^^^^^^
// Calls ts["foo"] - getter for 'foo' does not exist
engine.run("let ts = new_ts(); print(ts.bar);");
// ^^^^^^
// Calls ts["bar"] - getter for 'bar' does not exist
engine.run("let ts = new_ts(); ts.baz = 999;");
// ^^^^^^^^^^^^
// Calls ts["baz"] = 999 - setter for 'baz' does not exist
engine.run(r#"let ts = new_ts(); print(ts["hello"]);"#);
// ^^^^^^^^^^^
// Error: Property getter for 'hello' not a fallback for indexer
Custom Collection Types
A collection type holds a… well… collection of items. It can be homogeneous (all items are
the same type) or heterogeneous (items are of different types, use Dynamic
to hold).
Because their only purpose for existence is to hold a number of items, collection types commonly register the following methods.
Method | Description |
---|---|
len method and property | gets the total number of items in the collection |
clear | clears the collection |
contains | checks if a particular item exists in the collection |
add , += operator | adds a particular item to the collection |
remove , -= operator | removes a particular item from the collection |
merge or + operator | merges two collections, yielding a new collection with all items |
Collections are typically iterable.
It is customary to use Engine::register_iterator
to allow iterating the collection if
it implements IntoIterator
.
Alternative, register a specific type iterator for the custom type.
A plugin module makes defining an entire API for a custom type a snap.
Example
type MyBag = HashSet<MyItem>;
engine
.register_type_with_name::<MyBag>("MyBag")
.register_iterator::<MyBag>()
.register_fn("new_bag", || MyBag::new())
.register_fn("len", |col: &mut MyBag| col.len() as i64)
.register_get("len", |col: &mut MyBag| col.len() as i64)
.register_fn("clear", |col: &mut MyBag| col.clear())
.register_fn("contains", |col: &mut MyBag, item: i64| col.contains(&item))
.register_fn("add", |col: &mut MyBag, item: MyItem| col.insert(item))
.register_fn("+=", |col: &mut MyBag, item: MyItem| col.insert(item))
.register_fn("remove", |col: &mut MyBag, item: MyItem| col.remove(&item))
.register_fn("-=", |col: &mut MyBag, item: MyItem| col.remove(&item))
.register_fn("+", |mut col1: MyBag, col2: MyBag| {
col1.extend(col2.into_iter());
col1
});
What About Indexers?
Many users are tempted to register indexers for custom collections. This essentially makes the
original Rust type something similar to Vec<MyType>
.
Rhai’s standard Array
type is Vec<Dynamic>
which already holds an ordered, iterable and
indexable collection of dynamic items. Since Rhai has built-in support, manipulating arrays is fast.
In most circumstances, it is better to use Array
instead of a custom type.
Dynamic
implements FromIterator
for all iterable types and an Array
is created in the process.
So, converting a typed array (i.e. Vec<MyType>
) into an array in Rhai is as simple as calling .into()
.
// Say you have a custom typed array...
let my_custom_array: Vec<MyType> = do_lots_of_calc(42);
// Convert it into a 'Dynamic' that holds an array
let value: Dynamic = my_custom_array.into();
// Use is anywhere in Rhai...
scope.push("my_custom_array", value);
engine
// Raw function that returns a custom type
.register_fn("do_lots_of_calc_raw", do_lots_of_calc)
// Wrap function that return a custom typed array
.register_fn("do_lots_of_calc", |seed: i64| -> Dynamic {
let result = do_lots_of_calc(seed); // Vec<MyType>
result.into() // Array in Dynamic
});
TL;DR
Reason #1: Performance
A main reason why anybody would want to do this is to avoid the overhead of storing Dynamic
items.
This is why BLOB’s is a built-in data type in Rhai, even though it is actually defined as Vec<u8>
.
The overhead of using Dynamic
(16 bytes) versus u8
(1 byte) is worth the trouble, although the
performance gains may not be as pronounced as expected: benchmarks show a 15% speed improvement inside
a tight loop compared with using an array.
Vec<MyType>
, however, will be treated as an opaque custom type in Rhai, so performance is not optimized.
What you gain from avoiding Dynamic
, you pay back in terms of slower access to the Vec
as well as MyType
(which is treated as yet another opaque custom type).
Reason #2: API
Another reason why it shouldn’t be done is due to the large number of functions and methods that must be registered for each type of this sort. One only has to look at the vast API surface of arrays to see the common methods that a user would expect to be available.
Since Vec<Type>
looks, feels and quacks just like a normal array, and the usage syntax is almost equivalent (except
for the fact that the data type is restricted), users would be frustrated if they find that certain functions available for
arrays are not provided.
This is similar to JavaScript’s Typed Arrays. They are quite awkward to work with, and basically each has a full API definition that must be pre-registered.
Disable Custom Types
The no_object
feature disables support for custom types including:
-
method-style function calls (e.g.
obj.method()
), -
object maps and the
Map
type, -
the
register_get
,register_set
andregister_get_set
API’s forEngine
Printing for Custom Types
Provide These Functions
To use custom types for print
and debug
, or convert a custom type into a string,
it is necessary that the following functions, at minimum, be registered (assuming the custom type
is T: Display + Debug
).
Function | Signature | Typical implementation | Usage |
---|---|---|---|
to_string | |x: &mut T| -> String | x.to_string() | converts the custom type into a string |
to_debug | |x: &mut T| -> String | format!("{x:?}") | converts the custom type into a string in debug format |
If these functions are defined via a plugin module, be sure to include the #[rhai_fn(global)]
attribute
in order to make them available globally.
See this section for more details.
Also Consider These
The following functions are implemented using to_string
or to_debug
by default, but can be
overloaded with custom versions.
Function | Signature | Default | Usage |
---|---|---|---|
print | |x: &mut T| -> String | to_string | converts the custom type into a string for the print statement |
debug | |x: &mut T| -> String | to_debug | converts the custom type into a string for the debug statement |
+ operator | |s: &str, x: T| -> String | to_string | concatenates the custom type with another string |
+ operator | |x: &mut T, s: &str| -> String | to_string | concatenates another string with the custom type |
+= operator | |s: &mut ImmutableString, x: T| | to_string | appends the custom type to an existing string |
Modules
Rhai allows organizing functionalities (functions, both Rust-based and scripted, and variables) into independent modules.
A module has the type rhai::Module
and holds a collection of functions, variables, type
iterators and sub-modules.
It may contain entirely Rust functions, or it may encapsulate a Rhai script together with all the functions and variables defined by that script.
Other scripts then load this module and use the functions and variables exported.
Alternatively, modules can be registered directly into an Engine
and made available to scripts,
either globally or under individual static module namespaces.
Modules can be disabled via the no_module
feature.
Usage Patterns
Usage | API | Lookup | Sub-modules? | Variables? |
---|---|---|---|---|
Global module | Engine:: register_global_module | simple name | ignored | yes |
Static module | Engine:: register_static_module | namespace-qualified name | yes | yes |
Dynamic module | import statement | namespace-qualified name | yes | yes |
Create a Module in Rust
The Easy Way – Plugin
By far the simplest way to create a module is via a plugin module which converts a normal Rust module into a Rhai module via procedural macros.
The Hard Way – Module
API
Manually creating a module is possible via the Module
public API, which is volatile and may
change from time to time.
For the complete Module
public API, refer to the
documentation online.
Create a Module from an AST
Module::eval_ast_as_new
Module::eval_ast_as_new
encapsulates the entire AST
into each function call, merging the
module namespace with the global namespace.
Therefore, functions defined within the same module script can cross-call each other.
See Export Variables, Functions and Sub-Modules from Script for details on how to prepare a Rhai script for this purpose as well as to control which functions/variables to export.
A module can be created from a single script (or pre-compiled AST
) containing global
variables, functions and sub-modules via Module::eval_ast_as_new
.
When given an AST
, it is first evaluated (usually to import modules and set up global
constants used by functions), then the following items are exposed as members of the new module:
-
global variables and constants – all variables and constants exported via the
export
statement (those not exported remain hidden), -
imported modules that remain in the
Scope
at the end of a script run become sub-modules.
Examples
Don’t forget the export
statement, otherwise there will be no variables exposed by the
module other than non-private
functions (unless that’s intentional).
use rhai::{Engine, Module};
let engine = Engine::new();
// Compile a script into an 'AST'
let ast = engine.compile(
r#"
// Functions become module functions
fn calc(x) {
x + add_len(x, 1) // functions within the same module
// can always cross-call each other!
}
fn add_len(x, y) {
x + y.len
}
// Imported modules become sub-modules
import "another module" as extra;
// Variables defined at global level can become module variables
const x = 123;
let foo = 41;
let hello;
// Variable values become constant module variable values
foo = calc(foo);
hello = `hello, ${foo} worlds!`;
// Finally, export the variables and modules
export x as abc; // aliased variable name
export foo;
export hello;
"#)?;
// Convert the 'AST' into a module, using the 'Engine' to evaluate it first
// A copy of the entire 'AST' is encapsulated into each function,
// allowing functions in the module script to cross-call each other.
let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
// 'module' now contains:
// - sub-module: 'extra'
// - functions: 'calc', 'add_len'
// - constants: 'abc' (renamed from 'x'), 'foo', 'hello'
Make a Module Available to Scripts
Use Case 1 – Make It Globally Available
Engine::register_global_module
registers a shared module into the
global namespace.
This is by far the easiest way to expose a module’s functionalities to Rhai.
All functions, variables/constants and type iterators can be accessed without namespace qualifiers.
Sub-modules are ignored.
use rhai::{Engine, Module, FuncRegistration};
let mut module = Module::new(); // new module
// Add new function.
FuncRegistration::new("inc")
.with_params_info(&["x: i64", "i64"])
.set_into_module(&mut module, |x: i64| x + 1);
// Use 'Module::set_var' to add variables.
module.set_var("MYSTIC_NUMBER", 41_i64);
// Register the module into the global namespace of the Engine.
let mut engine = Engine::new();
engine.register_global_module(module.into());
// No need to import module...
engine.eval::<i64>("inc(MYSTIC_NUMBER)")? == 42;
Equivalent to Engine::register_XXX
Engine::register_fn
etc. are actually implemented by adding functions to an
internal module!
Registering a module via Engine::register_global_module
is essentially the same
as calling Engine::register_fn
(or any of the Engine::register_XXX
API) individually
on each top-level function within that module.
// The above is essentially the same as:
let mut engine = Engine::new();
engine.register_fn("inc", |x: i64| x + 1);
engine.eval::<i64>("inc(41)")? == 42; // no need to import module
Use Case 2 – Make It a Static Namespace
Engine::register_static_module
registers a module and under a specific
module namespace.
use rhai::{Engine, Module, FuncRegistration};
let mut module = Module::new(); // new module
// Add new function.
FuncRegistration::new("inc")
.with_params_info(&["x: i64", "i64"])
.set_into_module(&mut module, |x: i64| x + 1);
// Use 'Module::set_var' to add variables.
module.set_var("MYSTIC_NUMBER", 41_i64);
// Register the module into the Engine as the static module namespace path
// 'services::calc'
let mut engine = Engine::new();
engine.register_static_module("services::calc", module.into());
// Refer to the 'services::calc' module...
engine.eval::<i64>("services::calc::inc(services::calc::MYSTIC_NUMBER)")? == 42;
Expose functions to the global namespace
Type iterators are special — they are always exposed to the global namespace.
The Module
API can optionally expose functions to the global namespace
by setting the namespace
parameter to FnNamespace::Global
.
This way, getters/setters and indexers for custom types can work as expected.
use rhai::{Engine, Module, FuncRegistration, FnNamespace};
let mut module = Module::new(); // new module
// Add new function.
FuncRegistration::new("inc")
.with_params_info(&["x: i64", "i64"])
.with_namespace(FnNamespace::Global) // <- global namespace
.set_into_module(&mut module, |x: i64| x + 1);
// Use 'Module::set_var' to add variables.
module.set_var("MYSTIC_NUMBER", 41_i64);
// Register the module into the Engine as a static module namespace 'calc'
let mut engine = Engine::new();
engine.register_static_module("calc", module.into());
// 'inc' works when qualified by the namespace
engine.eval::<i64>("calc::inc(calc::MYSTIC_NUMBER)")? == 42;
// 'inc' also works without a namespace qualifier
// because it is exposed to the global namespace
engine.eval::<i64>("let x = calc::MYSTIC_NUMBER; x.inc()")? == 42;
engine.eval::<i64>("let x = calc::MYSTIC_NUMBER; inc(x)")? == 42;
Use Case 3 – Make It Dynamically Loadable
In order to dynamically load a custom module, there must be a module resolver which serves
the module when loaded via import
statements.
The easiest way is to use, for example, the StaticModuleResolver
to hold such
a custom module.
use rhai::{Engine, Scope, Module, FuncRegistration};
use rhai::module_resolvers::StaticModuleResolver;
let mut module = Module::new(); // new module
module.set_var("answer", 41_i64); // variable 'answer' under module
FuncRegistration::new("inc")
.with_params_info(&["x: i64"])
.set_into_module(&mut module, |x: i64| x + 1);
// Create the module resolver
let mut resolver = StaticModuleResolver::new();
// Add the module into the module resolver under the name 'question'
// They module can then be accessed via: 'import "question" as q;'
resolver.insert("question", module);
// Set the module resolver into the 'Engine'
let mut engine = Engine::new();
engine.set_module_resolver(resolver);
// Use namespace-qualified variables
engine.eval::<i64>(
r#"
import "question" as q;
q::answer + 1
"#)? == 42;
// Call namespace-qualified functions
engine.eval::<i64>(
r#"
import "question" as q;
q::inc(q::answer)
"#)? == 42;
Module Resolvers
See the section on Importing Modules for more details.
When encountering an import
statement, Rhai attempts to resolve the module based on the path string.
Module Resolvers are service types that implement the ModuleResolver
trait.
Set into Engine
An Engine
’s module resolver is set via a call to Engine::set_module_resolver
:
use rhai::module_resolvers::{DummyModuleResolver, StaticModuleResolver};
// Create a module resolver
let resolver = StaticModuleResolver::new();
// Register functions into 'resolver'...
// Use the module resolver
engine.set_module_resolver(resolver);
// Effectively disable 'import' statements by setting module resolver to
// the 'DummyModuleResolver' which acts as... well... a dummy.
engine.set_module_resolver(DummyModuleResolver::new());
Built-in Module Resolvers
There are a number of standard module resolvers built into Rhai, the default being the
FileModuleResolver
which simply loads a script file based on the path (with .rhai
extension attached) and execute it to form a module.
Built-in module resolvers are grouped under the rhai::module_resolvers
module.
DummyModuleResolver
DummyModuleResolver
is the default for no_std
or Engine::new_raw
.
This module resolver acts as a dummy and fails all module resolution calls.
FileModuleResolver
FileModuleResolver
is the default for Engine::new
.
The default module resolution service, not available for no_std
or WASM builds.
Loads a script file (based off the current directory or a specified one) with .rhai
extension.
Function Namespace
All functions in the global namespace, plus all those defined in the same module, are merged into a unified namespace.
All modules imported at global level via import
statements become sub-modules,
which are also available to functions defined within the same script file.
Base Directory
If the base directory is not set, then relative paths are based off the directory of the loading script.
This allows scripts to simply cross-load each other.
Relative paths are resolved relative to a root directory, which is usually the base directory.
The base directory can be set via FileModuleResolver::new_with_path
or
FileModuleResolver::set_base_path
.
Custom Scope
The set_scope
method adds an optional Scope
which will be used to optimize module scripts.
Caching
By default, modules are also cached so a script file is only evaluated once, even when repeatedly imported.
Unix Shebangs
On Unix-like systems, the shebang (#!
) is used at the very beginning of a script file to mark a
script with an interpreter (for Rhai this would be rhai-run
).
If a script file starts with #!
, the entire first line is skipped.
Because of this, Rhai scripts with shebangs at the beginning need no special processing.
#!/home/to/me/bin/rhai-run
// This is a Rhai script
let answer = 42;
print(`The answer is: ${answer}`);
Example
┌────────────────┐
│ my_module.rhai │
└────────────────┘
// This function overrides any in the main script.
private fn inner_message() { "hello! from module!" }
fn greet() {
print(inner_message()); // call function in module script
}
fn greet_main() {
print(main_message()); // call function not in module script
}
┌───────────┐
│ main.rhai │
└───────────┘
// This function is overridden by the module script.
fn inner_message() { "hi! from main!" }
// This function is found by the module script.
fn main_message() { "main here!" }
import "my_module" as m;
m::greet(); // prints "hello! from module!"
m::greet_main(); // prints "main here!"
Simulate Virtual Functions
When calling a namespace-qualified function defined within a module, other functions defined within the same module override any similar-named functions (with the same number of parameters) defined in the global namespace.
This is to ensure that a module acts as a self-contained unit and functions defined in the calling script do not override module code.
In some situations, however, it is actually beneficial to do it in reverse: have module functions call functions defined in the calling script (i.e. in the global namespace) if they exist, and only call those defined in the module if none are found.
One such situation is the need to provide a default implementation to a simulated virtual function:
┌────────────────┐
│ my_module.rhai │
└────────────────┘
// Do not do this (it will override the main script):
// fn message() { "hello! from module!" }
// This function acts as the default implementation.
private fn default_message() { "hello! from module!" }
// This function depends on a 'virtual' function 'message'
// which is not defined in the module script.
fn greet() {
if is_def_fn("message", 0) { // 'is_def_fn' detects if 'message' is defined.
print(message());
} else {
print(default_message());
}
}
┌───────────┐
│ main.rhai │
└───────────┘
// The main script defines 'message' which is needed by the module script.
fn message() { "hi! from main!" }
import "my_module" as m;
m::greet(); // prints "hi! from main!"
┌────────────┐
│ main2.rhai │
└────────────┘
// The main script does not define 'message' which is needed by the module script.
import "my_module" as m;
m::greet(); // prints "hello! from module!"
StaticModuleResolver
StaticModuleResolver
is often used with no_std
in embedded environments
without a file system.
Loads modules that are statically added.
Functions are searched in the global namespace by default.
use rhai::{Module, module_resolvers::StaticModuleResolver};
let module: Module = create_a_module();
let mut resolver = StaticModuleResolver::new();
resolver.insert("my_module", module);
engine.set_module_resolver(resolver);
ModuleResolversCollection
A collection of module resolvers.
Modules are resolved from each resolver in sequential order.
This is useful when multiple types of modules are needed simultaneously.
DylibModuleResolver
DylibModuleResolver
resides in the rhai-dylib
crate which must be specified
as a dependency:
[dependencies]
rhai-dylib = { version = "0.1" }
rhai-dylib
currently supports only Linux and Windows.
Parallel to how the FileModuleResolver
works, DylibModuleResolver
loads external
native Rust modules from compiled dynamic shared libraries (e.g. .so
in Linux and .dll
in
Windows).
Therefore, FileModuleResolver
loads Rhai script files while DylibModuleResolver
loads native Rust shared libraries. It is very common to have the two work together.
Example
use rhai::{Engine, Module};
use rhai::module_resolvers::{FileModuleResolver, ModuleResolversCollection};
use rhai_dylib::module_resolvers::DylibModuleResolver;
let mut engine = Engine::new();
// Use a module resolvers collection
let mut resolvers = ModuleResolversCollection::new();
// First search for script files in the file system
resolvers += FileModuleResolver::new();
// Then search for shared-library plugins in the file system
resolvers += DylibModuleResolver::new();
// Set the module resolver into the engine
engine.set_module_resolver(resolvers);
┌─────────────┐
│ Rhai Script │
└─────────────┘
// If there is 'path/to/my_module.rhai', load it.
// Otherwise, check for 'path/to/my_module.so' on Linux
// ('path/to/my_module.dll' on Windows).
import "path/to/my_module" as m;
m::greet();
Implement a Custom Module Resolver
For many applications in which Rhai is embedded, it is necessary to customize the way that modules are resolved. For instance, modules may need to be loaded from script texts stored in a database, not in the file system.
A module resolver must implement the ModuleResolver
trait, which contains only one
required function: resolve
.
When Rhai prepares to load a module, ModuleResolver::resolve
is called with the name
of the module path (i.e. the path specified in the import
statement).
Upon success, it should return a shared module wrapped by Rc
(or Arc
under sync
).
The module resolver should call Module::build_index
on the target module before returning it.
- This method flattens the entire module tree and indexes it for fast function name resolution.
- If the module is already indexed, calling this method has no effect.
-
If the path does not resolve to a valid module, return
EvalAltResult::ErrorModuleNotFound
. -
If the module failed to load, return
EvalAltResult::ErrorInModule
.
Example of a Custom Module Resolver
use rhai::{ModuleResolver, Module, Engine, EvalAltResult};
// Define a custom module resolver.
struct MyModuleResolver {}
// Implement the 'ModuleResolver' trait.
impl ModuleResolver for MyModuleResolver {
// Only required function.
fn resolve(
&self,
engine: &Engine, // reference to the current 'Engine'
source_path: Option<&str>, // path of the parent module
path: &str, // the module path
pos: Position, // position of the 'import' statement
) -> Result<Rc<Module>, Box<EvalAltResult>> {
// Check module path.
if is_valid_module_path(path) {
// Load the custom module
match load_secret_module(path) {
Ok(my_module) => {
my_module.build_index(); // index it
Rc::new(my_module) // make it shared
},
// Return 'EvalAltResult::ErrorInModule' upon loading error
Err(err) => Err(EvalAltResult::ErrorInModule(path.into(), Box::new(err), pos).into())
}
} else {
// Return 'EvalAltResult::ErrorModuleNotFound' if the path is invalid
Err(EvalAltResult::ErrorModuleNotFound(path.into(), pos).into())
}
}
}
let mut engine = Engine::new();
// Set the custom module resolver into the 'Engine'.
engine.set_module_resolver(MyModuleResolver {});
engine.run(
r#"
import "hello" as foo; // this 'import' statement will call
// 'MyModuleResolver::resolve' with "hello" as 'path'
foo:bar();
"#)?;
Advanced – ModuleResolver::resolve_ast
There is another function in the ModuleResolver
trait, resolve_ast
, which is a
low-level API intended for advanced usage scenarios.
ModuleResolver::resolve_ast
has a default implementation that simply returns None
,
which indicates that this API is not supported by the module resolver.
Any module resolver that serves modules based on Rhai scripts should implement
ModuleResolver::resolve_ast
. When called, the compiled AST
of the script should be returned.
ModuleResolver::resolve_ast
should not return an error if ModuleResolver::resolve
will not.
On the other hand, the same error should be returned if ModuleResolver::resolve
will return one.
Compile to a Self-Contained AST
When a script imports external modules that may not be available later on, it is
possible to eagerly pre-resolve these imports and embed them directly into a
self-contained AST
.
For instance, a system may periodically connect to a central source (e.g. a database) to load
scripts and compile them to AST
form. Afterwards, in order to conserve bandwidth (or due to
other physical limitations), it is disconnected from the central source for self-contained
operation.
Compile a script into a self-contained AST
via Engine::compile_into_self_contained
.
let mut engine = Engine::new();
// Compile script into self-contained AST using the current
// module resolver (default to `FileModuleResolver`) to pre-resolve
// 'import' statements.
let ast = engine.compile_into_self_contained(&mut scope, script)?;
// Make sure we can no longer resolve any module!
engine.set_module_resolver(DummyModuleResolver::new());
// The AST still evaluates fine, even with 'import' statements!
engine.run(&ast)?;
When such an AST
is evaluated, import
statements within are provided the pre-resolved
modules without going through the normal module resolution process.
Only Static Paths
Engine::compile_into_self_contained
only pre-resolves import
statements in the script
that are static, i.e. with a path that is a string literal.
// The following import is pre-resolved.
import "hello" as h;
if some_event() {
// The following import is pre-resolved.
import "hello" as h;
}
fn foo() {
// The following import is pre-resolved.
import "hello" as h;
}
// The following import is also pre-resolved because the expression
// is usually optimized into a single string during compilation.
import "he" + "llo" as h;
let module_name = "hello";
// The following import is NOT pre-resolved.
import module_name as h;
Plugin Modules
Rhai contains a robust plugin system that greatly simplifies registration of custom functionality.
Instead of using the complicated Engine::register_XXX
or Module
’s FuncRegistration
API to
register Rust functions, a plugin simplifies the work of creating and registering new
functionality to an Engine
.
Plugins are processed via a set of procedural macros under the rhai::plugin
module. These allow
registering Rust functions directly into an Engine
instance, or adding Rust modules as packages.
Import Prelude
When using the plugins system, the entire rhai::plugin
module must be imported as a prelude
because code generated will need these imports.
use rhai::plugin::*;
#[export_module]
When applied to a Rust module, the #[export_module]
attribute generates the necessary code and
metadata to allow Rhai access to its public (i.e. marked pub
) functions, constants, type aliases,
and sub-modules.
This code is exactly what would need to be written by hand to achieve the same goal, and is custom fit to each exported item.
All pub
functions become registered functions, constants become module constants, type aliases
become custom types, and sub-modules become Rhai sub-modules.
Module element | Example | Rhai module equivalent |
---|---|---|
pub constant | pub const FOO: i64 = 42; | constant |
pub type alias | pub type Foo = Bar<i64> | custom type |
pub function | pub fn foo(...) { ... } | function |
pub sub-module | pub mod foo { ... } | sub-module |
use rhai::plugin::*; // a "prelude" import for macros
// My custom type
pub struct TestStruct {
pub value: i64
}
#[export_module]
mod my_module {
// This type alias will register the friendly name 'ABC' for the
// custom type 'TestStruct'.
pub type ABC = TestStruct;
// This constant will be registered as the constant variable 'MY_NUMBER'.
// Ignored when registered as a global module.
pub const MY_NUMBER: i64 = 42;
// This function will be registered as 'greet'
// but is only available with the 'greetings' feature.
#[cfg(feature = "greetings")]
pub fn greet(name: &str) -> String {
format!("hello, {}!", name)
}
/// This function will be registered as 'get_num'.
///
/// If this is a Rust doc-comment, then it is included in the metadata.
pub fn get_num() -> i64 {
mystic_number()
}
/// This function will be registered as 'create_abc'.
pub fn create_abc(value: i64) -> ABC {
ABC { value }
}
/// This function will be registered as the 'value' property of type 'ABC'.
#[rhai_fn(get = "value")]
pub fn get_value(ts: &mut ABC) -> i64 {
ts.value
}
// This function will be registered as 'increment'.
// It will also be exposed to the global namespace since 'global' is set.
#[rhai_fn(global)]
pub fn increment(ts: &mut ABC) {
ts.value += 1;
}
// This function is not 'pub', so NOT registered.
fn mystic_number() -> i64 {
42
}
// This global function defines a custom operator '@'.
#[rhai_fn(name = "@", global)]
pub fn square_add(x: i64, y: i64) -> i64 {
x * x + y * y
}
// Sub-modules are ignored when the module is registered globally.
pub mod my_sub_module {
// This function is ignored when registered globally.
// Otherwise it is a valid registered function under a sub-module.
pub fn get_info() -> String {
"hello".to_string()
}
}
// Sub-modules are commonly used to put feature gates on a group of
// functions because feature gates cannot be put on function definitions.
// This is currently a limitation of the plugin procedural macros.
#[cfg(feature = "advanced_functions")]
pub mod advanced {
// This function is ignored when registered globally.
// Otherwise it is a valid registered function under a sub-module
// which only exists when the 'advanced_functions' feature is used.
pub fn advanced_calc(input: i64) -> i64 {
input * 2
}
}
}
If the metadata
feature is active, doc-comments (i.e. comments starting with ///
or wrapped
with /**
… */
) on plugin functions are extracted into metadata.
It is always a good idea to put doc-comments onto plugin modules and plugin functions, as they can be used to auto-generate documentation later on.
Usage
The plugin module can be registered into an Engine
as a normal module.
This is usually done via the exported_module!
macro.
The macro combine_with_exported_module!
can also be used to combine all the functions and variables
into an existing module, flattening the namespace – i.e. all
sub-modules are eliminated and their contents promoted to the top level. This is typical for
developing custom packages.
Register with Engine::register_global_module
The simplest way to register the plugin module into an Engine
is:
- use the
exported_module!
macro to turn it into a normal Rhai module, - call
Engine::register_global_module
to register it
fn main() {
let mut engine = Engine::new();
// The macro call creates a Rhai module from the plugin module.
let module = exported_module!(my_module);
// A module can simply be registered into the global namespace.
engine.register_global_module(module.into());
// Define a custom operator '@' with precedence of 160 (i.e. between +|- and *|/).
engine.register_custom_operator("@", 160).unwrap();
}
The functions contained within the module definition (i.e. greet
, get_num
, create_abc
and
increment
, the value
property getter), and the TestStruct
custom type
(with friendly name ABC
) are automatically registered into the Engine
when
Engine::register_global_module
is called.
let x = greet("world");
x == "hello, world!";
let x = greet(get_num().to_string());
x == "hello, 42!";
let x = get_num();
x == 42;
x @ x == 3528; // custom operator
let abc = create_abc(x);
type_of(abc) == "ABC";
abc.value == 42;
abc.increment();
abc.value == 43;
Register with Engine::register_static_module
Another simple way to register the plugin module into an Engine
is, again:
- use the
exported_module!
macro to turn it into a normal Rhai module, - call
Engine::register_static_module
to register it under a particular module namespace
fn main() {
let mut engine = Engine::new();
// The macro call creates a Rhai module from the plugin module.
let module = exported_module!(my_module);
// A module can simply be registered as a static module namespace.
engine.register_static_module("service", module.into());
// Define a custom operator '@' with precedence of 160 (i.e. between +|- and *|/).
engine.register_custom_operator("@", 160).unwrap();
}
The functions contained within the module definition (i.e. greet
, get_num
and increment
), plus
the constant MY_NUMBER
, are automatically registered under the module namespace service
:
let x = service::greet("world");
x == "hello, world!";
service::MY_NUMBER == 42;
let x = service::greet(service::get_num().to_string());
x == "hello, 42!";
let x = service::get_num();
x == 42;
x @ x == 3528; // custom operator
let abc = service::create_abc(x);
type_of(abc) == "ABC";
abc.value == 42;
service::increment(abc);
abc.value == 43;
Use #[rhai_fn(global)]
The default for all getters/setters and indexers defined in a plugin module is
#[rhai_fn(global)]
unless specifically overridden by #[rhai_fn(internal)]
.
All functions (usually methods) defined in the module and marked with #[rhai_fn(global)]
, all
type iterators and all custom types are automatically exposed to the global namespace, so
iteration, getters/setters and indexers for custom types can work as expected.
Therefore, in the example above, the increment
method (defined with #[rhai_fn(global)]
)
works fine when called in method-call style:
let x = 42;
x.increment();
x == 43;
Load Dynamically
See the module section for more information.
Using this directly as a dynamically-loadable Rhai module is almost the same, except that a
module resolver must be used to serve the module, and the module is loaded via import
statements.
Combine into Custom Package
Finally, the plugin module can also be used to develop a custom package, using
combine_with_exported_module!
which automatically flattens the module namespace so that all
functions in sub-modules are promoted to the top level namespace, all
sub-modules are eliminated, and all variables are ignored.
Due to flattening, sub-modules are often used conveniently as a grouping mechanism, especially to
put feature gates or compile-time gates (i.e. #[cfg(...)]
) on a large collection of functions
without having to duplicate the gates onto each individual function.
#[export_module]
mod my_module {
// Always available
pub fn func0() {}
// The following functions are only available under 'foo'.
// Use a sub-module for convenience, since all functions underneath
// will be flattened into the namespace.
#[cfg(feature = "foo")]
pub mod group_foo {
pub fn func1() {}
pub fn func2() {}
pub fn func3() {}
}
// The following functions are only available under 'bar'
#[cfg(feature = "bar")]
pub mod group_bar {
pub fn func4() {}
pub fn func5() {}
pub fn func6() {}
}
}
// The above is equivalent to:
#[export_module]
mod my_module_alternate {
pub fn func0() {}
#[cfg(feature = "foo")]
pub fn func1() {}
#[cfg(feature = "foo")]
pub fn func2() {}
#[cfg(feature = "foo")]
pub fn func3() {}
#[cfg(feature = "bar")]
pub fn func4() {}
#[cfg(feature = "bar")]
pub fn func5() {}
#[cfg(feature = "bar")]
pub fn func6() {}
}
// Registered functions:
// func0 - always available
// func1, func2, func3 - available under 'foo'
// func4, func5, func6 - available under 'bar'
// func0, func1, func2, func3, func4, func5, func6 - available under 'foo' and 'bar'
combine_with_exported_module!(module, "my_module_ID", my_module);
Functions Overloading and Operators
The first parameter of a function can also be of type NativeCallContext
.
Operators and overloaded functions can be specified via applying the #[rhai_fn(name = "...")]
attribute to individual functions.
The text string given as the name
parameter to #[rhai_fn]
is used to register the function with
the Engine
, disregarding the actual name of the function.
With #[rhai_fn(name = "...")]
, multiple functions may be registered under the same name in Rhai,
so long as they have different parameters.
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 parse error.
use rhai::plugin::*; // a "prelude" import for macros
#[export_module]
mod my_module {
// This is the '+' operator for 'TestStruct'.
#[rhai_fn(name = "+")]
pub fn add(obj: &mut TestStruct, value: i64) {
obj.prop += value;
}
// This function is 'calc (i64)'.
pub fn calc(num: i64) -> i64 {
...
}
// This function is 'calc (i64, bool)'.
#[rhai_fn(name = "calc")]
pub fn calc_with_option(num: i64, option: bool) -> i64 {
...
}
}
Getters, Setters and Indexers
Getters/setters and indexers default to #[rhai_fn(global)]
unless overridden by #[rhai_fn(internal)]
.
Functions can be marked as getters/setters and indexers for custom types via the
#[rhai_fn]
attribute, which is applied on a function level.
Attribute | Description |
---|---|
#[rhai_fn(get = " property")] | property getter |
#[rhai_fn(set = " property")] | property setter |
#[rhai_fn(index_get)] | index getter |
#[rhai_fn(index_set)] | index setter |
use rhai::plugin::*; // a "prelude" import for macros
#[export_module]
mod my_module {
// This is a normal function 'greet'.
pub fn greet(name: &str) -> String {
format!("hello, {}!", name)
}
// This is a getter for 'TestStruct::prop'.
#[rhai_fn(get = "prop", pure)]
pub fn get_prop(obj: &mut TestStruct) -> i64 {
obj.prop
}
// This is a setter for 'TestStruct::prop'.
#[rhai_fn(set = "prop")]
pub fn set_prop(obj: &mut TestStruct, value: i64) {
obj.prop = value;
}
// This is an index getter for 'TestStruct'.
#[rhai_fn(index_get)]
pub fn get_index(obj: &mut TestStruct, index: i64) -> bool {
obj.list[index]
}
// This is an index setter for 'TestStruct'.
#[rhai_fn(index_set)]
pub fn set_index(obj: &mut TestStruct, index: i64, state: bool) {
obj.list[index] = state;
}
}
Multiple Registrations
Parameters to the #[rhai_fn(...)]
attribute can be applied multiple times, separated by commas.
Multiple registrations is useful for name = "..."
, get = "..."
and set = "..."
to give
multiple alternative names to the same function.
use rhai::plugin::*; // a "prelude" import for macros
#[export_module]
mod my_module {
// This function can be called in five ways
#[rhai_fn(name = "get_prop_value", name = "prop", name = "+", set = "prop", index_get)]
pub fn prop_function(obj: &mut TestStruct, index: i64) -> i64 {
obj.prop[index]
}
}
The above function can be called in five ways:
Parameter for #[rhai_fn(...)] | Type | Call style |
---|---|---|
name = "get_prop_value" | method | get_prop_value(x, 0) , x.get_prop_value(0) |
name = "prop" | method | prop(x, 0) , x.prop(0) |
name = "+" | operator | x + 42 |
set = "prop" | setter | x.prop = 42 |
index_get | index getter | x[0] |
Pure Functions
Apply the #[rhai_fn(pure)]
attribute on a method function (i.e. one taking a &mut
first parameter)
to mark it as pure – i.e. it does not modify the &mut
parameter.
This is often done to avoid expensive cloning for methods or property getters that return information about a custom type and does not modify it.
Pure functions MUST NOT modify the &mut
parameter.
There is no checking.
Non-pure functions raise a runtime error when passed a constant value as the first &mut
parameter.
Pure functions can be passed a constant value as the first &mut
parameter.
use rhai::plugin::*; // a "prelude" import for macros
#[export_module]
mod my_module {
// This function can be passed a constant
#[rhai_fn(name = "add1", pure)]
pub fn add_scaled(array: &mut rhai::Array, x: i64) -> i64 {
array.iter().map(|v| v.as_int().unwrap()).fold(0, |(r, v)| r += v * x)
}
// This function CANNOT be passed a constant
#[rhai_fn(name = "add2")]
pub fn add_scaled2(array: &mut rhai::Array, x: i64) -> i64 {
array.iter().map(|v| v.as_int().unwrap()).fold(0, |(r, v)| r += v * x)
}
// This getter can be applied to a constant
#[rhai_fn(get = "first1", pure)]
pub fn get_first(array: &mut rhai::Array) -> i64 {
array[0]
}
// This getter CANNOT be applied to a constant
#[rhai_fn(get = "first2")]
pub fn get_first2(array: &mut rhai::Array) -> i64 {
array[0]
}
// The following is a syntax error because a setter is SUPPOSED to
// mutate the object. Therefore the 'pure' attribute cannot be used.
#[rhai_fn(get = "values", pure)]
pub fn set_values(array: &mut rhai::Array, value: i64) {
// ...
}
// The following is a volatile function which returns different values
// for each call.
#[rhai_fn(volatile)]
pub fn get_current_time() -> String {
// ...
}
}
When applied to a Rhai script:
// Constant
const VECTOR = [1, 2, 3, 4, 5, 6, 7];
let r = VECTOR.add1(2); // ok!
let r = VECTOR.add2(2); // runtime error: constant modified
let r = VECTOR.first1; // ok!
let r = VECTOR.first2; // runtime error: constant modified
Volatile Functions
A volatile function is one that does not guarantee the same result for the same input(s).
Most functions are non-volatile, meaning that they always generate the same result when called with the same arguments.
Common examples of volatile functions are:
-
a function that returns the current date and/or time
-
a function that looks up the current value of a variable in the environment
-
a function that reads from a file (which depends on the content of the file at the time of read)
-
a function that reads from a database or a cache (which depends on the content at the time of read)
When using Full Optimization, functions with constant arguments are called eagerly at compile time. However, volatile functions are never called.
Plugin functions are assumed to be non-volatile by default, unless marked with
#[rhai_fn(volatile)]
.
Fallible Functions
To register fallible functions (i.e. functions that may return errors), apply the
#[rhai_fn(return_raw)]
attribute on functions that return Result<T, Box<EvalAltResult>>
where T
is any clonable type.
use rhai::plugin::*; // a "prelude" import for macros
#[export_module]
mod my_module {
/// This overloads the '/' operator for i64.
#[rhai_fn(name = "/", return_raw)]
pub fn double_and_divide(x: i64, y: i64) -> Result<i64, Box<EvalAltResult>> {
if y == 0 {
Err("Division by zero!".into())
} else {
Ok((x * 2) / y)
}
}
}
A compilation error — usually something that says Result
does not implement
Clone
— is generated if a fallible function is missing #[rhai_fn(return_raw)]
.
It is another compilation error for the reverse — a function with
#[rhai_fn(return_raw)]
does not have the appropriate return type.
#[export_module]
Parameters
Parameters can be applied to the #[export_module]
attribute to override its default behavior.
Parameter | Description |
---|---|
none | exports only public (i.e. pub ) functions |
export_all | exports 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.
Parameters should be set on inner attributes to specify the desired behavior.
Attribute Parameter | Use with | Apply to | Description |
---|---|---|---|
skip | #[rhai_fn] #[rhai_mod] | any function or sub-module | do not export this function/sub-module |
global | #[rhai_fn] | any function | expose this function to the global namespace |
internal | #[rhai_fn] | any function | keep this function within the internal module namespace |
name = "..." | #[rhai_fn] #[rhai_mod] | any function or sub-module | registers function/sub-module under the specified name |
get = "..." | #[rhai_fn] | pub fn (&mut T) -> V | registers a property getter for the named property |
set = "..." | #[rhai_fn] | pub fn (&mut T, V) | registers a property setter for the named property |
index_get | #[rhai_fn] | pub fn (&mut T, X) -> V | registers an index getter |
index_set | #[rhai_fn] | pub fn (&mut T, X, V) | registers an index setter |
return_raw | #[rhai_fn] | pub fn (...) -> Result<V, Box<EvalAltResult>> | marks this as a fallible function |
pure | #[rhai_fn] | pub fn (&mut T, ...) -> ... | marks this as a pure function |
volatile | #[rhai_fn] | any function | marks this as a volatile function – i.e. it does not guarantee the same result for the same input(s). |
Packages
The built-in library of Rhai is provided as various packages that can be turned into shared
modules, which in turn can be registered into the global namespace of an Engine
via
Engine::register_global_module
.
Packages reside under rhai::packages::*
and the trait rhai::packages::Package
must be loaded in
order for packages to be used.
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).
Share a Package Among Multiple Engine
’s
Engine::register_global_module
and Engine::register_static_module
both require shared modules.
Once a package is created (e.g. via Package::new
), it can be registered into multiple instances of
Engine
, even across threads (under the sync
feature).
A package only has to be created once and essentially shared among multiple Engine
instances.
This is particularly useful when spawning large number of raw Engine
’s.
use rhai::Engine;
use rhai::packages::Package // load the 'Package' trait to use packages
use rhai::packages::CorePackage; // the 'core' package contains basic functionalities (e.g. arithmetic)
// Create a package - can be shared among multiple 'Engine' instances
let package = CorePackage::new();
let mut engines_collection: Vec<Engine> = Vec::new();
// Create 100 'raw' Engines
for _ in 0..100 {
let mut engine = Engine::new_raw();
// Register the package into the global namespace.
package.register_into_engine(&mut engine);
engines_collection.push(engine);
}
Built-In Packages
Engine::new
creates an Engine
with the StandardPackage
loaded.
Engine::new_raw
creates an Engine
with no package loaded.
Package | Description | In Core | In Standard |
---|---|---|---|
LanguageCorePackage | core functions for the Rhai language | yes | yes |
ArithmeticPackage | arithmetic operators (e.g. + , - , * , / ) for numeric types that are not built in (e.g. u16 ) | yes | yes |
BitFieldPackage | basic bit-field functions | no | yes |
BasicIteratorPackage | numeric ranges (e.g. range(1, 100, 5) ), iterators for arrays, strings, bit-fields and object maps | yes | yes |
LogicPackage | logical and comparison operators (e.g. == , > ) for numeric types that are not built in (e.g. u16 ) | no | yes |
BasicStringPackage | basic string functions (e.g. print , debug , len ) that are not built in | yes | yes |
BasicTimePackage | basic time functions (e.g. timestamps, not available under no_time or no_std ) | no | yes |
MoreStringPackage | additional string functions, including converting common types to string | no | yes |
BasicMathPackage | basic math functions (e.g. sin , sqrt ) | no | yes |
BasicArrayPackage | basic array functions (not available under no_index ) | no | yes |
BasicBlobPackage | basic BLOB functions (not available under no_index ) | no | yes |
BasicMapPackage | basic object map functions (not available under no_object ) | no | yes |
BasicFnPackage | basic methods for function pointers | yes | yes |
DebuggingPackage | basic functions for debugging (requires debugging ) | yes | yes |
CorePackage | basic essentials | yes | yes |
StandardPackage | standard library (default for Engine::new ) | no | yes |
CorePackage
If only minimal functionalities are required, register the CorePackage
instead.
use rhai::Engine;
use rhai::packages::{Package, CorePackage};
let mut engine = Engine::new_raw();
let package = CorePackage::new();
// Register the package into the 'Engine'.
package.register_into_engine(&mut engine);
Create a Custom Package
See also the One Engine Instance Per Call pattern.
The macro def_package!
can be used to create a custom package.
A custom package can aggregate many other packages into a single self-contained unit. More functions can be added on top of others.
Custom packages are extremely useful when multiple raw Engine
instances must be created such
that they all share the same set of functions.
def_package!
def_package! { /// Package description doc-comment pub name(variable) { : // package init code block : } // Multiple packages can be defined at the same time, // possibly with base packages and/or code to setup an Engine. /// Package description doc-comment pub(crate) name(variable) : base_package_1, base_package_2, ... { : // package init code block : } |> |engine| { : // engine setup code block : } /// A private package description doc-comment name(variable) { : // private package init code block : } : }
where:
Element | Description |
---|---|
description | doc-comment for the package |
pub etc. | visibility of the package |
name | name of the package, usually ending in …Package |
variable | a variable name holding a reference to the module forming the package, usually module or lib |
base_package | an external package type that is merged into this package as a dependency |
package init code block | a code block that initializes the package |
engine | a variable name holding a mutable reference to an Engine |
engine setup code block | a code block that performs setup tasks on an Engine during registration |
Examples
// Import necessary types and traits.
use rhai::def_package; // 'def_package!' macro
use rhai::packages::{ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage};
use rhai::{FuncRegistration, CustomType, TypeBuilder};
/// This is a custom type.
#[derive(Clone, CustomType)]
struct TestStruct {
foo: String,
bar: i64,
baz: bool
}
def_package! {
/// My own personal super package
// Aggregate other base packages (if any) simply by listing them after a colon.
pub MyPackage(module) : ArithmeticPackage, LogicPackage, BasicArrayPackage, BasicMapPackage
{
// Register additional Rust function.
FuncRegistration::new("get_bar_value")
.with_params_info(&["s: &mut TestStruct", "i64"])
.set_into_module(module, |s: &mut TestStruct| s.bar);
// Register a function for use as a custom operator.
FuncRegistration::new("@")
.with_namespace(FnNamespace::Global) // <- make it available globally.
.set_into_module(module, |x: i64, y: i64| x * x + y * y);
} |> |engine| {
// This optional block performs tasks on an 'Engine' instance,
// e.g. register custom types and/or custom operators/syntax.
// Register custom type.
engine.build_type::<TestStruct>();
// Define a custom operator '@' with precedence of 160
// (i.e. between +|- and *|/).
engine.register_custom_operator("@", 160).unwrap();
}
}
Base packages in the list after the colon (:
) can also have attributes (such as feature gates)!
def_package! {
// 'BasicArrayPackage' is used only under 'arrays' feature.
pub MyPackage(module) :
ArithmeticPackage,
LogicPackage,
#[cfg(feature = "arrays")]
BasicArrayPackage
{
...
}
}
A second code block (in the syntax of a closure) following a right-triangle symbol (|>
)
is run whenever the package is being registered.
It allows performing setup tasks directly on that Engine
, e.g. registering custom types,
custom operators and/or custom syntax.
def_package! {
pub MyPackage(module) {
:
:
} |> |engine| {
// Call methods on 'engine'
}
}
Create a Custom Package from a Plugin Module
This is exactly how Rhai’s built-in packages, such as BasicMathPackage
, are actually implemented.
By far the easiest way to create a custom package is to call plugin::combine_with_exported_module!
from within def_package!
which simply merges in all the functions defined within a plugin module.
Due to specific requirements of a package, plugin::combine_with_exported_module!
flattens all sub-modules (i.e. all functions and type iterators defined within sub-modules
are pulled up to the top level instead) and so there will not be any sub-modules added to the package.
Variables in the plugin module are ignored.
// Import necessary types and traits.
use rhai::def_package;
use rhai::packages::{ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage};
use rhai::plugin::*;
// Define plugin module.
#[export_module]
mod my_plugin_module {
// Custom type.
pub type ABC = TestStruct;
// Public constant.
pub const MY_NUMBER: i64 = 42;
// Public function.
pub fn greet(name: &str) -> String {
format!("hello, {}!", name)
}
// Non-public functions are by default not exported.
fn get_private_num() -> i64 {
42
}
// Public function.
pub fn get_num() -> i64 {
get_private_num()
}
// Custom operator.
#[rhai_fn(name = "@")]
pub fn square_add(x: i64, y: i64) -> i64 {
x * x + y * y
}
// A sub-module. If using 'combine_with_exported_module!', however,
// it will be flattened and all functions registered at the top level.
//
// Because of this flattening, sub-modules are very convenient for
// putting feature gates onto large groups of functions.
#[cfg(feature = "sub-num-feature")]
pub mod my_sub_module {
// Only available under 'sub-num-feature'.
pub fn get_sub_num() -> i64 {
0
}
}
}
def_package! {
/// My own personal super package
// Aggregate other base packages (if any) simply by listing them after a colon.
pub MyPackage(module) : ArithmeticPackage, LogicPackage, BasicArrayPackage, BasicMapPackage
{
// Merge all registered functions and constants from the plugin module
// into the custom package.
//
// The sub-module 'my_sub_module' is flattened and its functions
// registered at the top level.
//
// The text string name in the second parameter can be anything
// and is reserved for future use; it is recommended to be an
// ID string that uniquely identifies the plugin module.
//
// The constant variable, 'MY_NUMBER', is ignored.
//
// This call ends up registering three functions at the top level of
// the package:
// 1) 'greet'
// 2) 'get_num'
// 3) 'get_sub_num' (flattened from 'my_sub_module')
//
combine_with_exported_module!(module, "my-mod", my_plugin_module));
} |> |engine| {
// This optional block is used to set up an 'Engine' during registration.
// Define a custom operator '@' with precedence of 160
// (i.e. between +|- and *|/).
engine.register_custom_operator("@", 160).unwrap();
}
}
Create a Custom Package as an Independent Crate
Creating a custom package as an independent crate allows it to be shared by multiple projects.
Implementation
Cargo.toml
:
[package]
name = "my-package" # 'my-package' crate
[dependencies]
rhai = "1.20.0" # assuming 1.20.0 is the latest version
lib.rs
:
use rhai::def_package;
use rhai::plugin::*;
// This is a plugin module
#[export_module]
mod my_module {
// Constants are ignored when used as a package
pub const MY_NUMBER: i64 = 42;
pub fn greet(name: &str) -> String {
format!("hello, {}!", name)
}
pub fn get_num() -> i64 {
42
}
// This is a sub-module, but if using combine_with_exported_module!, it will
// be flattened and all functions registered at the top level.
pub mod my_sub_module {
pub fn get_sub_num() -> i64 {
0
}
}
}
// Define the package 'MyPackage' which is exported for the crate.
def_package! {
/// My own personal super package in a new crate!
pub MyPackage(module) {
combine_with_exported_module!(module, "my-functions", my_module);
}
}
External Packages
Following are external packages that can be used with Rhai for additional functionalities.
Package | Description |
---|---|
rhai-rand | generate random numbers, shuffling and sampling |
rhai-sci | functions for scientific computing |
rhai-ml | functions for AI and machine learning |
rhai-fs | read/write files in an external filesystem |
rhai-url | working with Urls via the url crate |
rhai-rand
: Random Number Generation, Shuffling and Sampling
rhai-rand
is an independent Rhai package that provides:
See https://docs.rs/rhai-rand for the list of functions.
On
crates.io
:rhai-rand
On
GitHub
:rhaiscript/rhai-rand
Package name:
RandomPackage
Dependency
Cargo.toml
:
[dependencies]
rhai = "1.20.0"
rhai-rand = "0.1" # use rhai-rand crate
Load Package into Engine
use rhai::Engine;
use rhai::packages::Package; // needed for 'Package' trait
use rhai_rand::RandomPackage;
let mut engine = Engine::new();
// Create new 'RandomPackage' instance
let random = RandomPackage::new();
// Load the package into the `Engine`
random.register_into_engine(&mut engine);
Features
Feature | Description | Default? | Should not be used with Rhai feature |
---|---|---|---|
float | enables random floating-point number generation | yes | no_float |
array | enables methods for arrays | yes | no_index |
metadata | enables functions metadata (turns on metadata in Rhai) | no |
Cargo.toml
:
[dependencies]
# Rhai is set for 'no_float', meaning no floating-point support
rhai = { version="1.20.0", features = ["no_float"] }
# Use 'default-features = false' to clear defaults, then only add 'array'
rhai-rand = { version="0.1", default-features = false, features = ["array"] }
rhai-sci
: Functions for Scientific Computing
rhai-sci
is an independent Rhai package that provides functions useful for
scientific computing, inspired by languages like MATLAB, Octave, and R.
See https://docs.rs/rhai-sci for the list of functions.
On
crates.io
:rhai-sci
On
GitHub
:rhaiscript/rhai-sci
Package name:
SciPackage
Dependency
Cargo.toml
:
[dependencies]
rhai = "1.20.0"
rhai-sci = "0.1" # use rhai-sci crate
Features
Feature | Description | Default? |
---|---|---|
metadata | enables functions metadata (turns on metadata in Rhai); necessary for running doc-tests | no |
io | enables the read_matrix function but pulls in several additional dependencies | yes |
nalgebra | enables the functions regress , inv , mtimes , horzcat , vertcat , and repmat but pulls in nalgebra and linregress . | yes |
rand | enables the rand function for generating random values and random matrices, but pulls in rand . | yes |
Load Package into Engine
use rhai::Engine;
use rhai::packages::Package; // needed for 'Package' trait
use rhai_sci::SciPackage;
let mut engine = Engine::new();
// Create new 'SciPackage' instance
let sci = SciPackage::new();
// Load the package into the [`Engine`]
sci.register_into_engine(&mut engine);
rhai-ml
: Functions for AI and Machine Learning
rhai-ml
is an independent Rhai package that provides functions useful for
artificial intelligence and machine learning.
See https://docs.rs/rhai-ml for the list of functions.
On
crates.io
:rhai-ml
On
GitHub
:rhaiscript/rhai-ml
Package name:
MLPackage
Dependency
Cargo.toml
:
[dependencies]
rhai = "1.20.0"
rhai-ml = "0.1" # use rhai-ml crate
Features
Feature | Description | Default? |
---|---|---|
metadata | enables functions metadata (turns on metadata in Rhai); necessary for running doc-tests | no |
Load Package into Engine
use rhai::Engine;
use rhai::packages::Package; // needed for 'Package' trait
use rhai_ml::MLPackage;
let mut engine = Engine::new();
// Create new 'MLPackage' instance
let ml = MLPackage::new();
// Load the package into the [`Engine`]
ml.register_into_engine(&mut engine);
rhai-fs
: Filesystem Access
rhai-fs
is an independent Rhai package that enables reading from and writing to files in an
external filesystem.
See https://docs.rs/rhai-fs for the list of functions.
On
crates.io
:rhai-fs
On
GitHub
:rhaiscript/rhai-fs
Package name:
FilesystemPackage
Dependency
Cargo.toml
:
[dependencies]
rhai = "1.20.0"
rhai-fs = "0.1" # use rhai-fs crate
Load Package into Engine
use rhai::Engine;
use rhai::packages::Package; // needed for 'Package' trait
use rhai_fs::FilesystemPackage;
let mut engine = Engine::new();
// Create new 'FilesystemPackage' instance
let fs = FilesystemPackage::new();
// Load the package into the `Engine`
fs.register_into_engine(&mut engine);
Example
// Create a file, or open it if already exists
let file = open_file("example.txt");
// Read the contents of the file (if any) into a BLOB
let blob_buf = file.read_blob();
print(`file contents: ${blob_buf}`);
// Update BLOB data
blob_buf.write_utf8(0..=0x20, "foobar");
print(`new file contents: ${blob_buf}`);
// Seek back to the beginning
file.seek(0);
// Overwrite the original file with new data
blob_buf.write_to_file(file);
Features
Feature | Description | Default? | Should be used with Rhai feature |
---|---|---|---|
no_array | removes support for arrays and BLOB’s | no | no_index |
metadata | enables functions metadata (turns on metadata in Rhai) | no |
rhai-url
: Working with Urls
rhai-url
is an independent Rhai package that enables working with Urls via the
url
crate.
See https://docs.rs/rhai-url for the list of functions.
On
crates.io
:rhai-url
On
GitHub
:rhaiscript/rhai-url
Package name:
FilesystemPackage
Dependency
Cargo.toml
:
[dependencies]
rhai = "1.20.0"
rhai-url = "0.0.1" # use rhai-url crate
Load Package into Engine
use rhai::Engine;
use rhai::packages::Package; // needed for 'Package' trait
use rhai_url::UrlPackage;
let mut engine = Engine::new();
// Create new 'UrlPackage' instance
let url = UrlPackage::new();
// Load the package into the `Engine`
url.register_into_engine(&mut engine);
Example
let url = Url("http://example.com/?q=query");
print(url); // prints 'http://example.com/?q=query'
print(url.href); // prints 'http://example.com/?q=query'
print(url.query); // prints 'q=query'
// fragment and hash are aliases
print(url.fragment); // prints ''
print(url.hash); // prints ''
url.query_clear();
print(url.query); // prints ''
url.query_remove("q");
url.query_append("q", "name");
print(url); // prints 'http://example.com/?q=name'
Comments
Comments are C-style, including /*
… */
pairs for block comments and //
for comments to the
end of the line.
Block comments can be nested.
let /* intruder comment */ name = "Bob";
// This is a very important one-line comment
/* This comment spans
multiple lines, so it
only makes sense that
it is even more important */
/* Fear not, Rhai satisfies all nesting needs with nested comments:
/*/*/*/*/**/*/*/*/*/
*/
Module Documentation
Comment lines starting with //!
make up the module documentation.
They are used to document the containing module – or for a Rhai script file, to document the file itself.
//! Documentation for this script file.
//! This script is used to calculate something and display the result.
fn calculate(x) {
...
}
fn display(msg) {
//! Module documentation can be placed anywhere within the file.
...
}
//! All module documentation lines will be collected into a single block.
For the example above, the module documentation block is:
//! Documentation for this script file.
//! This script is used to calculate something and display the result.
//! Module documentation can be placed anywhere within the file.
//! All module documentation lines will be collected into a single block.
Doc-Comments
Similar to Rust, comments starting with ///
(three slashes) or /**
(two asterisks)
are doc-comments.
Doc-comments can only appear in front of function definitions, not any other elements.
Therefore, doc-comments are not available under no_function
.
/// 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;
}
Long streams of //////
… and /*****
… do NOT form doc-comments.
This is consistent with popular [comment] block styles for C-like languages.
/////////////////////////////// <- this is not a doc-comment
// This is not a doc-comment // <- this is not a doc-comment
/////////////////////////////// <- this is not a doc-comment
// However, watch out for comment lines starting with '///'
////////////////////////////////////////// <- this is not a doc-comment
/// This, however, IS a doc-comment!!! /// <- doc-comment!
////////////////////////////////////////// <- this is not a doc-comment
/****************************************
* *
* This is also not a doc-comment block *
* so we don't have to put this in *
* front of a function. *
* *
****************************************/
Using Doc-Comments
Doc-comments are stored within the script’s AST
after compilation.
The AST::iter_functions
method provides a ScriptFnMetadata
instance for each function defined
within the script, which includes doc-comments.
Doc-comments never affect the evaluation of a script nor do they incur significant performance overhead. However, third party tools can take advantage of this information to auto-generate documentation for Rhai script functions.
Values and Types
The following primitive types are supported natively.
Category | Equivalent Rust types | type_of() | to_string() |
---|---|---|---|
System integer | rhai::INT (default i64 , i32 under only_i32 ) | "i32" or "i64" | "42" , "123" etc. |
Other integer number | u8 , i8 , u16 , i16 , u32 , i32 , u64 , i64 | "i32" , "u64" etc. | "42" , "123" etc. |
Integer numeric range | std::ops::Range<rhai::INT> , std::ops::RangeInclusive<rhai::INT> | "range" , "range=" | "2..7" , "0..=15" etc. |
Floating-point number (disabled with no_float ) | rhai::FLOAT (default f64 , f32 under f32_float ) | "f32" or "f64" | "123.4567" etc. |
Fixed precision decimal number (requires decimal ) | rust_decimal::Decimal | "decimal" | "42" , "123.4567" etc. |
Boolean value | bool | "bool" | "true" or "false" |
Unicode character | char | "char" | "A" , "x" etc. |
Immutable Unicode string | rhai::ImmutableString (Rc<SmartString> , Arc<SmartString> under sync ) | "string" | "hello" etc. |
Array (disabled with no_index ) | rhai::Array (Vec<Dynamic> ) | "array" | "[ 1, 2, 3 ]" etc. |
Byte array – BLOB (disabled with no_index ) | rhai::Blob (Vec<u8> ) | "blob" | "[01020304abcd]" etc. |
Object map (disabled with no_object ) | rhai::Map (BTreeMap<SmartString, Dynamic> ) | "map" | "#{ "a": 1, "b": true }" etc. |
Timestamp (disabled with no_time or no_std ) | std::time::Instant (instant::Instant if WASM build) | "timestamp" | "<timestamp>" |
Function pointer | rhai::FnPtr | "Fn" | "Fn(foo)" etc. |
Dynamic value (i.e. can be anything) | rhai::Dynamic | the actual type | actual value |
Shared value (a reference-counted, shared Dynamic value, created via closures, disabled with no_closure ) | the actual type | actual value | |
Nothing/void/nil/null/Unit (or whatever it is called) | () | "()" | "" (empty string) |
The various integer types are treated strictly distinct by Rhai, meaning that
i32
and i64
and u32
and u8
are completely different.
They cannot even be added together or compared with each other.
Nor can a smaller integer type be up-casted to a larger integer type.
This is very similar to Rust.
Default Types
The default integer type is i64
. If other integer types are not needed, it is possible to exclude
them and make a smaller build with the only_i64
feature.
If only 32-bit integers are needed, enabling the only_i32
feature will remove support for all
integer types other than i32
, including i64
.
This is useful on some 32-bit targets where using 64-bit integers incur a performance penalty.
Rhai’s default integer type is i64
, which is DIFFERENT from Rust’s i32
.
It is very easy to unsuspectingly set an i32
into Rhai, which still works but will incur a significant
runtime performance hit since the Engine
will treat i32
as an opaque custom type (unless using the
only_i32
feature).
i64
is the default even on 32-bit systems. To use i32
on 32-bit systems requires the only_i32
feature.
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.
Dynamic Values
A Dynamic
value can be any type, as long as it implements Clone
.
Under the sync
feature, all types must also be Send + Sync
.
let x = 42; // value is an integer
x = 123.456; // value is now a floating-point number
x = "hello"; // value is now a string
x = x.len > 0; // value is now a boolean
x = [x]; // value is now an array
x = #{x: x}; // value is now an object map
Use type_of()
to Get Value Type
Because type_of()
a Dynamic
value returns the type of the actual value,
it is usually used to perform type-specific actions based on the actual value’s type.
let mystery = get_some_dynamic_value();
switch type_of(mystery) {
"()" => print("Hey, I got the unit () here!"),
"i64" => print("Hey, I got an integer here!"),
"f64" => print("Hey, I got a float here!"),
"decimal" => print("Hey, I got a decimal here!"),
"range" => print("Hey, I got an exclusive range here!"),
"range=" => print("Hey, I got an inclusive range here!"),
"string" => print("Hey, I got a string here!"),
"bool" => print("Hey, I got a boolean here!"),
"array" => print("Hey, I got an array here!"),
"blob" => print("Hey, I got a BLOB here!"),
"map" => print("Hey, I got an object map here!"),
"Fn" => print("Hey, I got a function pointer here!"),
"timestamp" => print("Hey, I got a time-stamp here!"),
"TestStruct" => print("Hey, I got the TestStruct custom type here!"),
_ => print(`I don't know what this is: ${type_of(mystery)}`)
}
Parse from JSON
parse_json
is defined in the LanguageCorePackage
, which is excluded when using a raw Engine
.
It also requires the metadata
feature; the no_index
and no_object
features must not be set.
Use parse_json
to parse a JSON string into a Dynamic
value.
JSON type | Rhai type |
---|---|
number (no decimal point) | INT |
number (with decimal point) | FLOAT |
string | string |
boolean | bool |
Array | array |
Object | object map |
null | () |
type_of()
The type_of
function detects the actual type of a value.
This is useful because all variables are Dynamic
in nature.
// Use 'type_of()' to get the actual types of values
type_of('c') == "char";
type_of(42) == "i64";
let x = 123;
x.type_of() == "i64"; // method-call style is also OK
type_of(x) == "i64";
x = 99.999;
type_of(x) == "f64";
x = "hello";
if type_of(x) == "string" {
do_something_first_with_string(x);
}
switch type_of(x) {
"string" => do_something_with_string(x),
"char" => do_something_with_char(x),
"i64" => do_something_with_int(x),
"f64" => do_something_with_float(x),
"bool" => do_something_with_bool(x),
_ => throw `I cannot work with ${type_of(x)}!!!`
}
See here for the type_of
output of standard types.
type_of()
a custom type returns:
-
the friendly name, if registered via
Engine::register_type_with_name
-
the full Rust type path, if registered via
Engine::register_type
struct TestStruct1;
struct TestStruct2;
engine
// type_of(struct1) == "path::to::module::TestStruct1"
.register_type::<TestStruct1>()
// type_of(struct2) == "MyStruct"
.register_type_with_name::<TestStruct2>("MyStruct");
Interop Dynamic
Data with Rust
Create a Dynamic
from Rust Type
Rust typeT: Clone ,K: Into<String> | Unavailable under | Use API |
---|---|---|
INT (i64 or i32 ) | value.into() | |
FLOAT (f64 or f32 ) | no_float | value.into() |
Decimal (requires decimal ) | value.into() | |
bool | value.into() | |
() | value.into() | |
String , &str , ImmutableString | value.into() | |
char | value.into() | |
Array | no_index | Dynamic::from_array(value) |
Blob | no_index | Dynamic::from_blob(value) |
Vec<T> , &[T] , Iterator<T> | no_index | value.into() |
Map | no_object | Dynamic::from_map(value) |
HashMap<K, T> , HashSet<K> ,BTreeMap<K, T> , BTreeSet<K> | no_object | value.into() |
INT..INT , INT..=INT | value.into() | |
Rc<RwLock<T>> or Arc<Mutex<T>> | no_closure | value.into() |
Instant | no_time or no_std | value.into() |
All types (including above) | Dynamic::from(value) |
Type Checking and Casting
The try_cast
method does not panic but returns None
upon failure.
The try_cast_result
method also does not panic but returns the original value upon failure.
A Dynamic
value’s actual type can be checked via Dynamic::is
.
The cast
method then converts the value into a specific, known type.
Use clone_cast
to clone a reference to Dynamic
.
let list: Array = engine.eval("...")?; // return type is 'Array'
let item = list[0].clone(); // an element in an 'Array' is 'Dynamic'
item.is::<i64>() == true; // 'is' returns whether a 'Dynamic' value is of a particular type
let value = item.cast::<i64>(); // if the element is 'i64', this succeeds; otherwise it panics
let value: i64 = item.cast(); // type can also be inferred
let value = item.try_cast::<i64>()?; // 'try_cast' does not panic when the cast fails, but returns 'None'
let value = list[0].clone_cast::<i64>(); // use 'clone_cast' on '&Dynamic'
let value: i64 = list[0].clone_cast();
Type Name and Matching Types
The type_name
method gets the name of the actual type as a static string slice,
which can be match
-ed against.
This is a very simple and direct way to act on a Dynamic
value based on the actual type of
the data value.
let list: Array = engine.eval("...")?; // return type is 'Array'
let item = list[0]; // an element in an 'Array' is 'Dynamic'
match item.type_name() { // 'type_name' returns the name of the actual Rust type
"()" => ...
"i64" => ...
"f64" => ...
"rust_decimal::Decimal" => ...
"core::ops::range::Range<i64>" => ...
"core::ops::range::RangeInclusive<i64>" => ...
"alloc::string::String" => ...
"bool" => ...
"char" => ...
"rhai::FnPtr" => ...
"std::time::Instant" => ...
"crate::path::to::module::TestStruct" => ...
:
}
type_name
always returns the full Rust path name of the type, even when the type
has been registered with a friendly name via Engine::register_type_with_name
.
This behavior is different from that of the type_of
function in Rhai.
Getting a Reference to Data
Use Dynamic::read_lock
and Dynamic::write_lock
to get an immutable/mutable reference to the data
inside a Dynamic
.
struct TheGreatQuestion {
answer: i64
}
let question = TheGreatQuestion { answer: 42 };
let mut value: Dynamic = Dynamic::from(question);
let q_ref: &TheGreatQuestion =
&*value.read_lock::<TheGreatQuestion>().unwrap();
// ^^^^^^^^^^^^^^^^^^^^ cast to data type
println!("answer = {}", q_ref.answer); // prints 42
let q_mut: &mut TheGreatQuestion =
&mut *value.write_lock::<TheGreatQuestion>().unwrap();
// ^^^^^^^^^^^^^^^^^^^^ cast to data type
q_mut.answer = 0; // mutate value
let value = value.cast::<TheGreatQuestion>();
println!("new answer = {}", value.answer); // prints 0
As the naming shows, something is locked in order to allow accessing the data within a Dynamic
,
and that something is a shared value created by capturing variables from closures.
Shared values are implemented as Rc<RefCell<Dynamic>>
(Arc<RwLock<Dynamic>>
under sync
).
If the value is not a shared value, or if running under no_closure
where there is
no capturing, this API de-sugars to a simple reference cast.
In other words, there is no locking and reference counting overhead for the vast majority of non-shared values.
If the value is a shared value, then it is first locked and the returned lock guard allows access to the underlying value in the specified type.
Methods and Traits
The following methods are available when working with Dynamic
:
Method | Not available under | Return type | Description |
---|---|---|---|
type_name | &str | name of the value’s type | |
into_shared | no_closure | Dynamic | turn the value into a shared value |
flatten_clone | Dynamic | clone the value (a shared value, if any, is cloned into a separate copy) | |
flatten | Dynamic | clone the value into a separate copy if it is shared and there are multiple outstanding references, otherwise shared values are turned unshared | |
read_lock<T> | no_closure (pass thru’) | Option< guard to T> | lock the value for reading |
write_lock<T> | no_closure (pass thru’) | Option< guard to T> | lock the value exclusively for writing |
deep_scan | recursively scan for Dynamic values (e.g. items inside an array or object map, or curried arguments in a function pointer) |
Constructor instance methods
Method | Not available under | Value type | Data type |
---|---|---|---|
from_bool | bool | bool | |
from_int | INT | integer number | |
from_float | no_float | FLOAT | floating-point number |
from_decimal | non-decimal | Decimal | Decimal |
from_str | &str | string | |
from_char | char | character | |
from_array | no_index | Vec<T> | array |
from_blob | no_index | Vec<u8> | BLOB |
from_map | no_object | Map | object map |
from_timestamp | no_time or no_std | std::time::Instant (instant::Instant if WASM build) | timestamp |
from<T> | T | custom type |
Detection methods
Method | Not available under | Return type | Description |
---|---|---|---|
is<T> | bool | is the value of type T ? | |
is_variant | bool | is the value a trait object (i.e. not one of Rhai’s standard types)? | |
is_read_only | bool | is the value constant? A constant value should not be modified. | |
is_shared | no_closure | bool | is the value shared via a closure? |
is_locked | no_closure | bool | is the value shared and locked (i.e. currently being read)? |
is_unit | bool | is the value () ? | |
is_int | bool | is the value an integer? | |
is_float | no_float | bool | is the value a floating-point number? |
is_decimal | non-decimal | bool | is the value a Decimal ? |
is_bool | bool | is the value a bool ? | |
is_char | bool | is the value a character? | |
is_string | bool | is the value a string? | |
is_array | no_index | bool | is the value an array? |
is_blob | no_index | bool | is the value a BLOB? |
is_map | no_object | bool | is the value an object map? |
is_timestamp | no_time or no_std | bool | is the value a timestamp? |
Casting methods
The following methods cast a Dynamic
into a specific type:
Method | Not available under | Return type (error is name of actual type if &str ) |
---|---|---|
cast<T> | T (panics on failure) | |
try_cast<T> | Option<T> | |
try_cast_result<T> | Result<T, Dynamic> | |
clone_cast<T> | cloned copy of T (panics on failure) | |
as_unit | Result<(), &str> | |
as_int | Result<INT, &str> | |
as_float | no_float | Result<FLOAT, &str> |
as_decimal | non-decimal | Result<Decimal, &str> |
as_bool | Result<bool, &str> | |
as_char | Result<char, &str> | |
as_immutable_string_ref | Result<impl Deref<Target=ImmutableString>, &str> | |
as_immutable_string_mut | Result<impl DerefMut<Target=ImmutableString>, &str> | |
as_array_ref | no_index | Result<impl Deref<Target=Array>, &str> |
as_array_mut | no_index | Result<impl DerefMut<Target=Array>, &str> |
as_blob_ref | no_index | Result<impl Deref<Target=Blob>, &str> |
as_blob_mut | no_index | Result<impl DerefMut<Target=Blob>, &str> |
as_map_ref | no_object | Result<impl Deref<Target=Map>, &str> |
as_map_mut | no_object | Result<impl DerefMut<Target=Map>, &str> |
into_string | Result<String, &str> | |
into_immutable_string | Result<ImmutableString, &str> | |
into_array | no_index | Result<Array, &str> |
into_blob | no_index | Result<Blob, &str> |
into_typed_array<T> | no_index | Result<Vec<T>, &str> |
Constructor traits
The following constructor traits are implemented for Dynamic
where T: Clone
:
Trait | Not available under | Data type |
---|---|---|
From<()> | () | |
From<INT> | integer number | |
From<FLOAT> | no_float | floating-point number |
From<Decimal> | non-decimal | Decimal |
From<bool> | bool | |
From<S: Into<ImmutableString>> e.g. From<String> , From<&str> | ImmutableString | |
From<char> | character | |
From<Vec<T>> | no_index | array |
From<&[T]> | no_index | array |
From<BTreeMap<K: Into<SmartString>, T>> e.g. From<BTreeMap<String, T>> | no_object | object map |
From<BTreeSet<K: Into<SmartString>>> e.g. From<BTreeSet<String>> | no_object | object map |
From<HashMap<K: Into<SmartString>, T>> e.g. From<HashMap<String, T>> | no_object or no_std | object map |
From<HashSet<K: Into<SmartString>>> e.g. From<HashSet<String>> | no_object or no_std | object map |
From<FnPtr> | function pointer | |
From<Instant> | no_time or no_std | timestamp |
From<Rc<RefCell<Dynamic>>> | sync or no_closure | Dynamic |
From<Arc<RwLock<Dynamic>>> (sync ) | non-sync or no_closure | Dynamic |
FromIterator<X: IntoIterator<Item=T>> | no_index | array |
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
let x = 42;
x.tag == 0; // tag defaults to zero
x.tag = 123; // set tag value
set_tag(x, 123); // 'set_tag' function also works
x.tag == 123; // get updated tag value
x.tag() == 123; // method also works
tag(x) == 123; // function call style also works
x.tag[3..5] = 2; // tag can be used as a bit-field
x.tag[3..5] == 2;
let y = x;
y.tag == 123; // the tag is copied across assignment
y.tag = 3000000000; // runtime error: 3000000000 is too large for 'i32'
Practical Applications
Attaching arbitrary information together with a value has a lot of practical uses.
Identify code path
For example, it is easy to attach an ID number to a value to indicate how or why that value is originally set.
This is tremendously convenient for debugging purposes where it is necessary to figure out which code path a particular value went through.
After the script is verified, all tag assignment statements can simply be removed.
const ROUTE1 = 1;
const ROUTE2 = 2;
const ROUTE3 = 3;
const ERROR_ROUTE = 9;
fn some_complex_calculation(x) {
let result;
if some_complex_condition(x) {
result = 42;
result.tag = ROUTE1; // record route #1
} else if some_other_very_complex_condition(x) == 1 {
result = 123;
result.tag = ROUTE2; // record route #2
} else if some_non_understandable_calculation(x) > 0 {
result = 0;
result.tag = ROUTE3; // record route #3
} else {
result = -1;
result.tag = ERROR_ROUTE; // record error
}
result // this value now contains the tag
}
let my_result = some_complex_calculation(key);
// The code path that 'my_result' went through is now in its tag.
// It is now easy to trace how 'my_result' gets its final value.
print(`Result = ${my_result} and reason = ${my_result.tag}`);
Identify data source
It is convenient to use the tag value to record the source of a piece of data.
let x = [0, 1, 2, 3, 42, 99, 123];
// Store the index number of each value into its tag before
// filtering out all even numbers, leaving only odd numbers
let filtered = x.map(|v, i| { v.tag = i; v }).filter(|v| v.is_odd());
// The tag now contains the original index position
for (data, i) in filtered {
print(`${i + 1}: Value ${data} from position #${data.tag + 1}`);
}
Identify code conditions
The tag value may also contain a bit-field of up to 32 (16 under 32-bit targets) individual bits, recording up to 32 (or 16 under 32-bit targets) logic conditions that contributed to the value.
Again, after the script is verified, all tag assignment statements can simply be removed.
fn some_complex_calculation(x) {
let result = x;
// Check first condition
if some_complex_condition() {
result += 1;
result.tag[0] = true; // Set first bit in bit-field
}
// Check second condition
if some_other_very_complex_condition(x) == 1 {
result *= 10;
result.tag[1] = true; // Set second bit in bit-field
}
// Check third condition
if some_non_understandable_calculation(x) > 0 {
result -= 42;
result.tag[2] = true; // Set third bit in bit-field
}
// Check result
if result > 100 {
result = 0;
result.tag[3] = true; // Set forth bit in bit-field
}
result
}
let my_result = some_complex_calculation(42);
// The tag of 'my_result' now contains a bit-field indicating
// the result of each condition.
// It is now easy to trace how 'my_result' gets its final value.
// Use indexing on the tag to get at individual bits.
print(`Result = ${my_result}`);
print(`First condition = ${my_result.tag[0]}`);
print(`Second condition = ${my_result.tag[1]}`);
print(`Third condition = ${my_result.tag[2]}`);
print(`Result check = ${my_result.tag[3]}`);
Return auxillary info
Sometimes it is useful to return auxillary info from a function.
// Verify Bell's Inequality by calculating a norm
// and comparing it with a hypotenuse.
// https://en.wikipedia.org/wiki/Bell%27s_theorem
//
// Returns the smaller of the norm or hypotenuse.
// Tag is 1 if norm <= hypo, 0 if otherwise.
fn bells_inequality(x, y, z) {
let norm = sqrt(x ** 2 + y ** 2);
let result;
if norm <= z {
result = norm;
result.tag = 1;
} else {
result = z;
result.tag = 0;
}
result
}
let dist = bells_inequality(x, y, z);
print(`Value = ${dist}`);
if dist.tag == 1 {
print("Local realism maintained! Einstein rules!");
} else {
print("Spooky action at a distance detected! Einstein will hate this...");
}
Poor-man’s tuples
Rust has tuples but Rhai does not (nor does JavaScript in this sense).
Similar to the JavaScript situation, practical alternatives using Rhai include returning an object map or an array.
Both of these alternatives, however, incur overhead that may be wasteful when the amount of
additional information is small – e.g. in many cases, a single bool
, or a small number.
To return a number of small values from functions, the tag value as a bit-field is an ideal container without resorting to a full-blown object map or array.
// This function essentially returns a tuple of four numbers:
// (result, a, b, c)
fn complex_calc(x, y, z) {
let a = x + y;
let b = x - y + z;
let c = (a + b) * z / y;
let r = do_complex_calculation(a, b, c);
// Store 'a', 'b' and 'c' into tag if they are small
r.tag[0..8] = a;
r.tag[8..16] = b;
r.tag[16..32] = c;
r
}
// Deconstruct the tuple
let result = complex_calc(x, y, z);
let a = r.tag[0..8];
let b = r.tag[8..16];
let c = r.tag[16..32];
TL;DR
Due to byte alignment requirements on modern CPU’s, there are unused spaces in a Dynamic
type,
of the order of 4 bytes on 64-bit targets (2 bytes on 32-bit).
It is empty space that can be put to good use and not wasted, especially when Rhai does not have built-in support of tuples in order to return multiple values from functions.
Serialization and Deserialization of Dynamic
with serde
Rhai’s Dynamic
type supports serialization and deserialization by
serde
via the serde
feature.
Dynamic
works both as a serialization format as well as a data type that is serializable.
Serialize/Deserialize a Dynamic
With the serde
feature turned on, Dynamic
implements serde::Serialize
and
serde::Deserialize
, so it can easily be serialized and deserialized with serde
(for example,
to and from JSON via serde_json
).
let value: Dynamic = ...;
// Serialize 'Dynamic' to JSON
let json = serde_json::to_string(&value);
// Deserialize 'Dynamic' from JSON
let result: Dynamic = serde_json::from_str(&json);
Custom types are serialized as text strings of the value’s type name.
The serde_json
crate is quite heavy.
If only simple JSON parsing (i.e. only deserialization) of a hash object into a Rhai object map is required,
the Engine::parse_json
method is available as a cheap alternative,
but it does not provide the same level of correctness, nor are there any configurable options.
Dynamic
as Serialization Format
A Dynamic
can be seamlessly converted to and from any type that implements serde::Serialize
and/or serde::Deserialize
, acting as a serialization format.
Serialize Any Type to Dynamic
The function rhai::serde::to_dynamic
automatically converts any Rust type that implements
serde::Serialize
into a Dynamic
.
For primary types, this is usually not necessary because using Dynamic::from
is much
easier and is essentially the same thing. The only difference is treatment for integer values.
Dynamic::from
keeps different integer types intact, while rhai::serde::to_dynamic
converts them
all into INT
(i.e. the system integer type which is i64
or i32
depending on
the only_i32
feature).
Rust struct
’s (or any type that is marked as a serde
map) are converted into object maps while
Rust Vec
’s (or any type that is marked as a serde
sequence) are converted into arrays.
While it is also simple to serialize a Rust type to JSON
via serde
,
then use Engine::parse_json
to convert it into an object map,
rhai::serde::to_dynamic
serializes it to Dynamic
directly via serde
without going through the JSON
step.
use rhai::{Dynamic, Map};
use rhai::serde::to_dynamic;
#[derive(Debug, serde::Serialize)]
struct Point {
x: f64,
y: f64
}
#[derive(Debug, serde::Serialize)]
struct MyStruct {
a: i64,
b: Vec<String>,
c: bool,
d: Point
}
let x = MyStruct {
a: 42,
b: vec![ "hello".into(), "world".into() ],
c: true,
d: Point { x: 123.456, y: 999.0 }
};
// Convert the 'MyStruct' into a 'Dynamic'
let map: Dynamic = to_dynamic(x);
map.is::<Map>() == true;
Deserialize a Dynamic
into Any Type
The function rhai::serde::from_dynamic
automatically converts a Dynamic
value into any Rust type
that implements serde::Deserialize
.
In particular, object maps are converted into Rust struct
’s (or any type that is marked as
a serde
map) while arrays are converted into Rust Vec
’s (or any type that is marked
as a serde
sequence).
use rhai::{Engine, Dynamic};
use rhai::serde::from_dynamic;
#[derive(Debug, serde::Deserialize)]
struct Point {
x: f64,
y: f64
}
#[derive(Debug, serde::Deserialize)]
struct MyStruct {
a: i64,
b: Vec<String>,
c: bool,
d: Point
}
let engine = Engine::new();
let result: Dynamic = engine.eval(
r#"
{
a: 42,
b: [ "hello", "world" ],
c: true,
d: #{ x: 123.456, y: 999.0 }
}
"#)?;
// Convert the 'Dynamic' object map into 'MyStruct'
let x: MyStruct = from_dynamic(&result)?;
Numbers
Integers
Integers can also be conveniently manipulated as bit-fields.
Integer numbers follow C-style format with support for decimal, binary (0b
), octal (0o
) and hex (0x
) notations.
The default system integer type (also aliased to INT
) is i64
. It can be turned into i32
via the only_i32
feature.
Floating-Point Numbers
Both decimal and scientific notations can be used to represent floating-point numbers.
Floating-point numbers are also supported if not disabled with no_float
.
The default system floating-point type is f64
(also aliased to FLOAT
).
It can be turned into f32
via the f32_float
feature.
Decimal
Numbers
When rounding errors cannot be accepted, such as in financial calculations, the decimal
feature
turns on support for the Decimal
type, which is a fixed-precision floating-point
number with no rounding errors.
Number Literals
_
separators can be added freely and are ignored within a number – except at the very beginning or right after
a decimal point (.
).
Sample | Format | Value type | no_float | no_float + decimal |
---|---|---|---|---|
_123 | improper separator | |||
123_345 , -42 | decimal | INT | INT | INT |
0o07_76 | octal | INT | INT | INT |
0xab_cd_ef | hex | INT | INT | INT |
0b0101_1001 | binary | INT | INT | INT |
123._456 | improper separator | |||
123_456.78_9 | normal floating-point | FLOAT | syntax error | Decimal |
-42. | ending with decimal point | FLOAT | syntax error | Decimal |
123_456_.789e-10 | scientific notation | FLOAT | syntax error | Decimal |
.456 | missing leading 0 | |||
123.456e_10 | improper separator | |||
123.e-10 | 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.
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.
Numeric Operators
Numeric operators generally follow C styles.
Unary Operators
Operator | Description |
---|---|
+ | positive |
- | negative |
let number = +42;
number = -5;
number = -5 - +5;
-(-42) == +42; // two '-' equals '+'
// beware: '++' and '--' are reserved symbols
Binary Operators
Operator | Description | Result type | INT | FLOAT | Decimal |
---|---|---|---|---|---|
+ , += | plus | numeric | yes | yes, also with INT | yes, also with INT |
- , -= | minus | numeric | yes | yes, also with INT | yes, also with INT |
* , *= | multiply | numeric | yes | yes, also with INT | yes, also with INT |
/ , /= | divide (integer division if acting on integer types) | numeric | yes | yes, also with INT | yes, also with INT |
% , %= | modulo (remainder) | numeric | yes | yes, also with INT | yes, also with INT |
** , **= | power/exponentiation | numeric | yes | yes, also FLOAT**INT | no |
<< , <<= | left bit-shift (if negative number of bits, shift right instead) | numeric | yes | no | no |
>> , >>= | right bit-shift (if negative number of bits, shift left instead) | numeric | yes | no | no |
& , &= | bit-wise And | numeric | yes | no | no |
| , |= | bit-wise Or | numeric | yes | no | no |
^ , ^= | bit-wise Xor | numeric | yes | no | no |
== | equals to | bool | yes | yes, also with INT | yes, also with INT |
!= | not equals to | bool | yes | yes, also with INT | yes, also with INT |
> | greater than | bool | yes | yes, also with INT | yes, also with INT |
>= | greater than or equals to | bool | yes | yes, also with INT | yes, also with INT |
< | less than | bool | yes | yes, also with INT | yes, also with INT |
<= | less than or equals to | bool | yes | yes, also with INT | yes, also with INT |
.. | exclusive range | range | yes | no | no |
..= | inclusive range | range | yes | no | no |
Examples
let x = (1 + 2) * (6 - 4) / 2; // arithmetic, with parentheses
let reminder = 42 % 10; // modulo
let power = 42 ** 2; // power
let left_shifted = 42 << 3; // left shift
let right_shifted = 42 >> 3; // right shift
let bit_op = 42 | 99; // bit masking
Floating-Point Interoperates with Integers
When one of the operands to a binary arithmetic operator is floating-point, it works with INT
for
the other operand and the result is floating-point.
let x = 41.0 + 1; // 'FLOAT' + 'INT'
type_of(x) == "f64"; // result is 'FLOAT'
let x = 21 * 2.0; // 'FLOAT' * 'INT'
type_of(x) == "f64";
(x == 42) == true; // 'FLOAT' == 'INT'
(10 < x) == true; // 'INT' < 'FLOAT'
Decimal Interoperates with Integers
When one of the operands to a binary arithmetic operator is Decimal
,
it works with INT
for the other operand and the result is Decimal
.
let d = parse_decimal("2");
let x = d + 1; // 'Decimal' + 'INT'
type_of(x) == "decimal"; // result is 'Decimal'
let x = 21 * d; // 'Decimal' * 'INT'
type_of(x) == "decimal";
(x == 42) == true; // 'Decimal' == 'INT'
(10 < x) == true; // 'INT' < 'Decimal'
Unary Before Binary
In Rhai, unary operators take precedence over binary operators. This is especially important to
remember when handling operators such as **
which in some languages bind tighter than the unary
-
operator.
-2 + 2 == 0;
-2 - 2 == -4;
-2 * 2 == -4;
-2 / 2 == -1;
-2 % 2 == 0;
-2 ** 2 = 4; // means: (-2) ** 2
// in some languages this means: -(2 ** 2)
Numeric Functions
Integer Functions
The following standard functions are defined.
Function | Not available under | Package | Description |
---|---|---|---|
is_odd method and property | ArithmeticPackage | returns true if the value is an odd number, otherwise false | |
is_even method and property | ArithmeticPackage | returns true if the value is an even number, otherwise false | |
min | LogicPackage | returns the smaller of two numbers | |
max | LogicPackage | returns the larger of two numbers | |
to_float | no_float | BasicMathPackage | convert the value into f64 (f32 under f32_float ) |
to_decimal | non-decimal | BasicMathPackage | convert the value into Decimal |
Signed Numeric Functions
The following standard functions are defined in the ArithmeticPackage
(excluded when using a raw Engine
) and operate on i8
, i16
, i32
, i64
, f32
, f64
and
Decimal
(requires decimal
) only.
Function | Description |
---|---|
abs | absolute value |
sign | returns (INT ) −1 if negative, +1 if positive, 0 if zero |
is_zero method and property | returns true if the value is zero, otherwise false |
Floating-Point Functions
The following standard functions are defined in the BasicMathPackage
(excluded when using a raw Engine
) and operate on f64
(f32
under f32_float
) and
Decimal
(requires decimal
) only.
Category | Supports Decimal | Functions |
---|---|---|
Trigonometry | yes | sin , cos , tan |
Trigonometry | no | sinh , cosh , tanh in radians, hypot( x, y) |
Arc-trigonometry | no | asin , acos , atan( v) , atan( x, y) , asinh , acosh , atanh in radians |
Square root | yes | sqrt |
Exponential | yes | exp (base e) |
Logarithmic | yes | ln (base e) |
Logarithmic | yes | log (base 10) |
Logarithmic | no | log( x, base) |
Rounding | yes | floor , ceiling , round , int , fraction methods and properties |
Conversion | yes | to_int , to_decimal (requires decimal ), to_float (not under no_float ) |
Conversion | no | to_degrees , to_radians |
Comparison | yes | min , max (also inter-operates with integers) |
Testing | no | is_nan , is_finite , is_infinite methods and properties |
Decimal Rounding Functions
The following rounding methods are defined in the BasicMathPackage
(excluded when using a raw Engine
) and operate on Decimal
only,
which requires the decimal
feature.
Rounding type | Behavior | Methods |
---|---|---|
None | floor , ceiling , int , fraction methods and properties | |
Banker’s rounding | round to integer | round method and property |
Banker’s rounding | round to specified number of decimal points | round( decimal points) |
Round up | away from zero | round_up( decimal points) |
Round down | towards zero | round_down( decimal points) |
Round half-up | mid-point away from zero | round_half_up( decimal points) |
Round half-down | mid-point towards zero | round_half_down( decimal points) |
Parsing Functions
The following standard functions are defined in the BasicMathPackage
(excluded when using a raw Engine
) to parse numbers.
Function | No available under | Description |
---|---|---|
parse_int | converts a string to INT with an optional radix | |
parse_float | no_float and non-decimal | converts a string to FLOAT (Decimal under no_float and decimal ) |
parse_decimal | non-decimal | converts a string to Decimal |
Formatting Functions
The following standard functions are defined in the BasicStringPackage
(excluded when using a raw Engine
) to convert integer numbers into a string of hex, octal
or binary representations.
Function | Description |
---|---|
to_binary | converts an integer number to binary |
to_octal | converts an integer number to octal |
to_hex | converts an integer number to hex |
These formatting functions are defined for all available integer numbers – i.e. INT
, u8
,
i8
, u16
, i16
, u32
, i32
, u64
, i64
, u128
and i128
unless disabled by feature flags.
Floating-point Constants
The following functions return standard mathematical constants.
Function | Description |
---|---|
PI | returns the value of π |
E | returns the value of e |
Numerical Functions for Scientific Computing
Check out the rhai-sci
crate for more numerical functions.
Value Conversions
Convert Between Integer and Floating-Point
Function | Not available under | From type | To type |
---|---|---|---|
to_int | INT , FLOAT , Decimal | INT | |
to_float | no_float | INT , FLOAT , Decimal | FLOAT |
to_decimal | non-decimal | INT , FLOAT , Decimal | 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
Function | From type | To type |
---|---|---|
parse_int | string | INT |
parse_int with radix 2-36 | string | INT (specified radix) |
parse_float (not no_float ) | string | FLOAT |
parse_float (no_float +decimal ) | string | Decimal |
parse_decimal (requires decimal ) | string | Decimal |
let x = parse_float("123.4"); // parse as floating-point
x == 123.4;
type_of(x) == "f64";
let x = parse_decimal("123.4"); // parse as Decimal value
type_of(x) == "decimal";
let x = 1234.to_decimal() / 10; // alternate method to create a Decimal value
type_of(x) == "decimal";
let dec = parse_int("42"); // parse as integer
dec == 42;
type_of(dec) == "i64";
let dec = parse_int("42", 10); // radix = 10 is the default
dec == 42;
type_of(dec) == "i64";
let bin = parse_int("110", 2); // parse as binary (radix = 2)
bin == 0b110;
type_of(bin) == "i64";
let hex = parse_int("ab", 16); // parse as hex (radix = 16)
hex == 0xab;
type_of(hex) == "i64";
Format Numbers
Function | From type | To type | Format |
---|---|---|---|
to_binary | INT | string | binary (i.e. only 1 and 0 ) |
to_octal | INT | string | octal (i.e. 0 … 7 ) |
to_hex | INT | string | hex (i.e. 0 … f ) |
let x = 0x1234abcd;
x == 305441741;
x.to_string() == "305441741";
x.to_binary() == "10010001101001010101111001101";
x.to_octal() == "2215125715";
x.to_hex() == "1234abcd";
Ranges
Syntax
Numeric ranges can be constructed by the ..
(exclusive) or ..=
(inclusive) operators.
Exclusive range
start
..
end
An exclusive range does not include the last (i.e. “end”) value.
The Rust type of an exclusive range is std::ops::Range<INT>
.
type_of()
an exclusive range returns "range"
.
Inclusive range
start
..=
end
An inclusive range includes the last (i.e. “end”) value.
The Rust type of an inclusive range is std::ops::RangeInclusive<INT>
.
type_of()
an inclusive range returns "range="
.
Usage Scenarios
Ranges are commonly used in the following scenarios.
Scenario | Example |
---|---|
for statements | for n in 0..100 { ... } |
in expressions | if n in 0..100 { ... } |
switch expressions | switch n { 0..100 => ... } |
Bit-fields access | let x = n[2..6]; |
Bits iteration | for bit in n.bits(2..=9) { ... } |
Array range-based API’s | array.extract(2..8) |
BLOB range-based API’s | blob.parse_le_int(4..8) |
String range-based API’s | string.sub_string(4..=12) |
Characters iteration | for ch in string.bits(4..=12) { ... } |
Custom types | my_obj.action(3..=15, "foo"); |
Use as Parameter Type
Native Rust functions that take parameters of type std::ops::Range<INT>
or
std::ops::RangeInclusive<INT>
, when registered into an Engine
, accept ranges as arguments.
..
(exclusive range) and ..=
(inclusive range) are different types to Rhai
and they do not interoperate.
Two different versions of the same API must be registered to handle both range styles.
use std::ops::{Range, RangeInclusive};
/// The actual work function
fn do_work(obj: &mut TestStruct, from: i64, to: i64, inclusive: bool) {
...
}
let mut engine = Engine::new();
engine
/// Version of API that accepts an exclusive range
.register_fn("do_work", |obj: &mut TestStruct, range: Range<i64>|
do_work(obj, range.start, range.end, false)
)
/// Version of API that accepts an inclusive range
.register_fn("do_work", |obj: &mut TestStruct, range: RangeInclusive<i64>|
do_work(obj, range.start(), range.end(), true)
);
engine.run(
"
let obj = new_ts();
obj.do_work(0..12); // use exclusive range
obj.do_work(0..=11); // use inclusive range
")?;
Indexers Using Ranges
Indexers commonly use ranges as parameters.
use std::ops::{Range, RangeInclusive};
let mut engine = Engine::new();
engine
/// Version of indexer that accepts an exclusive range
.register_indexer_get_set(
|obj: &mut TestStruct, range: Range<i64>| -> bool { ... },
|obj: &mut TestStruct, range: Range<i64>, value: bool| { ... },
)
/// Version of indexer that accepts an inclusive range
.register_indexer_get_set(
|obj: &mut TestStruct, range: RangeInclusive<i64>| -> bool { ... },
|obj: &mut TestStruct, range: RangeInclusive<i64>, value: bool| { ... },
);
engine.run(
"
let obj = new_ts();
let x = obj[0..12]; // use exclusive range
obj[0..=11] = !x; // use inclusive range
")?;
Built-in Functions
The following methods (mostly defined in the BasicIteratorPackage
but
excluded when using a raw Engine
) operate on ranges.
Function | Parameter(s) | Description |
---|---|---|
start method and property | beginning of the range | |
end method and property | end of the range | |
contains , in operator | number to check | does this range contain the specified number? |
is_empty method and property | returns true if the range contains no items | |
is_inclusive method and property | is the range inclusive? | |
is_exclusive method and property | is the range exclusive? |
TL;DR
Rust has open-ended ranges, such as start..
, ..end
and ..=end
. They are not available in Rhai.
They are not needed because Rhai can overload functions.
Typically, an API accepting ranges as parameters would have equivalent versions that accept a
starting position and a length (the standard start + len
pair), as well as a versions that accept
only the starting position (the length assuming to the end).
In fact, usually all versions redirect to a call to one single version.
For example, a naive implementation of the extract
method for arrays (without any error handling)
would look like:
use std::ops::{Range, RangeInclusive};
// Version with exclusive range
#[rhai_fn(name = "extract", pure)]
pub fn extract_range(array: &mut Array, range: Range<i64>) -> Array {
array[range].to_vec()
}
// Version with inclusive range
#[rhai_fn(name = "extract", pure)]
pub fn extract_range2(array: &mut Array, range: RangeInclusive<i64>) -> Array {
extract_range(array, range.start()..range.end() + 1)
}
// Version with start
#[rhai_fn(name = "extract", pure)]
pub fn extract_to_end(array: &mut Array, start: i64) -> Array {
extract_range(array, start..start + array.len())
}
// Version with start+len
#[rhai_fn(name = "extract", pure)]
pub fn extract(array: &mut Array, start: i64, len: i64) -> Array {
extract_range(array, start..start + len)
}
Therefore, there should always be a function that can do what open-ended ranges are intended for.
The left-open form (i.e. ..end
and ..=end
) is trivially replaced by using zero as the starting
position with a length that corresponds to the end position (for ..end
).
The right-open form (i.e. start..
) is trivially replaced by the version taking a single starting position.
let x = [1, 2, 3, 4, 5];
x.extract(0..3); // normal range argument
// copies 'x' from positions 0-2
x.extract(2); // copies 'x' from position 2 onwards
// equivalent to '2..'
x.extract(0, 2); // copies 'x' from beginning for 2 items
// equivalent to '..2'
Integer as Bit-Fields
Nothing here cannot be done via standard bit-manipulation (i.e. shifting and masking).
Built-in support is more elegant and performant since it usually replaces a sequence of multiple steps.
Since bit-wise operators are defined on integer numbers, individual bits can also be accessed and manipulated via an indexing syntax.
If a bit is set (i.e. 1
), the index access returns true
.
If a bit is not set (i.e. 0
), the index access returns false
.
When a range is used, the bits within the range are shifted and extracted as an integer value.
Bit-fields are very commonly used in embedded systems which must squeeze data into limited memory. Built-in support makes handling them efficient.
Indexing an integer as a bit-field is disabled for the no_index
feature.
Syntax
From Least-Significant Bit (LSB)
Bits in a bit-field are accessed with zero-based, non-negative integer indices:
integer
[
index from 0 to 63 or 31]
integer
[
index from 0 to 63 or 31] =
true
orfalse
;
Ranges can also be used:
integer
[
start..
end]
integer[
start..=
end]
integer
[
start..
end] =
new integer value ;
integer[
start..=
end] =
new integer value ;
The maximum bit number that can be accessed is 63 (or 31 under only_i32
).
Bits outside of the range are ignored.
From Most-Significant Bit (MSB)
A negative index accesses a bit in the bit-field counting from the end, or from the most-significant bit, with −1 being the highest bit.
integer
[
index from −1 to −64 or −32]
integer
[
index from −1 to −64 or −32] =
true
orfalse
;
Ranges always count from the least-significant bit (LSB) and has no support for negative positions.
The maximum bit number that can be accessed is −64 (or −32 under only_i32
).
Bit-Field Functions
The following standard functions (defined in the BitFieldPackage
but excluded
when using a raw Engine
) operate on INT
bit-fields.
These functions are available even under the no_index
feature.
Function | Parameter(s) | Description |
---|---|---|
get_bit | bit number, counting from MSB if < 0 | returns the state of a bit: true if 1 , false if 0 |
set_bit |
| sets the state of a bit |
get_bits |
| extracts a number of bits, shifted towards LSB |
get_bits | range of bits | extracts a number of bits, shifted towards LSB |
set_bits |
| sets a number of bits from the new value |
set_bits |
| sets a number of bits from the new value |
bits method and property |
| allows iteration over the bits of a bit-field |
bits | range of bits | allows iteration over the bits of a bit-field |
Example
// Assume the following bits fields in a single 16-bit word:
// ┌─────────┬────────────┬──────┬─────────┐
// │ 15-12 │ 11-4 │ 3 │ 2-0 │
// ├─────────┼────────────┼──────┼─────────┤
// │ 0 │ 0-255 data │ flag │ command │
// └─────────┴────────────┴──────┴─────────┘
let value = read_start_hw_register(42);
let command = value.get_bits(0, 3); // Command = bits 0-2
let flag = value[3]; // Flag = bit 3
let data = value[4..=11]; // Data = bits 4-11
let data = value.get_bits(4..=11); // <- above is the same as this
let reserved = value.get_bits(-4); // Reserved = last 4 bits
if reserved != 0 {
throw reserved;
}
switch command {
0 => print(`Data = ${data}`),
1 => value[4..=11] = data / 2,
2 => value[3] = !flag,
_ => print(`Unknown: ${command}`)
}
Strings and Characters
Always limit the maximum length of strings.
String in Rhai contain any text sequence of valid Unicode characters.
Internally strings are stored in UTF-8 encoding.
type_of()
a string returns "string"
.
String and Character Literals
String and character literals follow JavaScript-style syntax.
Type | Quotes | Escapes? | Continuation? | Interpolation? |
---|---|---|---|---|
Normal string | "..." | yes | with \ | no |
Raw string | #..#"..."#..# | no | no | no |
Multi-line literal string | `...` | no | no | with ${...} |
Character | '...' | yes | no | no |
Strings can be built up from other strings and types via the +
operator
(provided by the MoreStringPackage
but excluded when using a raw Engine
).
This is particularly useful when printing output.
Standard Escape Sequences
Use the to_int
method to convert a Unicode character into its 32-bit Unicode encoding.
There is built-in support for Unicode (\u
xxxx or \U
xxxxxxxx) and hex (\x
xx) 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 sequence | Meaning |
---|---|
\\ | back-slash (\ ) |
\t | tab |
\r | carriage-return (CR ) |
\n | line-feed (LF ) |
\" or "" | double-quote (" ) |
\' | single-quote (' ) |
\x xx | ASCII character in 2-digit hex |
\u xxxx | Unicode character in 4-digit hex |
\U xxxxxxxx | Unicode character in 8-digit hex |
Line Continuation
For a normal string wrapped by double-quotes ("
), a back-slash (\
) character at the end of a
line indicates that the string continues onto the next line without any line-break.
Whitespace up to the indentation of the opening double-quote is ignored in order to enable lining up blocks of text.
Spaces are not added, so to separate one line with the next with a space, put a space before the
ending back-slash (\
) character.
let x = "hello, world!\
hello world again! \
this is the ""last"" time!!!";
// ^^^^^^ these whitespaces are ignored
// The above is the same as:
let x = "hello, world!hello world again! this is the \"last\" time!!!";
A string with continuation does not open up a new line. To do so, a new-line character must be manually inserted at the appropriate position.
let x = "hello, world!\n\
hello world again!\n\
this is the last time!!!";
// The above is the same as:
let x = "hello, world!\nhello world again!\nthis is the last time!!!";
If the ending double-quote is omitted, it is a syntax error.
let x = "hello
";
// ^ syntax error: unterminated string literal
Technically speaking, there is no difficulty in allowing strings to run for multiple lines without the continuation back-slash.
Rhai forces you to manually mark a continuation with a back-slash because the ending quote is easy to omit. Once it happens, the entire remainder of the script would become one giant, multi-line string.
This behavior is different from Rust, where string literals can run for multiple lines.
Raw Strings
A raw string is any text enclosed by a pair of double-quotes ("
), wrapped by hash (#
) characters.
The number of hash (#
) on each side must be the same.
Any text inside the double-quotes, as long as it is not a double-quote ("
) followed by the same
number of hash (#
) characters, is simply copied verbatim, including control codes and/or
line-breaks.
Raw strings are very useful for embedded regular expressions, file paths, and program code etc.
let x = #"Hello, I am a raw string! which means that I can contain
line-breaks, \ slashes (not escapes), "quotes" and even # characters!"#
// Use more than one '#' if you happen to have '"###...' inside the string...
let x = ###"In Rhai, you can write ##"hello"## as a raw string."###;
// ^^^ this is not the end of the raw string
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)]
Internally, a Rhai string is still stored compactly as a Rust UTF-8 string in order to save memory.
Therefore, getting the character at a particular index involves walking through the entire UTF-8 encoded bytes stream to extract individual Unicode characters, counting them on the way.
Because of this, indexing can be a slow procedure, especially for long strings. Along the same lines, getting the length of a string (which returns the number of characters, not bytes) can also be slow.
Sub-Strings
Sub-strings, or slices in some programming languages, are parts of strings.
In Rhai, a sub-string can be specified by indexing with a range of characters:
string
[
first character (starting from zero)..
last character (exclusive)]
string
[
first character (starting from zero)..=
last character (inclusive)]
Sub-string ranges always start from zero counting towards the end of the string. Negative ranges are not supported.
Examples
let name = "Bob";
let middle_initial = 'C';
let last = "Davis";
let full_name = `${name} ${middle_initial}. ${last}`;
full_name == "Bob C. Davis";
// String building with different types
let age = 42;
let record = `${full_name}: age ${age}`;
record == "Bob C. Davis: age 42";
// Unlike Rust, Rhai strings can be indexed to get a character
// (disabled with 'no_index')
let c = record[4];
c == 'C'; // single character
let slice = record[4..8]; // sub-string slice
slice == " C. D";
ts.s = record; // custom type properties can take strings
let c = ts.s[4];
c == 'C';
let c = ts.s[-4]; // negative index counts from the end
c == 'e';
let c = "foo"[0]; // indexing also works on string literals...
c == 'f';
let c = ("foo" + "bar")[5]; // ... and expressions returning strings
c == 'r';
let text = "hello, world!";
text[0] = 'H'; // modify a single character
text == "Hello, world!";
text[7..=11] = "Earth"; // modify a sub-string slice
text == "Hello, Earth!";
// Escape sequences in strings
record += " \u2764\n"; // escape sequence of '❤' in Unicode
record == "Bob C. Davis: age 42 ❤\n"; // '\n' = new-line
// Unlike Rust, Rhai strings can be directly modified character-by-character
// (disabled with 'no_index')
record[4] = '\x58'; // 0x58 = 'X'
record == "Bob X. Davis: age 42 ❤\n";
// Use 'in' to test if a substring (or character) exists in a string
"Davis" in record == true;
'X' in record == true;
'C' in record == false;
// Strings can be iterated with a 'for' statement, yielding characters
for ch in record {
print(ch);
}
Multi-Line Literal Strings
A string wrapped by a pair of back-tick (`
) characters is interpreted literally.
This means that every single character that lies between the two back-ticks is taken verbatim.
This include new-lines, whitespaces, escape characters etc.
let x = `hello, world! "\t\x42"
hello world again! 'x'
this is the last time!!! `;
// The above is the same as:
let x = "hello, world! \"\\t\\x42\"\n hello world again! 'x'\n this is the last time!!! ";
If a back-tick (`
) appears at the end of a line, then it is understood that the entire text
block starts from the next line; the starting new-line character is stripped.
let x = `
hello, world! "\t\x42"
hello world again! 'x'
this is the last time!!!
`;
// The above is the same as:
let x = " hello, world! \"\\t\\x42\"\n hello world again! 'x'\n this is the last time!!!\n";
To actually put a back-tick (`
) character inside a multi-line literal string, use two
back-ticks together (i.e. ``
).
let x = `I have a quote " as well as a back-tick `` here.`;
// The above is the same as:
let x = "I have a quote \" as well as a back-tick ` here.";
String Interpolation
🤦 Well, you just have to ask for the impossible, don’t you?
Currently there is no way to escape ${
. Build the string in three pieces:
`Interpolations start with "`
+ "${"
+ `" and end with }.`
Multi-line literal strings support string interpolation wrapped in ${
… }
.
${
… }
acts as a statements block and can contain anything that is allowed within a
statements block, including another interpolated string!
The last result of the block is taken as the value for interpolation.
Rhai uses to_string
to convert any value into a string, then physically joins all the
sub-strings together.
For convenience, if any interpolated value is a BLOB, however, it is automatically treated as a UTF-8 encoded string. That is because it is rarely useful to interpolate a BLOB into a string, but extremely useful to be able to directly manipulate UTF-8 encoded text.
let x = 42;
let y = 123;
let s = `x = ${x} and y = ${y}.`; // <- interpolated string
let s = ("x = " + {x} + " and y = " + {y} + "."); // <- de-sugars to this
s == "x = 42 and y = 123.";
let s = `
Undeniable logic:
1) Hello, ${let w = `${x} world`; if x > 1 { w += "s" } w}!
2) If ${y} > ${x} then it is ${y > x}!
`;
s == "Undeniable logic:\n1) Hello, 42 worlds!\n2) If 123 > 42 then it is true!\n";
let blob = blob(3, 0x21);
print(blob); // prints [212121]
print(`Data: ${blob}`); // prints "Data: !!!"
// BLOB is treated as UTF-8 encoded string
print(`Data: ${blob.to_string()}`); // prints "Data: [212121]"
The ImmutableString
Type
SmartString
is used because many strings in scripts are short (fewer than 24 ASCII characters).
All strings in Rhai are implemented as ImmutableString
, which is an alias to
Rc<SmartString>
(or Arc<SmartString>
under the sync
feature).
An ImmutableString
is immutable (i.e. never changes) and therefore can be shared among many users.
Cloning an ImmutableString
is cheap since it only copies an immutable reference.
Modifying an ImmutableString
causes it first to be cloned, and then the modification made to the copy.
Therefore, direct string modifications are expensive.
Avoid String
Parameters
ImmutableString
should be used in place of String
for function parameters because using String
is very inefficient (the argument is cloned during every function call).
A alternative is to use &str
which de-sugars to ImmutableString
.
A function with the first parameter being &mut String
does not match a string argument passed to it,
which has type ImmutableString
. In fact, &mut String
is treated as an opaque custom type.
fn slow(s: String) -> i64 { ... } // string is cloned each call
fn fast1(s: ImmutableString) -> i64 { ... } // cloning 'ImmutableString' is cheap
fn fast2(s: &str) -> i64 { ... } // de-sugars to above
fn bad(s: &mut String) { ... } // '&mut String' will not match string values
Differences from Rust Strings
Internally Rhai strings are stored as UTF-8 just like Rust (they are Rust String
s!),
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.
Although Rhai exposes a string as a simple array of characters which can be directly indexed to get at a particular character, such convenient syntax is an illusion.
Internally the string is still stored in UTF-8 (native Rust String
s).
All indexing operations actually require walking through the entire UTF-8 string to find the offset of the particular character position, and therefore is much slower than the simple array indexing for other scripting languages.
This implementation detail is hidden from the user but has a performance implication.
Avoid large scale character-based processing of strings; instead, build an actual array of
characters (via the split()
method) which can then be manipulated efficiently.
Standard String Functions
The following standard methods (mostly defined in the MoreStringPackage
but
excluded when using a raw Engine
) operate on strings (and possibly characters).
Function | Parameter(s) | Description |
---|---|---|
len method and property | none | returns the number of characters (not number of bytes) in the string |
bytes method and property | none | returns the number of bytes making up the UTF-8 string; for strings containing only ASCII characters, this is much faster than len |
is_empty method and property | none | returns true if the string is empty |
to_blob (not available under no_index ) | none | converts the string into an UTF-8 encoded byte-stream and returns it as a BLOB. |
to_chars (not available under no_index ) | none | splits the string by individual characters, returning them as an array |
get | position, counting from end if < 0 | gets the character at a certain position (() if the position is not valid) |
set |
| sets a certain position to a new character (no effect if the position is not valid) |
pad |
| pads the string with a character or a string to at least a specified length |
append , += operator | item to append | adds the display text of an item to the end of the string |
remove | character/string to remove | removes a character or a string from the string |
pop | (optional) number of characters to remove, none if ≤ 0, entire string if ≥ length | removes the last character (if no parameter) and returns it (() if empty); otherwise, removes the last number of characters and returns them as a string |
clear | none | empties the string |
truncate | target length | cuts off the string at exactly a specified number of characters |
to_upper | none | converts the string/character into upper-case as a new string/character and returns it |
to_lower | none | converts the string/character into lower-case as a new string/character and returns it |
make_upper | none | converts the string/character into upper-case |
make_lower | none | converts the string/character into lower-case |
trim | none | trims the string of whitespace at the beginning and end |
contains | character/sub-string to search for | checks if a certain character or sub-string occurs in the string |
starts_with | string | returns true if the string starts with a certain string |
ends_with | string | returns true if the string ends with a certain string |
min |
| returns the smaller of two characters/strings |
max |
| returns the larger of two characters/strings |
index_of |
| returns the position that a certain character or sub-string occurs in the string, or −1 if not found |
sub_string |
| extracts a sub-string |
sub_string | range of characters to extract, from beginning if ≤ 0, to end if ≥ length | extracts a sub-string |
split (not available under no_index ) | none | splits the string by whitespaces, returning an array of string segments |
split (not available under no_index ) | position to split at (in number of characters), counting from end if < 0, end if ≥ length | splits the string into two segments at the specified character position, returning an array of two string segments |
split (not available under no_index ) |
| splits the string by the specified delimiter, returning an array of string segments |
split_rev (not available under no_index ) |
| splits the string by the specified delimiter in reverse order, returning an array of string segments |
crop |
| retains only a portion of the string |
crop | range of characters to retain, from beginning if ≤ 0, to end if ≥ length | retains only a portion of the string |
replace |
| replaces a sub-string with another |
chars method and property |
| allows iteration of the characters inside the string |
Beware that functions that involve indexing into a string to get at individual characters,
e.g. sub_string
, require walking through the entire UTF-8 encoded bytes stream to extract
individual Unicode characters and counting them, which can be slow for long strings.
Building Strings
Strings can be built from segments via the +
operator.
Operator | Description |
---|---|
string += item | convert the item into a string, then append it to the first string |
string + item | convert the item into a string, then concatenate them as a new string |
item + string | convert the item into a string, then concatenate them as a new string |
let x = 42;
// Build string with '+'
let s = "The answer is: " + x + "!!!";
// Prints: "The answer is: 42!!!"
print(s);
Standard Operators Between Strings and/or Characters
The following standard operators inter-operate between strings and/or characters.
When one (or both) of the operands is a character, it is first converted into a one-character string before running the operator.
Operator | Description |
---|---|
+ , += | character/string concatenation |
- , -= | remove character/sub-string from string |
== | equals to |
!= | not equals to |
> | greater than |
>= | greater than or equals to |
< | less than |
<= | less than or equals to |
Interop with BLOB’s
For convenience, when a BLOB is appended to a string, or vice versa, it is treated as a UTF-8 encoded byte stream and automatically first converted into the appropriate string value.
That is because it is rarely useful to append a BLOB into a string, but extremely useful to be able to directly manipulate UTF-8 encoded text.
Operator | Description |
---|---|
+ , += | append a BLOB (as a UTF-8 encoded byte stream) to the end of the string |
+ | concatenate a BLOB (as a UTF-8 encoded byte stream) with a string |
Examples
let full_name == " Bob C. Davis ";
full_name.len == 14;
full_name.trim();
full_name.len == 12;
full_name == "Bob C. Davis";
full_name.pad(15, '$');
full_name.len == 15;
full_name == "Bob C. Davis$$$";
let n = full_name.index_of('$');
n == 12;
full_name.index_of("$$", n + 1) == 13;
full_name.sub_string(n, 3) == "$$$";
full_name.sub_string(n..n+3) == "$$$";
full_name.truncate(6);
full_name.len == 6;
full_name == "Bob C.";
full_name.replace("Bob", "John");
full_name.len == 7;
full_name == "John C.";
full_name.contains('C') == true;
full_name.contains("John") == true;
full_name.crop(5);
full_name == "C.";
full_name.crop(0, 1);
full_name == "C";
full_name.clear();
full_name.len == 0;
Strings Interner
Because strings are immutable (i.e. the use the type ImmutableString
instead of normal Rust String
),
each operation on a string actually creates a new ImmutableString
instance.
A strings interner can substantially reduce memory usage by reusing the same ImmutableString
instance for the same string content.
An Engine
contains a strings interner which is enabled by default
(disabled when using a raw Engine
).
The maximum number of strings to be interned can be set via
Engine::set_max_strings_interned
(set to zero to disable the strings interner).
Arrays
Always limit the maximum size of arrays.
Arrays are first-class citizens in Rhai.
All elements stored in an array are Dynamic
, and the array can freely grow or shrink with
elements added or removed.
The Rust type of a Rhai array is rhai::Array
which is an alias to Vec<Dynamic>
.
type_of()
an array returns "array"
.
Arrays are disabled via the no_index
feature.
Literal Syntax
Array literals are built within square brackets [
… ]
and separated by commas ,
:
[
value,
value,
…,
value]
[
value,
value,
…,
value,
]
// trailing comma is OK
Element Access Syntax
From beginning
Like C, arrays are accessed with zero-based, non-negative integer indices:
array
[
index position from 0 to (length−1)]
From end
A negative position accesses an element in the array counting from the end, with −1 being the last element.
array
[
index position from −1 to −length]
Out-of-Bounds Index
Trying to read from an index that is out of bounds causes an error.
For fine-tuned control on what happens when an out-of-bounds index is accessed, see Out-of-Bounds Index for Arrays.
Built-in Functions
The following methods (mostly defined in the BasicArrayPackage
but excluded
when using a raw Engine
) operate on arrays.
Function | Parameter(s) | Description |
---|---|---|
get | position, counting from end if < 0 | gets a copy of the element at a certain position (() if the position is not valid) |
set |
| sets a certain position to a new value (no effect if the position is not valid) |
push , += operator | element to append (not an array) | appends an element to the end |
append , += operator | array to append | concatenates the second array to the end of the first |
+ operator |
| concatenates the first array with the second |
== operator |
| are two arrays the same (elements compared with the == operator, if defined)? |
!= operator |
| are two arrays different (elements compared with the == operator, if defined)? |
insert |
| inserts an element at a certain position |
pop | none | removes the last element and returns it (() if empty) |
shift | none | removes the first element and returns it (() if empty) |
extract |
| extracts a portion of the array into a new array |
extract | range of elements to extract, from beginning if ≤ 0, to end if ≥ length | extracts a portion of the array into a new array |
remove | position, counting from end if < 0 | removes an element at a particular position and returns it (() if the position is not valid) |
reverse | none | reverses the array |
len method and property | none | returns the number of elements |
is_empty method and property | none | returns true if the array is empty |
pad |
| pads the array with an element to at least a specified length |
clear | none | empties the array |
truncate | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) |
chop | target length | cuts off the head of the array, leaving the tail at exactly a specified length |
split |
| splits the array into two arrays, starting from a specified position |
for_each | function pointer for processing elements | run through each element in the array in order, binding each to this and calling the processing function taking the following parameters:
|
drain | function pointer to predicate (usually a closure) | removes all elements (returning them) that return true when called with the predicate function taking the following parameters (if none, the array element is bound to this ):
|
drain |
| removes a portion of the array, returning the removed elements as a new array |
drain | range of elements to remove, from beginning if ≤ 0, to end if ≥ length | removes a portion of the array, returning the removed elements as a new array |
retain | function pointer to predicate (usually a closure) | removes all elements (returning them) that do not return true when called with the predicate function taking the following parameters (if none, the array element is bound to this ):
|
retain |
| retains a portion of the array, removes all other elements and returning them as a new array |
retain | range of elements to retain, from beginning if ≤ 0, to end if ≥ length | retains a portion of the array, removes all other bytes and returning them as a new array |
splice |
| replaces a portion of the array with another (not necessarily of the same length as the replaced portion) |
splice |
| replaces a portion of the array with another (not necessarily of the same length as the replaced portion) |
filter | function pointer to predicate (usually a closure) | constructs a new array with all elements that return true when called with the predicate function taking the following parameters (if none, the array element is bound to this ):
|
contains , in operator | element to find | does the array contain an element? The == operator (if defined) is used to compare custom types |
index_of |
| returns the position of the first element in the array that equals the supplied element (using the == operator, if defined), or −1 if not found |
index_of |
| returns the position of the first element in the array that returns true when called with the predicate function, or −1 if not found:
|
find |
| returns the first element in the array that returns true when called with the predicate function, or () if not found:
|
find_map |
| returns the first non-() value of the first element in the array when called with the predicate function, or () if not found:
|
dedup | (optional) function pointer to predicate (usually a closure); if omitted, the == operator is used, if defined | removes all but the first of consecutive elements in the array that return true when called with the predicate function (non-consecutive duplicates are not removed):1st & 2nd parameters: two elements in the array |
map | function pointer to conversion function (usually a closure) | constructs a new array with all elements mapped to the result of applying the conversion function taking the following parameters (if none, the array element is bound to this ):
|
reduce |
| reduces the array into a single value via the accumulator function taking the following parameters (if the second parameter is omitted, the array element is bound to this ):
|
reduce_rev |
| reduces the array (in reverse order) into a single value via the accumulator function taking the following parameters (if the second parameter is omitted, the array element is bound to this ):
|
zip |
| constructs a new array with all element pairs from two arrays mapped to the result of applying the conversion function taking the following parameters:
|
some | function pointer to predicate (usually a closure) | returns true if any element returns true when called with the predicate function taking the following parameters (if none, the array element is bound to this ):
|
all | function pointer to predicate (usually a closure) | returns true if all elements return true when called with the predicate function taking the following parameters (if none, the array element is bound to this ):
|
sort | function pointer to a comparison function (usually a closure) | sorts the array with a comparison function taking the following parameters:
|
sort | none | sorts a homogeneous array containing only elements of the same comparable built-in type (INT , FLOAT , Decimal , string, character, bool , () ) |
To use a custom type with arrays, a number of functions need to be manually implemented,
in particular the ==
operator in order to support the in
operator which uses ==
(via the
contains
method) to compare elements.
See the section on custom types for more details.
Examples
let y = [2, 3]; // y == [2, 3]
let y = [2, 3,]; // y == [2, 3]
y.insert(0, 1); // y == [1, 2, 3]
y.insert(999, 4); // y == [1, 2, 3, 4]
y.len == 4;
y[0] == 1;
y[1] == 2;
y[2] == 3;
y[3] == 4;
(1 in y) == true; // use 'in' to test if an element exists in the array
(42 in y) == false; // 'in' uses the 'contains' function, which uses the
// '==' operator (that users can override)
// to check if the target element exists in the array
y.contains(1) == true; // the above de-sugars to this
y[1] = 42; // y == [1, 42, 3, 4]
(42 in y) == true;
y.remove(2) == 3; // y == [1, 42, 4]
y.len == 3;
y[2] == 4; // elements after the removed element are shifted
ts.list = y; // arrays can be assigned completely (by value copy)
ts.list[1] == 42;
[1, 2, 3][0] == 1; // indexing on array literal
[1, 2, 3][-1] == 3; // negative position counts from the end
fn abc() {
[42, 43, 44] // a function returning an array
}
abc()[0] == 42;
y.push(4); // y == [1, 42, 4, 4]
y += 5; // y == [1, 42, 4, 4, 5]
y.len == 5;
y.shift() == 1; // y == [42, 4, 4, 5]
y.chop(3); // y == [4, 4, 5]
y.len == 3;
y.pop() == 5; // y == [4, 4]
y.len == 2;
for element in y { // arrays can be iterated with a 'for' statement
print(element);
}
y.pad(6, "hello"); // y == [4, 4, "hello", "hello", "hello", "hello"]
y.len == 6;
y.truncate(4); // y == [4, 4, "hello", "hello"]
y.len == 4;
y.clear(); // y == []
y.len == 0;
// The examples below use 'a' as the master array
let a = [42, 123, 99];
a.for_each(|| this *= 2);
a == [84, 246, 198];
a.for_each(|i| this /= 2);
a == [42, 123, 99];
a.map(|v| v + 1); // returns [43, 124, 100]
a.map(|| this + 1); // returns [43, 124, 100]
a.map(|v, i| v + i); // returns [42, 124, 101]
a.filter(|v| v > 50); // returns [123, 99]
a.filter(|| this > 50); // returns [123, 99]
a.filter(|v, i| i == 1); // returns [123]
a.filter("is_odd"); // returns [123, 99]
a.filter(Fn("is_odd")); // <- previous statement is equivalent to this...
a.filter(|v| is_odd(v)); // <- or this
a.some(|v| v > 50); // returns true
a.some(|| this > 50); // returns true
a.some(|v, i| v < i); // returns false
a.all(|v| v > 50); // returns false
a.all(|| this > 50); // returns false
a.all(|v, i| v > i); // returns true
// Reducing - initial value provided directly
a.reduce(|sum| sum + this, 0) == 264;
// Reducing - initial value provided directly
a.reduce(|sum, v| sum + v, 0) == 264;
// Reducing - initial value is '()'
a.reduce(
|sum, v| if sum.type_of() == "()" { v } else { sum + v }
) == 264;
// Reducing - initial value has index position == 0
a.reduce(|sum, v, i|
if i == 0 { v } else { sum + v }
) == 264;
// Reducing in reverse - initial value provided directly
a.reduce_rev(|sum| sum + this, 0) == 264;
// Reducing in reverse - initial value provided directly
a.reduce_rev(|sum, v| sum + v, 0) == 264;
// Reducing in reverse - initial value is '()'
a.reduce_rev(
|sum, v| if sum.type_of() == "()" { v } else { sum + v }
) == 264;
// Reducing in reverse - initial value has index position == 0
a.reduce_rev(|sum, v, i|
if i == 2 { v } else { sum + v }
) == 264;
// In-place modification
a.splice(1..=1, [1, 3, 2]); // a == [42, 1, 3, 2, 99]
a.extract(1..=3); // returns [1, 3, 2]
a.sort(|x, y| y - x); // a == [99, 42, 3, 2, 1]
a.sort(); // a == [1, 2, 3, 42, 99]
a.drain(|v| v <= 1); // a == [2, 3, 42, 99]
a.drain(|v, i| i ≥ 3); // a == [2, 3, 42]
a.retain(|v| v > 10); // a == [42]
a.retain(|v, i| i > 0); // a == []
BLOB’s
Always limit the maximum size of arrays.
BLOB’s (Binary Large OBjects), used to hold packed arrays of bytes, have built-in support in Rhai.
A BLOB has no literal representation, but is created via the blob
function, or simply returned as
the result of a function call (e.g. generate_thumbnail_image
that generates a thumbnail version of
a large image as a BLOB).
All items stored in a BLOB are bytes (i.e. u8
) and the BLOB can freely grow or shrink with bytes
added or removed.
The Rust type of a Rhai BLOB is rhai::Blob
which is an alias to Vec<u8>
.
type_of()
a BLOB returns "blob"
.
BLOB’s are disabled via the no_index
feature.
Element Access Syntax
From beginning
Like arrays, BLOB’s are accessed with zero-based, non-negative integer indices:
blob
[
index position from 0 to (length−1)]
From end
A negative position accesses an element in the BLOB counting from the end, with −1 being the last element.
blob
[
index position from −1 to −length]
The value of a particular byte in a BLOB is mapped to an INT
(which can be 64-bit or 32-bit
depending on the only_i32
feature).
Only the lowest 8 bits are significant, all other bits are ignored.
Create a BLOB
The function blob
allows creating an empty BLOB, optionally filling it to a required size with a
particular value (default zero).
let x = blob(); // empty BLOB
let x = blob(10); // BLOB with ten zeros
let x = blob(50, 42); // BLOB with 50x 42's
To quickly initialize a BLOB with a particular byte stream, the write_be
method can be used to
write eight bytes at a time (four under only_i32
) in big-endian byte order.
If fewer than eight bytes are needed, remember to right-pad the number as big-endian byte order is used.
let buf = blob(12, 0); // BLOB with 12x zeros
// Write eight bytes at a time, in big-endian order
buf.write_be(0, 8, 0xab_cd_ef_12_34_56_78_90);
buf.write_be(8, 8, 0x0a_0b_0c_0d_00_00_00_00);
// ^^^^^^^^^^^ remember to pad unused bytes
print(buf); // prints "[abcdef1234567890 0a0b0c0d]"
buf[3] == 0x12;
buf[10] == 0x0c;
// Under 'only_i32', write four bytes at a time:
buf.write_be(0, 4, 0xab_cd_ef_12);
buf.write_be(4, 4, 0x34_56_78_90);
buf.write_be(8, 4, 0x0a_0b_0c_0d);
Writing ASCII Bytes
For many embedded applications, it is necessary to encode an ASCII string as a byte stream.
Use the write_ascii
method to write ASCII strings into any specific range within a BLOB.
The following is an example of a building a 16-byte command to send to an embedded device.
// Assume the following 16-byte command for an embedded device:
// ┌─────────┬───────────────┬──────────────────────────────────┬───────┐
// │ 0 │ 1 │ 2-13 │ 14-15 │
// ├─────────┼───────────────┼──────────────────────────────────┼───────┤
// │ command │ string length │ ASCII string, max. 12 characters │ CRC │
// └─────────┴───────────────┴──────────────────────────────────┴───────┘
let buf = blob(16, 0); // initialize command buffer
let text = "foo & bar"; // text string to send to device
buf[0] = 0x42; // command code
buf[1] = s.len(); // length of string
buf.write_ascii(2..14, text); // write the string
let crc = buf.calc_crc(); // calculate CRC
buf.write_le(14, 2, crc); // write CRC
print(buf); // prints "[4209666f6f202620 626172000000abcd]"
// ^^ command code ^^^^ CRC
// ^^ string length
// ^^^^^^^^^^^^^^^^^^^ foo & bar
device.send(buf); // send command to device
The write_utf8
function writes a string in UTF-8 encoding.
UTF-8, however, is not very common for embedded applications.
Built-in Functions
The following functions (mostly defined in the BasicBlobPackage
but excluded
when using a raw Engine
) operate on BLOB’s.
Functions | Parameter(s) | Description |
---|---|---|
blob constructor function |
| creates a new BLOB, optionally of a particular length filled with an initial byte value (default = 0) |
to_array | none | converts the BLOB into an array of integers |
as_string | none | converts the BLOB into a string (the byte stream is interpreted as UTF-8) |
get | position, counting from end if < 0 | gets a copy of the byte at a certain position (0 if the position is not valid) |
set |
| sets a certain position to a new value (no effect if the position is not valid) |
push , append , += operator |
| appends a byte to the end |
append , += operator |
| concatenates the second BLOB to the end of the first |
append , += operator | concatenates a string or character (as UTF-8 encoded byte-stream) to the end of the BLOB | |
+ operator |
| creates a new string by concatenating the BLOB (as UTF-8 encoded byte-stream) with the the string |
+ operator |
| creates a new string by concatenating the BLOB (as UTF-8 encoded byte-stream) to the end of the string |
+ operator |
| concatenates the first BLOB with the second |
== operator |
| are two BLOB’s the same? |
!= operator |
| are two BLOB’s different? |
insert |
| inserts a byte at a certain position |
pop | none | removes the last byte and returns it (0 if empty) |
shift | none | removes the first byte and returns it (0 if empty) |
extract |
| extracts a portion of the BLOB into a new BLOB |
extract | range of bytes to extract, from beginning if ≤ 0, to end if ≥ length | extracts a portion of the BLOB into a new BLOB |
remove | position, counting from end if < 0 | removes a byte at a particular position and returns it (0 if the position is not valid) |
reverse | none | reverses the BLOB byte by byte |
len method and property | none | returns the number of bytes in the BLOB |
is_empty method and property | none | returns true if the BLOB is empty |
pad |
| pads the BLOB with a byte value to at least a specified length |
clear | none | empties the BLOB |
truncate | target length | cuts off the BLOB at exactly a specified length (discarding all subsequent bytes) |
chop | target length | cuts off the head of the BLOB, leaving the tail at exactly a specified length |
contains , in operator | byte value to find | does the BLOB contain a particular byte value? |
split |
| splits the BLOB into two BLOB’s, starting from a specified position |
drain |
| removes a portion of the BLOB, returning the removed bytes as a new BLOB |
drain | range of bytes to remove, from beginning if ≤ 0, to end if ≥ length | removes a portion of the BLOB, returning the removed bytes as a new BLOB |
retain |
| retains a portion of the BLOB, removes all other bytes and returning them as a new BLOB |
retain | range of bytes to retain, from beginning if ≤ 0, to end if ≥ length | retains a portion of the BLOB, removes all other bytes and returning them as a new BLOB |
splice |
| replaces a portion of the BLOB with another (not necessarily of the same length as the replaced portion) |
splice |
| replaces a portion of the BLOB with another (not necessarily of the same length as the replaced portion) |
parse_le_int |
| parses an integer at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_le_int | range of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under only_i32 ) | parses an integer at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_be_int |
| parses an integer at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_be_int | range of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under only_i32 ) | parses an integer at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_le_float (not available under no_float ) |
| parses a floating-point number at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_le_float (not available under no_float ) | range of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under f32_float ) | parses a floating-point number at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_be_float (not available under no_float ) |
| parses a floating-point number at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_be_float (not available under no_float ) | range of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under f32_float ) | parses a floating-point number at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
write_le | writes a value at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) | |
write_le | writes a value at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) | |
write_be | writes a value at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) | |
write_be | writes a value at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) | |
write_utf8 |
| writes a string to the particular offset in UTF-8 encoding |
write_utf8 | writes a string to the particular offset in UTF-8 encoding | |
write_ascii |
| writes a string to the particular offset in 7-bit ASCII encoding (non-ASCII characters are skipped) |
write_ascii | writes a string to the particular offset in 7-bit ASCII encoding (non-ASCII characters are skipped) |
Out-of-Bounds Index for Arrays
This is an advanced feature that requires the internals
feature to be enabled.
Normally, when an index is out-of-bounds for an array, an error is raised.
It is possible to completely control this behavior via a special callback function
registered into an Engine
via on_invalid_array_index
.
Using this callback, for instance, it is simple to instruct Rhai to extend the array to accommodate this new element, or to return a default value instead of raising an error.
Function Signature
The function signature passed to Engine::on_invalid_array_index
takes the following form.
Fn(array: &mut Array, index: i64, context: EvalContext) -> Result<Target, Box<EvalAltResult>>
where:
Parameter | Type | Description |
---|---|---|
array | &mut Array | the array being accessed |
index | i64 | index value |
context | EvalContext | the current evaluation context |
Return value
The return value is Result<Target, Box<EvalAltResult>>
.
Target
is an advanced type, available only under the internals
feature, that represents a
reference to a Dynamic
value.
It can be used to point to a particular value within the array or a new temporary value.
Example
engine.on_invalid_array_index(|arr, index, _| {
match index {
-100 => {
// The array can be modified in place
arr.push((42_i64).into());
// Return a mutable reference to an element
let value_ref = arr.last_mut().unwrap();
Ok(value_ref.into())
}
100 => {
// Return a temporary value (not a reference)
let value = Dynamic::from(100_i64);
Ok(value.into())
}
// Return the standard out-of-bounds error
_ => Err(EvalAltResult::ErrorArrayBounds(
arr.len(), index, Position::NONE
).into()),
}
});
Object Maps
Always limit the maximum size of object maps.
Object maps are hash dictionaries. Properties are all Dynamic
and can be freely added and retrieved.
The Rust type of a Rhai object map is rhai::Map
.
Currently it is an alias to BTreeMap<SmartString, Dynamic>
.
type_of()
an object map returns "map"
.
Object maps are disabled via the no_object
feature.
Normally, when properties are accessed, copies of the data values are made. This is normally slow.
Object maps have special treatment – properties are accessed via references, meaning that no copies of data values are made.
This makes object map access fast, especially when deep within a properties chain.
// 'obj' is a normal custom type
let x = obj.a.b.c.d;
// The above is equivalent to:
let a_value = obj.a; // temp copy of 'a'
let b_value = a_value.b; // temp copy of 'b'
let c_value = b_value.c; // temp copy of 'c'
let d_value = c_value.d; // temp copy of 'd'
let x = d_value;
// 'map' is an object map
let x = map.a.b.c.d; // direct access to 'd'
// 'a', 'b' and 'c' are not copied
map.a.b.c.d = 42; // directly modifies 'd' in 'a', 'b' and 'c'
// no copy of any property value is made
map.a.b.c.d.calc(); // directly calls 'calc' on 'd'
// no copy of any property value is made
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.
The vast majority of object maps contain just a few properties.
BTreeMap
performs significantly better than HashMap
when the number of entries is small.
Literal Syntax
Object map literals are built within braces #{
… }
with name:
value pairs separated by
commas ,
:
#{
property:
value,
…,
property:
value}
#{
property:
value,
…,
property:
value,
}
// trailing comma is OK
The property name can be a simple identifier following the same naming rules as variables, or a string literal without interpolation.
Property Access Syntax
Dot notation
The dot notation allows only property names that follow the same naming rules as variables.
object
.
property
Elvis notation
The Elvis notation is similar to the dot notation except that it returns ()
if the object
itself is ()
.
// returns () if object is ()
object?.
property
// no action if object is ()
object?.
property=
value;
Index notation
The index notation allows setting/getting properties of arbitrary names (even the empty string).
object
[
property]
Handle Non-Existent Properties
Trying to read a non-existent property returns ()
instead of causing an error.
This is similar to JavaScript where accessing a non-existent property returns undefined
.
let map = #{ foo: 42 };
// Regular property access
let x = map.foo; // x == 42
// Non-existent property
let x = map.bar; // x == ()
It is possible to force Rhai to return an EvalAltResult:: ErrorPropertyNotFound
via
Engine:: set_fail_on_invalid_map_property
.
For fine-tuned control on what happens when a non-existent property is accessed, see Non-Existent Property Handling for Object Maps.
Check for property existence
Use the in
operator to check whether a property exists in an object-map.
let map = #{ foo: 42 };
"foo" in map == true;
"bar" in map == false;
Short-circuit non-existent property access
Use the Elvis operator (?.
) to short-circuit further processing if the object is ()
.
x.a.b.foo(); // <- error if 'x', 'x.a' or 'x.a.b' is ()
x.a.b = 42; // <- error if 'x' or 'x.a' is ()
x?.a?.b?.foo(); // <- ok! returns () if 'x', 'x.a' or 'x.a.b' is ()
x?.a?.b = 42; // <- ok even if 'x' or 'x.a' is ()
Default property value
Using the null-coalescing operator to give non-existent properties default values.
let map = #{ foo: 42 };
// Regular property access
let x = map.foo; // x == 42
// Non-existent property
let x = map.bar; // x == ()
// Default value for property
let x = map.bar ?? 42; // x == 42
Built-in Functions
The following methods (defined in the BasicMapPackage
but excluded when using
a raw Engine
) operate on object maps.
Function | Parameter(s) | Description |
---|---|---|
get | property name | gets a copy of the value of a certain property (() if the property does not exist); behavior is not affected by Engine::fail_on_invalid_map_property |
set |
| sets a certain property to a new value (property is added if not already exists) |
len | none | returns the number of properties |
is_empty | none | returns true if the object map is empty |
clear | none | empties the object map |
remove | property name | removes a certain property and returns it (() if the property does not exist) |
+= operator, mixin | second object map | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) |
+ operator |
| merges the first object map with the second |
== operator |
| are the two object maps the same (elements compared with the == operator, if defined)? |
!= operator |
| are the two object maps different (elements compared with the == operator, if defined)? |
fill_with | second object map | adds in all properties of the second object map that do not exist in the object map |
contains , in operator | property name | does the object map contain a property of a particular name? |
keys | none | returns an array of all the property names (in random order), not available under no_index |
values | none | returns an array of all the property values (in random order), not available under no_index |
drain | function pointer to predicate (usually a closure) | removes all elements (returning them) that return true when called with the predicate function taking the following parameters:
|
retain | function pointer to predicate (usually a closure) | removes all elements (returning them) that do not return true when called with the predicate function taking the following parameters:
|
filter | function pointer to predicate (usually a closure) | constructs a object map with all elements that return true when called with the predicate function taking the following parameters:
|
to_json | none | returns a JSON representation of the object map (() is mapped to null , all other data types must be supported by JSON) |
Examples
let y = #{ // object map literal with 3 properties
a: 1,
bar: "hello",
"baz!$@": 123.456, // like JavaScript, you can use any string as property names...
"": false, // even the empty string!
`hello`: 999, // literal strings are also OK
a: 42, // <- syntax error: duplicated property name
`a${2}`: 42, // <- syntax error: property name cannot have string interpolation
};
y.a = 42; // access via dot notation
y.a == 42;
y.baz!$@ = 42; // <- syntax error: only proper variable names allowed in dot notation
y."baz!$@" = 42; // <- syntax error: strings not allowed in dot notation
y["baz!$@"] = 42; // access via index notation is OK
"baz!$@" in y == true; // use 'in' to test if a property exists in the object map
("z" in y) == false;
ts.obj = y; // object maps can be assigned completely (by value copy)
let foo = ts.list.a;
foo == 42;
let foo = #{ a:1, }; // trailing comma is OK
let foo = #{ a:1, b:2, c:3 }["a"];
let foo = #{ a:1, b:2, c:3 }.a;
foo == 1;
fn abc() {
{ a:1, b:2, c:3 } // a function returning an object map
}
let foo = abc().b;
foo == 2;
let foo = y["a"];
foo == 42;
y.contains("a") == true;
y.contains("xyz") == false;
y.xyz == (); // a non-existent property returns '()'
y["xyz"] == ();
y.len == (); // an object map has no property getter function
y.len() == 3; // method calls are OK
y.remove("a") == 1; // remove property
y.len() == 2;
y.contains("a") == false;
for name in y.keys() { // get an array of all the property names via 'keys'
print(name);
}
for val in y.values() { // get an array of all the property values via 'values'
print(val);
}
y.clear(); // empty the object map
y.len() == 0;
No Support for Property Getters
In order not to affect the speed of accessing properties in an object map, new property getters cannot be registered because they conflict with the syntax of property access.
A property getter function registered via Engine::register_get
, for example,
for a Map
will never be found – instead, the property will be looked up in the object map.
Properties should be registered as methods instead:
map.len // access property 'len', returns '()' if not found
map.len() // 'len' method - returns the number of properties
map.keys // access property 'keys', returns '()' if not found
map.keys() // 'keys' method - returns array of all property names
map.values // access property 'values', returns '()' if not found
map.values() // 'values' method - returns array of all property values
Parse an Object Map from JSON
Do It Without serde
A valid JSON object hash does not start with a hash character #
while a Rhai object map does.
That’s the only difference!
The syntax for an object map is extremely similar to the JSON representation of a object hash,
with the exception of null
values which can technically be mapped to ()
.
Use the Engine::parse_json
method to parse a piece of JSON into an object map.
// JSON string - notice that JSON property names are always quoted
// notice also that comments are acceptable within the JSON string
let json = r#"{
"a": 1, // <- this is an integer number
"b": true,
"c": 123.0, // <- this is a floating-point number
"$d e f!": "hello", // <- any text can be a property name
"^^^!!!": [1,42,"999"], // <- value can be array or another hash
"z": null // <- JSON 'null' value
}"#;
// Parse the JSON expression as an object map
// Set the second boolean parameter to true in order to map 'null' to '()'
let map = engine.parse_json(json, true)?;
map.len() == 6; // 'map' contains all properties in the JSON string
// Put the object map into a 'Scope'
let mut scope = Scope::new();
scope.push("map", map);
let result = engine.eval_with_scope::<i64>(&mut scope, r#"map["^^^!!!"].len()"#)?;
result == 3; // the object map is successfully used in the script
The JSON text must represent a single object hash – i.e. must be wrapped within braces
{
…}
.
It cannot be a primitive type (e.g. number, string etc.). Otherwise it cannot be converted into an object map and a type error is returned.
JSON numbers are all floating-point while Rhai supports integers (INT
) and floating-point (FLOAT
)
(except under no_float
).
Most common generators of JSON data distinguish between integer and floating-point values by always
serializing a floating-point number with a decimal point (i.e. 123.0
instead of 123
which is
assumed to be an integer).
This style can be used successfully with Rhai object maps.
Sub-objects are handled transparently by Engine::parse_json
.
It is not necessary to replace {
with #{
in order to fake a Rhai object map literal.
// JSON with sub-object 'b'.
let json = r#"{"a":1, "b":{"x":true, "y":false}}"#;
// 'parse_json' handles this just fine.
let map = engine.parse_json(json, false)?;
// 'map' contains two properties: 'a' and 'b'
map.len() == 2;
Internally, Engine::parse_json
cheats by treating the JSON text as a Rhai script.
That is why it even supports comments and arithmetic expressions in the JSON text, although it is not a good idea to rely on non-standard JSON formats.
A token remap filter is used to convert {
into #{
and null
to ()
.
Use serde
See Serialization/ Deserialization of Dynamic
with serde
for more details.
Remember, Engine::parse_json
is nothing more than a cheap alternative to true JSON parsing.
If strict correctness is needed, or for more configuration possibilities, turn on the
serde
feature to pull in serde
which enables
serialization and deserialization to/from multiple formats, including JSON.
Beware, though… the serde
crate is quite heavy.
Special Support for OOP via Object Maps
See the pattern on Simulating Object-Oriented Programming for more details.
Object maps can be used to simulate object-oriented programming (OOP) by storing data as properties and methods as properties holding function pointers.
If an object map’s property holds a function pointer, the property can simply be called like a normal method in method-call syntax.
This is a short-hand to avoid the more verbose syntax of using the call
function keyword.
When a property holding a function pointer or a closure is called like a method, it is replaced as a method call on the object map itself.
let obj = #{
data: 40,
action: || this.data += x // 'action' holds a closure
};
obj.action(2); // calls the function pointer with 'this' bound to 'obj'
obj.call(obj.action, 2); // <- the above de-sugars to this
obj.data == 42;
// To achieve the above with normal function pointer call will fail.
fn do_action(map, x) { map.data += x; } // 'map' is a copy
obj.action = do_action; // <- de-sugars to 'Fn("do_action")'
obj.action.call(obj, 2); // a copy of 'obj' is passed by value
obj.data == 42; // 'obj.data' is not changed
Non-Existent Property Handling for Object Maps
This is an advanced feature that requires the internals
feature to be enabled.
Normally, when a property is accessed from an object map that does not exist, ()
is returned.
Via Engine:: set_fail_on_invalid_map_property
, it is possible to make this an error
instead.
Other than that, it is possible to completely control this behavior via a special callback function
registered into an Engine
via on_map_missing_property
.
Using this callback, for instance, it is simple to instruct Rhai to create a new property in the object map on the fly, possibly with a default value, when a non-existent property is accessed.
Function Signature
The function signature passed to Engine::on_map_missing_property
takes the following form.
Fn(map: &mut Map, prop: &str, context: EvalContext) -> Result<Target, Box<EvalAltResult>>
where:
Parameter | Type | Description |
---|---|---|
map | &mut Map | the object map being accessed |
prop | &str | name of the property being accessed |
context | EvalContext | the current evaluation context |
Return value
The return value is Result<Target, Box<EvalAltResult>>
.
Target
is an advanced type, available only under the internals
feature, that represents a
reference to a Dynamic
value.
It can be used to point to a particular value within the object map.
Example
engine.on_map_missing_property(|map, prop, context| {
match prop {
"x" => {
// The object-map can be modified in place
map.insert("y".into(), (42_i64).into());
// Return a mutable reference to an element
let value_ref = map.get_mut("y").unwrap();
Ok(value_ref.into())
}
"z" => {
// Return a temporary value (not a reference)
let value = Dynamic::from(100_i64);
Ok(value.into())
}
// Return the standard property-not-found error
_ => Err(EvalAltResult::ErrorPropertyNotFound(
prop.to_string(), Position::NONE
).into()),
}
});
Timestamps
Timestamps are provided by the BasicTimePackage
(excluded when using a raw Engine
)
via the timestamp
function.
Timestamps are not available under no_time
or no_std
.
The Rust type of a timestamp is std::time::Instant
(instant::Instant
in WASM builds).
type_of()
a timestamp returns "timestamp"
.
Built-in Functions
The following methods (defined in the BasicTimePackage
but excluded when
using a raw Engine
) operate on timestamps.
Function | Parameter(s) | Description |
---|---|---|
elapsed method and property | none | returns the number of seconds since the timestamp |
+ operator | number of seconds to add | returns a new timestamp with a specified number of seconds added |
+= operator | number of seconds to add | adds a specified number of seconds to the timestamp |
- operator | number of seconds to subtract | returns a new timestamp with a specified number of seconds subtracted |
-= operator | number of seconds to subtract | subtracts a specified number of seconds from the timestamp |
- operator |
| returns the number of seconds between the two timestamps |
The following methods are defined in the LanguageCorePackage
but excluded
when using a raw Engine
.
Function | Not available under | Parameter(s) | Description |
---|---|---|---|
sleep | no_std | number of seconds to sleep | blocks the current thread for a specified number of seconds |
Examples
let now = timestamp();
// Do some lengthy operation...
if now.elapsed > 30.0 {
print("takes too long (over 30 seconds)!")
}
Keywords
The following are reserved keywords in Rhai.
Active keywords | Reserved keywords | Usage | Inactive under feature |
---|---|---|---|
true , false | constants | ||
let , const | var , static | variables | |
is_shared | shared values | no_closure | |
is | type checking | ||
if , else | goto | control flow | |
switch | match , case | switching and matching | |
do , while , loop , until , for , in , continue , break | looping | ||
fn , private , is_def_fn , this | public , protected , new | functions | no_function |
return | return values | ||
throw , try , catch | throw/catch exceptions | ||
import , export , as | use , with , module , package , super | modules | no_module |
global | automatic global module | no_function , no_module | |
Fn , call , curry | function pointers | ||
spawn , thread , go , sync , async , await , yield | threading/async | ||
type_of , print , debug , eval , is_def_var | special functions | ||
default , void , null , nil | special values |
Statements
Statements are terminated by semicolons ;
and they are mandatory,
except for the last statement in a block (enclosed by {
… }
pairs) where it can be omitted.
Semicolons can also be omitted for statement types that always end in a block – for example
the if
, while
, for
, loop
and switch
statements.
let a = 42; // normal assignment statement
let a = foo(42); // normal function call statement
foo < 42; // normal expression as statement
let a = { 40 + 2 }; // 'a' is set to the value of the statements block, which is the value of the last statement
// ^ the last statement does not require a terminating semicolon (but also works with it)
// ^ semicolon required here to terminate the 'let' statement
// it is a syntax error without it, even though it ends with '}'
// that is because the 'let' statement doesn't end in a block
if foo { a = 42 }
// ^ no need to terminate an if-statement with a semicolon
// that is because the 'if' statement ends in a block
4 * 10 + 2 // a statement which is just one expression - no ending semicolon is OK
// because it is the last statement of the whole block
Statements Block
Syntax
Statements blocks in Rhai are formed by enclosing zero or more statements within braces {
…}
.
{
statement;
statement;
… statement}
{
statement;
statement;
… statement;
}
// trailing semi-colon is optional
Closed scope
A statements block forms a closed scope.
Any variable and/or constant defined within the block are removed outside the block, so are modules imported within the block.
let x = 42;
let y = 18;
{
import "hello" as h;
const HELLO = 99;
let y = 0;
h::greet(); // ok
print(y + HELLO); // prints 99 (y is zero)
:
:
} // <- 'HELLO' and 'y' go away here...
print(x + y); // prints 60 (y is still 18)
print(HELLO); // <- error: 'HELLO' not found
h::greet(); // <- error: module 'h' not found
Statement Expression
This is different from Rust where, if the last statement is terminated by a semicolon, the block’s
return value defaults to ()
.
Like Rust, a statement can be used anywhere where an expression is expected.
These are called, for lack of a more creative name, “statement expressions.”
The last statement of a statements block is always the block’s return value when used as a statement, regardless of whether it is terminated by a semicolon or not.
If the last statement has no return value (e.g. variable definitions, assignments) then it is
assumed to be ()
.
let x = {
let foo = calc_something();
let bar = foo + baz;
bar.further_processing(); // <- this is the return value
}; // <- semicolon is needed here...
// The above is equivalent to:
let result;
{
let foo = calc_something();
let bar = foo + baz;
result = bar.further_processing();
}
let x = result;
// Statement expressions can be inserted inside normal expressions
// to avoid duplicated calculations
let x = foo(bar) + { let v = calc(); process(v, v.len, v.abs) } + baz;
// The above is equivalent to:
let foo_result = foo(bar);
let calc_result;
{
let v = calc();
result = process(v, v.len, v.abs); // <- avoid calculating 'v'
}
let x = foo_result + calc_result + baz;
// Statement expressions are also useful as function call arguments
// when side effects are desired
do_work(x, y, { let z = foo(x, y); print(z); z });
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// statement expression
Statement expressions can be disabled via Engine::set_allow_statement_expression
.
Variables
Valid Names
The unicode-xid-ident
feature expands the allowed characters for variable names to the set defined by
Unicode Standard Annex #31.
Variables in Rhai follow normal C naming rules – must contain only ASCII letters, digits and underscores _
.
Character set | Description |
---|---|
A … Z | Upper-case ASCII letters |
a … z | Lower-case ASCII letters |
0 … 9 | Digit characters |
_ | Underscore character |
However, unlike Rust, a variable name must also contain at least one ASCII letter, and an ASCII
letter must come before any digits. In other words, the first character that is not an underscore _
must be an ASCII letter and not a digit.
To reduce confusion (and subtle bugs) because, for instance, _1
can easily be misread (or mistyped)
as -1
.
Rhai is dynamic without type checking, so there is no compiler to catch these typos.
Therefore, some names acceptable to Rust, like _
, _42foo
, _1
etc., are not valid in Rhai.
For example: c3po
and _r2d2_
are valid variable names, but 3abc
and ____49steps
are not.
Variable names are case sensitive.
Variable names also cannot be the same as a keyword (active or reserved).
Rhai uses SmartString
which avoids allocations unless a string is over its internal limit
(23 ASCII characters on 64-bit, but only 11 ASCII characters on 32-bit).
On 64-bit systems, most variable names are shorter than 23 letters, so this is unlikely to become an issue.
However, on 32-bit systems, take care to limit, where possible, variable names to within 11 letters. This is particularly true for local variables inside a hot loop, where they are created and destroyed in rapid succession.
// The following is SLOW on 32-bit
for my_super_loop_variable in array {
print(`Super! ${my_super_loop_variable}`);
}
// Suggested revision:
for loop_var in array {
print(`Super! ${loop_var}`);
}
Declare a Variable
Variables are declared using the let
keyword.
Variables do not have to be given an initial value.
If none is provided, it defaults to ()
.
A variable defined within a statements block is local to that block.
let x; // ok - value is '()'
let x = 3; // ok
let _x = 42; // ok
let x_ = 42; // also ok
let _x_ = 42; // still ok
let _ = 123; // <- syntax error: illegal variable name
let _9 = 9; // <- syntax error: illegal variable name
let x = 42; // variable is 'x', lower case
let X = 123; // variable is 'X', upper case
print(x); // prints 42
print(X); // prints 123
{
let x = 999; // local variable 'x' shadows the 'x' in parent block
print(x); // prints 999
}
print(x); // prints 42 - the parent block's 'x' is not changed
let x = 0; // new variable 'x' shadows the old 'x'
print(x); // prints 0
is_def_var("x") == true;
is_def_var("_x") == true;
is_def_var("y") == false;
Use Before Definition
By default, variables do not need to be defined before they are used.
If a variable accessed by a script is not defined previously within the same script, it is assumed
to be provided via an external custom Scope
passed to the Engine
via the
Engine::XXX_with_scope
API.
let engine = Engine::new();
engine.run("print(answer)")?; // <- error: variable 'answer' not found
// Create custom scope
let mut scope = Scope::new();
// Add variable to custom scope
scope.push("answer", 42_i64);
// Run with custom scope
engine.run_with_scope(&mut scope,
"print(answer)" // <- prints 42
)?;
If no Scope
is used to evaluate the script (e.g. when using Engine::run
instead of
Engine::run_with_scope
), an undefined variable causes a runtime error when accessed.
Strict Variables Mode
With Engine::set_strict_variables
, it is possible to turn on
Strict Variables mode.
When strict variables mode is active, accessing a variable not previously defined within the same script directly causes a parse error when compiling the script.
let x = 42;
print(x); // prints 42
print(foo); // <- parse error under strict variables mode:
// variable 'foo' is undefined
Turn on strict variables mode if no Scope
is to be provided for script evaluation runs.
This way, variable access errors are caught during compile time instead of runtime.
Variable Shadowing
In Rhai, new variables automatically shadow existing ones of the same name. There is no error.
This behavior is consistent with Rust.
let x = 42;
let y = 123;
print(x); // prints 42
let x = 88; // <- 'x' is shadowed here
// At this point, it is no longer possible to access the
// original 'x' on the first line...
print(x); // prints 88
let x = 0; // <- 'x' is shadowed again
// At this point, it is no longer possible to access both
// previously-defined 'x'...
print(x); // prints 0
{
let x = 999; // <- 'x' is shadowed in a block
print(x); // prints 999
}
print(x); // prints 0 - shadowing within the block goes away
print(y); // prints 123 - 'y' is not shadowed
Set Engine::set_allow_shadowing
to false
to turn variables shadowing off.
let x = 42;
let x = 123; // <- syntax error: variable 'x' already defined
// when variables shadowing is disallowed
Strict Variables Mode
By default, Rhai looks up access to variables from the enclosing block scope,
working its way outwards until it reaches the top (global) level, then it
searches the Scope
(if any) that is passed into the Engine::eval_with_scope
call.
Setting Engine::set_strict_variables
to true
turns on Strict Variables Mode,
which requires that:
- all variables/constants be defined within the same script before use,
or they must be variables/constants within the provided
Scope
(if any), - modules must be imported, also within the same script, before use.
Within Strict Variables mode, any attempt to access a variable or module before definition/import results in a parse error.
This way, variable access errors (usually typos) are caught during compile time instead of runtime.
let x = 42;
let y = x * z; // <- parse error under strict variables mode:
// variable 'z' is not yet defined
let z = x + w; // <- parse error under strict variables mode:
// variable 'w' is undefined
foo::bar::baz(); // <- parse error under strict variables mode:
// module 'foo' is not yet defined
fn test1() {
foo::bar::baz(); // <- parse error under strict variables mode:
// module 'foo' is defined
}
import "my_module" as foo;
foo::bar::baz(); // ok!
print(foo::xyz); // ok!
let x = abc::def; // <- parse error under strict variables mode:
// module 'abc' is undefined
fn test2() {
foo:bar::baz(); // ok!
}
Why can’t function calls be checked for validity as well?
Rust functions in Rhai can be overloaded. This means that multiple versions of the same Rust function can exist under the same name, each accepting different numbers and/or types of arguments.
While it is possible to check, at compile time, whether a variable has been previously declared, it is impossible to predict, at compile time, the types of arguments to function calls, unless the function in question takes no parameters.
Therefore, it is impossible to check, at compile time, whether a function call is valid given that the types of arguments are unknown until runtime. QED.
Not to mention that it is also impossible to check for a function called via a function pointer.
Variable Definition Filter
Although it is easy to disable variable shadowing via Engine::set_allow_shadowing
,
sometimes more fine-grained control is needed.
For example, it may be the case that not all variables shadowing must be disallowed, but that only a particular variable name needs to be protected and not others. Or only under very special circumstances.
Under this scenario, it is possible to provide a filter closure to the Engine
via
Engine::on_def_var
that traps variable definitions (i.e. let
or
const
statements) in a Rhai script.
The filter is called when a variable or constant is defined both during runtime and compilation.
let mut engine = Engine::new();
// Register a variable definition filter.
engine.on_def_var(|is_runtime, info, context| {
match (info.name, info.is_const) {
// Disallow defining 'MYSTIC_NUMBER' as a constant!
("MYSTIC_NUMBER", true) => Ok(false),
// Disallow defining constants not at global level!
(_, true) if info.nesting_level > 0 => Ok(false),
// Throw any exception you like...
("hello", _) => Err(EvalAltResult::ErrorVariableNotFound(info.name.to_string(), Position::NONE).into()),
// Return Ok(true) to continue with normal variable definition.
_ => Ok(true)
}
});
Function Signature
The function signature passed to Engine::on_def_var
takes the following form.
Fn(is_runtime: bool, info: VarDefInfo, context: EvalContext) -> Result<bool, Box<EvalAltResult>>
where:
Parameter | Type | Description |
---|---|---|
is_runtime | bool | true if the variable definition event happens during runtime, false if during compilation |
info | VarDefInfo | information on the variable being defined |
context | EvalContext | the current evaluation context |
and VarDefInfo
is a simple struct
that contains the following fields:
Field | Type | Description |
---|---|---|
name | &str | variable name |
is_const | bool | true if the definition is a const ; false if it is a let |
nesting_level | usize | the current nesting level; the global level is zero |
will_shadow | bool | will this variable shadow an existing variable of the same name? |
and EvalContext
is a type that encapsulates the current evaluation context.
Return value
The return value is Result<bool, Box<EvalAltResult>>
where:
Value | Description |
---|---|
Ok(true) | normal variable definition should continue |
Ok(false) | throws a runtime or compilation error |
Err(Box<EvalAltResult>) | error that is reflected back to the Engine |
During compilation (i.e. when is_runtime
is false
), EvalAltResult::ErrorParsing
is passed
through as the compilation error.
All other errors map to ParseErrorType::ForbiddenVariable
.
Variable Resolver
By default, Rhai looks up access to variables from the enclosing block scope, working its way
outwards until it reaches the top (global) level, then it searches the Scope
that is passed into
the Engine::eval
call.
There is a built-in facility for advanced users to hook into the variable resolution service and to override its default behavior.
To do so, provide a closure to the Engine
via Engine::on_var
.
let mut engine = Engine::new();
// Register a variable resolver.
engine.on_var(|name, index, context| {
match name {
"MYSTIC_NUMBER" => Ok(Some(42_i64.into())),
// Override a variable - make it not found even if it exists!
"DO_NOT_USE" => Err(EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::NONE).into()),
// Silently maps 'chameleon' into 'innocent'.
"chameleon" => context.scope().get_value("innocent").map(Some).ok_or_else(||
EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::NONE).into()
),
// Return Ok(None) to continue with the normal variable resolution process.
_ => Ok(None)
}
});
-
Avoid having to maintain a custom
Scope
with all variables regardless of need (because a script may not use them all). -
Short-circuit variable access, essentially overriding standard behavior.
-
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.
-
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>>
where:
Parameter | Type | Description |
---|---|---|
name | &str | variable name |
index | usize | an 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 | EvalContext | mutable reference to the current evaluation context |
and EvalContext
is a type that encapsulates the current evaluation context.
Return value
The return value is Result<Option<Dynamic>, Box<EvalAltResult>>
where:
Value | Description |
---|---|
Ok(None) | normal variable resolution process should continue, i.e. continue searching through the Scope |
Ok(Some(value)) | value (a Dynamic ) of the variable, treated as a constant |
Err(Box<EvalAltResult>) | error that is reflected back to the Engine , normally EvalAltResult::ErrorVariableNotFound to indicate that the variable does not exist, but it can be any EvalAltResult . |
Constants
Constants can be defined using the const
keyword and are immutable.
const X; // 'X' is a constant '()'
const X = 40 + 2; // 'X' is a constant 42
print(X * 2); // prints 84
X = 123; // <- syntax error: constant modified
Constants follow the same naming rules as variables, but as a convention are often named with all-capital letters.
Manually Add Constant into Custom Scope
A constant value holding a custom type essentially acts as a singleton.
It is possible to add a constant into a custom Scope
via Scope::push_constant
so it’ll be
available to scripts running with that Scope
.
use rhai::{Engine, Scope};
#[derive(Debug, Clone)]
struct TestStruct(i64); // custom type
let mut engine = Engine::new();
engine
.register_type_with_name::<TestStruct>("TestStruct") // register custom type
.register_get("value", |obj: &mut TestStruct| obj.0), // property getter
.register_fn("update_value",
|obj: &mut TestStruct, value: i64| obj.0 = value // mutating method
);
let script =
"
MY_NUMBER.update_value(42);
print(MY_NUMBER.value);
";
let ast = engine.compile(script)?;
let mut scope = Scope::new(); // create custom scope
scope.push_constant("MY_NUMBER", TestStruct(123_i64)); // add constant variable
// Beware: constant objects can still be modified via a method call!
engine.run_ast_with_scope(&mut scope, &ast)?; // prints 42
// Running the script directly, as below, is less desirable because
// the constant 'MY_NUMBER' will be propagated and copied into each usage
// during the script optimization step
engine.run_with_scope(&mut scope, script)?;
Caveat – Constants Can be Modified via Rust
In plugin functions, &mut
parameters disallow constant values by default.
This is different from the Engine::register_XXX
API.
However, if a plugin function is marked with #[export_fn(pure)]
or #[rhai_fn(pure)]
,
it is assumed pure (i.e. will not modify its arguments) and so constants are allowed.
A custom type stored as a constant cannot be modified via script, but can be modified via a
registered Rust function that takes a first &mut
parameter – because there is no way for
Rhai to know whether the Rust function modifies its argument!
By default, native Rust functions with a first &mut
parameter always allow constants to be passed
to them. This is because using &mut
can avoid unnecessary cloning of a custom type value, even
though it is actually not modified – for example returning the size of a collection type.
In line with intuition, Rhai is smart enough to always pass a cloned copy of a constant as the
first &mut
argument if the function is called in normal function call style.
If it is called as a method, however, the Rust function will be able to modify the constant’s value.
Also, property setters and indexers are always assumed to mutate the first
&mut
parameter and so they always raise errors when passed constants by default.
// For the below, assume 'increment' is a Rust function with '&mut' first parameter
const X = 42; // a constant
increment(X); // call 'increment' in normal FUNCTION-CALL style
// since 'X' is constant, a COPY is passed instead
X == 42; // value is 'X" is unchanged
X.increment(); // call 'increment' in METHOD-CALL style
X == 43; // value of 'X' is changed!
// must use 'Dynamic::is_read_only' to check if parameter is constant
fn double() {
this *= 2; // function doubles 'this'
}
let y = 1; // 'y' is not constant and mutable
y.double(); // double it...
y == 2; // value of 'y' is changed as expected
X.double(); // since 'X' is constant, a COPY is passed to 'this'
X == 43; // value of 'X' is unchanged by script
Rhai assumes that constants are never changed, even via Rust functions.
This is important to keep in mind because the script optimizer by default does constant propagation as a operation.
If a constant is eventually modified by a Rust function, the optimizer will not see the updated value and will propagate the original initialization value instead.
Dynamic::is_read_only
can be used to detect whether a Dynamic
value is constant or not within
a Rust function.
Automatic Global Module
When a constant is declared at global scope, it is added to a special module called global
.
Functions can access those constants via the special global
module.
Naturally, the automatic global
module is not available under no_function
nor no_module
.
const CONSTANT = 42; // this constant is automatically added to 'global'
{
const INNER = 0; // this constant is not at global level
} // <- it goes away here
fn foo(x) {
x *= global::CONSTANT; // ok! 'CONSTANT' exists in 'global'
x * global::INNER // <- error: constant 'INNER' not found in 'global'
}
Override global
It is possible to override the automatic global module by importing another module
under the name global
.
import "foo" as global; // import a module as 'global'
const CONSTANT = 42; // this constant is NOT added to 'global'
fn foo(x) {
global::CONSTANT // <- error: constant 'CONSTANT' not found in 'global'
}
Assignments
Value assignments to variables use the =
symbol.
let foo = 42;
bar = 123 * 456 - 789;
x[1][2].prop = do_calculation();
Valid Assignment Targets
The left-hand-side (LHS) of an assignment statement must be a valid l-value, which must be rooted in a variable, potentially extended via indexing or properties.
Expressions that are not valid l-values cannot be assigned to.
x = 42; // variable is an l-value
x[1][2][3] = 42 // variable indexing is an l-value
x.prop1.prop2 = 42; // variable property is an l-value
foo(x) = 42; // syntax error: function call is not an l-value
x.foo() = 42; // syntax error: method call is not an l-value
(x + y) = 42; // syntax error: binary op is not an l-value
Values are Cloned
Values assigned are always cloned. So care must be taken when assigning large data types (such as arrays).
x = y; // value of 'y' is cloned
x == y; // both 'x' and 'y' hold different copies
// of the same value
Moving Data
When assigning large data types, sometimes it is desirable to move the data instead of cloning it.
Use the take
function (defined in the LangCorePackage
but excluded
when using a raw Engine
) to move data.
The original variable is left with ()
x = take(y); // value of 'y' is moved to 'x'
y == (); // 'y' now holds '()'
x != y; // 'x' holds the original value of 'y'
Return large data types from functions
take
is convenient when returning large data types from a function.
fn get_large_value_naive() {
let large_result = do_complex_calculation();
large_result.done = true;
// Return a cloned copy of the result, then the
// local variable 'large_result' is thrown away!
large_result
}
fn get_large_value_smart() {
let large_result = do_complex_calculation();
large_result.done = true;
// Return the result without cloning!
// Method style call is also OK.
large_result.take()
}
Assigning large data types to object map properties
take
is useful when assigning large data types to object map properties.
let x = [];
// Build a large array
for n in 0..1000000 { x += n; }
// The following clones the large array from 'x'.
// Both 'my_object.my_property' and 'x' now hold exact copies
// of the same large array!
my_object.my_property = x;
// Move it to object map property via 'take' without cloning.
// 'x' now holds '()'.
my_object.my_property = x.take();
// Without 'take', the following must be done to avoid cloning:
my_object.my_property = [];
for n in 0..1000000 { my_object.my_property += n; }
Compound Assignments
Compound assignments are assignments with a binary operator attached.
number += 8; // number = number + 8
number -= 7; // number = number - 7
number *= 6; // number = number * 6
number /= 5; // number = number / 5
number %= 4; // number = number % 4
number **= 3; // number = number ** 3
number <<= 2; // number = number << 2
number >>= 1; // number = number >> 1
number &= 0x00ff; // number = number & 0x00ff;
number |= 0x00ff; // number = number | 0x00ff;
number ^= 0x00ff; // number = number ^ 0x00ff;
The Flexible +=
The the +
and +=
operators are often overloaded to perform build-up
operations for different data types.
Build strings
let my_str = "abc";
my_str += "ABC";
my_str += 12345;
my_str == "abcABC12345"
Concatenate arrays
let my_array = [1, 2, 3];
my_array += [4, 5];
my_array == [1, 2, 3, 4, 5];
Concatenate BLOB’s
let my_blob = blob(3, 0x42);
my_blob += blob(5, 0x89);
my_blob.to_string() == "[4242428989898989]";
Mix two object maps together
let my_obj = #{ a:1, b:2 };
my_obj += #{ c:3, d:4, e:5 };
my_obj == #{ a:1, b:2, c:3, d:4, e:5 };
Add seconds to timestamps
let now = timestamp();
now += 42.0;
(now - timestamp()).round() == 42.0;
Logic Operators
Comparison Operators
Operator | Description ( x operator y ) | x , y same type or are numeric | x , y different types |
---|---|---|---|
== | x is equals to y | error if not defined | false if not defined |
!= | x is not equals to y | error if not defined | true if not defined |
> | x is greater than y | error if not defined | false if not defined |
>= | x is greater than or equals to y | error if not defined | false if not defined |
< | x is less than y | error if not defined | false if not defined |
<= | x is less than or equals to y | error if not defined | false if not defined |
Comparison operators between most values of the same type are built in for all standard types.
Others are defined in the LogicPackage
but excluded when using a raw Engine
.
Floating-point numbers interoperate with integers
Comparing a floating-point number (FLOAT
) with an integer is also supported.
42 == 42.0; // true
42.0 == 42; // true
42.0 > 42; // false
42 >= 42.0; // true
42.0 < 42; // false
Decimal numbers interoperate with integers
Comparing a Decimal
number with an integer is also supported.
let d = parse_decimal("42");
42 == d; // true
d == 42; // true
d > 42; // false
42 >= d; // true
d < 42; // false
Strings interoperate with characters
Comparing a string with a character is also supported, with the character first turned into a string before performing the comparison.
'x' == "x"; // true
"" < 'a'; // true
'x' > "hello"; // false
Comparing different types defaults to false
Comparing two values of different data types defaults to false
unless the appropriate operator
functions have been registered.
The exception is !=
(not equals) which defaults to true
. This is in line with intuition.
42 > "42"; // false: i64 cannot be compared with string
42 <= "42"; // false: i64 cannot be compared with string
let ts = new_ts(); // custom type
ts == 42; // false: different types cannot be compared
ts != 42; // true: different types cannot be compared
ts == ts; // error: '==' not defined for the custom type
Safety valve: Comparing different numeric types has no default
Beware that the above default does NOT apply to numeric values of different types
(e.g. comparison between i64
and u16
, i32
and f64
) – when multiple numeric types are
used it is too easy to mess up and for subtle errors to creep in.
// Assume variable 'x' = 42_u16, 'y' = 42_u16 (both types of u16)
x == y; // true: '==' operator for u16 is built-in
x == "hello"; // false: different non-numeric operand types default to false
x == 42; // error: ==(u16, i64) not defined, no default for numeric types
42 == y; // error: ==(i64, u16) not defined, no default for numeric types
Caution: Beware operators for custom types
It is strongly recommended that, when defining operators for custom types, always define the
full set of six operators together, or at least the ==
and !=
pair.
Operators are completely separate from each other. For example:
-
!=
does not equal!(==)
-
>
does not equal!(<=)
-
<=
does not equal<
plus==
-
<=
does not imply<
Therefore, if a custom type misses an operator definition, it simply raises an error or returns the default.
This behavior can be counter-intuitive.
let ts = new_ts(); // custom type with '<=' and '==' defined
ts <= ts; // true: '<=' defined
ts < ts; // error: '<' not defined, even though '<=' is
ts == ts; // true: '==' defined
ts != ts; // error: '!=' not defined, even though '==' is
Boolean Operators
All boolean operators are built in for the bool
data type.
Operator | Description | Arity | Short-circuits? |
---|---|---|---|
! (prefix) | NOT | unary | no |
&& | AND | binary | yes |
& | AND | binary | no |
|| | OR | binary | yes |
| | OR | binary | no |
Double boolean operators &&
and ||
short-circuit – meaning that the second operand will not be evaluated
if the first one already proves the condition wrong.
Single boolean operators &
and |
always evaluate both operands.
a() || b(); // b() is not evaluated if a() is true
a() && b(); // b() is not evaluated if a() is false
a() | b(); // both a() and b() are evaluated
a() & b(); // both a() and b() are evaluated
Null-Coalescing Operator
Operator | Description | Arity | Short-circuits? |
---|---|---|---|
?? | Null-coalesce | binary | yes |
The null-coalescing operator (??
) returns the first operand if it is not ()
, or the second
operand if the first operand is ()
.
It short-circuits – meaning that the second operand will not be evaluated if the first
operand is not ()
.
a ?? b // returns 'a' if it is not (), otherwise 'b'
a() ?? b(); // b() is only evaluated if a() is ()
Use the null-coalescing operator to implement default values for non-existent object map properties.
let map = #{ foo: 42 };
// Regular property access
let x = map.foo; // x == 42
// Non-existent property
let x = map.bar; // x == ()
// Default value for property
let x = map.bar ?? 42; // x == 42
Short-circuit loops and early returns
The following statements are allowed to follow the null-coalescing operator:
This means that you can use the null-coalescing operator to short-circuit loops and/or
early-return from functions when the value tested is ()
.
let total = 0;
for value in list {
// Whenever 'calculate' returns '()', the loop stops
total += calculate(value) ?? break;
}
In Operator
The in
operator is simply syntactic sugar for a call to the contains
function.
Similarly, !in
is a call to !contains
.
The in
operator is used to check for containment – i.e. whether a particular collection
data type contains a particular item.
Similarly, !in
is used to check for non-existence – i.e. it is true
if a particular
collection data type does not contain a particular item.
42 in array;
array.contains(42); // <- the above is equivalent to this
123 !in array;
!array.contains(123); // <- the above is equivalent to this
Built-in Support for Standard Data Types
Data type | Check for |
---|---|
Numeric range | integer number |
Array | contained item |
Object map | property name |
String | sub-string or character |
Examples
let array = [1, "abc", 42, ()];
42 in array == true; // check array for item
let map = #{
foo: 42,
bar: true,
baz: "hello"
};
"foo" in map == true; // check object map for property name
'w' in "hello, world!" == true; // check string for character
'w' !in "hello, world!" == false;
"wor" in "hello, world" == true; // check string for sub-string
42 in -100..100 == true; // check range for number
Array Items Comparison
The default implementation of the in
operator for arrays uses the ==
operator (if defined)
to compare items.
For a custom type, ==
defaults to false
when comparing it with a value of of the same type.
See the section on Logic Operators for more details.
let ts = new_ts(); // assume 'new_ts' returns a custom type
let array = [1, 2, 3, ts, 42, 999];
// ^^ custom type
42 in array == true; // 42 cannot be compared with 'ts'
// so it defaults to 'false'
// because == operator is not defined
Custom Implementation of contains
The in
and !in
operators map directly to a call to a function contains
with the two operands switched.
// This expression...
item in container
// maps to this...
contains(container, item)
// or...
container.contains(item)
Support for the in
and !in
operators can be easily extended to other types by registering a
custom binary function named contains
with the correct parameter types.
Since !in
maps to !(... in ...)
, contains
is enough to support both operators.
let mut engine = Engine::new();
engine.register_type::<TestStruct>()
.register_fn("new_ts", || TestStruct::new())
.register_fn("contains", |ts: &mut TestStruct, item: i64| -> bool {
// Remember the parameters are switched from the 'in' expression
ts.contains(item)
});
// Now the 'in' operator can be used for 'TestStruct' and integer
engine.run(
r#"
let ts = new_ts();
if 42 in ts { // this calls 'ts.contains(42)'
print("I got 42!");
} else if 123 !in ts { // this calls '!ts.contains(123)'
print("I ain't got 123!");
}
let err = "hello" in ts; // <- runtime error: 'contains' not found
// for 'TestStruct' and string
"#)?;
If Statement
if
statements follow C syntax.
if foo(x) {
print("It's true!");
} else if bar == baz {
print("It's true again!");
} else if baz.is_foo() {
print("Yet again true.");
} else if foo(bar - baz) {
print("True again... this is getting boring.");
} else {
print("It's finally false!");
}
Unlike C, the condition expression does not need to be enclosed in parentheses (
…)
, but all
branches of the if
statement must be enclosed within braces {
…}
, even when there is only
one statement inside the branch.
Like Rust, there is no ambiguity regarding which if
clause a branch belongs to.
// Rhai is not C!
if (decision) print(42);
// ^ syntax error, expecting '{'
If Expression
Like Rust, if
statements can also be used as expressions, replacing the ? :
conditional
operators in other C-like languages.
if
expressions can be disabled via Engine::set_allow_if_expression
.
// The following is equivalent to C: int x = 1 + (decision ? 42 : 123) / 2;
let x = 1 + if decision { 42 } else { 123 } / 2;
x == 22;
let x = if decision { 42 }; // no else branch defaults to '()'
x == ();
Beware that, like Rust, if
is parsed primarily as a statement where it makes sense.
This is to avoid surprises.
fn index_of(x) {
// 'if' is parsed primarily as a statement
if this.contains(x) {
return this.find_index(x)
}
-1
}
The above will not be parsed as a single expression:
fn index_of(x) {
if this.contains(x) { return this.find_index(x) } - 1
// error due to '() - 1' ^
}
To force parsing as an expression, parentheses are required:
fn calc_index(b, offset) {
(if b { 1 } else { 0 }) + offset
// ^---------------------^ parentheses
}
Switch Statement
The switch
statement allows matching on literal values, and it mostly follows Rust’s match
syntax.
switch calc_secret_value(x) {
1 => print("It's one!"),
2 => {
// A statements block instead of a one-line statement
print("It's two!");
print("Again!");
}
3 => print("Go!"),
// A list of alternatives
4 | 5 | 6 => print("Some small number!"),
// _ is the default when no case matches. It must be the last case.
_ => print(`Oops! Something's wrong: ${x}`)
}
Default Case
A default case (i.e. when no other cases match) can be specified with _
.
switch wrong_default {
1 => 2,
_ => 9, // <- syntax error: default case not the last
2 => 3,
3 => 4, // <- ending with extra comma is OK
}
switch wrong_default {
1 => 2,
2 => 3,
3 => 4,
_ => 8, // <- syntax error: default case not the last
_ => 9
}
Array and Object Map Literals Also Work
The switch
expression can match against any literal, including array and object map literals.
// Match on arrays
switch [foo, bar, baz] {
["hello", 42, true] => ...,
["hello", 123, false] => ...,
["world", 1, true] => ...,
_ => ...
}
// Match on object maps
switch map {
#{ a: 1, b: 2, c: true } => ...,
#{ a: 42, d: "hello" } => ...,
_ => ...
}
Switching on arrays is very useful when working with Rust enums (see this section for more details).
Case Conditions
Similar to Rust, each case (except the default case at the end) can provide an optional condition
that must evaluate to true
in order for the case to match.
All cases are checked in order, so an earlier case that matches will override all later cases.
let result = switch calc_secret_value(x) {
1 if some_external_condition(x, y, z) => 100,
1 | 2 | 3 if x < foo => 200, // <- all alternatives share the same condition
2 if bar() => 999,
2 => "two", // <- fallback value for 2
2 => "dead code", // <- this case is a duplicate and will never match
// because the previous case matches first
5 if CONDITION => 123, // <- value for 5 matching condition
5 => "five", // <- fallback value for 5
_ if CONDITION => 8888 // <- syntax error: default case cannot have condition
};
Case conditions, together with type_of()
, makes it extremely easy to work with
values which may be of several different types (like properties in a JSON object).
switch value.type_of() {
// if 'value' is a string...
"string" if value.len() < 5 => ...,
"string" => ...,
// if 'value' is an array...
"array" => ...,
// if 'value' is an object map...
"map" if value.prop == 42 => ...,
"map" => ...,
// if 'value' is a number...
"i64" if value > 0 => ...,
"i64" => ...,
// anything else: probably an error...
_ => ...
}
Range Cases
Because of their popularity, literal integer ranges can also be used as switch
cases.
Numeric ranges are only searched when the switch
value is itself a number (including
floating-point and Decimal
). They never match any other data types.
let x = 42;
switch x {
'x' => ..., // no match: wrong data type
1 => ..., // <- specific numeric cases are checked first
2 => ..., // <- but these do not match
0..50 if x > 45 => ..., // no match: condition is 'false'
-10..20 => ..., // no match: not in range
0..50 => ..., // <- MATCH!!!
30..100 => ..., // no match: even though it is within range,
// the previous case matches first
42 => ..., // <- syntax error: numeric cases cannot follow range cases
}
Difference From if
-else if
Chain
Although a switch
expression looks almost the same as an if
-else if
chain, there are
subtle differences between the two.
Look-up Table vs x == y
A switch
expression matches through hashing via a look-up table. Therefore, matching is very
fast. Walking down an if
-else if
chain is much slower.
On the other hand, operators can be overloaded in Rhai, meaning that it is
possible to override the ==
operator for integers such that x == y
returns a different result
from the built-in default.
switch
expressions do not use the ==
operator for comparison; instead, they hash the data
values and jump directly to the correct statements via a pre-compiled look-up table. This makes
matching extremely efficient, but it also means that overloading the ==
operator will have no effect.
Therefore, in environments where it is desirable to overload the ==
operator for standard types – though it is difficult to think of valid scenarios where you’d
want 1 == 1
to return something other than true
– avoid using the switch
expression.
Efficiency
Because the switch
expression works through a look-up table, it is very efficient even for large
number of cases; in fact, switching is an O(1) operation regardless of the size of the data and
number of cases to match.
A long if
-else if
chain becomes increasingly slower with each additional case because
essentially an O(n) linear scan is performed.
Switch Expression
Like if
, switch
also works as an expression.
switch
expressions can be disabled via Engine::set_allow_switch_expression
.
let x = switch foo { 1 => true, _ => false };
func(switch foo {
"hello" => 42,
"world" => 123,
_ => 0
});
// The above is somewhat equivalent to:
let x = if foo == 1 { true } else { false };
if foo == "hello" {
func(42);
} else if foo == "world" {
func(123);
} else {
func(0);
}
While Loop
while
loops follow C syntax.
Like C, continue
can be used to skip to the next iteration, by-passing all following statements;
break
can be used to break out of the loop unconditionally.
while
loops can be disabled via Engine::set_allow_looping
.
let x = 10;
while x > 0 {
x -= 1;
if x < 6 { continue; } // skip to the next iteration
print(x);
if x == 5 { break; } // break out of while loop
}
While Expression
Like Rust, while
statements can also be used as expressions.
The break
statement takes an optional expression that provides the return value.
The default return value of a while
expression is ()
.
Loop expressions can be disabled via Engine::set_allow_loop_expressions
.
let x = 0;
// 'while' can be used just like an expression
let result = while x < 100 {
if is_magic_number(x) {
// if the 'while' loop breaks here, return a specific value
break get_magic_result(x);
}
x += 1;
// ... if the 'while' loop exits here, the return value is ()
};
if result == () {
print("Magic number not found!");
} else {
print(`Magic result = ${result}!`);
}
Do Loop
do
loops have two opposite variants: 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.
do
loops can be disabled via Engine::set_allow_looping
.
let x = 10;
do {
x -= 1;
if x < 6 { continue; } // skip to the next iteration
print(x);
if x == 5 { break; } // break out of do loop
} while x > 0;
do {
x -= 1;
if x < 6 { continue; } // skip to the next iteration
print(x);
if x == 5 { break; } // break out of do loop
} until x == 0;
Do Expression
Like Rust, do
statements can also be used as expressions.
The break
statement takes an optional expression that provides the return value.
The default return value of a do
expression is ()
.
Loop expressions can be disabled via Engine::set_allow_loop_expressions
.
let x = 0;
// 'do' can be used just like an expression
let result = do {
if is_magic_number(x) {
// if the 'do' loop breaks here, return a specific value
break get_magic_result(x);
}
x += 1;
// ... if the 'do' loop exits here, the return value is ()
} until x >= 100;
if result == () {
print("Magic number not found!");
} else {
print(`Magic result = ${result}!`);
}
Infinite Loop
Infinite loops follow Rust syntax.
Like Rust, continue
can be used to skip to the next iteration, by-passing all following statements;
break
can be used to break out of the loop unconditionally.
loop
can be disabled via Engine::set_allow_looping
.
let x = 10;
loop {
x -= 1;
if x > 5 { continue; } // skip to the next iteration
print(x);
if x == 0 { break; } // break out of loop
}
A loop
statement without a break
statement inside its loop block is infinite.
There is no way for the loop to stop iterating.
Loop Expression
Like Rust, loop
statements can also be used as expressions.
The break
statement takes an optional expression that provides the return value.
The default return value of a loop
expression is ()
.
Loop expressions can be disabled via Engine::set_allow_loop_expressions
.
let x = 0;
// 'loop' can be used just like an expression
let result = loop {
if is_magic_number(x) {
// if the loop breaks here, return a specific value
break get_magic_result(x);
}
x += 1;
// ... if the loop exits here, the return value is ()
};
if result == () {
print("Magic number not found!");
} else {
print(`Magic result = ${result}!`);
}
For Loop
Iterating through a numeric range or an array, or any type with a registered type iterator,
is provided by the for
… in
loop.
There are two alternative syntaxes, one including a counter variable:
for
variablein
expression{
…}
for (
variable,
counter)
in
expression{
…}
for
loops can be disabled via Engine::set_allow_looping
.
Break or Continue
Like C, continue
can be used to skip to the next iteration, by-passing all following statements.
break
can be used to break out of the loop unconditionally.
For Expression
Unlike Rust, for
statements can also be used as expressions.
The break
statement takes an optional expression that provides the return value.
The default return value of a for
expression is ()
.
Loop expressions can be disabled via Engine::set_allow_loop_expressions
.
let a = [42, 123, 999, 0, true, "hello", "world!", 987.6543];
// 'for' can be used just like an expression
let index = for (item, count) in a {
// if the 'for' loop breaks here, return a specific value
switch item.type_of() {
"i64" if item.is_even => break count,
"f64" if item.to_int().is_even => break count,
}
// ... if the 'for' loop exits here, the return value is ()
};
if index == () {
print("Magic number not found!");
} else {
print(`Magic number found at index ${index}!`);
}
Counter Variable
The counter variable, if specified, starts from zero, incrementing upwards.
let a = [42, 123, 999, 0, true, "hello", "world!", 987.6543];
// Loop through the array
for (item, count) in a {
if x.type_of() == "string" {
continue; // skip to the next iteration
}
// 'item' contains a copy of each element during each iteration
// 'count' increments (starting from zero) for each iteration
print(`Item #${count + 1} = ${item}`);
if x == 42 { break; } // break out of for loop
}
Standard Iterable Types
Certain standard types are iterable via a for
statement.
Iterate Through Arrays
Iterating through an array yields cloned copies of each element.
let a = [1, 3, 5, 7, 9, 42];
// Loop through the array
for x in a {
if x > 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
Iterate Through Strings
Iterating through a string yields individual characters.
The chars
method also allow iterating through characters in a string, optionally accepting the
character position to start from (counting from the end if negative), as well as the number of
characters to iterate (defaults to all).
char
also accepts a range which can be created via the ..
(exclusive) and ..=
(inclusive) operators.
let s = "hello, world!";
// Iterate through all the characters.
for ch in s {
print(ch);
}
// Iterate starting from the 3rd character and stopping at the 7th.
for ch in s.chars(2, 5) {
if ch > 'z' { continue; } // skip to the next iteration
print(ch);
if x == '@' { break; } // break out of for loop
}
// Iterate starting from the 3rd character and stopping at the end.
for ch in s.chars(2..s.len) {
if ch > 'z' { continue; } // skip to the next iteration
print(ch);
if x == '@' { break; } // break out of for loop
}
Iterate Through Numeric Ranges
Ranges are created via the ..
(exclusive) and ..=
(inclusive) operators.
The range
function similarly creates exclusive ranges, plus allowing optional step values.
// Iterate starting from 0 and stopping at 49
// The step is assumed to be 1 when omitted for integers
for x in 0..50 {
if x > 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
// The 'range' function is just the same
for x in range(0, 50) {
if x > 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
// The 'range' function also takes a step
for x in range(0, 50, 3) { // step by 3
if x > 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
// The 'range' function can also step backwards
for x in range(50..0, -3) { // step down by -3
if x < 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
// It works also for floating-point numbers
for x in range(5.0, 0.0, -2.0) { // step down by -2.0
if x < 10 { continue; } // skip to the next iteration
print(x);
if x == 4.2 { break; } // break out of for loop
}
Iterate Through Bit-Fields
The bits
function allows iterating through an integer as a bit-field.
bits
optionally accepts the bit number to start from (counting from the most-significant-bit if
negative), as well as the number of bits to iterate (defaults all).
bits
also accepts a range which can be created via the ..
(exclusive) and ..=
(inclusive) operators.
let x = 0b_1001110010_1101100010_1100010100;
let num_on = 0;
// Iterate through all the bits
for bit in x.bits() {
if bit { num_on += 1; }
}
print(`There are ${num_on} bits turned on!`);
const START = 3;
// Iterate through all the bits from 3 through 12
for (bit, index) in x.bits(START, 10) {
print(`Bit #${index} is ${if bit { "ON" } else { "OFF" }}!`);
if index >= 7 { break; } // break out of for loop
}
// Iterate through all the bits from 3 through 12
for (bit, index) in x.bits(3..=12) {
print(`Bit #${index} is ${if bit { "ON" } else { "OFF" }}!`);
if index >= 7 { break; } // break out of for loop
}
Iterate Through Object Maps
Two methods, keys
and values
, return arrays containing cloned copies
of all property names and values of an object map, respectively.
These arrays can be iterated.
let map = #{a:1, b:3, c:5, d:7, e:9};
// Property names are returned in unsorted, random order
for x in map.keys() {
if x > 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
// Property values are returned in unsorted, random order
for val in map.values() {
print(val);
}
Make a Custom Type Iterable
Type iterators are already defined for built-in standard types such as strings, ranges, bit-fields, arrays and object maps.
That’s why they can be used with the for
loop.
If a custom type is iterable, the for
loop can be used to iterate through
its items in sequence, as long as it has a type iterator registered.
Engine::register_iterator<T>
allows registration of a type iterator for any type
that implements IntoIterator
.
With a type iterator registered, the custom type can be iterated through.
// Custom type
#[derive(Debug, Clone)]
struct TestStruct { fields: Vec<i64> }
// Implement 'IntoIterator' trait
impl IntoIterator<Item = i64> for TestStruct {
type Item = i64;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.fields.into_iter()
}
}
let mut engine = Engine::new();
// Register API and type iterator for 'TestStruct'
engine.register_type_with_name::<TestStruct>("TestStruct")
.register_fn("new_ts", || TestStruct { fields: vec![1, 2, 3, 42] })
.register_iterator::<TestStruct>();
// 'TestStruct' is now iterable
engine.run(
"
for value in new_ts() {
...
}
")?;
Engine::register_iterator_result
allows registration of a fallible type iterator –
i.e. an iterator that returns Result<T, Box<EvalAltResult>>
.
On in very rare situations will this be necessary though.
Return Value
return
The return
statement is used to immediately stop evaluation and exist the current context
(typically a function call) yielding a return value.
return; // equivalent to return ();
return 123 + 456; // returns 579
A return
statement at global level stops the entire script evaluation,
the return value is taken as the result of the script evaluation.
A return
statement inside a function call exits with a return value to the caller.
exit
Similar to the return
statement, the exit
function is used to immediately stop evaluation,
but it does so regardless of where it is called from, even deep inside nested function calls.
fn foo() {
exit(42); // exit with result 42
}
fn bar() {
foo();
}
fn baz() {
bar();
}
let x = baz(); // exits with result 42
print(x); // <- this is never run
The exit
function is defined in the LanguageCorePackage
but excluded when using a raw Engine
.
Function | Parameter(s) | Description |
---|---|---|
exit | result value (optional) | immediately terminate script evaluation (default result value is () ) |
Throw Exception on Error
All Engine
evaluation API methods return Result<T, Box<rhai::EvalAltResult>>
with EvalAltResult
holding error information.
To deliberately return an error, use the throw
keyword.
if some_bad_condition_has_happened {
throw error; // 'throw' any value as the exception
}
throw; // defaults to '()'
Exceptions thrown via throw
in the script can be captured in Rust by matching
EvalAltResult::ErrorRuntime(value, position)
with the exception value captured by value
.
let result = engine.eval::<i64>(
"
let x = 42;
if x > 0 {
throw x;
}
").expect_err();
println!("{result}"); // prints "Runtime error: 42 (line 5, position 15)"
Catch a Thrown Exception
It is possible to catch an exception instead of having it abort the evaluation
of the entire script via the try
… catch
statement common to many C-like languages.
fn code_that_throws() {
throw 42;
}
try
{
code_that_throws();
}
catch (err) // 'err' captures the thrown exception value
{
print(err); // prints 42
}
Catch Exceptions
When an exception is thrown via a throw
statement, evaluation of the script halts and the
Engine
returns with EvalAltResult::ErrorRuntime
containing the exception value thrown.
It is possible, via the try
… catch
statement, to catch exceptions, optionally with an
error variable.
try
{
…}
catch
{
…}
try
{
…}
catch
(
error variable)
{
…}
// Catch an exception and capturing its value
try
{
throw 42;
}
catch (err) // 'err' captures the thrown exception value
{
print(err); // prints 42
}
// Catch an exception without capturing its value
try
{
print(42/0); // deliberate divide-by-zero exception
}
catch // no error variable - exception value is discarded
{
print("Ouch!");
}
// Exception in the 'catch' block
try
{
print(42/0); // throw divide-by-zero exception
}
catch
{
print("You seem to be dividing by zero here...");
throw "die"; // a 'throw' statement inside a 'catch' block
// throws a new exception
}
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.
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
}
Many script-oriented exceptions can be caught via try
… catch
.
Error type | Error value |
---|---|
Runtime error thrown by a throw statement | value in throw statement |
Arithmetic error | object map |
Variable not found | object map |
Function not found | object map |
Module not found | object map |
Unbound this | object map |
Data type mismatch | object map |
Assignment to a calculated/constant value | object map |
Array/string/bit-field indexing out-of-bounds | object map |
Indexing with an inappropriate data type | object map |
Error in property access | object map |
for statement on a type without a type iterator | object map |
Data race detected | object map |
Other runtime error | object 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.
Some exceptions cannot be caught.
Error type | Notes |
---|---|
System error – e.g. script file not found | system errors are not recoverable |
Syntax error during parsing | invalid script |
Custom syntax mismatch error | incompatible Engine instance |
Script evaluation metrics exceeding limits | safety protection |
Script evaluation manually terminated | safety protection |
Functions
Rhai supports defining functions in script via the fn
keyword, with a syntax that is very similar to Rust without types.
Valid function names are the same as valid variable names.
fn add(x, y) {
x + y
}
fn sub(x, y,) { // trailing comma in parameters list is OK
x - y
}
add(2, 3) == 5;
sub(2, 3,) == -1; // trailing comma in arguments list is OK
Defining functions can be disabled via the no_function
feature.
Use is_def_fn
(not available under no_function
) to detect if a Rhai function is defined
(and therefore callable) based on its name and the number of parameters (arity).
fn foo(x) { x + 1 }
is_def_fn("foo", 1) == true;
is_def_fn("foo", 0) == false;
is_def_fn("foo", 2) == false;
is_def_fn("bar", 1) == false;
Implicit Return
Just like in Rust, an implicit return can be used. In fact, the last statement of a block is
always the block’s return value regardless of whether it is terminated with a semicolon ;
.
This is different from Rust.
fn add(x, y) { // implicit return:
x + y; // value of the last statement (no need for ending semicolon)
// is used as the return value
}
fn add2(x) {
return x + 2; // explicit return
}
add(2, 3) == 5;
add2(42) == 44;
Global Definitions Only
Functions can only be defined at the global level, never inside a block or another function.
Again, this is different from Rust.
// Global level is OK
fn add(x, y) {
x + y
}
// The following will not compile
fn do_addition(x) {
fn add_y(n) { // <- syntax error: cannot define inside another function
n + y
}
add_y(x)
}
No Access to External Scope
Functions are not closures. They do not capture the calling environment and can only access their own parameters.
They cannot access variables external to the function itself.
let x = 42;
fn foo() {
x // <- error: variable 'x' not found
}
But Can Call Other Functions and Access Modules
All functions in the same AST
can call each other.
fn foo(x) { // function defined in the global namespace
x + 1
}
fn bar(x) {
foo(x) // ok! function 'foo' can be called
}
In addition, modules imported at global level can be accessed.
import "hello" as hey;
import "world" as woo;
{
import "x" as xyz; // <- this module is not at global level
} // <- it goes away here
fn foo(x) {
hey::process(x); // ok! imported module 'hey' can be accessed
print(woo::value); // ok! imported module 'woo' can be accessed
xyz::do_work(); // <- error: module 'xyz' not found
}
Automatic Global Module
When a constant is declared at global scope, it is added to a special module called global
.
Functions can access those constants via the special global
module.
Naturally, the automatic global
module is not available under no_function
nor no_module
.
const CONSTANT = 42; // this constant is automatically added to 'global'
let hello = 1; // variables are not added to 'global'
{
const INNER = 0; // this constant is not at global level
} // <- it goes away here
fn foo(x) {
x * global::hello // <- error: variable 'hello' not found in 'global'
x * global::CONSTANT // ok! 'CONSTANT' exists in 'global'
x * global::INNER // <- error: constant 'INNER' not found in 'global'
}
Use Before Definition Allowed
Unlike C/C++, functions in Rhai can be defined anywhere at global level.
A function does not need to be defined prior to being used in a script; a statement in the script can freely call a function defined afterwards.
This is similar to Rust and many other modern languages, such as JavaScript’s function
keyword.
let x = foo(41); // <- I can do this!
fn foo(x) { // <- define 'foo' after use
x + 1
}
Arguments are Passed by Value
Functions defined in script always take Dynamic
parameters (i.e. they can be of any types).
Therefore, functions with the same name and same number of parameters are equivalent.
All arguments are passed by value, so all Rhai script-defined functions are pure (i.e. they never modify their arguments).
Any update to an argument will not be reflected back to the caller.
fn change(s) { // 's' is passed by value
s = 42; // only a COPY of 's' is changed
}
let x = 500;
change(x);
x == 500; // 'x' is NOT changed!
The only possibility for a Rhai script-defined function to modify an external variable is
via the this
pointer.
this
– Simulating an Object Method
The only way for a script-defined function to change an external value is via this
.
Arguments passed to script-defined functions are always by value because functions are pure.
However, functions can also be called in method-call style (not available under no_object
):
object
.
method(
parameters …)
When a function is called this way, the keyword this
binds to the object in the method call and
can be changed.
fn change() { // note that the method does not need a parameter
this = 42; // 'this' binds to the object in method-call
}
let x = 500;
x.change(); // call 'change' in method-call style, 'this' binds to 'x'
x == 42; // 'x' is changed!
change(); // <- error: 'this' is unbound
Elvis Operator
The Elvis operator can be used to short-circuit the method call when the object itself is ()
.
object
?.
method(
parameters …)
In the above, the method is never called if object is ()
.
Restrict the Type of this
in Function Definitions
Methods defined this way are automatically exposed to the global namespace.
In many cases it may be desirable to implement methods for different custom types using script-defined functions.
The Problem
Doing so is brittle and requires a lot of type checking code because there can only be one function definition for the same name and arity:
// Really painful way to define a method called 'do_update' on various data types
fn do_update(x) {
switch type_of(this) {
"i64" => this *= x,
"string" => this.len += x,
"bool" if this => this *= x,
"bool" => this *= 42,
"MyType" => this.update(x),
"Strange-Type#Name::with_!@#symbols" => this.update(x),
_ => throw `I don't know how to handle ${type_of(this)}`!`
}
}
The Solution
With a special syntax, it is possible to restrict a function to be callable only when the object
pointed to by this
is of a certain type:
fn
type name.
method(
parameters …) {
…}
or in quotes if the type name is not a valid identifier itself:
fn
"
type name string"
.
method(
parameters …) {
…}
Needless to say, this typed method definition style is not available under no_object
.
int
can be used in place of the system integer type (usually i64
or i32
).
float
can be used in place of the system floating-point type (usually f64
or f32
).
Using these make scripts more portable.
Examples
/// This 'do_update' can only be called on objects of type 'MyType' in method style
fn MyType.do_update(x, y) {
this.update(x * y);
}
/// This 'do_update' can only be called on objects of type 'Strange-Type#Name::with_!@#symbols'
/// (which can be specified via 'Engine::register_type_with_name') in method style
fn "Strange-Type#Name::with_!@#symbols".do_update(x, y) {
this.update(x * y);
}
/// Define a blanket version
fn do_update(x, y) {
this = `${this}, ${x}, ${y}`;
}
/// This 'do_update' can only be called on integers in method style
fn int.do_update(x, y) {
this += x * y
}
let obj = create_my_type(); // 'x' is 'MyType'
obj.type_of() == "MyType";
obj.do_update(42, 123); // ok!
let x = 42; // 'x' is an integer
x.type_of() == "i64";
x.do_update(42, 123); // ok!
let x = true; // 'x' is a boolean
x.type_of() == "bool";
x.do_update(42, 123); // <- this works because there is a blanket version
// Use 'is_def_fn' with three parameters to test for typed methods
is_def_fn("MyType", "do_update", 2) == true;
is_def_fn("int", "do_update", 2) == true;
Bind to this
for Module Functions
The Problem
The method-call syntax is not possible for functions imported from modules.
import "my_module" as foo;
let x = 42;
x.foo::change_value(1); // <- syntax error
The Solution
In order to call a module function as a method, it must be defined with a restriction on the
type of object pointed to by this
:
┌────────────────┐
│ my_module.rhai │
└────────────────┘
// This is a typed method function requiring 'this' to be an integer.
// Typed methods are automatically marked global when importing this module.
fn int.change_value(offset) {
// 'this' is guaranteed to be an integer
this += offset;
}
┌───────────┐
│ main.rhai │
└───────────┘
import "my_module";
let x = 42;
x.change_value(1); // ok!
x == 43;
Function Overloading
Functions defined in script can be overloaded by arity (i.e. they are resolved purely upon the
function’s name and number of parameters, but not parameter types since all parameters are the
same type – Dynamic
).
New definitions overwrite previous definitions of the same name and number of parameters.
fn foo(x, y, z) {
print(`Three!!! ${x}, ${y}, ${z}`);
}
fn foo(x) {
print(`One! ${x}`);
}
fn foo(x, y) {
print(`Two! ${x}, ${y}`);
}
fn foo() {
print("None.");
}
fn foo(x) { // <- overwrites previous definition
print(`HA! NEW ONE! ${x}`);
}
foo(1,2,3); // prints "Three!!! 1,2,3"
foo(42); // prints "HA! NEW ONE! 42"
foo(1,2); // prints "Two!! 1,2"
foo(); // prints "None."
Function Namespaces
Each Function is a Separate Compilation Unit
Functions in Rhai are pure and they form individual compilation units.
This means that individual functions can be separated, exported, re-grouped, imported, and generally mix-’n-matched with other completely unrelated scripts.
For example, the AST::merge
and AST::combine
methods (or the equivalent +
and +=
operators)
allow combining all functions in one AST
into another, forming a new, unified, group of functions.
Namespace Types
In general, there are two main types of namespaces where functions are looked up:
Namespace | Quantity | Source | Lookup | Sub-modules? | Variables? |
---|---|---|---|---|---|
Global | one | simple name | ignored | ignored | |
Module | many | namespace-qualified name | yes | yes |
Module Namespaces
There can be multiple module namespaces at any time during a script evaluation, usually loaded via
the import
statement.
Static module namespaces can also be registered into an Engine
via Engine::register_static_module
.
Functions and variables in module namespaces are isolated and encapsulated within their own environments.
They must be called or accessed in a namespace-qualified manner.
import "my_module" as m; // new module namespace 'm' created via 'import'
let x = m::calc_result(); // namespace-qualified function call
let y = m::MY_NUMBER; // namespace-qualified variable/constant access
let z = calc_result(); // <- error: function 'calc_result' not found
// in global namespace!
Global Namespace
There is one global namespace for every Engine
, which includes (in the following search order):
-
all 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
viaregister_global_module
, -
functions defined in modules registered into the
Engine
viaregister_static_module
that are specifically marked for exposure to the global namespace (e.g. via the#[rhai(global)]
attribute in a plugin module). -
functions defined in imported modules that are specifically marked for exposure to the global namespace (e.g. via the
#[rhai(global)]
attribute in a plugin module).
Anywhere in a Rhai script, when a function call is made, the function is searched within the global namespace, in the above search order.
Therefore, function calls in Rhai are late bound – meaning that the function called cannot be determined or guaranteed; there is no way to lock down the function being called. This aspect is very similar to JavaScript before ES6 modules.
// Compile a script into AST
let ast1 = engine.compile(
r#"
fn get_message() {
"Hello!" // greeting message
}
fn say_hello() {
print(get_message()); // prints message
}
say_hello();
"#)?;
// Compile another script with an overriding function
let ast2 = engine.compile(r#"fn get_message() { "Boo!" }"#)?;
// Combine the two AST's
ast1 += ast2; // 'message' will be overwritten
engine.run_ast(&ast1)?; // prints 'Boo!'
Therefore, care must be taken when cross-calling functions to make sure that the correct function is called.
The only practical way to ensure that a function is a correct one is to use modules –
i.e. define the function in a separate module and then import
it:
┌──────────────┐
│ message.rhai │
└──────────────┘
fn get_message() { "Hello!" }
┌─────────────┐
│ script.rhai │
└─────────────┘
import "message" as msg;
fn say_hello() {
print(msg::get_message());
}
say_hello();
Function Pointers
It is possible to store a function pointer in a variable just like a normal value.
A function pointer is created via the Fn
function, which takes a string parameter.
Call a function pointer via the call
method.
Short-Hand Notation
Having to write Fn("foo")
in order to create a function pointer to the function foo
is a chore,
so there is a short-hand available.
A function pointer to any script-defined function within the same script can be obtained simply by referring to the function’s name.
fn foo() { ... } // function definition
let f = foo; // function pointer to 'foo'
let f = Fn("foo"); // <- the above is equivalent to this
let g = bar; // error: variable 'bar' not found
The short-hand notation is particularly useful when passing functions as closure arguments.
fn is_even(n) { n % 2 == 0 }
let array = [1, 2, 3, 4, 5];
array.filter(is_even);
array.filter(Fn("is_even")); // <- the above is equivalent to this
array.filter(|n| n % 2 == 0); // <- ... or this
Built-in Functions
The following standard methods (mostly defined in the BasicFnPackage
but
excluded when using a raw Engine
) operate on function pointers.
Function | Parameter(s) | Description |
---|---|---|
name method and property | none | returns the name of the function encapsulated by the function pointer |
is_anonymous method and property | none | does the function pointer refer to an anonymous function? Not available under no_function . |
call | arguments | calls the function matching the function pointer’s name with the arguments |
Examples
fn foo(x) { 41 + x }
let func = Fn("foo"); // use the 'Fn' function to create a function pointer
let func = foo; // <- short-hand: equivalent to 'Fn("foo")'
print(func); // prints 'Fn(foo)'
let func = fn_name.Fn(); // <- error: 'Fn' cannot be called in method-call style
func.type_of() == "Fn"; // type_of() as function pointer is 'Fn'
func.name == "foo";
func.call(1) == 42; // call a function pointer with the 'call' method
foo(1) == 42; // <- the above de-sugars to this
call(func, 1); // normal function call style also works for 'call'
let len = Fn("len"); // 'Fn' also works with registered native Rust functions
len.call("hello") == 5;
let fn_name = "hello"; // the function name does not have to exist yet
let hello = Fn(fn_name + "_world");
hello.call(0); // error: function not found - 'hello_world (i64)'
Because of their dynamic nature, function pointers cannot refer to functions in import
-ed modules.
They can only refer to functions within the global namespace.
import "foo" as f; // assume there is 'f::do_work()'
f::do_work(); // works!
let p = Fn("f::do_work"); // error: invalid function name
fn do_work_now() { // call it from a local function
f::do_work();
}
let p = Fn("do_work_now");
p.call(); // works!
Dynamic Dispatch
The purpose of function pointers is to enable rudimentary dynamic dispatch, meaning to determine, at runtime, which function to call among a group.
Although it is possible to simulate dynamic dispatch via a number and a large
if-then-else-if
statement, using function pointers significantly simplifies the code.
let x = some_calculation();
// These are the functions to call depending on the value of 'x'
fn method1(x) { ... }
fn method2(x) { ... }
fn method3(x) { ... }
// Traditional - using decision variable
let func = sign(x);
// Dispatch with if-statement
if func == -1 {
method1(42);
} else if func == 0 {
method2(42);
} else if func == 1 {
method3(42);
}
// Using pure function pointer
let func = if x < 0 {
method1
} else if x == 0 {
method2
} else if x > 0 {
method3
};
// Dynamic dispatch
func.call(42);
// Using functions map
let map = [ method1, method2, method3 ];
let func = sign(x) + 1;
// Dynamic dispatch
map[func].call(42);
Bind the this
Pointer
When call
is called as a method but not on a function pointer, it is possible to dynamically dispatch
to a function call while binding the object in the method call to the this
pointer of the function.
To achieve this, pass the function pointer as the first argument to call
:
fn add(x) { // define function which uses 'this'
this += x;
}
let func = add; // function pointer to 'add'
func.call(1); // error: 'this' pointer is not bound
let x = 41;
func.call(x, 1); // error: function 'add (i64, i64)' not found
call(func, x, 1); // error: function 'add (i64, i64)' not found
x.call(func, 1); // 'this' is bound to 'x', dispatched to 'func'
x == 42;
Beware that this only works for method-call style.
Normal function-call style cannot bind the this
pointer (for syntactic reasons).
Therefore, obviously, binding the this
pointer is unsupported under no_object
.
Call a Function Pointer within a Rust Function (as a Callback)
It is completely normal to register a Rust function with an Engine
that takes parameters
whose types are function pointers. The Rust type in question is rhai::FnPtr
.
A function pointer in Rhai is essentially syntactic sugar wrapping the name of a function
to call in script. Therefore, the script’s execution context (i.e. NativeCallContext
)
is needed in order to call a function pointer.
use rhai::{Engine, FnPtr, NativeCallContext};
let mut engine = Engine::new();
// A function expecting a callback in form of a function pointer.
fn super_call(context: NativeCallContext, callback: FnPtr, value: i64)
-> Result<String, Box<EvalAltResult>>
{
// Use 'FnPtr::call_within_context' to call the function pointer using the call context.
// 'FnPtr::call_within_context' automatically casts to the required result type.
callback.call_within_context(&context, (value,))
// ^^^^^^^^ arguments passed in tuple
}
engine.register_fn("super_call", super_call);
Call a Function Pointer Directly
The FnPtr::call
method allows the function pointer to be called directly on any Engine
and
AST
, making it possible to reuse the FnPtr
data type in may different calls and scripting
environments.
use rhai::{Engine, FnPtr};
let engine = Engine::new();
// Compile script to AST
let ast = engine.compile(
r#"
let test = "hello";
|x| test + x // this creates a closure
"#)?;
// Save the closure together with captured variables
let fn_ptr = engine.eval_ast::<FnPtr>(&ast)?;
// 'f' captures: the Engine, the AST, and the closure
let f = move |x: i64| -> Result<String, _> {
fn_ptr.call(&engine, &ast, (x,))
};
// 'f' can be called like a normal function
let result = f(42)?;
result == "hello42";
Function Pointer Currying
It is possible to curry a function pointer by providing partial (or all) arguments.
Currying is done via the curry
keyword and produces a new function pointer which carries
the curried arguments.
When the curried function pointer is called, the curried arguments are inserted starting from the left.
The actual call arguments should be reduced by the number of curried arguments.
fn mul(x, y) { // function with two parameters
x * y
}
let func = mul; // <- de-sugars to 'Fn("mul")'
func.call(21, 2) == 42; // two arguments are required for 'mul'
let curried = func.curry(21); // currying produces a new function pointer which
// carries 21 as the first argument
let curried = curry(func, 21); // function-call style also works
curried.call(2) == 42; // <- de-sugars to 'func.call(21, 2)'
// only one argument is now required
Anonymous Functions
Many functions in the standard API expect function pointer as parameters.
For example:
// Function 'double' defined here - used only once
fn double(x) { 2 * x }
// Function 'square' defined here - again used only once
fn square(x) { x * x }
let x = [1, 2, 3, 4, 5];
// Pass a function pointer to 'double'
let y = x.map(double);
// Pass a function pointer to 'square' using Fn(...) notation
let z = y.map(Fn("square"));
Sometimes it gets tedious to define separate functions only to dispatch them via single function pointers – essentially, those functions are only ever called in one place.
This scenario is especially common when simulating object-oriented programming (OOP).
// Define functions one-by-one
fn obj_inc(x, y) { this.data += x * y; }
fn obj_dec(x) { this.data -= x; }
fn obj_print() { print(this.data); }
// Define object
let obj = #{
data: 42,
increment: obj_inc, // use function pointers to
decrement: obj_dec, // refer to method functions
print: obj_print
};
Syntax
Anonymous functions have a syntax similar to Rust’s closures (they are not the same).
|
param 1,
param 2,
…,
param n|
statement
|
param 1,
param 2,
…,
param n| {
statements…}
No parameters:
||
statement
|| {
statements…}
Anonymous functions can be disabled via Engine::set_allow_anonymous_function
.
Rewrite Using Anonymous Functions
The above can be rewritten using anonymous functions.
let x = [1, 2, 3, 4, 5];
let y = x.map(|x| 2 * x);
let z = y.map(|x| x * x);
let obj = #{
data: 42,
increment: |x, y| this.data += x * y, // one statement
decrement: |x| this.data -= x, // one statement
print_obj: || {
print(this.data); // full function body
}
};
This de-sugars to:
// Automatically generated...
fn anon_fn_0001(x) { 2 * x }
fn anon_fn_0002(x) { x * x }
fn anon_fn_0003(x, y) { this.data += x * y; }
fn anon_fn_0004(x) { this.data -= x; }
fn anon_fn_0005() { print(this.data); }
let x = [1, 2, 3, 4, 5];
let y = x.map(anon_fn_0001);
let z = y.map(anon_fn_0002);
let obj = #{
data: 42,
increment: anon_fn_0003,
decrement: anon_fn_0004,
print: anon_fn_0005
};
Closures
Although anonymous functions de-sugar to standard function definitions, they differ from standard functions because they can captures variables that are not defined within the current scope, but are instead defined in an external scope – i.e. where the anonymous function is created.
All variables that are accessible during the time the anonymous function is created are automatically captured when they are used, as long as they are not shadowed by local variables defined within the function’s.
The captured variables are automatically converted into reference-counted shared values
(Rc<RefCell<Dynamic>>
, or Arc<RwLock<Dynamic>>
under sync
).
Therefore, similar to closures in many languages, these captured shared values persist through reference counting, and may be read or modified even after the variables that hold them go out of scope and no longer exist.
Capturing external variables can be turned off via the no_closure
feature.
Examples
let x = 1; // a normal variable
x.is_shared() == false;
let f = |y| x + y; // variable 'x' is auto-curried (captured) into 'f'
x.is_shared() == true; // 'x' is now a shared value!
f.call(2) == 3; // 1 + 2 == 3
x = 40; // changing 'x'...
f.call(2) == 42; // the value of 'x' is 40 because 'x' is shared
// The above de-sugars into something like this:
fn anon_0001(x, y) { x + y } // parameter 'x' is inserted
make_shared(x); // convert variable 'x' into a shared value
let f = anon_0001.curry(x); // shared 'x' is curried
Rust does not have data races, but that doesn’t mean Rhai doesn’t.
Avoid performing a method call on a captured shared variable (which essentially takes a mutable reference to the shared object) while using that same variable as a parameter in the method call – this is a sure-fire way to generate a data race error.
If a shared value is used as the this
pointer in a method call to a closure function,
then the same shared value must not be captured inside that function, or a data race
will occur and the script will terminate with an error.
let x = 20;
x.is_shared() == false; // 'x' is not shared, so no data race is possible
let f = |a| this += x + a; // 'x' is captured in this closure
x.is_shared() == true; // now 'x' is shared
x.call(f, 2); // <- error: data race detected on 'x'
Under the sync
feature, shared values are guarded with a RwLock
, meaning that data race
conditions no longer raise an error.
Instead, they wait endlessly for the RwLock
to be freed, and thus can become deadlocks.
On the other hand, since the same thread (i.e. the Engine
thread) that is holding the lock
is attempting to read it again, this may also panic
depending on the O/S.
let x = 20;
let f = |a| this += x + a; // 'x' is captured in this closure
// Under `sync`, the following may wait forever, or may panic,
// because 'x' is locked as the `this` pointer but also accessed
// via a captured shared value.
x.call(f, 2);
TL;DR
The actual implementation of closures de-sugars to:
-
Keeping track of what variables are accessed inside the anonymous function,
-
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.
-
The variable is added to the parameters list of the anonymous function, at the front.
-
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.
-
The shared value is then curried into the function pointer itself, essentially carrying a reference to that shared value and inserting it into future calls of the function.
This process is called Automatic Currying, and is the mechanism through which Rhai simulates closures.
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:
- A function pointer to the function body of the closure,
- A data structure containing references to the captured shared variables on the heap.
Usually a language implementation passes the structure containing references to captured shared variables into the function pointer, the function body taking this data structure as an additional parameter.
This is essentially what Rhai does, except that Rhai passes each variable individually as separate parameters to the function, instead of creating a structure and passing that structure as a single parameter. This is the only difference.
Therefore, in most languages, essentially all closures are implemented as automatic currying of shared variables hoisted into the heap, automatically passing those variables as parameters into the function. Rhai just brings this directly up to the front.
Functions and Custom Types Metadata
Exporting metadata requires the metadata
feature.
Functions
The metadata of a function means all relevant information related to a function’s definition including:
-
Its callable name
-
Its access mode (public or private)
-
Its parameter names and types (if any)
-
Its return value and type (if any)
-
Its nature (i.e. native Rust or Rhai-scripted)
-
Its purpose, in the form of doc-comments
-
Usage notes, warnings, examples etc., in the form of doc-comments
A function’s signature encapsulates the first four pieces of information in a single concise line of definition:
[private]
name(
param 1:
type 1,
param 2:
type 2,
…,
param n:
type n) ->
return type
Custom Types
The metadata of a custom type include:
-
Its full Rust type name
-
Its pretty-print display name (which can be the same as its Rust type name)
-
Its purpose, in the form of doc-comments
Get Functions Metadata in Scripts
The built-in function get_fn_metadata_list
returns an array of object maps, each containing
the metadata of one script-defined function in scope.
get_fn_metadata_list
is defined in the LanguageCorePackage
, which is
excluded when using a raw Engine
.
get_fn_metadata_list
has a few versions taking different parameters:
Signature | Description |
---|---|
get_fn_metadata_list() | returns an array for all script-defined functions |
get_fn_metadata_list(name) | returns an array containing all script-defined functions matching a specified name |
get_fn_metadata_list(name, params) | returns an array containing all script-defined functions matching a specified name and accepting the specified number of parameters |
Functions from the following sources are returned, in order:
- Encapsulated script environment (e.g. when loading a module from a script file),
- Current script,
- Modules registered via
Engine::register_global_module
(latest registrations first) - Modules imported via the
import
statement (latest imports first), - Modules added via
Engine::register_static_module
(latest registrations first)
The return value is an array of object maps (so get_fn_metadata_list
is also not available under
no_index
or no_object
), containing the following fields.
Field | Type | Optional? | Description |
---|---|---|---|
namespace | string | yes | the module namespace if the function is defined within a module |
access | string | no | "public" if the function is public,"private" if it is private |
name | string | no | function name |
params | array of strings | no | parameter names |
this_type | string | yes | restrict the type of this if the function is a method |
is_anonymous | bool | no | is this function an anonymous function? |
comments | array of strings | yes | doc-comments, if any, one per line |
Get Scripted Functions Metadata from AST
Use AST::iter_functions
to iterate through all the script-defined functions in an AST
.
ScriptFnMetadata
The type returned from the iterator is ScriptFnMetadata
with the following fields:
Field | Requires | Type | Description |
---|---|---|---|
name | &str | Name of function | |
params | Vec<&str> | Number of parameters | |
access | FnAccess | • FnAccess::Public (public)• FnAccess::Private (private ) | |
comments | metadata | Vec<&str> | Doc-comments, if any, one per line |
Get Native Function Signatures
Engine::gen_fn_signatures
As part of a reflections API, Engine::gen_fn_signatures
returns a list of function signatures
(as Vec<String>
), each corresponding to a particular native function available to that Engine
instance.
name
(
param 1:
type 1,
param 2:
type 2,
…,
param n:
type n) ->
return type
The metadata
feature must be used to turn on this API.
Sources
Functions from the following sources are included, in order:
- Native Rust functions registered into the global namespace via the
Engine::register_XXX
API - Public (i.e. non-
private
) functions (native Rust or Rhai scripted) in global sub-modules registered viaEngine::register_static_module
. - Native Rust functions in external packages registered via
Engine::register_global_module
- Native Rust functions in built-in packages (optional)
Functions Metadata
Beware, however, that not all function signatures contain parameters and return value information.
Engine::register_XXX
For instance, functions registered via Engine::register_XXX
contain no information on the names of
parameter because Rust simply does not make such metadata available natively.
Type names, however, are provided.
A function registered under the name foo
with three parameters.
foo(_: i64, _: char, _: &str) -> String
An operator function. Notice that function names do not need to be valid identifiers.
+=(_: &mut i64, _: i64)
A property setter.
Notice that function names do not need to be valid identifiers.
In this case, the first parameter should be &mut T
of the custom type and the return value is ()
:
set$prop(_: &mut TestStruct, _: i64)
Script-Defined Functions
Script-defined function signatures contain parameter names.
Since all parameters, as well as the return value, are Dynamic
the types are simply not shown.
foo(x, y, z)
is probably defined simply as:
/// This is a doc-comment, included in this function's metadata.
fn foo(x, y, z) {
...
}
which is really the same as:
foo(x: Dynamic, y: Dynamic, z: Dynamic) -> Result<Dynamic, Box<EvalAltResult>>
Plugin Functions
Functions defined in plugin modules are the best. They contain all metadata describing the functions, including doc-comments.
For example, a plugin function combine
:
/// This is a doc-comment, included in this function's metadata.
combine(list: &mut MyStruct<i64>, num: usize, name: &str) -> bool
Notice that function names do not need to be valid identifiers.
For example, an operator defined as a fallible function in a plugin module via
#[rhai_fn(name="+=", return_raw)]
returns Result<bool, Box<EvalAltResult>>
:
+=(list: &mut MyStruct<i64>, value: &str) -> Result<bool, Box<EvalAltResult>>
For example, a property getter defined in a plugin module:
get$prop(obj: &mut MyStruct<i64>) -> String
Export Functions Metadata to JSON
Engine::gen_fn_metadata_to_json
Engine::gen_fn_metadata_with_ast_to_json
As part of a reflections API, Engine::gen_fn_metadata_to_json
and the corresponding
Engine::gen_fn_metadata_with_ast_to_json
export the full list of custom types and
functions metadata in JSON format.
The metadata
feature is required for this API, which also pulls in the
serde_json
crate.
Sources
Functions and custom types from the following sources are included:
- Script-defined functions in an
AST
(forEngine::gen_fn_metadata_with_ast_to_json
) - Native Rust functions registered into the global namespace via the
Engine::register_XXX
API - Custom types registered into the global namespace via the
Engine::register_type_with_name
API - Public (i.e. non-
private
) functions (native Rust or Rhai scripted) and custom types in static modules registered viaEngine::register_static_module
- Native Rust functions and custom types in external packages registered via
Engine::register_global_module
- Native Rust functions and custom types in built-in packages (optional)
JSON Schema
The JSON schema used to hold metadata is very simple, containing a nested structure of
modules
, a list of customTypes
and a list of functions
.
Module Schema
{
"doc": "//! Module documentation",
"modules":
{
"sub_module_1": /* namespace 'sub_module_1' */
{
"modules":
{
"sub_sub_module_A": /* namespace 'sub_module_1::sub_sub_module_A' */
{
"doc": "//! Module documentation can also occur in any sub-module",
"customTypes": /* custom types exported in 'sub_module_1::sub_sub_module_A' */
[
{ ... custom type metadata ... },
{ ... custom type metadata ... },
{ ... custom type metadata ... }
...
],
"functions": /* functions exported in 'sub_module_1::sub_sub_module_A' */
[
{ ... function metadata ... },
{ ... function metadata ... },
{ ... function metadata ... },
{ ... function metadata ... }
...
]
},
"sub_sub_module_B": /* namespace 'sub_module_1::sub_sub_module_B' */
{
...
}
}
},
"sub_module_2": /* namespace 'sub_module_2' */
{
...
},
...
},
"customTypes": /* custom types registered globally */
[
{ ... custom type metadata ... },
{ ... custom type metadata ... },
{ ... custom type metadata ... },
...
],
"functions": /* functions registered globally or in the 'AST' */
[
{ ... function metadata ... },
{ ... function metadata ... },
{ ... function metadata ... },
{ ... function metadata ... },
...
]
}
Custom Type Metadata Schema
{
"typeName": "alloc::string::String", /* name of Rust type */
"displayName": "MyType",
"docComments": /* omitted if none */
[
"/// My super-string type.",
...
]
}
Function Metadata Schema
{
"baseHash": 9876543210, /* partial hash with only number of parameters */
"fullHash": 1234567890, /* full hash with actual parameter types */
"namespace": "internal" | "global",
"access": "public" | "private",
"name": "fn_name",
"isAnonymous": false,
"type": "native" | "script",
"numParams": 42, /* number of parameters */
"params": /* omitted if no parameters */
[
{ "name": "param_1", "type": "type_1" },
{ "name": "param_2" }, /* no type name */
{ "type": "type_3" }, /* no parameter name */
...
],
"thisType": "this_type", /* omitted if none */
"returnType": "ret_type", /* omitted if () or unknown */
"signature": "[private] fn_name(param_1: type_1, param_2, _: type_3) -> ret_type",
"docComments": /* omitted if none */
[
"/// doc-comment line 1",
"/// doc-comment line 2",
"/** doc-comment block */",
...
]
}
Generate Definition Files for Language Server
Rhai’s language server works with IDE’s to provide integrated support for the Rhai scripting language.
Functions and modules registered with an Engine
can output their metadata
into definition files which are used by the language server.
Definitions are generated via the Engine::definitions
and Engine::definitions_with_scope
API.
This API requires the metadata
and internals
feature.
Configurable Options
The Definitions
type supports the following options in a fluent method-chaining style.
Option | Method | Default |
---|---|---|
Write headers in definition files? | with_headers | false |
Include standard packages in definition files? | include_standard_packages | true |
engine
.definitions()
.with_headers(true) // write headers in all files
.include_standard_packages(false) // skip standard packages
.write_to_dir("path/to/my/definitions")
.unwrap();
Example
use rhai::{Engine, Scope};
use rhai::plugin::*;
// Plugin module: 'general_kenobi'
#[export_module]
pub mod general_kenobi {
use std::convert::TryInto;
/// Returns a string where "hello there" is repeated 'n' times.
pub fn hello_there(n: i64) -> String {
"hello there ".repeat(n.try_into().unwrap())
}
}
// Create scripting engine
let mut engine = Engine::new();
// Create custom Scope
let mut scope = Scope::new();
// This variable will also show up in the generated definition file.
scope.push("hello_there", "hello there");
// Static module namespaces will generate independent definition files.
engine.register_static_module(
"general_kenobi",
exported_module!(general_kenobi).into()
);
// Custom operators will also show up in the generated definition file.
engine.register_custom_operator("minus", 100).unwrap();
engine.register_fn("minus", |a: i64, b: i64| a - b);
engine.run_with_scope(&mut scope,
"hello_there = general_kenobi::hello_there(4 minus 2);"
)?;
// Output definition files in the specified directory.
engine
.definitions()
.write_to_dir("path/to/my/definitions")
.unwrap();
// Output definition files in the specified directory.
// Variables in the provided 'Scope' are included.
engine
.definitions_with_scope(&scope)
.write_to_dir("path/to/my/definitions")
.unwrap();
// Output a single definition file with everything merged.
// Variables in the provided 'Scope' are included.
engine
.definitions_with_scope(&scope)
.write_to_file("path/to/my/definitions/all_in_one.d.rhai")
.unwrap();
// Output functions metadata to a JSON string.
// Functions in standard packages are skipped and not included.
let json = engine
.definitions()
.include_standard_packages(false) // skip standard packages
.unwrap();
Definition Files
The generated definition files will look like the following.
┌───────────────────────┐
│ general_kenobi.d.rhai │
└───────────────────────┘
module general_kenobi;
/// Returns a string where "hello there" is repeated 'n' times.
fn hello_there(n: int) -> String;
┌──────────────────┐
│ __scope__.d.rhai │
└──────────────────┘
module static;
let hello_there;
┌───────────────────┐
│ __static__.d.rhai │
└───────────────────┘
module static;
op minus(int, int) -> int;
:
:
┌────────────────────┐
│ __builtin__.d.rhai │
└────────────────────┘
module static;
:
:
┌──────────────────────────────┐
│ __builtin-operators__.d.rhai │
└──────────────────────────────┘
module static;
:
:
All-in-One Definition File
Definitions::write_to_file
generates a single definition file with everything merged in, like the following.
module static;
op minus(int, int) -> int;
:
:
module general_kenobi {
/// Returns a string where "hello there" is repeated 'n' times.
fn hello_there(n: int) -> String;
}
let hello_there;
print
and debug
The print
and debug
functions default to printing to stdout
, with debug
using standard debug formatting.
print("hello"); // prints "hello" to stdout
print(1 + 2 + 3); // prints "6" to stdout
let x = 42;
print(`hello${x}`); // prints "hello42" to stdout
debug("world!"); // prints "world!" to stdout using debug formatting
Override print
and debug
with Callback Functions
When embedding Rhai into an application, it is usually necessary to trap print
and debug
output
(for logging into a tracking log, for example) with the Engine::on_print
and Engine::on_debug
methods.
// Any function or closure that takes an '&str' argument can be used to override 'print'.
engine.on_print(|x| println!("hello: {x}"));
// Any function or closure that takes a '&str', an 'Option<&str>' and a 'Position' argument
// can be used to override 'debug'.
engine.on_debug(|x, src, pos| {
let src = src.unwrap_or("unknown");
println!("DEBUG of {src} at {pos:?}: {s}")
});
// Example: quick-'n-dirty logging
let logbook = Arc::new(RwLock::new(Vec::<String>::new()));
// Redirect print/debug output to 'log'
let log = logbook.clone();
engine.on_print(move |s| {
let entry = format!("entry: {}", s);
log.write().unwrap().push(entry);
});
let log = logbook.clone();
engine.on_debug(move |s, src, pos| {
let src = src.unwrap_or("unknown");
let entry = format!("DEBUG of {src} at {pos:?}: {s}");
log.write().unwrap().push(entry);
});
// Evaluate script
engine.run(script)?;
// 'logbook' captures all the 'print' and 'debug' output
for entry in logbook.read().unwrap().iter() {
println!("{entry}");
}
on_debug
Callback Signature
The function signature passed to [Engine::on_debug]
takes the following form.
Fn(text: &str, source: Option<&str>, pos: Position)
where:
Parameter | Type | Description |
---|---|---|
text | &str | text to display |
source | Option<&str> | source of the current evaluation, if any |
pos | Position | position (line number and character offset) of the debug call |
The source of a script evaluation is any text string provided to an AST
via AST::set_source
.
Modules
Rhai allows organizing code (functions, both Rust-based or script-based, and variables) into modules.
Modules can be disabled via the no_module
feature.
A module has the type Module
and holds a collection of functions, variables, type iterators and sub-modules.
It may be created entirely from Rust functions, or it may encapsulate a Rhai script together with the functions
and variables defined by that script.
Other scripts can then load this module and use the functions and variables exported as if they were defined inside the same script.
Export Variables, Functions and Sub-Modules From a Script
See Create a Module from AST for more details.
The easiest way to expose a collection of functions as a self-contained module is to do it via a Rhai script itself.
The script text is evaluated.
Variables are then selectively exposed via the export
statement.
Functions defined by the script are automatically exported, unless marked as private
.
Modules loaded within this module at the global level become sub-modules and are also automatically exported.
Export Global Variables
The export
statement, which can only be at global level, exposes a selected variable as member of a module.
Variables not exported are private and hidden. They are merely used to initialize the module, but cannot be accessed from outside.
Everything exported from a module is constant (i.e. read-only).
// This is a module script.
let hidden = 123; // variable not exported - default hidden
let x = 42; // this will be exported below
export x; // the variable 'x' is exported under its own name
export const x = 42; // convenient short-hand to declare a constant and export it
// under its own name
export let x = 123; // variables can be exported as well, though it'll still be constant
export x as answer; // the variable 'x' is exported under the alias 'answer'
// another script can load this module and access 'x' as 'module::answer'
{
let inner = 0; // local variable - it disappears when the statements block ends,
// therefore it is not 'global' and cannot be exported
export inner; // <- syntax error: cannot export a local variable
}
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.
// This is a module script.
fn inc(x) { x + 1 } // script-defined function - default public
private fn foo() {} // private function - hidden
Sub-Modules
All loaded modules are automatically exported as sub-modules.
To prevent a module from being exported, load it inside a block statement so that it goes away at the end of the block.
// This is a module script.
import "hello" as foo; // <- exported
{
import "world" as bar; // <- not exported
}
Import a Module
See Module Resolvers for more details.
Before a module can be used (via an import
statement) in a script, there must be a
module resolver registered into the Engine
, the default being the FileModuleResolver
.
import
Statement
A module that is only import
-ed but not given any name is simply run.
This is a very simple way to run another script file from within a script.
A module can be imported via the import
statement, and be given a name.
Its members can be accessed via ::
similar to C++.
import "crypto_banner"; // run the script file 'crypto_banner.rhai' without creating an imported module
import "crypto" as lock; // run the script file 'crypto.rhai' and import it as a module named 'lock'
const SECRET_NUMBER = 42;
let mod_file = `crypto_${SECRET_NUMBER}`;
import mod_file as my_mod; // load the script file "crypto_42.rhai" and import it as a module named 'my_mod'
// notice that module path names can be dynamically constructed!
// any expression that evaluates to a string is acceptable after the 'import' keyword
lock::encrypt(secret); // use functions defined under the module via '::'
lock::hash::sha256(key); // sub-modules are also supported
print(lock::status); // module variables are constants
lock::status = "off"; // <- runtime error: cannot modify a constant
Modules imported via import
statements are only accessible inside the relevant block scope.
import "hacker" as h; // import module - visible globally
if secured { // <- new block scope
let mod = "crypt";
import mod + "o" as c; // import module (the path needs not be a constant string)
let x = c::encrypt(key); // use a function in the module
h::hack(x); // global module 'h' is visible here
} // <- module 'c' disappears at the end of the block scope
h::hack(something); // this works as 'h' is visible
c::encrypt(something); // <- this causes a run-time error because
// module 'c' is no longer available!
fn foo(something) {
h::hack(something); // <- this also works as 'h' is visible
}
for x in 0..1000 {
import "crypto" as c; // <- importing a module inside a loop is a Very Bad Idea™
c.encrypt(something);
}
import
statements can appear anywhere a normal statement can be, but in the vast majority of cases they are
usually grouped at the top (beginning) of a script for manageability and visibility.
It is not advised to deviate from this common practice unless there is a Very Good Reason™.
Especially, do not place an import
statement within a loop; doing so will repeatedly re-load the
same module during every iteration of the loop!
Beware of import cycles – i.e. recursively loading the same module. This is a sure-fire way to
cause a stack overflow in the Engine
, unless stopped by setting a limit for maximum number of modules.
For instance, importing itself always causes an infinite recursion:
┌────────────┐
│ hello.rhai │
└────────────┘
import "hello" as foo; // import itself - infinite recursion!
foo::do_something();
Modules cross-referencing also cause infinite recursion:
┌────────────┐
│ hello.rhai │
└────────────┘
import "world" as foo;
foo::do_something();
┌────────────┐
│ world.rhai │
└────────────┘
import "hello" as bar;
bar::do_something_else();
eval
Function
Or “How to Shoot Yourself in the Foot even Easier”
Saving the best for last, there is the ever-dreaded… eval
function!
let x = 10;
fn foo(x) { x += 12; x }
let script =
"
let y = x;
y += foo(y);
x + y
";
let result = eval(script); // <- look, JavaScript, we can also do this!
result == 42;
x == 10; // prints 10 - arguments are passed by value
y == 32; // prints 32 - variables defined in 'eval' persist!
eval("{ let z = y }"); // to keep a variable local, use a statements block
print(z); // <- error: variable 'z' not found
"print(42)".eval(); // <- nope... method-call style doesn't work with 'eval'
Script segments passed to eval
execute inside the current Scope
, so they can access and modify
everything, including all variables that are visible at that position in code!
let script = "x += 32";
let x = 10;
eval(script); // variable 'x' is visible!
print(x); // prints 42
// The above is equivalent to:
let script = "x += 32";
let x = 10;
x += 32;
print(x);
eval
can also be used to define new variables and do other things normally forbidden inside
a function call.
let script = "let x = 42";
eval(script);
print(x); // prints 42
Treat it as if the script segments are physically pasted in at the position of the eval
call.
For those who subscribe to the (very sensible) motto of “eval
is evil”,
disable eval
via Engine::disable_symbol
.
// Disable usage of 'eval'
engine.disable_symbol("eval");
Or course we do.
Having the possibility of an eval
call disrupts any predictability in the Rhai script,
thus disabling a large number of optimizations.
Brendan Eich puts it well: “it is just too easy to implement.” (source wanted)
Safety and Protection Against DOS Attacks
For scripting systems open to untrusted user-land scripts, it is always best to limit the amount of resources used by a script so that it does not consume more resources that it is allowed to.
These are common vectors for Denial of Service (DOS) attacks.
Most Important Resources
-
Continuously grow a string, an array, a BLOB or object map until all memory is consumed.
-
Continuously create new variables with large data until all memory is consumed.
-
Continuously define new functions all memory is consumed (e.g. a simple closure
||
, as short as two characters, is a function – an attractive target for DOS attacks).
-
Deep recursive call that exhausts the call stack.
-
Large array or object map literal that exhausts the stack during parsing.
-
Degenerated deep expression with so many levels that the parser exhausts the call stack when parsing the expression; or even deeply-nested statements blocks, if nested deep enough.
-
Numeric overflows and/or underflows.
-
Divide by zero.
-
Bad floating-point representations.
-
Read from and/or write to private, secret, sensitive data.
Such security breach may put the entire system at risk.
The internals
feature
The internals
feature allows third-party access to Rust internal data types and functions (for
example, the AST
and related types).
This is usually a Very Bad Idea™ because:
-
Messing up Rhai’s internal data structures will easily create panics that bring down the host environment, violating the Don’t Panic guarantee.
-
Allowing access to internal types may open up new attack vectors.
-
Internal Rhai types and functions are volatile, so they may change from version to version and break code.
Use internals
only if the operating environment has absolutely no safety concerns – you’d
be surprised under how few scenarios this assumption holds.
One example of such an environment is a Rhai scripting Engine
compiled to WASM where the
AST
is further translated to include environment-specific modifications.
Don’t Panic Guarantee – Any Panic is a Bug
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.
All these safe-guards can be turned off via the unchecked
feature, which disables all safety
checks (even fatal ones).
This increases script evaluation performance somewhat, but very easy for a malicious script to bring down the host system.
Sand-Boxing – Block Access to External Data
Rhai is sand-boxed so a script can never read from outside its own environment.
Furthermore, an Engine
created non-mut
cannot mutate any state, including itself
(and therefore it is also re-entrant).
It is highly recommended that Engine
’s be created immutable as much as possible.
let mut engine = Engine::new();
// Use the fluent API to configure an 'Engine'
engine.register_get("field", get_field)
.register_set("field", set_field)
.register_fn("do_work", action);
// Then turn it into an immutable instance
let engine = engine;
// 'engine' is immutable...
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>>
):
-
Control Layer pattern.
-
Singleton Command Object pattern.
Limiting Run Time
Track Progress and Force-Termination
Operations count does not indicate the proportion of work already done – thus it is not real progress tracking.
The real progress can be estimated based on the expected number of operations in a typical run.
It is impossible to know when, or even whether, a script run will end (a.k.a. the Halting Problem).
When dealing with third-party untrusted scripts that may be malicious, in order to track evaluation
progress and force-terminate a script prematurely (for any reason), provide a closure to the
Engine
via Engine::on_progress
.
The closure passed to Engine::on_progress
will be called once for every operation.
Progress tracking is disabled with the unchecked
feature.
Examples
Periodic Logging
let mut engine = Engine::new();
engine.on_progress(|count| { // parameter is number of operations already performed
if count % 1000 == 0 {
println!("{count}"); // print out a progress log every 1,000 operations
}
None // return 'None' to continue running the script
// return 'Some(token)' to immediately terminate the script
});
Limit running time
let mut engine = Engine::new();
let start = get_time(); // get the current system time
engine.on_progress(move |_| {
let now = get_time();
if now.duration_since(start).as_secs() > 60 {
// Return a dummy token just to force-terminate the script
// after running for more than 60 seconds!
Some(Dynamic::UNIT)
} else {
// Continue
None
}
});
Function Signature of Callback
The signature of the closure to pass to Engine::on_progress
is as follows.
Fn(operations: u64) -> Option<Dynamic>
Return value
Value | Effect |
---|---|
Some(token) | terminate immediately, with token (a Dynamic value) as termination token |
None | continue script evaluation |
Termination Token
The termination token is commonly used to provide information on the reason behind the termination decision.
The Dynamic
value returned is a termination token.
A script that is manually terminated returns with the error EvalAltResult::ErrorTerminated(token, position)
wrapping this value.
If the termination token is not needed, simply return Some(Dynamic::UNIT)
to terminate the script
run with ()
as the token.
Limiting Memory Usage
During Evaluation
To prevent out-of-memory failures, provide a closure to Engine::on_progress
to track
memory usage and force-terminate a malicious script before it can bring down the host system.
Most O/S provides system calls to obtain the current memory usage of the process.
let mut engine = Engine::new();
const MAX_MEMORY: usize = 10 * 1024 * 1024; // 10MB
engine.on_progress(|_| {
// Call a system function to obtain the current memory usage
let memory_usage = get_current_progress_memory_usage();
if memory_usage > MAX_MEMORY {
// Terminate the script
Some(Dynamic::UNIT)
} else {
// Continue
None
}
});
During Parsing
A malicious script can be carefully crafted such that it consumes all available memory during the parsing stage.
Protect against this by via a closure to Engine::on_parse_token
.
let mut engine = Engine::new();
const MAX_MEMORY: usize = 10 * 1024 * 1024; // 10MB
engine.on_parse_token(|token, _, _| {
// Call a system function to obtain the current memory usage
let memory_usage = get_current_progress_memory_usage();
if memory_usage > MAX_MEMORY {
// Terminate parsing
Token::LexError(
LexError::Runtime("out of memory".into()).into()
)
} else {
// Continue
token
}
});
Limiting Stack Usage
Most O/S differentiates between heap and stack memory.
Usually the stack (around 1MB) is much smaller than the heap (multiple MB’s or even GB’s).
Therefore, it is possible for a carefully-crafted script to consume all available stack memory (such as deeply-nested expressions) and crash the host system, even though there is ample heap memory available.
Calculate Stack Usage
Some O/S’s provide system calls to get the stack size and/or amount of free stack memory, but these are in the minority.
In order to determine the amount of stack memory actually used, it is necessary to perform some pointer arithmetic.
The trick is to get the address of a stack-allocated variable in the very beginning and compare it to the address of another variable.
// Create a variable on the stack.
let stack_base_ref = Dynamic::UNIT;
// Get a pointer to it.
let stack_base: *const Dynamic = &stack_base_ref;
// ... do a lot of work here ...
// Create another variable on the stack.
let stack_top = Dynamic::UNIT;
// Get a pointer to it.
let stack_top: *const Dynamic = &stack_top;
let usage = unsafe { stack_top.offset_from(stack_base) };
In many cases, the amount of stack memory used is actually negative (meaning that the base variable is in a higher memory address than the current variable).
That is because, for many architectures, the stack grows downwards and the heap grows upwards in order to maximize memory usage efficiency.
During Evaluation
To prevent stack-overflow failures, provide a closure to Engine::on_progress
to
track stack usage and force-terminate a malicious script before it can bring down the host system.
let mut engine = Engine::new();
const MAX_STACK: usize = 100 * 1024; // 10KB
// Create a variable on the stack.
let stack_base_ref = Dynamic::UNIT;
// Get a pointer to it.
let stack_base: *const Dynamic = &stack_base_ref;
engine.on_progress(move |_| {
// Create another variable on the stack.
let stack_top = Dynamic::UNIT;
// Get a pointer to it.
let stack_top: *const Dynamic = &stack_top;
let usage = unsafe { stack_base.offset_from(stack_top) };
if usage > MAX_STACK {
// Terminate the script
Some(Dynamic::UNIT)
} else {
// Continue
None
}
});
During Parsing
A malicious script can be carefully crafted such that it consumes all stack memory during the parsing stage.
Protect against this by via a closure to Engine::on_parse_token
.
let mut engine = Engine::new();
const MAX_STACK: usize = 100 * 1024; // 10KB
// Create a variable on the stack.
let stack_base_ref = Dynamic::UNIT;
// Get a pointer to it.
let stack_base: *const Dynamic = &stack_base_ref;
engine.on_parse_token(|token, _, _| {
// Create another variable on the stack.
let stack_top = Dynamic::UNIT;
// Get a pointer to it.
let stack_top: *const Dynamic = &stack_top;
let usage = unsafe { stack_base.offset_from(stack_top) };
if usage > MAX_STACK {
// Terminate parsing
Token::LexError(
LexError::Runtime("stack-overflow".into()).into()
)
} else {
// Continue
token
}
});
Built-In Safety Limits
Rhai has a number of safety limits built into the Engine
.
All these limits can be disabled, for higher performance (but higher risks as well), via the
unchecked
feature.
Maximum Length of Strings
Rhai by default does not limit how long a string can be.
This can be changed via the Engine::set_max_string_size
method, with zero being
unlimited (the default).
A script attempting to create a string literal longer than the maximum length will terminate with a parse error.
Any script operation that produces a string longer than the maximum also terminates the script with an error.
This check can be disabled via the unchecked
feature for higher performance (but higher risks as well).
let mut engine = Engine::new();
engine.set_max_string_size(500); // allow strings only up to 500 bytes long (in UTF-8 format)
engine.set_max_string_size(0); // allow unlimited string length
Be conservative when setting a maximum limit and always consider the fact that a registered function may grow a string’s length without Rhai noticing until the very end.
For instance, the built-in +
operator for strings concatenates two strings together to form one longer string;
if both strings are slightly below the maximum length limit, the resultant string may be almost twice the maximum length.
Maximum Size of Arrays
Rhai by default does not limit how large an array or a BLOB can be.
This can be changed via the Engine::set_max_array_size
method, with zero being
unlimited (the default).
A script attempting to create an array literal larger than the maximum will terminate with a parse error.
Any script operation that produces an array or a BLOB larger than the maximum also terminates the script with an error result.
This check can be disabled via the unchecked
feature for higher performance (but higher risks as well).
let mut engine = Engine::new();
engine.set_max_array_size(500); // allow arrays only up to 500 items
engine.set_max_array_size(0); // allow unlimited arrays
Be conservative when setting a maximum limit and always consider the fact that a registered function may grow an array’s or BLOB’s size without Rhai noticing until the very end.
For instance, the built-in +
operator for arrays and BLOB’s concatenates two of them together
to form one larger array or BLOB; if both sources are slightly below the maximum size limit,
the result may be almost twice the maximum size.
As a malicious script may also create a deeply-nested array which consumes huge amounts of memory while each individual array still stays under the maximum size limit, Rhai also recursively adds up the sizes of all strings, arrays, [blobs] and object maps contained within each array to make sure that the aggregate sizes of none of these data structures exceed their respective maximum size limits (if any).
// Small, innocent array...
let small_array = [42]; // 1-deep... 1 item, 1 array
// ... becomes huge when multiplied!
small_array.push(small_array); // 2-deep... 2 items, 2 arrays
small_array.push(small_array); // 3-deep... 4 items, 4 arrays
small_array.push(small_array); // 4-deep... 8 items, 8 arrays
small_array.push(small_array); // 5-deep... 16 items, 16 arrays
:
:
small_array.push(small_array); // <- Rhai raises an error somewhere here
small_array.push(small_array); // when the TOTAL number of items in
small_array.push(small_array); // the entire array tree exceeds limit
// Or this abomination...
let a = [ 42 ];
loop {
a[0] = a; // <- only 1 item, but infinite number of arrays
}
Maximum Size of Object Maps
Rhai by default does not limit how large (i.e. the number of properties) an object map can be.
This can be changed via Engine::set_max_map_size
, with zero being unlimited (the default).
A script attempting to create an object map literal with more properties than the maximum will terminate with a parse error.
Any script operation that produces an object map with more properties than the maximum also terminates the script with an error.
This check can be disabled via the unchecked
feature for higher performance (but higher risks as well).
let mut engine = Engine::new();
engine.set_max_map_size(500); // allow object maps with only up to 500 properties
engine.set_max_map_size(0); // allow unlimited object maps
Be conservative when setting a maximum limit and always consider the fact that a registered function may grow an object map’s size without Rhai noticing until the very end.
For instance, the built-in +
operator for object maps concatenates two object maps together to
form one larger object map; if both object maps are slightly below the maximum size limit, the
resultant object map may be almost twice the maximum size.
As a malicious script may create a deeply-nested object map which consumes huge amounts of memory while each individual object map still stays under the maximum size limit, Rhai also recursively adds up the sizes of all strings, arrays and object maps contained within each object map to make sure that the aggregate sizes of none of these data structures exceed their respective maximum size limits (if any).
// Small, innocent object map...
let small_map: #{ x: 42 }; // 1-deep... 1 item, 1 object map
// ... becomes huge when multiplied!
small_map.y = small_map; // 2-deep... 2 items, 2 object maps
small_map.y = small_map; // 3-deep... 4 items, 4 object maps
small_map.y = small_map; // 4-deep... 8 items, 8 object maps
small_map.y = small_map; // 5-deep... 16 items, 16 object maps
:
:
small_map.y = small_map; // <- Rhai raises an error somewhere here
small_map.y = small_map; // when the TOTAL number of items in
small_map.y = small_map; // the entire array tree exceeds limit
// Or this abomination...
let map = #{ x: 42 };
loop {
map.x = map; // <- only 1 item, but infinite number of object maps
}
Maximum Number of Operations
In Rhai, it is trivial to construct infinite loops, or scripts that run for a very long time.
loop { ... } // infinite loop
while 1 < 2 { ... } // loop with always-true condition
Rhai by default does not limit how much time or CPU a script consumes.
This can be changed via the Engine::set_max_operations
method, with zero being
unlimited (the default).
The operations count is intended to be a very course-grained measurement of the amount of CPU that a script has consumed, allowing the system to impose a hard upper limit on computing resources.
A script exceeding the maximum operations count terminates with an error result. This can be
disabled via the unchecked
feature for higher performance (but higher risks as well).
let mut engine = Engine::new();
engine.set_max_operations(500); // allow only up to 500 operations for this script
engine.set_max_operations(0); // allow unlimited operations
The concept of one single operation in Rhai is volatile – it roughly equals one expression node, loading one variable/constant, one operator call, one iteration of a loop, or one function call etc. with sub-expressions, statements and function calls executed inside these contexts accumulated on top.
A good rule-of-thumb is that one simple non-trivial expression consumes on average 5-10 operations.
One operation can take an unspecified amount of time and real CPU cycles, depending on the particulars. For example, loading a constant consumes very few CPU cycles, while calling an external Rust function, though also counted as only one operation, may consume much more computing resources.
To help visualize, think of an operation as roughly equals to one instruction of a hypothetical CPU which includes specialized instructions, such as function call, load module etc., each taking up one CPU cycle to execute.
Maximum Number of Variables
Rhai by default does not limit how many variables/constants can be defined within a single Scope
.
This can be changed via the Engine::set_max_variables
method. Notice that setting the
maximum number of variables to zero does not indicate unlimited variables, but disallows
defining any variable altogether.
A script attempting to define more than the maximum number of variables/constants will terminate with an error result.
This check can be disabled via the unchecked
feature for higher performance (but higher risks as well).
let mut engine = Engine::new();
engine.set_max_variables(5); // allow defining only up to 5 variables
engine.set_max_variables(0); // disallow defining any variable (maximum = zero)
engine.set_max_variables(1000); // set to a large number for effectively unlimited variables
It is possible to reuse a variable such that it is counted only once.
let x = 42; // counted as 1 variable
let y = 123;
let x = 0; // previous 'x' reused: not counted as new variable
Maximum Number of Functions
Rhai by default does not limit how many functions can be defined in a script.
This can be changed via the Engine::set_max_functions
method. Notice that setting the
maximum number of functions to zero does not indicate unlimited functions, but disallows
defining any scripted function altogether.
A script attempting to load more than the maximum number of functions will terminate with a parse error.
This check can be disabled via the unchecked
feature for higher performance (but higher risks as well).
let mut engine = Engine::new();
engine.set_max_functions(5); // allow defining only up to 5 functions
engine.set_max_functions(0); // disallow defining function (maximum = zero)
engine.set_max_functions(1000); // set to a large number for effectively unlimited functions
Maximum Number of Modules
Rhai by default does not limit how many modules can be loaded via import
statements.
This can be changed via the Engine::set_max_modules
method. Notice that setting the
maximum number of modules to zero does not indicate unlimited modules, but disallows loading any
module altogether.
A script attempting to load more than the maximum number of modules will terminate with an error result.
This limit can also be used to stop import
-loops (i.e. cycles of modules referring to
each other).
This check can be disabled via the unchecked
feature for higher performance (but higher risks as well).
let mut engine = Engine::new();
engine.set_max_modules(5); // allow loading only up to 5 modules
engine.set_max_modules(0); // disallow loading any module (maximum = zero)
engine.set_max_modules(1000); // set to a large number for effectively unlimited modules
Maximum Call Stack Depth
In Rhai, it is trivial for a function call to perform infinite recursion (or a very deeply-nested recursion) such that all stack space is exhausted.
// This is a function that, when called, recurses forever.
fn recurse_forever() {
recurse_forever();
}
The main stack-size of a program is not determined by Rust but is platform-dependent.
See this on-line Rust docs for more details.
Because of its intended embedded usage, Rhai, by default, limits function calls to a maximum depth of 64 levels (8 levels in debug build) in order to fit into most platforms’ default stack sizes.
This limit may be changed via the Engine::set_max_call_levels
method.
A script exceeding the maximum call stack depth will terminate with an error result.
This check can be disabled via the unchecked
feature for higher performance (but higher risks as well).
let mut engine = Engine::new();
engine.set_max_call_levels(10); // allow only up to 10 levels of function calls
engine.set_max_call_levels(0); // allow no function calls at all (max depth = zero)
When setting this limit, care must be also be taken to the evaluation depth of each statement within a function.
It is entirely possible for a malicious script to embed a recursive call deep inside a nested expression or statements block (see maximum statement depth).
fn bad_function(n) {
// Bail out long before reaching the limit
if n > 10 {
return;
}
// Nest many, many levels deep...
if check_1() {
if check_2() {
if check_3() {
if check_4() {
:
if check_n() {
bad_function(n+1); // <- recursive call!
}
:
}
}
}
}
}
// The function call below may still overflow the stack!
bad_function(0);
While the stack size of a program’s main thread is platform-specific, Rust defaults to a stack size of 2MB for spawned threads.
This default can further be changed such that a spawned thread has as large a stack as needed.
See the on-line Rust docs for more details.
Therefore, in order to relax the stack size limit for scripts, run the Engine
in a separate
spawned thread with a larger stack.
Maximum Expression Nesting Depth
Rhai by default limits statement and expression nesting to a maximum depth of 64 (which should be plenty) when they are at global level, but only a depth of 32 when they are within function bodies.
For debug builds, these limits are set further downwards to 32 and 16 respectively.
That is because it is possible to overflow the Engine
’s stack when it tries to recursively parse
an extremely deeply-nested code stream.
// The following, if long enough, can easily cause stack overflow during parsing.
let a = (1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(...)+1)))))))))));
This limit may be changed via Engine::set_max_expr_depths
.
There are two limits to set, one for the maximum depth at global level, and the other for function bodies.
A script exceeding the maximum nesting depths will terminate with a parse error. The malicious
AST
will not be able to get past parsing in the first place.
This check can be disabled via the unchecked
feature for higher performance (but higher risks as well).
let mut engine = Engine::new();
engine.set_max_expr_depths(50, 5); // allow nesting up to 50 layers of expressions/statements
// at global level, but only 5 inside functions
Multiple layers of expressions may be generated for a simple language construct, even though it may correspond to only one AST node.
That is because the Rhai parser internally runs a recursive chain of function calls and it is important that a malicious script does not panic the parser in the first place.
Functions are placed under stricter limits because of the multiplicative effect of recursion.
A function can effectively call itself while deep inside an expression chain within the function body, thereby overflowing the stack even when the level of recursion is within limit.
fn deep_calc(a, n) {
(a+(a+(a+(a+(a+(a+(a+(a+(a+ ... (a+deep_calc(a,n+1)) ... )))))))))
// ^^^^^^^^^^^^^^^^ recursive call!
}
let a = 42;
let result = (a+(a+(a+(a+(a+(a+(a+(a+(a+ ... (a+deep_calc(a,0)) ... )))))))));
In the contrived example above, each recursive call to the function deep_calc
adds the total
number of nested expression layers to Rhai’s evaluation stack. Sooner or later (most likely sooner
than the limit for maximum depth of function calls is reached), a stack
overflow can be expected.
In general, make sure that C x ( 5 + F ) + S
layered calls do not cause a stack overflow, where:
C
= maximum call stack depth,F
= maximum statement depth for functions,S
= maximum statement depth at global level.
Turning Off Safety Checks
Checked Arithmetic
By default, all arithmetic calculations in Rhai are checked, meaning that the script terminates with a runtime error whenever it detects a numeric over-flow/under-flow condition or an invalid floating-point operation.
This checking can be turned off via the unchecked
feature for higher performance (but higher
risks as well).
let x = 1_000_000_000_000;
x * x; // Normal build - runtime error: multiplication overflow
x * x; // 'unchecked' debug build - panic!
// 'unchecked' release build - overflow with no error
x / 0; // Normal build - runtime error: division by zero
x / 0; // 'unchecked' build - panic!
Other Safety Checks
In addition to overflows, there are many other safety checks performed by Rhai at runtime.
unchecked
turns them all off as well, such as…
Infinite loops
// Normal build - runtime error: exceeds maximum number of operations
loop {
foo();
}
// 'unchecked' build - never terminates!
loop {
foo();
}
Infinite recursion
fn foo() {
foo();
}
foo(); // Normal build - runtime error: exceeds maximum stack depth
foo(); // 'unchecked' build - panic due to stack overflow!
Gigantic data structures
let x = [];
// Normal build - runtime error: array exceeds maximum size
loop {
x += 42;
}
// 'unchecked' build - panic due to out-of-memory!
loop {
x += 42;
}
Improper range iteration
// Normal build - runtime error: zero step
for x in range(0, 10, 0) { ... }
// 'unchecked' build - never terminates!
for x in range(0, 10, 0) { ... }
// Normal build - empty range
for x in range(0, 10, -1) { ... }
// 'unchecked' build - panic due to numeric underflow!
for x in range(0, 10, -1) { ... }
Script Optimization
Rhai includes an optimizer that tries to optimize a script after parsing. This can reduce resource utilization and increase execution speed.
Script optimization can be turned off via the no_optimize
feature.
Optimization Levels
There are three levels of optimization: None
, Simple
and Full
.
The default is Simple
.
An Engine
’s optimization level is set via Engine::set_optimization_level
.
// Turn on aggressive optimizations
engine.set_optimization_level(rhai::OptimizationLevel::Full);
None
None
is obvious – no optimization on the AST is performed.
Simple
(Default)
Simple
performs only relatively safe optimizations without causing side-effects (i.e. it only
relies on static analysis and built-in operators for constant standard types, and will not
perform any external function calls).
Overriding a built-in operator in the Engine
afterwards has no effect after the
optimizer replaces an expression with its calculated value.
Full
Full
is much more aggressive, including calling external functions on constant arguments to
determine their results.
One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators.
Optimization Passes
Script optimization is performed via multiple passes. Each pass does a specific optimization.
The optimization is completed when no passes can simplify the AST
any further.
Built-in Optimization Passes
Pass | Description |
---|---|
Dead code elimination | Eliminates code that cannot be reached |
Constants propagation | Replaces constants with values |
Compound assignments rewrite | Rewrites assignments into compound assignments |
Eager operator evaluation | Eagerly calls operators with constant arguments |
Eager function evaluation | Eagerly calls functions with constant arguments |
Dead Code Elimination
Nobody deliberately writes scripts with dead code (we hope).
They are, however, extremely common in template-based machine-generated scripts.
Rhai attempts to eliminate dead code.
“Dead code” is code that does nothing and has no side effects.
Example is an pure expression by itself as a statement (allowed in Rhai). The result of the expression is calculated then immediately discarded and not used.
{
let x = 999; // NOT eliminated: variable may be used later on (perhaps even an 'eval')
123; // eliminated: no effect
"hello"; // eliminated: no effect
[1, 2, x, 4]; // eliminated: no effect
if 42 > 0 { // '42 > 0' is replaced by 'true' and the first branch promoted
foo(42); // promoted, NOT eliminated: the function 'foo' may have side-effects
} else {
bar(x); // eliminated: branch is never reached
}
let z = x; // eliminated: local variable, no side-effects, and only pure afterwards
666 // NOT eliminated: this is the return value of the block,
// and the block is the last one so this is the return value of the whole script
}
The above script optimizes to:
{
let x = 999;
foo(42);
666
}
Constants Propagation
Constants propagation is commonly used to:
-
remove dead code,
-
avoid variable lookups,
-
pre-calculate constant expressions.
const ABC = true;
const X = 41;
if ABC || calc(X+1) { print("done!"); } // 'ABC' is constant so replaced by 'true'...
// 'X' is constant so replaced by 41...
if true || calc(42) { print("done!"); } // '41+1' is replaced by 42
// since '||' short-circuits, 'calc' is never called
if true { print("done!"); } // <- the line above is equivalent to this
print("done!"); // <- the line above is further simplified to this
// because the condition is always true
Constant values can be provided in a custom Scope
object to the Engine
for optimization purposes.
use rhai::{Engine, Scope};
let engine = Engine::new();
let mut scope = Scope::new();
// Add constant to custom scope
scope.push_constant("ABC", true);
// Evaluate script with custom scope
engine.run_with_scope(&mut scope,
r#"
if ABC { // 'ABC' is replaced by 'true'
print("done!");
}
"#)?;
Constants defined in modules that are registered into an Engine
via
Engine::register_global_module
are used in optimization.
use rhai::{Engine, Module};
let mut engine = Engine::new();
let mut module = Module::new();
// Add constant to module
module.set_var("ABC", true);
// Register global module
engine.register_global_module(module.into());
// Evaluate script
engine.run(
r#"
if ABC { // 'ABC' is replaced by 'true'
print("done!");
}
"#)?;
Constants defined at global level typically cannot be seen by script functions because they are pure.
const MY_CONSTANT = 42; // <- constant defined at global level
print(MY_CONSTANT); // <- optimized to: print(42)
fn foo() {
MY_CONSTANT // <- not optimized: 'foo' cannot see 'MY_CONSTANT'
}
print(foo()); // error: 'MY_CONSTANT' not found
When constants are provided in a custom Scope
(e.g. via Engine::compile_with_scope
,
Engine::eval_with_scope
or Engine::run_with_scope
), or in a module registered via
Engine::register_global_module
, instead of defined within the same script, they are also
propagated to functions.
This is usually the intuitive usage and behavior expected by regular users, even though it means that a script will behave differently (essentially a runtime error) when script optimization is disabled.
use rhai::{Engine, Scope};
let engine = Engine::new();
let mut scope = Scope::new();
// Add constant to custom scope
scope.push_constant("MY_CONSTANT", 42_i64);
engine.run_with_scope(&mut scope,
"
print(MY_CONSTANT); // optimized to: print(42)
fn foo() {
MY_CONSTANT // optimized to: fn foo() { 42 }
}
print(foo()); // prints 42
")?;
The script will act differently when script optimization is disabled because script functions
are pure and typically cannot see constants within the custom Scope
.
Therefore, constants in functions now throw a runtime error.
use rhai::{Engine, Scope, OptimizationLevel};
let mut engine = Engine::new();
// Turn off script optimization, no constants propagation is performed
engine.set_optimization_level(OptimizationLevel::None);
let mut scope = Scope::new();
// Add constant to custom scope
scope.push_constant("MY_CONSTANT", 42_i64);
engine.run_with_scope(&mut scope,
"
print(MY_CONSTANT); // prints 42
fn foo() {
MY_CONSTANT // <- 'foo' cannot see 'MY_CONSTANT'
}
print(foo()); // error: 'MY_CONSTANT' not found
")?;
Constants propagation replaces each usage of the constant with a clone of its value.
This may have negative implications to performance if the constant value is expensive to clone (e.g. if the type is very large).
let mut scope = Scope::new();
// Push a large constant into the scope...
let big_type = AVeryLargeType::take_long_time_to_create();
scope.push_constant("MY_BIG_TYPE", big_type);
// Causes each usage of 'MY_BIG_TYPE' in the script below to be replaced
// by cloned copies of 'AVeryLargeType'.
let result = engine.run_with_scope(&mut scope,
"
let value = MY_BIG_TYPE.value;
let data = MY_BIG_TYPE.data;
let len = MY_BIG_TYPE.len();
let has_options = MY_BIG_TYPE.has_options();
let num_options = MY_BIG_TYPE.options_len();
")?;
To avoid this, compile the script first to an AST
without the constants, then evaluate the
AST
(e.g. with Engine::eval_ast_with_scope
or Engine::run_ast_with_scope
) together with
the constants.
If the constants are modified later on (yes, it is possible, via Rust methods), the modified values will not show up in the optimized script. Only the initialization values of constants are ever retained.
const MY_SECRET_ANSWER = 42;
MY_SECRET_ANSWER.update_to(666); // assume 'update_to(&mut i64)' is a Rust function
print(MY_SECRET_ANSWER); // prints 42 because the constant is propagated
This is almost never a problem because real-world scripts seldom modify a constant, but the possibility is always there.
Compound Assignment Rewrite
Usually, a compound assignment (e.g. +=
for append) takes a mutable first parameter
(i.e. &mut
) while the corresponding simple operator (i.e. +
) does not.
The script optimizer rewrites normal assignments into compound assignments wherever possible in order to avoid unnecessary cloning.
let big = create_some_very_big_type();
big = big + 1;
// ^ 'big' is cloned here
// The above is equivalent to:
let temp_value = big + 1;
big = temp_value;
big += 1; // <- 'big' is NOT cloned
Only simple variable references are optimized.
No common sub-expression elimination is performed by Rhai.
x = x + 1; // <- this statement...
x += 1; // <- ... is rewritten to this
x[y] = x[y] + 1; // <- but this is not,
// so MUCH slower...
x[y] += 1; // <- ... than this
Eager Operator Evaluation
Most operators are actually function calls, and those functions can be overridden, so whether they are optimized away depends on the situation:
Rhai guarantees that no external function will be run, which may trigger side-effects
(unless the optimization level is OptimizationLevel::Full
).
-
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.
// The following is most likely generated by machine.
const DECISION = 1; // this is an integer, one of the standard types
if DECISION == 1 { // this is optimized into 'true'
:
} else if DECISION == 2 { // this is optimized into 'false'
:
} else if DECISION == 3 { // this is optimized into 'false'
:
} else {
:
}
// Or an equivalent using 'switch':
switch DECISION {
1 => ..., // this statement is promoted
2 => ..., // this statement is eliminated
3 => ..., // this statement is eliminated
_ => ... // this statement is eliminated
}
Pre-Evaluation of Constant Expressions
Because of the eager evaluation of operators for standard types, many constant expressions will be evaluated and replaced by the result.
let x = (1+2) * 3 - 4/5 % 6; // will be replaced by 'let x = 9'
let y = (1 > 2) || (3 <= 4); // will be replaced by 'let y = true'
For operators that are not optimized away due to one of the above reasons, the function calls are simply left behind.
// Assume 'new_state' returns some custom type that is NOT one of the standard types.
// Also assume that the '==' operator is defined for that custom type.
const DECISION_1 = new_state(1);
const DECISION_2 = new_state(2);
const DECISION_3 = new_state(3);
if DECISION == 1 { // NOT optimized away because the operator is not built-in
: // and may cause side-effects if called!
:
} else if DECISION == 2 { // same here, NOT optimized away
:
} else if DECISION == 3 { // same here, NOT optimized away
:
} else {
:
}
Alternatively, turn the optimizer to OptimizationLevel::Full
.
Eager Function Evaluation When Using Full Optimization Level
When the optimization level is OptimizationLevel::Full
, the Engine
assumes all functions to
be pure and will eagerly evaluated all function calls with constant arguments, using the result
to replace the call.
This also applies to all operators (which are implemented as functions).
// When compiling the following with OptimizationLevel::Full...
const DECISION = 1;
// this condition is now eliminated because 'sign(DECISION) > 0'
if sign(DECISION) > 0 // <- is a call to the 'sign' and '>' functions, and they return 'true'
{
print("hello!"); // <- this block is promoted to the parent level
} else {
print("boo!"); // <- this block is eliminated because it is never reached
}
print("hello!"); // <- the above is equivalent to this
// ('print' and 'debug' are handled specially)
Yes! Very!
// Nuclear silo control
if launch_nukes && president_okeyed {
print("This is NOT a drill!");
update_defcon(1);
start_world_war(3);
launch_all_nukes();
} else {
print("This is a drill. Thank you for your cooperation.");
}
In the script above (well… as if nuclear silos will one day be controlled by Rhai scripts),
the functions update_defcon
, start_world_war
and launch_all_nukes
will be evaluated
during compilation because they have constant arguments.
The variables launch_nukes
and president_okeyed
are never checked, because the script
actually has not yet been run! The functions are called during compilation.
This is, obviously, not what you want.
Moral of the story: compile with an Engine
that does not have any functions registered.
Register functions AFTER compilation.
Good question! There are two reasons:
-
A function call may result in cleaner code than the resultant value. In Rust, this would have been handled via a
const
function. -
Evaluating a value to a custom type that has no representation in script.
// A complex function that returns a unique ID based on the arguments
let id = make_unique_id(123, "hello", true);
// The above is arguably clearer than:
// let id = 835781293546; // generated from 123, "hello" and true
// A custom type that cannot be represented in script
let complex_obj = make_complex_obj(42);
Side-Effect Considerations for Full Optimization Level
All of Rhai’s built-in functions (and operators which are implemented as functions) are pure
(i.e. they do not mutate state nor cause any side-effects, with the exception of print
and debug
which are handled specially) so using OptimizationLevel::Full
is usually quite safe unless
custom types and functions are registered.
If custom functions are registered, they may be called (or maybe not, if the calls happen to lie within a pruned code block).
If custom functions are registered to overload built-in operators, they will also be called when
the operators are used (in an if
statement, for example), potentially causing side-effects.
-
Always register custom types and functions after compiling scripts if
OptimizationLevel::Full
is used. -
DO NOT depend on knowledge that the functions have no side-effects, because those functions can change later on and, when that happens, existing scripts may break in subtle ways.
Volatility Considerations for Full Optimization Level
Even if a custom function does not mutate state nor cause side-effects, it may still be volatile, i.e. it depends on external environment and does not guarantee the same result for the same inputs.
A perfect example is a function that gets the current time – obviously each run will return a different value!
print(get_current_time(true)); // prints the current time
// notice the call to 'get_current_time'
// has constant arguments
// The above, under full optimization level, is rewritten to:
print("10:25AM"); // the function call is replaced by
// its result at the time of optimization!
Avoid using OptimizationLevel::Full
if volatile custom functions are involved.
The optimizer, when using OptimizationLevel::Full
, merrily assumes that all functions are
non-volatile, so when it finds constant arguments (or none) it eagerly executes the function
call and replaces it with the result.
This causes the script to behave differently from the intended semantics.
All native functions are assumed to be non-volatile, meaning that they are eagerly called under
OptimizationLevel::Full
when all arguments are constant (or none).
It is possible to mark a function defined within a plugin module as volatile to prevent this behavior.
#[export_module]
mod my_module {
// This function is marked 'volatile' and will not be
// eagerly executed even under OptimizationLevel::Full.
#[rhai_fn(volatile)]
pub get_current_time(am_pm: bool) -> String {
// ...
}
}
Subtle Semantic Changes After Optimization
Some optimizations can alter subtle semantics of the script, causing the script to behave differently when run with or without optimization.
Typically, this involves some form of error that may arise in the original, unoptimized script but is optimized away by the script optimizer.
Needless to say, it is usually a Very Bad Idea™ to depend on a script failing with a runtime error or such kind of subtleties.
If it turns out to be necessary (why? I would never guess), turn script optimization off by setting
the optimization level to OptimizationLevel::None
.
Disappearing Runtime Errors
For example:
if true { // condition always true
123.456; // eliminated
hello; // eliminated, EVEN THOUGH the variable doesn't exist!
foo(42) // promoted up-level
}
foo(42) // <- the above optimizes to this
If the original script were evaluated instead, it would have been an error –
the variable hello
does not exist, so the script would have been terminated at that point
with a runtime error.
In fact, any errors inside a statement that has been eliminated will silently disappear.
print("start!");
if my_decision { /* do nothing... */ } // eliminated due to no effect
print("end!");
// The above optimizes to:
print("start!");
print("end!");
In the script above, if my_decision
holds anything other than a boolean value,
the script should have been terminated due to a type error.
However, after optimization, the entire if
statement is removed (because an access to
my_decision
produces no side-effects), thus the script silently runs to completion without errors.
Eliminated Useless Work
Another example is more subtle – that of an empty loop body.
// ... say, the 'Engine' is limited to no more than 10,000 operations...
// The following should fail because it exceeds the operations limit:
for n in 0..42000 {
// empty loop
}
// The above is optimized away because the loop body is empty
// and the iterations simply do nothing.
()
Normally, and empty loop body inside a for
statement with a pure iterator does nothing and can
be safely eliminated.
Thus the script now runs silently to completion without errors.
Without optimization, the script may fail by exceeding the maximum number of operations allowed.
Re-Optimize an AST
Sometimes it is more efficient to store one single, large script with delimited code blocks guarded by
constant variables. This script is compiled once to an AST
.
Then, depending on the execution environment, constants are passed into the Engine
and the
AST
is re-optimized based on those constants via Engine::optimize_ast
, effectively pruning
out unused code sections.
The final, optimized AST
is then used for evaluations.
// Compile master script to AST
let master_ast = engine.compile(
"
fn do_work() {
// Constants in scope are also propagated into functions
print(SCENARIO);
}
switch SCENARIO {
1 => do_work(),
2 => do_something(),
3 => do_something_else(),
_ => do_nothing()
}
")?;
for n in 0..5_i64 {
// Create a new scope - put constants in it to aid optimization
let mut scope = Scope::new();
scope.push_constant("SCENARIO", n);
// Re-optimize the AST
let new_ast = engine.optimize_ast(&scope, master_ast.clone(), OptimizationLevel::Simple);
// Run it
engine.run_ast(&new_ast)?;
}
Manage AST
’s
When compiling a Rhai script to an AST
, the following data are packaged together as a single unit:
Data | Type | Description | Requires feature | Access API |
---|---|---|---|---|
Source name | ImmutableString | optional text name to identify the source of the script | source(&self) ,clone_source(&self) ,set_source(&mut self, source) ,clear_source(&mut self) | |
Module documentation | Vec<SmartString> | documentation of the script | metadata | doc(&self) ,clear_doc(&mut self) |
Statements | Vec<Stmt> | list of script statements at global level | internals | statements(&self) ,statements_mut(&mut self) |
Functions | Shared<Module> | functions defined in the script | internals ,not no_function | shared_lib(&self) |
Embedded module resolver | StaticModuleResolver | embedded module resolver for self-contained AST | internals ,not no_module | resolver(&self) |
Most of the AST
API is available only under the internals
feature.
Use the source name to identify the source script in errors – useful when multiple modules are imported recursively.
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
:
Method | Description |
---|---|
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
:
Method | Description |
---|---|
clone_statements_only(&self) | clone the AST into a new AST with only the statements, excluding functions |
clear_statements(&mut self) | remove all statements from the AST , leaving only functions |
iter_literal_variables(&self, constants, variables) | return an iterator on all top-level literal constant and/or variable definitions in the AST |
Merge and Combine AST’s
The following methods merge one AST
with another:
Method | Description |
---|---|
merge(&self, &ast) ,+ operator | append the second AST to this AST , yielding a new AST that is a combination of the two; statements are simply appended, functions in the second AST of the same name and arity override similar functions in this AST |
merge_filtered(&self, &ast, filter) | append the second AST (but only functions that pass the predicate filter) to this AST , yielding a new AST that is a combination of the two; statements are simply appended, functions in the second AST of the same name and arity override similar functions in this AST |
combine(&mut self, ast) ,+= operator | append the second AST to this AST ; statements are simply appended, functions in the second AST of the same name and arity override similar functions in this AST |
combine_filtered(&mut self, ast, filter) | append the second AST (but only functions that pass the predicate filter) to this AST ; statements are simply appended, functions in the second AST of the same name and arity override similar functions in this AST |
When statements are appended, beware that this may change the semantics of the script.
// First script
let ast1 = engine.compile(
"
fn foo(x) { 42 + x }
foo(1)
")?;
// Second script
let ast2 = engine.compile(
"
fn foo(n) { `hello${n}` }
foo("!")
")?;
// Merge them
let merged = ast1.merge(&ast2);
// Notice that using the '+' operator also works:
let merged = &ast1 + &ast2;
merged
in the above example essentially contains the following script program:
fn foo(n) { `hello${n}` } // <- definition of first 'foo' is overwritten
foo(1) // <- notice this will be "hello1" instead of 43,
// but it is no longer the return value
foo("!") // <- returns "hello!"
Walk an AST
The internals
feature allows access to internal Rhai data structures, particularly the nodes
that make up the AST
.
AST node types
There are a few useful types when walking an AST
:
Type | Description |
---|---|
ASTNode | an enum with two variants: Expr or Stmt |
Expr | an expression |
Stmt | a statement |
BinaryExpr | a sub-type containing the LHS and RHS of a binary expression |
FnCallExpr | a sub-type containing information on a function call |
CustomExpr | a sub-type containing information on a custom syntax expression |
The AST::walk
method takes a callback function and recursively walks the AST
in depth-first
manner, with the parent node visited before its children.
Callback function signature
The signature of the callback function takes the following form.
FnMut(&[ASTNode]) -> bool
The single argument passed to the method contains a slice of ASTNode
types representing the path
from the current node to the root of the AST
.
Return true
to continue walking the AST
, or false
to terminate.
Children visit order
The order of visits to the children of each node type:
Node type | Children visit order |
---|---|
if statement |
|
switch statement |
|
while , do , loop statement |
|
for statement |
|
return statement | return value expression |
throw statement | exception value expression |
try … catch statement |
|
import statement | path expression |
Array literal | each of the element expressions, in order |
Object map literal | each of the element expressions, in order |
Interpolated string | each of the string/expression segments, in order |
Indexing |
|
Field access/method call |
|
&& , || , ?? |
|
Function call, operator expression | each of the argument expressions, in order |
let , const statement | value expression |
Assignment statement |
|
Statements block | each of the statements, in order |
Custom syntax expression | each of the inputs stream, in order |
All others | single child (if any) |
Use the Low-Level API to Register a Rust Function
When a native Rust function is registered with an Engine
using the register_XXX
API, Rhai
transparently converts all function arguments from Dynamic
into the correct types before calling
the function.
For more power and flexibility, there is a low-level API to work directly with Dynamic
values
without the conversions.
Raw Function Registration
The Engine::register_raw_fn
method is marked volatile, meaning that it may be changed without warning.
If this is acceptable, then using this method to register a Rust function opens up more opportunities.
engine.register_raw_fn(
"increment_by", // function name
&[ // a slice containing parameter types
std::any::TypeId::of::<i64>(), // type of first parameter
std::any::TypeId::of::<i64>() // type of second parameter
],
|context, args| { // fixed function signature
// Arguments are guaranteed to be correct in number and of the correct types.
// But remember this is Rust, so you can keep only one mutable reference at any one time!
// Therefore, get a '&mut' reference to the first argument _last_.
// Alternatively, use `args.split_first_mut()` etc. to split the slice first.
let y = *args[1].read_lock::<i64>().unwrap(); // get a reference to the second argument
// then copy it because it is a primary type
let y = args[1].take().cast::<i64>(); // alternatively, directly 'consume' it
let y = args[1].as_int().unwrap(); // alternatively, use 'as_xxx()'
let x = args[0].write_lock::<i64>().unwrap(); // get a '&mut' reference to the first argument
*x += y; // perform the action
Ok(Dynamic::UNIT) // must be 'Result<Dynamic, Box<EvalAltResult>>'
}
);
// The above is the same as (in fact, internally they are equivalent):
engine.register_fn("increment_by", |x: &mut i64, y: i64| *x += y);
Function Signature
The function signature passed to Engine::register_raw_fn
takes the following form.
Fn(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<T, Box<EvalAltResult>>
where:
Parameter | Type | Description |
---|---|---|
T | impl Clone | return type of the function |
context | NativeCallContext | the current native call context, useful for recursively calling functions on the same Engine |
args | &mut [&mut Dynamic] | a slice containing &mut references to Dynamic values.The slice is guaranteed to contain enough arguments of the correct types. |
Return value
The return value is the result of the function call.
Remember, in Rhai, all arguments except the first one are always passed by value (i.e. cloned). Therefore, it is unnecessary to ever mutate any argument except the first one, as all mutations will be on the cloned copy.
Extract The First &mut
Argument (If Any)
To extract the first &mut
argument passed by reference from the args
parameter (&mut [&mut Dynamic]
),
use the following to get a mutable reference to the underlying value:
let value: &mut T = &mut *args[0].write_lock::<T>().unwrap();
*value = ... // overwrite the existing value of the first `&mut` parameter
When there is a mutable reference to the first &mut
argument, there can be no other immutable
references to args
, otherwise the Rust borrow checker will complain.
Therefore, always extract the mutable reference last, after all other arguments are taken.
Extract Other Pass-By-Value Arguments
To extract an argument passed by value from the args
parameter (&mut [&mut Dynamic]
), use the following statements.
Argument type | Access statement (n = argument position) | Result | Original value |
---|---|---|---|
INT | args[n].as_int().unwrap() | INT | untouched |
FLOAT | args[n].as_float().unwrap() | FLOAT | untouched |
Decimal | args[n].as_decimal().unwrap() | Decimal | untouched |
bool | args[n].as_bool().unwrap() | bool | untouched |
char | args[n].as_char().unwrap() | char | untouched |
() | args[n].as_unit().unwrap() | () | untouched |
String | &*args[n].as_immutable_string_ref().unwrap() | &ImmutableString | untouched |
String (consumed) | args[n].take().cast::<ImmutableString>() | ImmutableString | () |
Others | &*args[n].read_lock::<T>().unwrap() | &T | untouched |
Others (consumed) | args[n].take().cast::<T>() | T | () |
Example – Pass a Callback to a Rust Function
The low-level API is useful when there is a need to interact with the scripting Engine
within a function.
The following example registers a function that takes a function pointer as an argument,
then calls it within the same Engine
. This way, a callback function can be provided
to a native Rust function.
The example also showcases the use of FnPtr::call_raw
, a low-level API which allows binding the
this
pointer to the function pointer call.
use rhai::{Engine, FnPtr};
let mut engine = Engine::new();
// Register a Rust function
engine.register_raw_fn(
"bar",
&[
std::any::TypeId::of::<i64>(), // parameter types
std::any::TypeId::of::<FnPtr>(),
std::any::TypeId::of::<i64>(),
],
|context, args| {
// 'args' is guaranteed to contain enough arguments of the correct types
let fp = args[1].take().cast::<FnPtr>(); // 2nd argument - function pointer
let value = args[2].take(); // 3rd argument - function argument
// The 1st argument holds the 'this' pointer.
// This should be done last as it gets a mutable reference to 'args'.
let this_ptr = args.get_mut(0).unwrap();
// Use 'FnPtr::call_raw' to call the function pointer with the context
// while also binding the 'this' pointer!
fp.call_raw(&context, Some(this_ptr), [value])
},
);
let result = engine.eval::<i64>(
r#"
fn foo(x) { this += x; } // script-defined function 'foo'
let x = 41; // object
x.bar(foo, 1); // pass 'foo' as function pointer
x
"#)?;
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:
-
If it is a primary type other than string, use
as_xxx()
as above; -
Directly consume that argument via
arg[i].take()
as above. -
Use
split_first_mut
to partition the slice:
// Partition the slice
let (first, rest) = args.split_first_mut().unwrap();
// Mutable reference to the first parameter, of type '&mut A'
let this_ptr = &mut *first.write_lock::<A>().unwrap();
// Immutable reference to the second value parameter, of type '&B'
// This can be mutable but there is no point because the parameter is passed by value
let value_ref = &*rest[0].read_lock::<B>().unwrap();
EvalContext
Many functions in advanced API’s contain a parameter of type EvalContext
in order to allow the
current evaluation state to be accessed and/or modified.
EvalContext
encapsulates the current evaluation context and exposes the following methods.
Method | Return type | Description |
---|---|---|
scope() | &Scope | reference to the current Scope |
scope_mut() | &mut Scope | mutable reference to the current Scope ; variables can be added to/removed from it |
engine() | &Engine | reference to the current Engine |
source() | Option<&str> | reference to the current source, if any |
tag() | &Dynamic | reference to the custom state that is persistent during the current run |
tag_mut() | &mut Dynamic | mutable reference to the custom state that is persistent during the current run |
iter_imports() | impl Iterator<Item = (&str, &Module )> | iterator of the current stack of modules imported via import statements, in reverse order (i.e. later modules come first); not available under no_module |
global_runtime_state() | &GlobalRuntimeState | reference to the current global runtime state (including the stack of modules imported via import statements) |
global_runtime_state_mut() | &mut &mut GlobalRuntimeState | mutable reference to the current global runtime state; use this to access the debugger field in order to set/clear break-points |
iter_namespaces() | impl Iterator<Item = &Module > | iterator of the namespaces (as modules) containing all script-defined functions, in reverse order (i.e. later modules come first) |
namespaces() | &[&Module] | reference to the namespaces (as modules) containing all script-defined functions |
this_ptr() | Option<&Dynamic> | reference to the current bound this pointer, if any |
this_ptr_mut() | &mut Option<&mut Dynamic> | mutable reference to the current bound this pointer, if any |
call_level() | usize | the current nesting level of function calls |
Call a Function Within the Caller’s Scope
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 actually run the function call within the Scope
of the parent caller – i.e. the Scope
that makes the function call – and
access/mutate variables defined there.
fn foo(y) { // function accesses 'x' and 'y', but 'x' is not defined
x += y; // 'x' is modified in this function
let z = 0; // 'z' is defined in this function's scope
x
}
let x = 1; // 'x' is defined here in the parent scope
foo(41); // error: variable 'x' not found
// Calling a function with a '!' causes it to run within the caller's scope
foo!(41) == 42; // the function can access and mutate the value of 'x'!
x == 42; // 'x' is changed!
z == 0; // <- error: variable 'z' not found
x.method!(); // <- syntax error: not allowed in method-call style
// Also works for function pointers
let f = foo; // <- de-sugars to 'Fn("foo")'
call!(f, 42) == 84; // must use function-call style
x == 84; // 'x' is changed once again
f.call!(41); // <- syntax error: not allowed in method-call style
// But not allowed for module functions
import "hello" as h;
h::greet!(); // <- syntax error: not allowed in namespace-qualified calls
Functions relying on the calling Scope
is often a Very Bad Idea™ because it makes code
almost impossible to reason about and maintain, as their behaviors are volatile and unpredictable.
Rhai functions are normally pure, meaning that you can rely on the fact that they never mutate the outside environment. Using this syntax breaks this guarantee.
Functions called in this manner behave more like macros that are expanded inline than actual function calls, thus the syntax is also similar to Rust’s macro invocations.
This usage should be at the last resort.
YOU HAVE BEEN WARNED.
Use Rhai in Dynamic Libraries
Sometimes functions registered into a Rhai Engine
come from dynamic libraries (a.k.a. shared
libraries in Linux or DLL’s in Windows), which are compiled separately from the main binary, not
statically linked but loaded dynamically at runtime.
The project rhai-dylib
demonstrates an API for creating dynamically-loadable libraries
for use with a Rhai Engine
.
Problem Symptom
The symptom is usually Function Not Found errors even though the relevant functions (within the
dynamic library) have already been registered into the Engine
.
This usually happens when a mutable reference to the Engine
is passed into an entry-point
function exposed by the dynamic library and the Engine
’s function registration API is called
inside the dynamic library.
Problem Cause
To counter DOS attacks, the hasher used by Rhai, ahash
, automatically generates a different
seed for hashing during each compilation and execution run.
This means that hash values generated by the hasher will not be stable – they change during each compile, and during each run.
This creates hash mismatches between the main binary and the loaded dynamic library because, as they are not linked together at compile time, two independent copies of the hasher reside in them, resulting in different hashes for even the same function signature.
Solution
Use static hashing for force predictable hashes.
Use Rhai as a Domain-Specific Language (DSL)
Rhai can be successfully used as a domain-specific language (DSL).
Expressions Only
In many DSL scenarios, only evaluation of expressions is needed.
The Engine::eval_expression_XXX
API can be used to restrict a script to
expressions only.
Unicode Standard Annex #31 Identifiers
Variable names and other identifiers do not necessarily need to be ASCII-only.
The unicode-xid-ident
feature, when turned on, causes Rhai to allow variable names and
identifiers that follow Unicode Standard Annex #31.
This is sometimes useful in a non-English DSL.
Disable Keywords and/or Operators
In some DSL scenarios, it is necessary to further restrict the language to exclude certain language features that are not necessary or dangerous to the application.
For example, a DSL may disable the while
loop while keeping all other statement types intact.
It is possible, in Rhai, to surgically disable keywords and operators.
Custom Operators
Some DSL scenarios require special operators that make sense only for that specific environment. In such cases, it is possible to define custom operators in Rhai.
let animal = "rabbit";
let food = "carrot";
animal eats food // custom operator 'eats'
eats(animal, food) // <- the above actually de-sugars to this
let x = foo # bar; // custom operator '#'
let x = #(foo, bar) // <- the above actually de-sugars to this
Although a custom operator always de-sugars to a simple function call, nevertheless it makes the DSL syntax much simpler and expressive.
Custom Syntax
For advanced DSL scenarios, it is possible to define entire expression syntax – essentially custom statement types.
For example, the following is a SQL-like syntax for some obscure DSL operation:
let table = [..., ..., ..., ...];
// Syntax = calculate $ident$ ( $expr$ -> $ident$ ) => $ident$ : $expr$
let total = calculate sum(table->price) => row : row.weight > 50;
// Note: There is nothing special about those symbols; to make it look exactly like SQL:
// Syntax = SELECT $ident$ ( $ident$ ) AS $ident$ FROM $expr$ WHERE $expr$
let total = SELECT sum(price) AS row FROM table WHERE row.weight > 50;
After registering this custom syntax with Rhai, it can be used anywhere inside a script as a normal expression.
For its evaluation, the callback function will receive the following list of inputs:
inputs[0] = "sum"
– math operatorinputs[1] = "price"
– field nameinputs[2] = "row"
– loop variable nameinputs[3] = Expression(table)
– data sourceinputs[4] = Expression(row.weight > 50)
– filter predicate
Other identifiers, such as "calculate"
, "FROM"
, as well as symbols such as ->
and :
etc.,
are parsed in the order defined within the custom syntax.
Remap Tokens During Parsing
The Rhai Engine
first parses a script into a stream of tokens.
Tokens have the type Token
which is only exported under internals
.
The function Engine::on_parse_token
, available only under internals
, allows registration of a
mapper function that converts (remaps) a Token
into another.
Since it is called for every token parsed from the script, this token mapper function can also be used to implement safety checks against, say, stack-overflow or out-of-memory situations during parsing.
See here for more details.
Function Signature
Raise a parse error by returning Token::LexError
as the mapped token.
The function signature passed to Engine::on_parse_token
takes the following form.
Fn(token: Token, pos: Position, state: &TokenizeState) -> Token
where:
Parameter | Type | Description |
---|---|---|
token | Token | the next symbol parsed |
pos | Position | location of the token |
state | &TokenizeState | current state of the tokenizer |
Example
use rhai::{Engine, FLOAT, Token};
let mut engine = Engine::new();
// Register a token mapper function.
engine.on_parse_token(|token, pos, state| {
match token {
// Change 'begin' ... 'end' to '{' ... '}'
Token::Identifier(s) if &s == "begin" => Token::LeftBrace,
Token::Identifier(s) if &s == "end" => Token::RightBrace,
// Change all integer literals to floating-point
Token::IntegerConstant(n) => Token::FloatConstant((n as FLOAT).into()),
// Disallow '()'
Token::Unit => Token::LexError(
LexError::ImproperSymbol("()".to_string(), "".to_string()).into()
),
// Pass through all other tokens unchanged
_ => token
}
});
Disable Certain Keywords and/or Operators
For certain embedded usage, it is sometimes necessary to restrict the language to a strict subset of Rhai to prevent usage of certain language features.
Rhai supports surgically disabling a keyword or operator via Engine::disable_symbol
.
use rhai::Engine;
let mut engine = Engine::new();
engine
.disable_symbol("if") // disable the 'if' keyword
.disable_symbol("+="); // disable the '+=' operator
// The following all return parse errors.
engine.compile("let x = if true { 42 } else { 0 };")?;
// ^ 'if' is rejected as a reserved keyword
engine.compile("let x = 40 + 2; x += 1;")?;
// ^ '+=' is not recognized as an operator
// ^ other operators are not affected
Disable Looping
For certain scripts, especially those in embedded usage for straight calculations, or where Rhai
script AST
’s are eventually transcribed into some other instruction set, looping may be
undesirable as it may not be supported by the application itself.
Rhai looping constructs include the while
, loop
, do
and for
statements.
Although it is possible to disable these keywords via
Engine::disable_symbol
, it is simpler to disable all looping
via Engine::set_allow_looping
.
use rhai::Engine;
let mut engine = Engine::new();
// Disable looping
engine.set_allow_looping(false);
// The following all return parse errors.
engine.compile("while x == y { x += 1; }")?;
engine.compile(r#"loop { print("hello world!"); }"#)?;
engine.compile("do { x += 1; } until x > 10;")?;
engine.compile("for n in 0..10 { print(n); }")?;
Custom Operators
See this section for details on operator precedence.
For use as a DSL (Domain-Specific Languages), it is sometimes more convenient to augment Rhai with customized operators performing specific logic.
Engine::register_custom_operator
registers a keyword as a custom operator, giving it a particular
precedence (which cannot be zero).
Support for custom operators can be disabled via the no_custom_syntax
feature.
Example
use rhai::Engine;
let mut engine = Engine::new();
// Register a custom operator '#' and give it a precedence of 160
// (i.e. between +|- and *|/)
// Also register the implementation of the custom operator as a function
engine.register_custom_operator("#", 160)?
.register_fn("#", |x: i64, y: i64| (x * y) - (x + y));
// The custom operator can be used in expressions
let result = engine.eval_expression::<i64>("1 + 2 * 3 # 4 - 5 / 6")?;
// ^ custom operator
// The above is equivalent to: 1 + ((2 * 3) # 4) - (5 / 6)
result == 15;
Alternatives to a Custom Operator
Custom operators are merely syntactic sugar. They map directly to registered functions.
let mut engine = Engine::new();
// Define 'foo' operator
engine.register_custom_operator("foo", 160)?;
engine.eval::<i64>("1 + 2 * 3 foo 4 - 5 / 6")?; // use custom operator
engine.eval::<i64>("1 + foo(2 * 3, 4) - 5 / 6")?; // <- above is equivalent to this
A script using custom operators can always be pre-processed, via a pre-processor application, into a syntax that uses the corresponding function calls.
Using Engine::register_custom_operator
merely enables a convenient shortcut.
Must be a Valid Identifier or Reserved Symbol
All custom operators must be identifiers that follow the same naming rules as variables.
Alternatively, they can also be reserved symbols, disabled operators or keywords.
engine.register_custom_operator("foo", 20)?; // 'foo' is a valid custom operator
engine.register_custom_operator("#", 20)?; // the reserved symbol '#' is also
// a valid custom operator
engine.register_custom_operator("+", 30)?; // <- error: '+' is an active operator
engine.register_custom_operator("=>", 30)?; // <- error: '=>' is an active symbol
Binary Operators Only
All custom operators must be binary (i.e. they take two operands). Unary custom operators are not supported.
// Register unary '#' operator
engine.register_custom_operator("#", 160)?
.register_fn("#", |x: i64| x * x);
engine.eval::<i64>("# 42")?; // <- syntax error
Operator Precedence
All operators in Rhai has a precedence indicating how tightly they bind.
A higher precedence binds more tightly than a lower precedence, so *
and /
binds before +
and -
etc.
When registering a custom operator, the operator’s precedence must also be provided.
The following precedence table shows the built-in precedence of standard Rhai operators:
Category | Operators | Binding | Precedence (0-255) |
---|---|---|---|
Logic and bit masks | || , | , ^ | left | 30 |
Logic and bit masks | && , & | left | 60 |
Comparisons | == , != | left | 90 |
Containment | in | left | 110 |
Comparisons | > , >= , < , <= | left | 130 |
Null-coalesce | ?? | left | 135 |
Ranges | .. , ..= | left | 140 |
Arithmetic | + , - | left | 150 |
Arithmetic | * , / , % | left | 180 |
Arithmetic | ** | right | 190 |
Bit-shifts | << , >> | left | 210 |
Unary operators | + , - , ! | right | highest |
Object field access | . , ?. | 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:
Stick with standard language syntax as much as possible.
Having to learn Rhai is bad enough, no sane user would ever want to learn yet another obscure language syntax just to do something.
Try custom operators first. A custom syntax should be considered a last resort.
-
Where 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.
Custom syntax can be disabled via the no_custom_syntax
feature.
How to Do It
Step One – Design The Syntax
A custom syntax is simply a list of symbols.
These symbol types can be used:
- Standard keywords
- Standard operators
- Reserved symbols.
- Identifiers following the variable naming rules.
$expr$
– any valid expression, statement or statements block.$block$
– any valid statements block (i.e. must be enclosed by{
…}
).$func$
– any valid closure, or any valid statements block as the body of a closure with no parameters (if notno_function
).$ident$
– any variable name.$symbol$
– any symbol, active or reserved.$bool$
– a boolean value.$int$
– an integer number.$float$
– a floating-point number (if notno_float
).$string$
– a string literal.
The first symbol must be an identifier
There is no specific limit on the combination and sequencing of each symbol type, except the first symbol which must be a custom keyword that follows the naming rules of variables.
The first symbol also cannot be a normal keyword unless it is disabled. Any valid identifier that is not an active keyword works fine, even if it is a reserved keyword.
The first symbol must be unique
Rhai uses the first symbol as a clue to parse custom syntax.
Therefore, at any one time, there can only be one custom syntax starting with each unique symbol.
Any new custom syntax definition using the same first symbol simply overwrites the previous one.
Example
exec [ $ident$ $symbol$ $int$ ] <- $expr$ : $block$
The above syntax is made up of a stream of symbols:
Position | Input slot | Symbol | Description |
---|---|---|---|
1 | exec | custom keyword | |
2 | [ | the left bracket symbol | |
2 | 0 | $ident$ | a variable name |
3 | 1 | $symbol$ | the operator |
4 | 2 | $int$ | an integer number |
5 | ] | the right bracket symbol | |
6 | <- | the left-arrow symbol (which is a reserved symbol in Rhai). | |
7 | 3 | $expr$ | an expression, which may be enclosed with { … } , or not. |
8 | : | the colon symbol | |
9 | 4 | $block$ | a statements block, which must be enclosed with { … } . |
This syntax matches the following sample code and generates five inputs (one for each non-keyword):
// Assuming the 'exec' custom syntax implementation declares the variable 'hello':
let x = exec [hello < 42] <- foo(1, 2) : {
hello += bar(hello);
baz(hello);
};
print(x); // variable 'x' has a value returned by the custom syntax
print(hello); // variable declared by a custom syntax persists!
Step Two – Implementation
Any custom syntax must include an implementation of it.
Function signature
The signature of an implementation function is as follows.
Fn(context: &mut EvalContext, inputs: &[Expression]) -> Result<Dynamic, Box<EvalAltResult>>
where:
Parameter | Type | Description |
---|---|---|
context | &mut EvalContext | mutable reference to the current evaluation context |
inputs | &[Expression] | a list of input expression trees |
and EvalContext
is a type that encapsulates the current evaluation context.
Return value
Return value is the result of evaluating the custom syntax expression.
Access arguments
The most important argument is inputs
where the matched identifiers ($ident$
), expressions/statements ($expr$
)
and statements blocks ($block$
) are provided.
To access a particular argument, use the following patterns:
Argument type | Pattern (n = slot in inputs ) | Result type | Description |
---|---|---|---|
$ident$ | inputs[n].get_string_value().unwrap() | &str | variable name |
$symbol$ | inputs[n].get_literal_value::<ImmutableString>().unwrap() | ImmutableString | symbol literal |
$expr$ | &inputs[n] | &Expression | an expression tree |
$block$ | &inputs[n] | &Expression | an expression tree |
$func$ | &inputs[n] | &Expression | an expression tree (output is a function pointer) |
$bool$ | inputs[n].get_literal_value::<bool>().unwrap() | bool | boolean value |
$int$ | inputs[n].get_literal_value::<INT>().unwrap() | INT | integer number |
$float$ | inputs[n].get_literal_value::<FLOAT>().unwrap() | FLOAT | floating-point number |
$string$ | inputs[n].get_literal_value::<ImmutableString>().unwrap() inputs[n].get_string_value().unwrap() | ImmutableString &str | string text |
Get literal constants
Several argument types represent literal constants that can be obtained directly via
Expression::get_literal_value<T>
or Expression::get_string_value
(for strings).
let expression = &inputs[0];
// Use 'get_literal_value' with a turbo-fish type to extract the value
let string_value = expression.get_literal_value::<ImmutableString>().unwrap();
let string_slice = expression.get_string_value().unwrap();
let float_value = expression.get_literal_value::<FLOAT>().unwrap();
// Or assign directly to a variable with type...
let int_value: i64 = expression.get_literal_value().unwrap();
// Or use type inference!
let bool_value = expression.get_literal_value().unwrap();
if bool_value { ... } // 'bool_value' inferred to be 'bool'
Evaluate an expression tree
Use the EvalContext::eval_expression_tree
method to evaluate an arbitrary expression tree
within the current evaluation context.
let expression = &inputs[0];
let result = context.eval_expression_tree(expression)?;
Retain variables in block scope
When an expression tree actually contains a statements block (i.e. $block
), local
variables/constants defined within that block are usually removed at the end of the block.
Sometimes it is useful to retain these local variables/constants for further processing (e.g. collecting new variables into an object map).
As such, evaluate the expression tree using the EvalContext::eval_expression_tree_raw
method which
contains a parameter to control whether the statements block should be rewound.
// Assume 'expression' contains a statements block with local variable definitions
let expression = &inputs[0];
let result = context.eval_expression_tree_raw(expression, false)?;
// Variables defined within 'expression' persist in context.scope()
Declare variables
New variables/constants maybe declared (usually with a variable name that is passed in via $ident$
).
It can simply be pushed into the Scope
.
let var_name = inputs[0].get_string_value().unwrap();
let expression = &inputs[1];
context.scope_mut().push(var_name, 0_i64); // declare new variable
let result = context.eval_expression_tree(expression)?;
Step Three – Register the Custom Syntax
Use Engine::register_custom_syntax
to register a custom syntax.
Again, beware that the first symbol must be unique. If there already exists a custom syntax starting with that symbol, the previous syntax will be overwritten.
The syntax is passed simply as a slice of &str
.
// Custom syntax implementation
fn implementation_func(context: &mut EvalContext, inputs: &[Expression]) -> Result<Dynamic, Box<EvalAltResult>> {
let var_name = inputs[0].get_string_value().unwrap();
let stmt = &inputs[1];
let condition = &inputs[2];
// Push new variable into the scope BEFORE 'context.eval_expression_tree'
context.scope_mut().push(var_name.to_string(), 0_i64);
let mut count = 0_i64;
loop {
// Evaluate the statements block
context.eval_expression_tree(stmt)?;
count += 1;
// Declare a new variable every three turns...
if count % 3 == 0 {
context.scope_mut().push(format!("{var_name}{count}"), count);
}
// Evaluate the condition expression
let expr_result = !context.eval_expression_tree(condition)?;
match expr_result.as_bool() {
Ok(true) => (),
Ok(false) => break,
Err(err) => return Err(EvalAltResult::ErrorMismatchDataType(
"bool".to_string(),
err.to_string(),
condition.position(),
).into()),
}
}
Ok(Dynamic::UNIT)
}
// Register the custom syntax (sample): exec<x> -> { x += 1 } while x < 0
engine.register_custom_syntax(
[ "exec", "<", "$ident$", ">", "->", "$block$", "while", "$expr$" ], // the custom syntax
true, // variables declared within this custom syntax
implementation_func
)?;
Remember that a custom syntax acts as an expression, so it can show up practically anywhere:
// Use as an expression:
let foo = (exec<x> -> { x += 1 } while x < 42) * 100;
// New variables are successfully declared...
x == 42;
x3 == 3;
x6 == 6;
// Use as a function call argument:
do_something(exec<x> -> { x += 1 } while x < 42, 24, true);
// Use as a statement:
exec<x> -> { x += 1 } while x < 0;
// ^ terminate statement with ';' unless the custom
// syntax already ends with '}'
Step Four – Disable Unneeded Statement Types
When a DSL needs a custom syntax, most likely than not it is extremely specialized. Therefore, many statement types actually may not make sense under the same usage scenario.
So, while at it, better disable those built-in keywords and operators that should not be used by the user. The would leave only the bare minimum language surface exposed, together with the custom syntax that is tailor-designed for the scenario.
A keyword or operator that is disabled can still be used in a custom syntax.
In an extreme case, it is possible to disable every keyword in the language, leaving only custom syntax (plus possibly expressions). But again, Don’t Do It™ – unless you are certain of what you’re doing.
Step Five – Document
For custom syntax, documentation is crucial.
Make sure there are lots of examples for users to follow.
Step Six – Profit!
Practical Example – Matrix Literal
Say you’d want to use something like ndarray
to manipulate matrices.
However, you’d like to write matrix literals in a more intuitive syntax than an array of arrays.
In other words, you’d like to turn:
// Array of arrays
let matrix = [ [ a, b, 0 ],
[ -b, a, 0 ],
[ 0, 0, c * d ] ];
into:
// Directly parse to an ndarray::Array (look ma, no commas!)
let matrix = @| a b 0 |
| -b a 0 |
| 0 0 c*d |;
This can easily be done via a custom syntax, which yields a syntax that is more pleasing.
// Disable the '|' symbol since it'll conflict with the bit-wise OR operator.
// Do this BEFORE registering the custom syntax.
engine.disable_symbol("|");
engine.register_custom_syntax(
["@", "|", "$expr$", "$expr$", "$expr$", "|",
"|", "$expr$", "$expr$", "$expr$", "|",
"|", "$expr$", "$expr$", "$expr$", "|"
],
false,
|context, inputs| {
use ndarray::arr2;
let mut values = [[0.0; 3]; 3];
for y in 0..3 {
for x in 0..3 {
let offset = y * 3 + x;
match context.eval_expression_tree(&inputs[offset])?.as_float() {
Ok(v) => values[y][x] = v,
Err(typ) => return Err(Box::new(EvalAltResult::ErrorMismatchDataType(
"float".to_string(), typ.to_string(),
inputs[offset].position()
)))
}
}
}
let matrix = arr2(&values);
Ok(Dynamic::from(matrix))
},
)?;
For matrices of flexible dimensions, check out custom syntax parsers.
Practical Example – Defining Temporary Variables
It is possible to define temporary variables/constants which are available only to code blocks within the custom syntax.
engine.register_custom_syntax(
[ "with", "offset", "(", "$expr$", ",", "$expr$", ")", "$block$", ],
true, // must be true in order to define new variables
|context, inputs| {
// Get the two offsets
let x = context.eval_expression_tree(&inputs[0])?.as_int().map_err(|typ| Box::new(
EvalAltResult::ErrorMismatchDataType("integer".to_string(), typ.to_string(), inputs[0].position())
))?;
let y = context.eval_expression_tree(&inputs[1])?.as_int().map_err(|typ| Box::new(
EvalAltResult::ErrorMismatchDataType("integer".to_string(), typ.to_string(), inputs[1].position())
))?;
// Add them as temporary constants into the scope, available only to the code block
let orig_len = context.scope().len();
context.scope_mut().push_constant("x", x);
context.scope_mut().push_constant("y", y);
// Run the code block
let result = context.eval_expression_tree(&inputs[2]);
// Remove the temporary constants from the scope so they don't leak outside
context.scope_mut().rewind(orig_len);
// Return the result
result
},
)?;
Practical Example – Recreating C’s Ternary Operator
Rhai has if-expressions, but sometimes a C-style ternary operator is more concise.
// A custom syntax must start with a unique symbol, so we use 'iff'.
// Register the custom syntax: iff condition ? true-value : false-value
engine.register_custom_syntax(
["iff", "$expr$", "?", "$expr$", ":", "$expr$"],
false,
|context, inputs| match context.eval_expression_tree(&inputs[0])?.as_bool() {
Ok(true) => context.eval_expression_tree(&inputs[1]),
Ok(false) => context.eval_expression_tree(&inputs[2]),
Err(typ) => Err(Box::new(EvalAltResult::ErrorMismatchDataType(
"bool".to_string(), typ.to_string(), inputs[0].position()
))),
},
)?;
The code in the example above is essentially what the if
statement does internally, and since
custom syntax is pre-parsed, there really is no performance penalty!
Practical Example – Recreating JavaScript’s var
Statement
The following example recreates a statement similar to the var
variable declaration syntax in
JavaScript, which creates a global variable if one doesn’t already exist.
There is currently no equivalent in Rhai.
// Register the custom syntax: var x = ???
engine.register_custom_syntax([ "var", "$ident$", "=", "$expr$" ], true, |context, inputs| {
let var_name = inputs[0].get_string_value().unwrap().to_string();
let expr = &inputs[1];
// Evaluate the expression
let value = context.eval_expression_tree(expr)?;
// Push a new variable into the scope if it doesn't already exist.
// Otherwise just set its value.
if !context.scope().is_constant(var_name).unwrap_or(false) {
context.scope_mut().set_value(var_name.to_string(), value);
Ok(Dynamic::UNIT)
} else {
Err(format!("variable {} is constant", var_name).into())
}
})?;
Really Advanced – Custom Parsers
Sometimes it is desirable to have multiple custom syntax starting with the same symbol.
This is especially common for command-style syntax where the second symbol calls a particular command:
// The following simulates a command-style syntax, all starting with 'perform'.
perform hello world; // A fixed sequence of symbols
perform action 42; // Perform a system action with a parameter
perform update system; // Update the system
perform check all; // Check all system settings
perform cleanup; // Clean up the system
perform add something; // Add something to the system
perform remove something; // Delete something from the system
Alternatively, a custom syntax may have variable length, with a termination symbol:
// The following is a variable-length list terminated by '>'
tags < "foo", "bar", 123, ... , x+y, true >
For even more flexibility in order to handle these advanced use cases, there is a low level API for custom syntax that allows the registration of an entire mini-parser.
Use Engine::register_custom_syntax_with_state_raw
to register a custom syntax parser together
with an implementation function, both of which accept a custom user-defined state value.
How Custom Parsers Work
Leading Symbol
Under this API, the leading symbol for a custom parser is no longer restricted to be valid identifiers.
It can either be:
-
an identifier that isn’t a normal keyword unless disabled, or
-
a valid symbol (see list) which is not a normal operator unless disabled.
Parser Function Signature
The custom syntax parser has the following signature.
Fn(symbols: &[ImmutableString], look_ahead: &str, state: &mut Dynamic) -> Result<Option<ImmutableString>, ParseError>
where:
Parameter | Type | Description |
---|---|---|
symbols | &[ImmutableString] | a slice of symbols that have been parsed so far, possibly containing $expr$ and/or $block$ ; $ident$ and other literal markers are replaced by the actual text |
look_ahead | &str | a string slice containing the next symbol that is about to be read |
state | &mut Dynamic | mutable reference to a user-defined state |
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.
Parameter #1 – Symbols Parsed So Far
The symbols parsed so far are provided as a slice of ImmutableString
s.
The custom parser can inspect this symbols stream to determine the next symbol to parse.
Argument type | Value |
---|---|
text string | text value |
$ident$ | identifier name |
$symbol$ | symbol literal |
$expr$ | $expr$ |
$block$ | $block$ |
$func$ | $func$ |
$bool$ | true or false |
$int$ | value of number |
$float$ | value of number |
$string$ | string text |
Parameter #2 – Look-Ahead Symbol
The look-ahead symbol 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.
Parameter #3 – User-Defined Custom State
The state’s value starts off as ()
.
Its type is Dynamic
, possible to hold any value.
Usually it is set to an object map that contains information on the state of parsing.
Return value
The return value is Result<Option<ImmutableString>, ParseError>
where:
Value | Description |
---|---|
Ok(None) | parsing is complete and there is no more symbol to match |
Ok(Some(symbol)) | the next symbol to match, which can also be $expr$ , $ident$ , $block$ etc. |
Err(error) | error that is reflected back to the Engine – normally ParseError( ParseErrorType::BadInput( LexError::ImproperSymbol(message) ), Position::NONE) to indicate that there is a syntax error, but it can be any ParseError . |
A custom parser always returns Some
with the next symbol expected (which can be $ident$
,
$expr$
, $block$
etc.) or None
if parsing should terminate (without reading the
look-ahead symbol).
The $$
return symbol short-cut
A return symbol starting with $$
is treated specially.
Like None
, it also terminates parsing, but at the same time it adds this symbol as text into the
inputs stream at the end.
This is typically used to inform the implementation function which custom syntax variant was actually parsed.
fn implementation_fn(context: &mut EvalContext, inputs: &[Expression], state: &Dynamic) -> Result<Dynamic, Box<EvalAltResult>>
{
// Get the last symbol
let key = inputs.last().unwrap().get_string_value().unwrap();
// Make sure it starts with '$$'
assert!(key.starts_with("$$"));
// Execute the custom syntax expression
match key {
"$$hello" => { ... }
"$$world" => { ... }
"$$foo" => { ... }
"$$bar" => { ... }
_ => Err(...)
}
}
$$
is a convenient short-cut. An alternative method is to pass such information in the user-defined
custom state.
Implementation Function Signature
The signature of an implementation function for Engine::register_custom_syntax_with_state_raw
is
as follows, which is slightly different from the function for Engine::register_custom_syntax
.
Fn(context: &mut EvalContext, inputs: &[Expression], state: &Dynamic) -> Result<Dynamic, Box<EvalAltResult>>
where:
Parameter | Type | Description |
---|---|---|
context | &mut EvalContext | mutable reference to the current evaluation context |
inputs | &[Expression] | a list of input expression trees |
state | &Dynamic | reference to the user-defined state |
Custom Parser Example
engine.register_custom_syntax_with_state_raw(
// The leading symbol - which needs not be an identifier.
"perform",
// The custom parser implementation - always returns the next symbol expected
// 'look_ahead' is the next symbol about to be read
//
// Return symbols starting with '$$' also terminate parsing but allows us
// to determine which syntax variant was actually parsed so we can perform the
// appropriate action. This is a convenient short-cut to keeping the value
// inside the state.
//
// The return type is 'Option<ImmutableString>' to allow common text strings
// to be interned and shared easily, reducing allocations during parsing.
|symbols, look_ahead, state| match symbols.len() {
// perform ...
1 => Ok(Some("$ident$".into())),
// perform command ...
2 => match symbols[1].as_str() {
"action" => Ok(Some("$expr$".into())),
"hello" => Ok(Some("world".into())),
"update" | "check" | "add" | "remove" => Ok(Some("$ident$".into())),
"cleanup" => Ok(Some("$$cleanup".into())),
cmd => Err(LexError::ImproperSymbol(format!("Improper command: {cmd}"))
.into_err(Position::NONE)),
},
// perform command arg ...
3 => match (symbols[1].as_str(), symbols[2].as_str()) {
("action", _) => Ok(Some("$$action".into())),
("hello", "world") => Ok(Some("$$hello-world".into())),
("update", arg) => match arg {
"system" => Ok(Some("$$update-system".into())),
"client" => Ok(Some("$$update-client".into())),
_ => Err(LexError::ImproperSymbol(format!("Cannot update {arg}"))
.into_err(Position::NONE))
},
("check", arg) => Ok(Some("$$check".into())),
("add", arg) => Ok(Some("$$add".into())),
("remove", arg) => Ok(Some("$$remove".into())),
(cmd, arg) => Err(LexError::ImproperSymbol(
format!("Invalid argument for command {cmd}: {arg}")
).into_err(Position::NONE)),
},
_ => unreachable!(),
},
// No variables declared/removed by this custom syntax
false,
// Implementation function
|context, inputs, state| {
let cmd = inputs.last().unwrap().get_string_value().unwrap();
match cmd {
"$$cleanup" => { ... }
"$$action" => { ... }
"$$update-system" => { ... }
"$$update-client" => { ... }
"$$check" => { ... }
"$$add" => { ... }
"$$remove" => { ... }
_ => Err(format!("Invalid command: {cmd}"))
}
}
);
Debugging Interface
For systems open to external user-created scripts, it is usually desirable to provide a debugging
experience to the user. The alternative is to provide a custom implementation of debug
via
Engine::on_debug
that traps debug output to show in a side panel, for example, which is actually
extremely simple.
Nevertheless, in some systems, it may not be convenient, or even possible, for the user to debug his
or her scripts simply via good-old print
or debug
statements – the system does not
have any facility for printed output, for instance.
Or the system may require more advanced debugging facilities than mere print
statements –
such as break-points.
For these advanced scenarios, Rhai contains a Debugging interface, turned on via the debugging
feature (which implies the internals
feature).
The debugging interface resides under the debugger
sub-module.
The rhai-dbg
bin tool shows a simple example of
employing the debugging interface to create a debugger for Rhai scripts!
Built-in Functions
The following functions (defined in the DebuggingPackage
but excluded when
using a raw Engine
) provides runtime information for debugging purposes.
Function | Parameter(s) | Not available under | Description |
---|---|---|---|
back_trace | none | no_function , no_index | returns an array of object maps or strings, each containing one level of function call;returns an empty array if no debugger is registered |
// This recursive function prints its own call stack during each run
fn foo(x) {
print(back_trace()); // prints the current call stack
if x > 0 {
foo(x - 1)
}
}
Register with the Debugger
Hooking up a debugging interface is as simple as providing closures to the Engine
’s built-in
debugger via Engine::register_debugger
.
use rhai::debugger::{ASTNode, DebuggerCommand};
let mut engine = Engine::new();
engine.register_debugger(
// Provide a callback to initialize the debugger state
|engine, mut debugger| {
debugger.set_state(...);
debugger
},
// Provide a callback for each debugging step
|context, event, node, source, pos| {
...
DebuggerCommand::StepOver
}
);
The type debugger::Debugger
allows for manipulating break-points, among others.
The Engine
’s debugger instance can be accessed via context.global_runtime_state().debugger()
(immutable)
or context.global_runtime_state_mut().debugger_mut()
(mutable).
Callback Functions Signature
There are two callback functions to register for the debugger.
The first is simply a function to initialize the state of the debugger with the following signature.
Fn(&Engine, debugger::Debugger) -> debugger::Debugger
The second callback is a function which will be called by the debugger during each step, with the following signature.
Fn(context: EvalContext, event: debugger::DebuggerEvent, node: ASTNode, source: &str, pos: Position) -> Result<debugger::DebuggerCommand, Box<EvalAltResult>>
where:
Parameter | Type | Description |
---|---|---|
context | EvalContext | the current evaluation context |
event | DebuggerEvent | an enum indicating the event that triggered the debugger |
node | ASTNode | an enum with two variants: Expr or Stmt , corresponding to the current expression node or statement node in the AST |
source | &str | the source of the current AST , or empty if none |
pos | Position | position of the current node, same as node.position() |
and EvalContext
is a type that encapsulates the current evaluation context.
Event
The event
parameter of the second closure passed to Engine::register_debugger
contains a
debugger::DebuggerEvent
which is an enum
with the following variants.
DebuggerEvent variant | Description |
---|---|
Start | the debugger is triggered at the beginning of evaluation |
Step | the debugger is triggered at the next step of evaluation |
BreakPoint( n) | the debugger is triggered by the n-th break-point |
FunctionExitWithValue( r) | the debugger is triggered by a function call returning with value r which is &Dynamic |
FunctionExitWithError( err) | the debugger is triggered by a function call exiting with error err which is &EvalAltResult |
End | the debugger is triggered at the end of evaluation |
Return value
When a script starts evaluation, the debugger always stops at the very first AST
node
with the event
parameter set to DebuggerStatus::Start
.
This allows initialization to be done (e.g. setting up break-points).
The second closure passed to Engine::register_debugger
will be called when stepping into or over
expressions and statements, or when break-points are hit.
The return type of the closure is Result<debugger::DebuggerCommand, Box<EvalAltResult>>
.
If an error is returned, the script evaluation at that particular instance returns with that
particular error. It is thus possible to abort the script evaluation by returning an error that is
not catchable, such as EvalAltResult::ErrorTerminated
.
If no error is returned, then the return debugger::DebuggerCommand
variant determines the
continued behavior of the debugger.
DebuggerCommand variant | Behavior | gdb equivalent |
---|---|---|
Continue | continue with normal script evaluation | continue |
StepInto | run to the next expression or statement, diving into functions | step |
StepOver | run to the next expression or statement, skipping over functions | |
Next | run to the next statement, skipping over functions | next |
FunctionExit | run to the end of the current function call; debugger is triggered before the function call returns and the Scope cleared | finish |
Debugger State
Sometimes it is useful to keep a persistent state within the debugger.
The Engine::register_debugger
API accepts a function that returns the initial value of the
debugger’s state, which is a Dynamic
and can hold any value.
This state value is the stored into the debugger’s custom state.
Access the Debugger State
Use EvalContext::global_runtime_state().debugger()
(immutable) or
EvalContext::global_runtime_state_mut().debugger_mut()
(mutable) to gain access to the current
debugger::Debugger
instance.
The following debugger::Debugger
methods allow access to the custom debugger state.
Method | Parameter type | Return type | Description |
---|---|---|---|
state | none | &Dynamic | returns the custom state |
state_mut | none | &mut Dynamic | returns a mutable reference to the custom state |
set_state | impl Into<Dynamic> | none | sets the value of the custom state |
Example
engine.register_debugger(
|engine, mut debugger| {
// Say, use an object map for the debugger state
let mut state = Map::new();
// Initialize properties
state.insert("hello".into(), 42_64.into());
state.insert("foo".into(), false.into());
debugger.set_state(state);
debugger
},
|context, node, source, pos| {
// Print debugger state - which is an object map
let state = context.global_runtime_state().debugger().state();
println!("Current state = {state}");
// Get the state as an object map
let mut state = context.global_runtime_state_mut()
.debugger_mut().state_mut()
.write_lock::<Map>().unwrap();
// Read state
let hello = state.get("hello").unwrap().as_int().unwrap();
// Modify state
state.insert("hello".into(), (hello + 1).into());
state.insert("foo".into(), true.into());
state.insert("something_new".into(), "hello, world!".into());
// Continue with debugging
Ok(DebuggerCommand::StepInto)
}
);
Call Stack
Each “frame” in the call stack corresponds to one layer of function call (script-defined or native Rust).
A call stack frame has the type debugger::CallStackFrame
.
The debugger keeps a call stack of function calls with argument values.
This call stack can be examined to determine the control flow at any particular point.
The Debugger::call_stack
method returns a slice of all call stack frames.
use rhai::debugger::*;
let debugger = &mut context.global_runtime_state().debugger();
// Get depth of the call stack.
let depth = debugger.call_stack().len();
// Display all function calls
for frame in debugger.call_stack().iter() {
println!("{frame}");
}
Break-Points
A break-point always stops the current evaluation and calls the debugging callback.
A break-point is represented by the debugger::BreakPoint
type, which is an enum
with
the following variants.
BreakPoint variant | Not available under | Description |
---|---|---|
AtPosition { source, pos, enabled } | no_position | breaks at the specified position in the specified source (empty if none); if pos is at beginning of line, breaks anywhere on the line |
AtFunctionName { name, enabled } | breaks when a function matching the specified name is called (can be operator) | |
AtFunctionCall { name, args, enabled } | breaks when a function matching the specified name (can be operator) and the specified number of arguments is called | |
AtProperty { name, enabled } | no_object | breaks at the specified property access |
Access Break-Points
The following debugger::Debugger
methods allow access to break-points for manipulation.
Method | Return type | Description |
---|---|---|
break_points | &[BreakPoint] | returns a slice of all BreakPoint ’s |
break_points_mut | &mut Vec<BreakPoint> | returns a mutable reference to all BreakPoint ’s |
Example
use rhai::debugger::*;
let debugger = &mut context.global_runtime_state_mut().debugger_mut();
// Get number of break-points.
let num_break_points = debugger.break_points().len();
// Add a new break-point on calls to 'foo(_, _, _)'
debugger.break_points_mut().push(
BreakPoint::AtFunctionCall { name: "foo".into(), args: 3 }
);
// Display all break-points
for bp in debugger.break_points().iter() {
println!("{bp}");
}
// Clear all break-points
debugger.break_points_mut().clear();
Implement a Debugging Server
Sometimes it is desirable to embed a debugging server inside the application such that an external debugger interface can connect to the application’s running instance at runtime.
This way, when scripts are run within the application, it is easy for an external interface to debug those scripts as they run.
Such connections may take the form of any communication channel, for example a TCP/IP connection, a named pipe, or an MPSC channel.
Example
Server side
The following example assumes bi-direction, blocking messaging channels, such as a WebSocket connection, with a server that accepts connections and creates those channels.
use rhai::debugger::{ASTNode, DebuggerCommand};
let mut engine = Engine::new();
engine.register_debugger(
// Use the initialization callback to set up the communications channel
// and listen to it
|engine, mut debugger| {
// Create server that will listen to requests
let mut server = MyCommServer::new();
server.listen("localhost:8080");
// Wrap it up in a shared locked cell so it can be 'Clone'
let server = Rc::new(RefCell::new(server));
// Store the channel in the debugger state
debugger.set_state(Dynamic::from(server));
debugger
},
// Trigger the server during each debugger stop point
|context, event, node, source, pos| {
// Get the state
let mut state = context.tag_mut();
// Get the server
let mut server = state.write_lock::<MyCommServer>().unwrap();
// Send the event to the server - blocking call
server.send_message(...);
// Receive command - blocking call
match server.receive_message() {
None => DebuggerCommand::StepOver,
// Decode command
Ok(...) => { ... }
Ok(...) => { ... }
Ok(...) => { ... }
Ok(...) => { ... }
Ok(...) => { ... }
:
:
}
}
);
Client side
The client can be any system that can work with WebSockets for messaging.
// Connect to the application's debugger
let webSocket = new WebSocket("wss://localhost:8080");
webSocket.on_message = (event) => {
let msg = JSON.parse(event.data);
switch msg.type {
// handle debugging events from the application...
case "step": {
:
}
:
:
}
};
// Send command to the application
webSocket.send("step-over");
Object-Oriented Programming (OOP)
Rhai does not have objects per se and is not object-oriented (in the traditional sense), but it is possible to simulate object-oriented programming.
Regardless of whether object-oriented programming (OOP) should be treated as a pattern or an anti-pattern (the programming world is split 50-50 on this), there are always users who would like to write Rhai in “the OOP way.”
Rust itself is not object-oriented in the traditional sense; JavaScript also isn’t, but that didn’t prevent generations of programmers trying to shoehorn a class-based inheritance system onto it.
So… as soon as Rhai gained in usage, way way before version 1.0, PR’s started coming in to make it possible to write Rhai in “the OOP way.”
Use Object Maps to Simulate OOP
Rhai’s object maps has special support for OOP.
Rhai concept | Maps to OOP |
---|---|
Object maps | objects |
Object map properties holding values | properties |
Object map properties that hold function pointers | methods |
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
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. It can be turned off via the no_closure
feature.
let factor = 1;
// Define the object
let obj = #{
data: 0, // object field
increment: |x| this.data += x, // 'this' binds to 'obj'
update: |x| this.data = x * factor, // 'this' binds to 'obj', 'factor' is captured
action: || print(this.data) // 'this' binds to 'obj'
};
// Use the object
obj.increment(1);
obj.action(); // prints 1
obj.update(42);
obj.action(); // prints 42
factor = 2;
obj.update(42);
obj.action(); // prints 84
Simulating Inheritance with Polyfills
The fill_with
method of object maps can be conveniently used to polyfill default method
implementations from a base class, as per OOP lingo.
Do not use the mixin
method because it overwrites existing fields.
// Define base class
let BaseClass = #{
factor: 1,
data: 42,
get_data: || this.data * 2,
update: |x| this.data += x * this.factor
};
let obj = #{
// Override base class field
factor: 100,
// Override base class method
// Notice that the base class can also be accessed, if in scope
get_data: || this.call(BaseClass.get_data) * 999,
}
// Polyfill missing fields/methods
obj.fill_with(BaseClass);
// By this point, 'obj' has the following:
//
// #{
// factor: 100
// data: 42,
// get_data: || this.call(BaseClass.get_data) * 999,
// update: |x| this.data += x * this.factor
// }
// obj.get_data() => (this.data (42) * 2) * 999
obj.get_data() == 83916;
obj.update(1);
obj.data == 142
Prototypical Inheritance via Mixin
Some languages like JavaScript has prototypical inheritance, which bases inheritance on a prototype object.
It is possible to simulate this form of inheritance using object maps, leveraging the fact that, in Rhai, all values are cloned and there are no pointers. This significantly simplifies coding logic.
// Define prototype 'class'
const PrototypeClass = #{
field: 42,
get_field: || this.field,
set_field: |x| this.field = x
};
// Create instances of the 'class'
let obj1 = PrototypeClass; // a copy of 'PrototypeClass'
obj1.get_field() == 42;
let obj2 = PrototypeClass; // a copy of 'PrototypeClass'
obj2.mixin(#{ // override fields and methods
field: 1,
get_field: || this.field * 2
};
obj2.get_field() == 2;
let obj2 = PrototypeClass + #{ // compact syntax with '+'
field: 1,
get_field: || this.field * 2
};
obj2.get_field() == 2;
// Inheritance chain
const ParentClass = #{
field: 123,
new_field: 0,
action: || print(this.new_field * this.field)
};
const ChildClass = #{
action: || {
this.field = this.new_field;
this.new_field = ();
}
}
let obj3 = PrototypeClass + ParentClass + ChildClass;
// Alternate formulation
const ParentClass = PrototypeClass + #{
field: 123,
new_field: 0,
action: || print(this.new_field * this.field)
};
const ChildClass = ParentClass + #{
action: || {
this.field = this.new_field;
this.new_field = ();
}
}
let obj3 = ChildClass; // a copy of 'ChildClass'
Scriptable Event Handler with State
In many usage scenarios, a scripting engine is used to provide flexibility in event handling.
That means to execute certain actions in response to certain events that occur at run-time, and scripts are used to provide flexibility for coding those actions.
You’d be surprised how many applications fit under this pattern – they are all essentially event handling systems.
Because of the importance of this pattern, runnable examples are included.
See the Examples section for details.
-
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.
-
State may be provided by the system or the user, or both.
-
Default implementations of event handlers can be provided.
-
An event handler object is declared that holds the following items:
-
User-provided state is initialized by a function called via
Engine::call_fn_with_options
. -
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.
Basic Infrastructure
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.
use rhai::{Engine, Scope, AST};
// Event handler
struct Handler {
// Scripting engine
pub engine: Engine,
// Use a custom 'Scope' to keep stored state
pub scope: Scope<'static>,
// Program script
pub ast: AST
}
Register API for custom types
Custom types are often used to hold state. The easiest way to register an entire API is via a plugin module.
use rhai::plugin::*;
// A custom type to a hold state value.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct TestStruct {
data: i64
}
// Plugin module containing API to TestStruct
#[export_module]
mod test_struct_api {
#[rhai_fn(global)]
pub fn new_state(value: i64) -> TestStruct {
TestStruct { data: value }
}
#[rhai_fn(global)]
pub fn func1(obj: &mut TestStruct) -> bool {
:
}
#[rhai_fn(global)]
pub fn func2(obj: &mut TestStruct) -> i64 {
:
}
pub fn process(data: i64) -> i64 {
:
}
#[rhai_fn(get = "value", pure)]
pub fn get_value(obj: &mut TestStruct) -> i64 {
obj.data
}
#[rhai_fn(set = "value")]
pub fn set_value(obj: &mut TestStruct, value: i64) {
obj.data = value;
}
}
Initialize handler object
Steps to initialize the event handler:
- Register an API with the
Engine
, - Create a custom
Scope
to serve as the stored state, - Add default state variables into the custom
Scope
, - Optionally, call an initiation function to create new state variables;
Engine::call_fn_with_options
is used instead ofEngine::call_fn
so that variables created inside the function will not be removed from the customScope
upon exit, - Get the handler script and compile it,
- Store the compiled
AST
for future evaluations, - Run the
AST
to initialize event handler state variables.
impl Handler {
// Create a new 'Handler'.
pub fn new(path: impl Into<PathBuf>) -> Self {
let mut engine = Engine::new();
// Register custom types and API's
engine.register_type_with_name::<TestStruct>("TestStruct")
.register_global_module(exported_module!(test_struct_api).into());
// Create a custom 'Scope' to hold state
let mut scope = Scope::new();
// Add any system-provided state into the custom 'Scope'.
// Constants can be used to optimize the script.
scope.push_constant("MY_CONSTANT", 42_i64);
:
:
// Initialize state variables
:
:
// Compile the handler script.
// In a real application you'd be handling errors...
let ast = engine.compile_file_with_scope(&mut scope, path).unwrap();
// The event handler is essentially these three items:
Self { 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, via Engine::call_fn
.
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) -> Dynamic {
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,)).unwrap(),
// 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,)).unwrap(),
// 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.starts_with("update") =>
{
// Default implementation of 'update' event handler.
self.scope.set_value("obj_state", TestStruct::new(42));
// Turn function-not-found into a success.
Ok(Dynamic::UNIT)
}
_ => Err(err)
}).unwrap(),
// In a real application you'd be handling unknown events...
_ => panic!("unknown event: {}", event_name)
}
}
}
Scripting Styles
Depending on needs and scripting style, there are three different ways to implement this pattern.
Main style | JS style | Map style | |
---|---|---|---|
States store | custom Scope | object map bound to this | object map in custom Scope |
Access state variable | normal variable | property of this | property of state variable |
Access global constants? | yes | yes | yes |
Add new state variable? | init function only | all functions | all functions |
Add new global constants? | yes | no | no |
OOP-style functions on states? | no | yes | yes |
Detect system-provided initial states? | no | yes | yes |
Local variable may shadow state variable? | yes | no | no |
Benefits | simple | fewer surprises | versatile |
Disadvantages |
|
|
|
Scriptable Event Handler with State
Main Style
A runnable example of this implementation is included.
See the Examples section for details.
Initialize Handler Instance with Engine::call_fn_with_options
Use Engine::call_fn_with_options
instead of Engine::call_fn
in order to retain new variables
defined inside the custom Scope
when running the init
function.
impl Handler {
// Create a new 'Handler'.
pub fn new(path: impl Into<PathBuf>) -> Self {
let mut engine = Engine::new();
:
// Code omitted
:
// Run the 'init' function to initialize the state, retaining variables.
let options = CallFnOptions::new()
.eval_ast(false) // do not re-evaluate the AST
.rewind_scope(false); // do not rewind scope
// In a real application you'd again be handling errors...
engine.call_fn_with_options(options, &mut scope, &ast, "init", ()).unwrap();
:
// Code omitted
:
Self { engine, scope, ast }
}
}
Handler Scripting Style
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.
Sample script
/// Initialize user-provided state (shadows system-provided state, if any).
/// Because 'CallFnOptions::rewind_scope' is 'false', new variables introduced
/// will remain inside the custom 'Scope'.
fn init() {
// Add 'bool_state' and 'obj_state' as new state variables
let bool_state = false;
let obj_state = new_state(0);
// Constants can also be added!
const EXTRA_CONSTANT = "hello, world!";
}
/// Without 'OOP' support, the can only be a function.
fn log(value, data) {
print(`State = ${value}, data = ${data}`);
}
/// 'start' event handler
fn start(data) {
if bool_state {
throw "Already started!";
}
if obj_state.func1() || obj_state.func2() {
throw "Conditions not yet ready to start!";
}
bool_state = true;
obj_state.value = data;
// Constants 'MY_CONSTANT' and 'EXTRA_CONSTANT'
// in custom scope are also visible!
print(`MY_CONSTANT = ${MY_CONSTANT}`);
print(`EXTRA_CONSTANT = ${EXTRA_CONSTANT}`);
}
/// 'end' event handler
fn end(data) {
if !bool_state {
throw "Not yet started!";
}
if !obj_state.func1() && !obj_state.func2() {
throw "Conditions not yet ready to end!";
}
bool_state = false;
obj_state.value = data;
}
/// 'update' event handler
fn update(data) {
obj_state.value += process(data);
// Without OOP support, can only call function
log(obj_state.value, data);
}
Disadvantages of This Style
This style is simple to implement and intuitive to use, but it is not very flexible.
New user state variables are introduced by evaluating a special initialization script function
(e.g. init
) that defines them, and Engine::call_fn_with_scope
is used to keep them inside the
custom Scope
(together with setting CallFnOptions::rewind_scope
to false
).
However, this has the disadvantage that no other function can introduce new state variables,
otherwise they’d simply shadow existing variables in the custom Scope
. Thus, functions
are called during events via Engine::call_fn
which does not retain any variables.
When there are a large number of state variables, this style also makes it easy for local variables defined in user functions to accidentally shadow a state variable with a variable that just happens to be the same name.
// 'start' event handler
fn start(data) {
let bool_state = false; // <- oops! bad variable name!
: // there is now no way to access the
: // state variable 'bool_state'...
if bool_state { // <- 'bool_state' is not the right thing
... // unless this is what you actually want
}
:
:
}
Scriptable Event Handler with State
JS Style
A runnable example of this implementation is included.
See the Examples section for details.
Keep State in Object Map
This style allows defining new user state variables everywhere by packaging them all inside an
object map, which is then exposed via the this
pointer.
Because this scripting style resembles JavaScript, it is so named.
State variables can be freely created by all functions (not just the init
function).
The event handler type needs to hold this object map instead of a custom Scope
.
use rhai::{Engine, Scope, Dynamic, AST};
// Event handler
struct Handler {
// Scripting engine
pub engine: Engine,
// The custom 'Scope' can be used to hold global constants
pub scope: Scope<'static>,
// Use an object map (as a 'Dynamic') to keep stored state
pub states: Dynamic,
// Program script
pub ast: AST
}
Bind Object Map to this
Pointer
Initialization can simply be done via binding the object map containing global states to the
this
pointer.
impl Handler {
// Create a new 'Handler'.
pub fn new(path: impl Into<PathBuf>) -> Self {
let mut engine = Engine::new();
:
// Code omitted
:
// Use an object map to hold state
let mut states = Map::new();
// Default states can be added
states.insert("bool_state".into(), Dynamic::FALSE);
// Convert the object map into 'Dynamic'
let mut states: Dynamic = states.into();
// Use 'call_fn_with_options' instead of 'call_fn' to bind the 'this' pointer
let options = CallFnOptions::new()
.eval_ast(false) // do not re-evaluate the AST
.rewind_scope(true) // rewind scope
.bind_this_ptr(&mut states); // bind the 'this' pointer
// In a real application you'd again be handling errors...
engine.call_fn_with_options(options, &mut scope, &ast, "init", ()).unwrap();
:
// Code omitted
:
Self { engine, scope, states, ast }
}
}
Bind this
Pointer During Events Handling
Events handling should also use Engine::call_fn_with_options
to bind the object map containing
global states to the this
pointer via CallFnOptions::this_ptr
.
pub fn on_event(&mut self, event_name: &str, event_data: i64) -> Dynamic {
let engine = &self.engine;
let scope = &mut self.scope;
let states = &mut self.states;
let ast = &self.ast;
let options = CallFnOptions::new()
.eval_ast(false) // do not re-evaluate the AST
.rewind_scope(true) // rewind scope
.bind_this_ptr(&mut states); // bind the 'this' pointer
match event_name {
// In a real application you'd be handling errors...
"start" => engine.call_fn_with_options(options, scope, ast, "start", (event_data,)).unwrap(),
:
:
}
}
Handler Scripting Style
Because the stored state is kept in an object map, which in turn is bound to this
, it is
necessary for functions to always access or modify these state variables via the this
pointer.
As it is impossible to declare a local variable named this
, there is no risk of accidentally
shadowing a state variable.
Because an object map is used to hold state values, it is even possible to add user-defined functions, leveraging the OOP support for object maps.
Sample script
/// Initialize user-provided state.
/// State is stored inside an object map bound to 'this'.
fn init() {
// Can detect system-provided default states!
// Add 'bool_state' as new state variable if one does not exist
if "bool_state" !in this {
this.bool_state = false;
}
// Add 'obj_state' as new state variable (overwrites any existing)
this.obj_state = new_state(0);
// Can also add OOP-style functions!
this.log = |x| print(`State = ${this.obj_state.value}, data = ${x}`);
}
/// 'start' event handler
fn start(data) {
// Access state variables via 'this'
if this.bool_state {
throw "Already started!";
}
// New state variables can be created anywhere
this.start_mode = data;
if this.obj_state.func1() || this.obj_state.func2() {
throw "Conditions not yet ready to start!";
}
this.bool_state = true;
this.obj_state.value = data;
// Constant 'MY_CONSTANT' in custom scope is also visible!
print(`MY_CONSTANT = ${MY_CONSTANT}`);
}
/// 'end' event handler
fn end(data) {
if !this.bool_state || !("start_mode" in this) {
throw "Not yet started!";
}
if !this.obj_state.func1() && !this.obj_state.func2() {
throw "Conditions not yet ready to end!";
}
this.bool_state = false;
this.obj_state.value = data;
}
/// 'update' event handler
fn update(data) {
this.obj_state.value += process(data);
// Call user-defined function OOP-style!
this.log(data);
}
Scriptable Event Handler with State
Map Style
A runnable example of this implementation is included.
See the Examples section for details.
I Hate this
! How Can I Get Rid of It?
You’re using Rust and you don’t want people to think you’re writing lowly JavaScript?
Taking inspiration from the JS Style, a slight modification of the
Main Style is to store all states inside an object map inside a custom Scope
.
Nevertheless, instead of writing this.variable_name
everywhere to access a state variable
(in the JS Style), you’d write state.variable_name
instead.
It is up to you to decide whether this is an improvement!
Handler Initialization
Notice that a variable definition filter is used to prevent shadowing of the states object map.
Implementation wise, this style follows closely the Main Style, but a single
object map is added to the custom Scope
which holds all state values.
Global constants can still be added to the custom Scope
as normal and used through the script.
Calls to the init
function no longer need to avoid rewinding the Scope
because state
variables are added as properties under the states object map.
impl Handler {
// Create a new 'Handler'.
pub fn new(path: impl Into<PathBuf>) -> Self {
let mut engine = Engine::new();
// Forbid shadowing of 'state' variable
engine.on_def_var(|_, info, _| Ok(info.name != "state"));
:
// Code omitted
:
// Use an object map to hold state
let mut states = Map::new();
// Default states can be added
states.insert("bool_state".into(), Dynamic::FALSE);
// Add the main states-holding object map and call it 'state'
scope.push("state", states);
// Just a simple 'call_fn' can do here because we're rewinding the 'Scope'
// In a real application you'd again be handling errors...
engine.call_fn(&mut scope, &ast, "init", ()).unwrap();
:
// Code omitted
:
Self { engine, scope, ast }
}
}
Handler Scripting Style
The stored state is kept in an object map in the custom Scope
.
In this example, that object map is named state
, but it can be any name.
User-defined functions in state
Because an object map is used to hold state values, it is even possible to add user-defined functions, leveraging the OOP support for object maps.
However, within these user-defined functions, the this
pointer binds to the object map.
Therefore, the variable-accessing syntax is different from the main body of the script.
fn do_action() {
// Access state: `state.xxx`
state.number = 42;
// Add OOP functions - you still need to use `this`...
state.log = |x| print(`State = ${this.value}, data = ${x}`);
}
Sample script
/// Initialize user-provided state.
/// State is stored inside an object map bound to 'state'.
fn init() {
// Add 'bool_state' as new state variable if one does not exist
if "bool_state" !in state {
state.bool_state = false;
}
// Add 'obj_state' as new state variable (overwrites any existing)
state.obj_state = new_state(0);
// Can also add OOP-style functions!
state.log = |x| print(`State = ${this.obj_state.value}, data = ${x}`);
}
/// 'start' event handler
fn start(data) {
// Can detect system-provided default states!
// Access state variables in 'state'
if state.bool_state {
throw "Already started!";
}
// New values can be added to the state
state.start_mode = data;
if state.obj_state.func1() || state.obj_state.func2() {
throw "Conditions not yet ready to start!";
}
state.bool_state = true;
state.obj_state.value = data;
// Constant 'MY_CONSTANT' in custom scope is also visible!
print(`MY_CONSTANT = ${MY_CONSTANT}`);
}
/// 'end' event handler
fn end(data) {
if !state.bool_state || "start_mode" !in state {
throw "Not yet started!";
}
if !state.obj_state.func1() && !state.obj_state.func2() {
throw "Conditions not yet ready to end!";
}
state.bool_state = false;
state.obj_state.value = data;
}
/// 'update' event handler
fn update(data) {
state.obj_state.value += process(data);
// Call user-defined function OOP-style!
state.log(data);
}
Scriptable Control Layer Over Rust Backend
-
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.
-
Expose a Control API.
-
Leverage function overloading to simplify the API design.
-
Since Rhai is sand-boxed, it cannot mutate anything outside of its internal environment. To perform external actions via an API, the actual system must be wrapped in a
RefCell
(orRwLock
/Mutex
forsync
) and shared to theEngine
.
Although this usage pattern appears a perfect fit for game logic, avoid writing the entire game in Rhai. Performance will not be acceptable.
Implement as much functionalities of the game engine in Rust as possible. Rhai integrates well with Rust so this is usually not a hinderance.
Lift as much out of Rhai as possible. Use Rhai only for the logic that must be dynamic or hot-loadable.
Implementation
There are two broad ways for Rhai to control an external system, both of which involve wrapping the system in a shared, interior-mutated object.
This is one way which does not involve exposing the data structures of the external system, but only through exposing an abstract API primarily made up of functions.
Use this when the API is relatively simple and clean, and the number of functions is small enough.
For a complex API involving lots of functions, or an API that has a clear object structure, use the Singleton Command Object pattern instead.
Functional API
Assume that a system provides the following functional API:
struct EnergizerBunny;
impl EnergizerBunny {
pub fn new () -> Self { ... }
pub fn go (&mut self) { ... }
pub fn stop (&mut self) { ... }
pub fn is_going (&self) { ... }
pub fn get_speed (&self) -> i64 { ... }
pub fn set_speed (&mut self, speed: i64) { ... }
}
Wrap API in shared object
pub type SharedBunny = Rc<RefCell<EnergizerBunny>>;
or in multi-threaded environments with the sync
feature, use one of the following:
pub type SharedBunny = Arc<RwLock<EnergizerBunny>>;
pub type SharedBunny = Arc<Mutex<EnergizerBunny>>;
Register control API
The trick to building a Control API is to clone the shared API object and move it into each function registration via a closure.
Therefore, it is not possible to use a plugin module to achieve this, and each function must be registered one after another.
// Notice 'move' is used to move the shared API object into the closure.
let b = bunny.clone();
engine.register_fn("bunny_power", move |on: bool| {
if on {
if b.borrow().is_going() {
println!("Still going...");
} else {
b.borrow_mut().go();
}
} else {
if b.borrow().is_going() {
b.borrow_mut().stop();
} else {
println!("Already out of battery!");
}
}
});
let b = bunny.clone();
engine.register_fn("bunny_is_going", move || b.borrow().is_going());
let b = bunny.clone();
engine.register_fn("bunny_get_speed", move ||
if b.borrow().is_going() { b.borrow().get_speed() } else { 0 }
);
let b = bunny.clone();
engine.register_fn("bunny_set_speed", move |speed: i64| -> Result<_, Box<EvalAltResult>>
if speed <= 0 {
return Err("Speed must be positive!".into());
} else if speed > 100 {
return Err("Bunny will be going too fast!".into());
}
if b.borrow().is_going() {
b.borrow_mut().set_speed(speed)
} else {
return Err("Bunny is not yet going!".into());
}
Ok(())
);
Use the API
if !bunny_is_going() { bunny_power(true); }
if bunny_get_speed() > 50 { bunny_set_speed(50); }
Singleton Command Object
-
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.
-
Expose a Command type with an API. The
no_object
feature must not be on. -
Leverage function overloading to simplify the API design.
-
Since Rhai is sand-boxed, it cannot mutate anything outside of its internal environment. To perform external actions via an API, the command object type must be wrapped in a
RefCell
(orRwLock
/Mutex
forsync
) and shared to theEngine
. -
Load each command object into a custom
Scope
as constant variables. -
Control each command object in script via the constants.
Implementation
There are two broad ways for Rhai to control an external system, both of which involve wrapping the system in a shared, interior-mutated object.
This is the other way which involves directly exposing the data structures of the external system as a name singleton object in the scripting space.
Use this when the API is complex but has a clear object structure.
For a relatively simple API that is action-based and not object-based, use the Control Layer pattern instead.
Functional API
Assume the following command object type:
struct EnergizerBunny { ... }
impl EnergizerBunny {
pub fn new () -> Self { ... }
pub fn go (&mut self) { ... }
pub fn stop (&mut self) { ... }
pub fn is_going (&self) -> bool { ... }
pub fn get_speed (&self) -> i64 { ... }
pub fn set_speed (&mut self, speed: i64) { ... }
pub fn turn (&mut self, left_turn: bool) { ... }
}
Wrap command object type as shared
pub type SharedBunny = Rc<RefCell<EnergizerBunny>>;
or in multi-threaded environments with the sync
feature, use one of the following:
pub type SharedBunny = Arc<RwLock<EnergizerBunny>>;
pub type SharedBunny = Arc<Mutex<EnergizerBunny>>;
Develop a plugin with methods and getters/setters
The easiest way to develop a complete set of API for a custom type is via a plugin module.
Notice that putting pure
in #[rhai_fn(...)]
allows a getter/setter to operate
on a constant without raising an error. Therefore, it is needed on all functions.
use rhai::plugin::*;
// Remember to put 'pure' on all functions, or they'll choke on constants!
#[export_module]
pub mod bunny_api {
// Custom type 'SharedBunny' will be called 'EnergizerBunny' in scripts
pub type EnergizerBunny = SharedBunny;
// This constant is also available to scripts
pub const MAX_SPEED: i64 = 100;
#[rhai_fn(get = "power", pure)]
pub fn get_power(bunny: &mut EnergizerBunny) -> bool {
bunny.borrow().is_going()
}
#[rhai_fn(set = "power", pure)]
pub fn set_power(bunny: &mut EnergizerBunny, 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 EnergizerBunny) -> 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 EnergizerBunny, 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(())
}
}
#[rhai_fn(pure)]
pub fn turn_left(bunny: &mut EnergizerBunny) {
if bunny.borrow().is_going() {
bunny.borrow_mut().turn(true);
}
}
#[rhai_fn(pure)]
pub fn turn_right(bunny: &mut EnergizerBunny) {
if bunny.borrow().is_going() {
bunny.borrow_mut().turn(false);
}
}
}
engine.register_global_module(exported_module!(bunny_api).into());
It is customary to register a friendly display name for any custom type involved in the plugin.
This can easily be done via a type alias in the plugin module.
Compile script into AST
let ast = engine.compile(script)?;
Push constant command object into custom scope and run AST
let bunny: SharedBunny = Rc::new(RefCell::new(EnergizerBunny::new()));
let mut scope = Scope::new();
// Add the singleton 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.run_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.run_with_scope(&mut scope, script)?;
It is usually desirable to prevent shadowing of the singleton command object.
This can be easily achieved via a variable definition filter.
// Now the script can no longer define a variable named 'BUNNY'
engine.on_def_var(|_, info, _| Ok(info.name != "BUNNY"));
Use the command API in script
// Access the command object via constant variable 'BUNNY'.
if !BUNNY.power { BUNNY.power = true; }
if BUNNY.speed > 50 { BUNNY.speed = 50; }
BUNNY.turn_left();
let BUNNY = 42; // <- syntax error if variable definition filter is set
Loadable Configuration
-
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
orYAML
. -
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.
-
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
(orRwLock
/Mutex
forsync
) and shared to theEngine
.
Implementation
Configuration type
#[derive(Debug, Clone, Default)]
struct Config {
id: String,
some_field: i64,
some_list: Vec<String>,
some_map: HashMap<String, bool>,
}
Make shared object
type SharedConfig = Rc<RefCell<Config>>;
let config = SharedConfig::default();
or in multi-threaded environments with the sync
feature, use one of the following:
type SharedConfig = Arc<RwLock<Config>>;
type SharedConfig = Arc<Mutex<Config>>;
Register config API
The trick to building a Config API is to clone the shared configuration object and move it into each function registration via a closure.
Therefore, it is not possible to use a plugin module to achieve this, and each function must be registered one after another.
// Notice 'move' is used to move the shared configuration object into the closure.
let cfg = config.clone();
engine.register_fn("config_set_id", move |id: String| cfg.borrow_mut().id = id);
let cfg = config.clone();
engine.register_fn("config_get_id", move || cfg.borrow().id.clone());
let cfg = config.clone();
engine.register_fn("config_set", move |value: i64| cfg.borrow_mut().some_field = value);
// Remember Rhai functions can be overloaded when designing the API.
let cfg = config.clone();
engine.register_fn("config_add", move |value: String|
cfg.borrow_mut().some_list.push(value)
);
let cfg = config.clone();
engine.register_fn("config_add", move |values: &mut Array|
cfg.borrow_mut().some_list.extend(values.into_iter().map(|v| v.to_string()))
);
let cfg = config.clone();
engine.register_fn("config_add", move |key: String, value: bool|
cfg.borrow_mut().some_map.insert(key, value)
);
let cfg = config.clone();
engine.register_fn("config_contains", move |value: String|
cfg.borrow().some_list.contains(&value)
);
let cfg = config.clone();
engine.register_fn("config_is_set", move |value: String|
cfg.borrow().some_map.get(&value).cloned().unwrap_or(false)
);
Configuration script
┌────────────────┐
│ my_config.rhai │
└────────────────┘
config_set_id("hello");
config_add("foo"); // add to list
config_add("bar", true); // add to map
if config_contains("hey") || config_is_set("hey") {
config_add("baz", false); // add to map
}
Load the configuration
import "my_config"; // run configuration script without creating a module
let id = config_get_id();
id == "hello";
Consider a Custom Syntax
This is probably one of the few scenarios where a custom syntax can be recommended.
A properly-designed custom syntax can make the configuration file clean, simple to write, easy to understand and quick to modify.
For example, the above configuration example may be expressed by this custom syntax:
┌────────────────┐
│ my_config.rhai │
└────────────────┘
// Configure ID
id "hello";
// Add to list
list + "foo";
// Add to map
map "bar" => true;
if config contains "hey" || config is_set "hey" {
map "baz" => false;
}
Notice that contains
and is_set
may also be implemented as a custom operator.
Multi-Layered Functions
-
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.
Assuming a LOB (line-of-business) system for a large MNC (multi-national corporation) with branches, facilities and offices across the globe.
The system needs to provide basic, corporate-wide policies to be enforced through the worldwide organization, but also cater for country- or region-specific rules, practices and regulations.
Layer | Description |
---|---|
corporate | corporate-wide policies |
regional | regional policy overrides |
country | country-specific modifications for legal compliance |
office | special treatments for individual office locations |
-
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 usingAST::combine
(or the+=
operator), overriding any existing functions.
Examples
Assume the following four scripts, one for each layer:
┌────────────────┐
│ corporate.rhai │
└────────────────┘
// Default implementation of 'foo'.
fn foo(x) { x + 1 }
// Default implementation of 'bar'.
fn bar(x, y) { x + y }
// Default implementation of 'no_touch'.
fn no_touch() { throw "do not touch me!"; }
┌───────────────┐
│ regional.rhai │
└───────────────┘
// Specific implementation of 'foo'.
fn foo(x) { x * 2 }
// New implementation for this layer.
fn baz() { print("hello!"); }
┌──────────────┐
│ country.rhai │
└──────────────┘
// Specific implementation of 'bar'.
fn bar(x, y) { x - y }
// Specific implementation of 'baz'.
fn baz() { print("hey!"); }
┌─────────────┐
│ office.rhai │
└─────────────┘
// Specific implementation of 'foo'.
fn foo(x) { x + 42 }
Load and combine them sequentially:
let engine = Engine::new();
// Compile the baseline layer.
let mut ast = engine.compile_file("corporate.rhai".into())?;
// Combine the first layer.
let lowest = engine.compile_file("regional.rhai".into())?;
ast += lowest;
// Combine the second layer.
let middle = engine.compile_file("country.rhai".into())?;
ast += middle;
// Combine the third layer.
let highest = engine.compile_file("office.rhai".into())?;
ast += highest;
// Now, 'ast' contains the following functions:
//
// fn no_touch() { // from 'corporate.rhai'
// throw "do not touch me!";
// }
// fn foo(x) { x + 42 } // from 'office.rhai'
// fn bar(x, y) { x - y } // from 'country.rhai'
// fn baz() { print("hey!"); } // from 'country.rhai'
Unfortunately, there is no super
call that calls the base implementation (i.e. no way for a
higher-layer function to call an equivalent lower-layer function).
Hot Reloading
-
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.
Implementation
Embed scripting engine and script into system
Say, a system has a Rhai Engine
plus a compiled script (in AST
form), with the AST
kept
with interior mutability…
// Main system object
struct System {
engine: Engine,
script: Rc<RefCell<AST>>,
:
}
// Embed Rhai 'Engine' and control script
let engine = Engine::new();
let ast = engine.compile_file("config.rhai")?;
let mut system = System { engine, script: Rc::new(RefCell::new(ast)) };
// Handle events with script functions
system.on_event(|sys: &System, event: &str, data: Map| {
let mut scope = Scope::new();
// Call script function which is the same name as the event
sys.engine.call_fn(&mut scope, sys.script.borrow(), event, (data,)).unwrap();
result
});
Hot reload entire script upon change
If the control scripts are small enough and changes are infrequent, it is much simpler just to
recompile the whole set of script and replace the original AST
with the new one.
// Watch for script file change
system.watch(|sys: &System, file: &str| {
// Compile the new script
let ast = sys.engine.compile_file(file.into())?;
// Hot reload - just replace the old script!
*sys.script.borrow_mut() = ast;
Ok(())
});
Hot patch specific functions
If the control scripts are large and complicated, and if the system can detect changes to specific functions, it is also possible to patch just the changed functions.
// Watch for changes in the script
system.watch_for_script_change(|sys: &mut System, fn_name: &str| {
// Get the script file that contains the function
let script = get_script_file_path(fn_name);
// Compile the new script
let mut patch_ast = sys.engine.compile_file(script)?;
// Remove everything other than the specified function
patch_ast.clear_statements();
patch_ast.retain_functions(|_, _, name, _| name == fn_name);
// Hot reload (via +=) only those functions in the script!
*sys.script.borrow_mut() += patch_ast;
});
For a multi-threaded environments, replace Rc
with Arc
, RefCell
with RwLock
or Mutex
, and
turn on the sync
feature.
Builder Pattern / Fluent API
-
An API uses the Builder Pattern or a fluent API.
-
The builder type is not necessarily
Clone
.
- Wrap the builder type in shared interior mutability (aka
Rc<RefCell<T>>
orArc<RwLock<T>>
).
This same pattern can be used to implement any fluent API.
Implementation With Clonable Builder Type
This assumes that the builder type implements Clone
.
This is the most common scenario.
/// Builder for `Foo` instances.
#[derive(Clone)]
pub struct FooBuilder {
/// The `foo` option.
foo: i64,
/// The `bar` option.
bar: bool,
/// The `baz` option.
baz: String,
}
/// `FooBuilder` API which uses moves.
impl FooBuilder {
/// Creates a new builder for `Foo`.
pub fn new() -> Self {
Self { foo: 0, bar: false, baz: String::new() }
}
/// Sets the `foo` option.
pub fn with_foo(mut self, foo: i64) -> Self {
self.foo = foo;
self
}
/// Sets the `bar` option.
pub fn with_bar(mut self, bar: bool) -> Self {
self.bar = bar;
self
}
/// Sets the `baz` option.
pub fn with_baz(mut self, baz: &str) -> Self {
self.baz = baz.to_string();
self
}
/// Builds the `Foo` instance.
pub fn build(self) -> Foo {
Foo { foo: self.foo, bar: self.bar, baz: self.baz }
}
}
let mut engine = Engine::new();
engine
.register_fn("get_foo", FooBuilder::new)
.register_fn("with_foo", FooBuilder::with_foo)
.register_fn("with_bar", FooBuilder::with_bar)
.register_fn("with_baz", FooBuilder::with_baz)
.register_fn("create", FooBuilder::build);
Implementation With Mutable Reference
This assumes that the builder type’s API uses mutable references.
The builder type does not need to implement Clone
.
use rhai::plugin::*;
/// Builder for `Foo` instances.
/// Notice that this type does not need to be `Clone`.
pub struct FooBuilder {
/// The `foo` option.
foo: i64,
/// The `bar` option.
bar: bool,
/// The `baz` option.
baz: String,
}
/// Builder type API uses mutable references.
impl FooBuilder {
/// Creates a new builder for `Foo`.
pub fn new() -> Self {
Self { foo: 0, bar: false, baz: String::new() }
}
/// Sets the `foo` option.
pub fn with_foo(&mut self, foo: i64) -> &mut Self {
self.foo = foo; self
}
/// Sets the `bar` option.
pub fn with_bar(&mut self, bar: bool) -> &mut Self {
self.bar = bar; self
}
/// Sets the `baz` option.
pub fn with_baz(&mut self, baz: &str) -> &mut Self {
self.baz = baz.to_string(); self
}
/// Builds the `Foo` instance.
pub fn build(&self) -> Foo {
Foo { foo: self.foo, bar: self.bar, baz: self.baz.clone() }
}
}
/// Builder for `Foo`.
#[export_module]
pub mod foo_builder {
use super::{Foo, FooBuilder as BuilderImpl};
use std::cell::RefCell;
use std::rc::Rc;
/// The builder for `Foo`.
// This type is `Clone`.
pub type FooBuilder = Rc<RefCell<super::BuilderImpl>>;
/// Creates a new builder for `Foo`.
pub fn default() -> FooBuilder {
Rc::new(RefCell::new(BuilderImpl::new()))
}
/// Sets the `foo` option.
#[rhai_fn(global, pure)]
pub fn with_foo(builder: &mut FooBuilder, foo: i64) -> FooBuilder {
builder.set_foo(foo);
builder.clone()
}
/// Sets the `bar` option.
#[rhai_fn(global, pure)]
pub fn with_bar(builder: &mut FooBuilder, bar: bool) -> FooBuilder {
builder.set_bar(bar);
builder.clone()
}
/// Sets the `baz` option.
#[rhai_fn(global, pure)]
pub fn with_baz(builder: &mut FooBuilder, baz: &str) -> FooBuilder {
builder.set_baz(baz);
builder.clone()
}
/// Builds the `Foo` instance.
#[rhai_fn(global, pure)]
pub fn create(builder: &mut FooBuilder) -> Foo {
builder.borrow().build()
}
}
Implementation With Moves
What if the builder type’s API relies on moves instead of mutable references?
And the builder type does not implement Clone
?
Not too worry: the following trick has you covered!
use rhai::plugin::*;
/// Builder for `Foo` instances.
/// Notice that this type does not need to be `Clone`.
pub struct FooBuilder {
/// The `foo` option.
foo: i64,
/// The `bar` option.
bar: bool,
/// The `baz` option.
baz: String,
}
/// `FooBuilder` API which uses moves.
impl FooBuilder {
/// Creates a new builder for `Foo`.
pub fn new() -> Self {
Self { foo: 0, bar: false, baz: String::new() }
}
/// Sets the `foo` option.
pub fn with_foo(mut self, foo: i64) -> Self {
self.foo = foo;
self
}
/// Sets the `bar` option.
pub fn with_bar(mut self, bar: bool) -> Self {
self.bar = bar;
self
}
/// Sets the `baz` option.
pub fn with_baz(mut self, baz: &str) -> Self {
self.baz = baz.to_string();
self
}
/// Builds the `Foo` instance.
pub fn build(self) -> Foo {
Foo { foo: self.foo, bar: self.bar, baz: self.baz }
}
}
/// Builder for `Foo`.
#[export_module]
pub mod foo_builder {
use super::{Foo, FooBuilder as BuilderImpl};
use std::cell::RefCell;
use std::mem;
use std::rc::Rc;
/// The builder for `Foo`.
// This type is `Clone`.
// An `Option` is used for easy extraction of the builder type.
// If it is `None` then the builder is already consumed.
pub type FooBuilder = Rc<RefCell<Option<BuilderImpl>>>;
/// Creates a new builder for `Foo`.
pub fn default() -> FooBuilder {
Rc::new(RefCell::new(Some(BuilderImpl::new())))
}
/// Sets the `foo` option.
#[rhai_fn(return_raw, global, pure)]
pub fn with_foo(builder: &mut FooBuilder, foo: i64) -> Result<FooBuilder, Box<EvalAltResult>> {
let b = &mut *builder.borrow_mut();
if let Some(obj) = mem::take(b) {
*b = Some(obj.with_foo(foo));
Ok(builder.clone())
} else {
Err("Builder is already consumed".into())
}
}
/// Sets the `bar` option.
#[rhai_fn(return_raw, global, pure)]
pub fn with_bar(builder: &mut FooBuilder, bar: bool) -> Result<FooBuilder, Box<EvalAltResult>> {
let b = &mut *builder.borrow_mut();
if let Some(obj) = mem::take(b) {
*b = Some(obj.with_bar(bar));
Ok(builder.clone())
} else {
Err("Builder is already consumed".into())
}
}
/// Sets the `baz` option.
#[rhai_fn(return_raw, global, pure)]
pub fn with_baz(builder: &mut FooBuilder, baz: &str) -> Result<FooBuilder, Box<EvalAltResult>> {
let b = &mut *builder.borrow_mut();
if let Some(obj) = mem::take(b) {
*b = Some(obj.with_baz(baz));
Ok(builder.clone())
} else {
Err("Builder is already consumed".into())
}
}
/// Builds the `Foo` instance.
#[rhai_fn(return_raw, global, pure)]
pub fn create(builder: &mut FooBuilder) -> Result<Foo, Box<EvalAltResult>> {
let b = &mut *builder.borrow_mut();
if let Some(obj) = mem::take(b) {
Ok(obj.build())
} else {
Err("Builder is already consumed".into())
}
}
}
Usage
It is easy to see that the Rhai script API mirrors the Rust API almost perfectly.
┌──────┐
│ Rust │
└──────┘
let mut engine = Engine::new();
engine.register_static_module("Foo", exported_module!(foo_builder).into());
let foo = FooBuilder::new().with_foo(42).with_bar(true).with_baz("Hello").build();
┌─────────────┐
│ Rhai script │
└─────────────┘
let foo = Foo::default().with_foo(42).with_bar(true).with_baz("Hello").create();
Dynamic Constants Provider
-
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.
-
Use a variable resolver to intercept variable access.
-
Only load a variable when it is being used.
-
Perform a lookup based on variable name, and provide the correct data value.
-
May even perform back-end network access or look up the latest value from a database.
Implementation
let mut engine = Engine::new();
// Create shared data provider.
// Assume that SystemValuesProvider::get(&str) -> Option<value> gets a value.
let provider = Arc::new(SystemValuesProvider::new());
// Clone the shared provider
let db = provider.clone();
// Register a variable resolver.
// Move the shared provider into the closure.
engine.on_var(move |name, _, _, _| Ok(db.get(name).map(Dynamic::from)));
All values provided by a variable resolver are constants due to their dynamic nature. They cannot be assigned to.
In order to change values in an external system, register a dedicated API for that purpose.
Global Constants
-
Script has a lot of duplicated constants used inside functions.
-
For easier management, constants are declared at the top of the script.
-
As Rhai functions are pure, they cannot access constants declared at global level except through
global
. -
Sprinkling large number of
global::CONSTANT
throughout the script makes it slow and cumbersome. -
Using
global
or a variable resolver defeats constants propagation in script optimization.
-
The key to global constants is to use them to optimize a script. Otherwise, it would be just as simple to pass the constants into a custom
Scope
instead. -
The script is first compiled into an
AST
, and all constants are extracted. -
The constants are then supplied to re-optimize the
AST
. -
This pattern also works under Strict Variables Mode.
Example
Assume that the following Rhai script needs to work (but it doesn’t).
// These are constants
const FOO = 1;
const BAR = 123;
const MAGIC_NUMBER = 42;
fn get_magic() {
MAGIC_NUMBER // <- oops! 'MAGIC_NUMBER' not found!
}
fn calc_foo(x) {
x * global::FOO // <- works but cumbersome; not desirable!
}
let magic = get_magic() * BAR;
let x = calc_foo(magic);
print(x);
Step 1 – Compile Script into AST
Compile the script into AST
form.
Normally, it is useful to disable optimizations at this stage since
the AST
will be re-optimized later.
Strict Variables Mode must be OFF for this to work.
// Turn Strict Variables Mode OFF (if necessary)
engine.set_strict_variables(false);
// Turn optimizations OFF
engine.set_optimization_level(OptimizationLevel::None);
let ast = engine.compile("...")?;
Step 2 – Extract Constants
Use AST::iter_literal_variables
to extract top-level constants from the AST
.
let mut scope = Scope::new();
// Extract all top-level constants without running the script
ast.iter_literal_variables(true, false).for_each(|(name, _, value)|
scope.push_constant(name, value);
);
// 'scope' now contains: FOO, BAR, MAGIC_NUMBER
Step 3a – Propagate Constants
Re-optimize the AST
using the new constants.
// Turn optimization back ON
engine.set_optimization_level(OptimizationLevel::Simple);
let ast = engine.optimize_ast(&scope, ast, engine.optimization_level());
Step 3b – Recompile Script (Alternative)
If Strict Variables Mode is used, however, it is necessary to re-compile the script in order to detect undefined variable usages.
// Turn Strict Variables Mode back ON
engine.set_strict_variables(true);
// Turn optimization back ON
engine.set_optimization_level(OptimizationLevel::Simple);
// Re-compile the script using constants in 'scope'
let ast = engine.compile_with_scope(&scope, "...")?;
Step 4 – Run the Script
At this step, the AST
is now optimized with constants propagated into all access sites.
The script essentially becomes:
// These are constants
const FOO = 1;
const BAR = 123;
const MAGIC_NUMBER = 42;
fn get_magic() {
42 // <- constant replaced by value
}
fn calc_foo(x) {
x * global::FOO
}
let magic = get_magic() * 123; // <- constant replaced by value
let x = calc_foo(magic);
print(x);
Run it via Engine::run_ast
or Engine::eval_ast
.
// The 'scope' is no longer necessary
engine.run_ast(&ast)?;
Mutable Global State
Don’t Do It™
Generations of programmers struggled to get around mutable global state (a.k.a. the window
object)
in the design of JavaScript.
In contrast to global constants, mutable global states are strongly discouraged because:
-
It is a sure-fire way to create race conditions – that is why Rust does not support it;
-
It adds considerably to debug complexity – it is difficult to reason, in large code bases, where/when a state value is being modified;
-
It forces hard (but obscure) dependencies between separate pieces of code that are difficult to break when the need arises;
-
It is almost impossible to add new layers of redirection and/or abstraction afterwards without major surgery.
Alternative – Use this
In the majority of the such scenarios, there is only one mutable global state of interest.
Therefore, it is a much better solution to bind that global state to the this
pointer.
// Say this is a mutable global state...
let state = #{ counter: 0 };
// This function tries to access the global 'state'
// which will fail.
fn inc() {
state.counter += 1;
}
// The function should be written with 'this'
fn inc() {
this.counter += 1;
}
state.inc(); // call 'inc' with 'state' bound to 'this'
// Or this way... why hard-code the state in the first place?
fn inc() {
this += 1;
}
state.counter.inc();
There are good reasons why using this
is a better solution:
- the state is never hidden – it is always clear to see what is being modified
- it is just as fast – the
this
pointer works by reference - you can pass other states in, in the future, without changing the script code
- there are no hard links within functions that will be difficult to unravel
- only the variable bound to
this
is ever modified; everything else is immutable
This is not something that Rhai encourages. You Have Been Warned™.
There are two ways…
Option 1 – Get/Set Functions
This is similar to the Control Layer pattern.
Use get/set functions to read/write the global mutable state.
// The globally mutable shared value
let value = Rc::new(RefCell::new(42));
// Register an API to access the globally mutable shared value
let v = value.clone();
engine.register_fn("get_global_value", move || *v.borrow());
let v = value.clone();
engine.register_fn("set_global_value", move |value: i64| *v.borrow_mut() = value);
These functions can be used in script functions to access the shared global state.
fn foo() {
let current = get_global_value(); // Get global state value
current += 1;
set_global_value(current); // Modify global state value
}
This option is preferred because it is possible to modify the get/set functions later on to add/change functionalities without introducing breaking script changes.
Option 2 – Variable Resolver
Declare a variable resolver that returns a shared value which is the global state.
// Use a shared value as the global state
let value: Dynamic = 1.into();
let mut value = value.into_shared(); // convert into shared value
// Clone the shared value
let v = value.clone();
// Register a variable resolver.
engine.on_var(move |name, _, _| {
match name
"value" => Ok(Some(v.clone())),
_ => Ok(None)
}
});
// The shared global state can be modified
*value.write_lock::<i64>().unwrap() = 42;
The global state variable can now be used just like a normal local variable, including modifications.
fn foo() {
value = value * 2;
// ^ global variable can be read
// ^ global variable can also be modified
}
This option makes mutable global state so easy to implement that it should actually be considered an Anti-Pattern.
Working With Rust Enums
Rust enum variants are not considered separate types.
Although Rhai integrates fine with Rust enums (treated transparently as custom types), it is impossible (short of registering a complete API) to distinguish between individual variants and to extract internal data from them.
Enums in Rust can hold data and are typically used with pattern matching.
Unlike Rust, Rhai does not have built-in pattern matching capabilities, so working with enum variants that contain embedded data is not an easy proposition.
Since Rhai is dynamic and variables can hold any type of data, they are essentially enums by nature.
Multiple distinct types can be stored in a single Dynamic
without merging them into an enum
as variants.
This section outlines a number of possible solutions to work with Rust enums.
Simulate an Enum API
A plugin module is extremely handy in creating an entire API for a custom enum type.
use rhai::plugin::*;
use rhai::{Dynamic, Engine, EvalAltResult};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum MyEnum {
Foo,
Bar(i64),
Baz(String, bool),
}
// Create a plugin module with functions constructing the 'MyEnum' variants
#[export_module]
mod MyEnumModule {
// Constructors for 'MyEnum' variants
/// `MyEnum::Foo` with no inner data.
pub const Foo: MyEnum = MyEnum::Foo;
/// `MyEnum::Bar(value)`
pub fn Bar(value: i64) -> MyEnum { MyEnum::Bar(value) }
/// `MyEnum::Baz(name, flag)`
pub fn Baz(name: String, flag: bool) -> MyEnum { MyEnum::Baz(name, flag) }
/// Return the current variant of `MyEnum`.
#[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()
}
}
/// Return the inner value.
#[rhai_fn(global, get = "value", pure)]
pub fn get_value(my_enum: &mut MyEnum) -> Dynamic {
match my_enum {
MyEnum::Foo => Dynamic::UNIT,
MyEnum::Bar(x) => Dynamic::from(x),
MyEnum::Baz(_, f) => Dynamic::from(f),
}
}
// Access to inner values by position
/// Return the value kept in the first position of `MyEnum`.
#[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)
}
}
/// Return the value kept in the second position of `MyEnum`.
#[rhai_fn(global, get = "field_1", pure)]
pub fn get_field_1(my_enum: &mut MyEnum) -> Dynamic {
match my_enum {
MyEnum::Foo | MyEnum::Bar(_) => Dynamic::UNIT,
MyEnum::Baz(_, x) => Dynamic::from(x)
}
}
// Printing
#[rhai_fn(global, name = "to_string", name = "to_debug", pure)]
pub fn to_string(my_enum: &mut MyEnum) -> String {
format!("{my_enum:?}")
}
// '==' and '!=' operators
#[rhai_fn(global, name = "==", pure)]
pub fn eq(my_enum: &mut MyEnum, my_enum2: MyEnum) -> bool {
my_enum == &my_enum2
}
#[rhai_fn(global, name = "!=", pure)]
pub fn neq(my_enum: &mut MyEnum, my_enum2: MyEnum) -> bool {
my_enum != &my_enum2
}
}
let mut engine = Engine::new();
// Load the module as the module namespace "MyEnum"
engine.register_type_with_name::<MyEnum>("MyEnum")
.register_static_module("MyEnum", exported_module!(MyEnumModule).into());
With this API in place, working with enums feels almost the same as in Rust:
let x = MyEnum::Foo;
let y = MyEnum::Bar(42);
let z = MyEnum::Baz("hello", true);
x == MyEnum::Foo;
y != MyEnum::Bar(0);
// Detect enum types
x.enum_type == "Foo";
y.enum_type == "Bar";
z.enum_type == "Baz";
// Extract enum fields
x.value == ();
y.value == 42;
z.value == ();
x.name == ();
y.name == ();
z.name == "hello";
y.field_0 == 42;
y.field_1 == ();
z.field_0 == "hello";
z.field_1 == true;
For enums containing only variants with no inner data, it is convenient to use a simple macro to create such a plugin module.
// The 'create_enum_module!' macro
macro_rules! create_enum_module {
($module:ident : $typ:ty => $($variant:ident),+) => {
#[export_module]
pub mod $module {
$(
#[allow(non_upper_case_globals)]
pub const $variant: $typ = <$typ>::$variant;
)*
}
};
}
// The enum
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum MyEnum { Foo, Bar, Baz, Hello, World }
// This creates a plugin module called 'my_enum_module'
expand_enum! { my_enum_module: MyEnum => Foo, Bar, Baz, Hello, World }
Use Enums With switch
Since enums are internally treated as custom types, they are not literals and cannot be used as
a match case in switch
statements. This is quite a limitation because the equivalent match
statement is commonly used in Rust to work with enums and bind variables to variant-internal data.
It is possible, however, to switch
through enum variants based on their types:
switch my_enum.enum_type {
"Foo" => ...,
"Bar" => {
let value = foo.value;
...
}
"Baz" => {
let name = foo.name;
let flag = foo.flag;
...
}
}
Use switch
Through Arrays
Another way to work with Rust enums in a switch
statement is through exposing the internal data
(or at least those that act as effective discriminants) of each enum variant as a variable-length
array, usually with the name of the variant as the first item for convenience:
use rhai::Array;
engine.register_get("enum_data", |my_enum: &mut MyEnum| {
match my_enum {
MyEnum::Foo => vec![ "Foo".into() ] as Array,
// Say, skip the data field because it is not
// used as a discriminant
MyEnum::Bar(value) => vec![ "Bar".into() ] as Array,
// Say, all fields act as discriminants
MyEnum::Baz(name, flag) => vec![
"Baz".into(), name.clone().into(), (*flag).into()
] as Array
}
});
Then it is a simple matter to match an enum via a switch
expression.
// Assume 'value' = 'MyEnum::Baz("hello", true)'
// 'enum_data' creates a variable-length array with 'MyEnum' data
let x = switch value.enum_data {
["Foo"] => 1,
["Bar"] => value.field_1,
["Baz", "hello", false] => 4,
["Baz", "hello", true] => 5,
_ => 9
};
x == 5;
// Which is essentially the same as:
let x = switch [value.type, value.field_0, value.field_1] {
["Foo", (), ()] => 1,
["Bar", 42, ()] => 42,
["Bar", 123, ()] => 123,
:
["Baz", "hello", false] => 4,
["Baz", "hello", true] => 5,
_ => 9
}
Usually, a helper method returns an array of values that can uniquely determine the switch
case
based on actual usage requirements – which means that it probably skips fields that contain
data instead of discriminants.
Then switch
is used to very quickly match through a large number of array shapes and jump to the
appropriate case implementation.
Data fields can then be extracted from the enum independently.
Simulate Macros to Simplify Scripts
-
Scripts need to access existing data in variables.
-
The particular fields to access correspond to long/complex expressions (e.g. long indexing and/or property chains
foo[x][y].bar[z].baz
). -
Usage is prevalent inside the scripts, requiring extensive duplications of code that are prone to typos and errors.
-
There are a few such variables to modify at the same time – otherwise, it would be simpler to bind the
this
pointer to the variable.
-
Pick a macro syntax that is unlikely to conflict with content in literal strings.
-
Before script evaluation/compilation, globally replace macros with their corresponding expansions.
Pick a Macro Syntax
The technique described here is to simulate macros. They are not REAL macros.
Pick a syntax that is intuitive for the domain but unlikely to occur naturally inside string literals.
Sample Syntax | Sample usage |
---|---|
#FOO | #FOO = 42; |
$Bar | $Bar.work(); |
<Baz> | print(<Baz>); |
#HELLO# | let x = #HELLO#; |
%HEY% | %HEY% += 1; |
Avoid normal syntax that may show up inside a string literal.
For example, if using Target
as a macro:
// This script...
Target.do_damage(10);
if Target.hp <= 0 {
print("Target is destroyed!");
}
// Will turn to this...
entities["monster"].do_damage(10);
if entities["monster"].hp <= 0 {
// Text in string literal erroneously replaced!
print("entities["monster"] is destroyed!");
}
Global Search/Replace
// Replace macros with expansions
let script = script.replace("#FOO", "foo[x][y].bar[z].baz");
let mut scope = Scope::new();
// Add global variables
scope.push("foo", ...);
scope.push_constant("x", ...);
scope.push_constant("y", ...);
scope.push_constant("z", ...);
// Run the script as normal
engine.run_with_scope(&mut scope, script)?;
print(`Found entity FOO at (${x},${y},${z})`);
let speed = #FOO.speed;
if speed < 42 {
#FOO.speed *= 2;
} else {
#FOO.teleport(#FOO.home());
}
print(`FOO is now at (${ #FOO.current_location() })`);
After macro expansion, the character positions of different script elements will be shifted based on the length of the expanded text.
Therefore, error positions may no longer point to the correct locations in the original, unexpanded scripts.
Line numbers are not affected.
One Engine
Instance Per Call
-
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.
Procedure
-
Gather up all common custom functions into a custom package.
-
This custom package should also include standard packages needed. For example, to duplicate
Engine::new
, use aStandardPackage
. -
Packages are sharable, so using a custom package is much cheaper than registering all the functions one by one.
-
-
Always use
Engine::new_raw
to create a rawEngine
, instead ofEngine::new
which is much more expensive. A rawEngine
is extremely cheap to create. -
Register the custom package with the raw
Engine
viaPackage::register_into_engine
.
Examples
use rhai::def_package;
use rhai::packages::{Package, StandardPackage};
use rhai::FuncRegistration;
// Define the custom package 'MyCustomPackage'.
def_package! {
/// My own personal super-duper custom package
// Aggregate other base packages simply by listing them after the colon.
pub MyCustomPackage(module) : StandardPackage {
// Register additional Rust functions.
FuncRegistration::new("foo")
.with_params(&["s: ImmutableString", "i64"])
.set_into_module(module, |s: ImmutableString| foo(s.into_owned()));
}
}
let ast = /* ... some AST ... */;
let my_custom_package = 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
my_custom_package.register_into_engine(&mut engine);
// Evaluate script
engine.run_ast(&ast)?;
}
Multi-Threaded Synchronization
Example
use rhai::{Engine};
fn main() {
// Channel: Script -> Master
let (tx_script, rx_master) = std::sync::mpsc::channel();
// Channel: Master -> Script
let (tx_master, rx_script) = std::sync::mpsc::channel();
// Spawn thread with Engine
std::thread::spawn(move || {
// Create Engine
let mut engine = Engine::new();
// Register API
// Notice that the API functions are blocking
engine.register_fn("get", move || rx_script.recv().unwrap())
.register_fn("put", move |v: i64| tx_script.send(v).unwrap());
// Run script
engine.run(
r#"
print("Starting script loop...");
loop {
// The following call blocks until there is data
// in the channel
let x = get();
print(`Script Read: ${x}`);
x += 1;
print(`Script Write: ${x}`);
// The following call blocks until the data
// is successfully sent to the channel
put(x);
}
"#).unwrap();
});
// This is the main processing thread
println!("Starting main loop...");
let mut value = 0_i64;
while value < 10 {
println!("Value: {value}");
// Send value to script
tx_master.send(value).unwrap();
// Receive value from script
value = rx_master.recv().unwrap();
}
}
Considerations for sync
std::mpsc::Sender
and std::mpsc::Receiver
are not Sync
, therefore they cannot be used in
registered functions if the sync
feature is enabled.
In that situation, it is possible to wrap the Sender
and Receiver
each in a Mutex
or RwLock
,
which makes them Sync
.
This, however, incurs the additional overhead of locking and unlocking the Mutex
or RwLock
during every function call, which is technically not necessary because there are no other references
to them.
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.
Blocking/Async Function Calls
Otherwise, you reinvent the Callback Hell which is JavaScript before all the async extensions.
-
This pattern is based upon the Multi-Threaded Synchronization pattern.
-
An independent thread is used to run the scripting
Engine
. -
An MPSC channel (or any other appropriate synchronization primitive) is used to send function call arguments, packaged as a message, to another Rust thread that will perform the actual async calls.
-
Results are marshaled back to the
Engine
thread via another MPSC channel.
Implementation
See the Multi-Threaded Synchronization pattern.
-
Spawn a thread to run the scripting
Engine
. Usually thesync
feature is NOT used for this pattern. -
Spawn another thread (the
worker
thread) that can perform the actual async calls in Rust. This thread may actually be the main thread of the program. -
Create a pair of MPSC channels (named
command
andreply
below) for full-duplex communications between the two threads. -
Register async API function to the
Engine
with a closure that captures the MPSC end-points. -
If there are more than one async function, the receive end-point on the
reply
channel can simply be cloned. The send end-point on thecommand
channel can be wrapped in anArc<Mutex<Channel>>
for shared access. -
In the async function, the name of the function and call arguments are serialized into JSON (or any appropriate message format) and sent to
command
channel, where they’ll be removed by theworker
thread and the appropriate async function called. -
The
Engine
blocks on the function call, waiting for a reply message on thereply
channel. -
When the async function call complete on the
worker
thread, the result is sent back to theEngine
thread via thereply
channel. -
After the result is obtained from the
reply
channel, theEngine
returns it as the return value of the function call, ending the block and continuing evaluation.
Passing External References to Rhai (Unsafe)
As with anything unsafe
, don’t do this unless you have exhausted all possible alternatives.
There are usually alternatives.
-
A system where an embedded
Engine
is used to call scripts within a callback function/closure from some external system.This is extremely common when working with an ECS (Entity Component System) or a GUI library where the script must draw via the provided graphics context.
-
Said external system provides only references (mutable or immutable) to work with their internal states.
-
Such states are not available outside of references (and therefore cannot be made shared).
-
Said external system’s API require such states to function.
-
It is impossible to pass references into Rhai because the
Engine
does not track lifetimes.
-
Make a newtype that wraps an integer which is set to the CPU’s pointer size (typically
usize
). -
transmute
the reference provided by the system into an integer and store it within the newtype. This requiresunsafe
code. -
Use the newtype as a handle for all registered API functions,
transmute
the integer back to a reference before use. This also requiresunsafe
code.
Make absolutely sure that the newtype is never stored anywhere permanent (e.g. in a Scope
)
nor does it ever live outside of the reference’s scope!
Otherwise, a crash is the system being nice to you…
Example
/// Newtype wrapping a reference (pointer) cast into 'usize'
/// together with a unique ID for protection.
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub struct WorldHandle(usize, i64);
/// Create handle from reference
impl From<&mut World> for WorldHandle {
fn from(world: &mut World) -> Self {
Self::new(world)
}
}
/// Recover reference from handle
impl AsMut<World> for WorldHandle {
fn as_mut(&mut self) -> &mut World {
unsafe { std::mem::transmute(self.0) }
}
}
impl WorldHandle {
/// Create handle from reference, using a random number as unique ID
pub fn new(world: &mut World) -> Self {
let handle =unsafe { std::mem::transmute(world) };
let unique_id = rand::random();
Self(handle, unique_id)
}
/// Get the unique ID of this instance
pub fn unique_id(&self) -> i64 {
self.1
}
}
/// API for handle to 'World'
#[export_module]
pub mod handle_module {
pub type Handle = WorldHandle;
/// Draw a bunch of pretty shapes.
#[rhai_fn(return_raw)]
pub fn draw(context: NativeCallContext, handle: &mut Handle, shapes: Array) -> Result<(), Box<EvalAltResult>> {
// Double check the pointer is still fresh
// by comparing the handle's unique ID with
// the version stored in the engine's tag!
if handle.unique_id() != context.tag() {
return "Ouch! The handle is stale!".into();
}
// Get the reference to 'World'
let world: &mut World = handle.as_mut();
// ... work with reference
world.draw_really_pretty_shapes(shapes);
Ok(())
}
}
// Register API for handle type
engine.register_global_module(exported_module!(handle_module).into());
// Successfully pass a reference to 'World' into Rhai
super_ecs_system.query(...).for_each(|world: &mut World| {
// Create handle from reference to 'World'
let handle: WorldHandle = world.into();
// Set the handle's ID into the engine's tag.
// Alternatively, use 'Engine::call_fn_with_options'.
engine.set_default_tag(handle.unique_id());
// Add handle into scope
let mut scope = Scope::new();
scope.push("world", handle);
// Work with handle
engine.run_with_scope(&mut scope, "world.draw([1, 2, 3])")
// Make sure no instance of 'handle' leaks outside this scope!
});
It is of utmost importance that no instance of the handle newtype ever leaks outside of the appropriate scope where the wrapped reference may no longer be valid.
For example, do not allow the script to store a copy of the handle anywhere that can potentially be persistent (e.g. within an object map).
Safety check via unique ID
One solution, as illustrated in the example, is to always tag each handle instance together with
a unique random ID. That same ID can then be set into the Engine
(via Engine::set_default_tag
)
before running scripts.
Before executing any API function, first check whether the handle’s ID matches that of the current Engine
(via NativeCallContext::tag
). If they differ, the handle is stale and should never be used;
an error should be returned instead.
Alternative to Engine::set_default_tag
Alternatively, if the Engine
cannot be made mutable, use Engine::call_fn_with_options
to set the ID before directly calling a script function in a compiled AST
.
Static Hashing
To counter DOS attacks, the hasher used by Rhai, ahash
, automatically generates a different
seed for hashing during each compilation and execution run.
This means that hash values generated by the hasher will not be stable – they change during each compile, and during each run.
For certain niche scenarios, however, dynamic hashes are undesirable.
So, when static hashing is employed, all hashing uses a fixed, predictable seed all the time.
Under static hashing, the same function signature always generate the same hash value, given the same seed.
A fixed hashing seed enlarges the attack surface of Rhai to malicious intent (e.g. DOS attacks).
Set at Run-Time
Call the static function rhai::config::hashing::set_ahash_seed
with four u64
numbers that make
up the hashing seed.
The seed specified is always used to initialize the hasher.
// Set specific hashing seed - do this BEFORE anything else!
rhai::config::hashing::set_ahash_seed(Some([123, 456, 789, 42]))?;
// ... from now on, all hashing will be predictable
let engine = Engine::new();
rhai::config::hashing::set_ahash_seed
can only ever be called once,
and must be called BEFORE performing any operation on Rhai (e.g. creating
an Engine
).
Calling it a second time simply returns an error.
Set at Compile Time
The hashing seed can also be provided, at compile time, via the environment variable
RHAI_AHASH_SEED
, with four u64
numbers that must be specified in Rust array literal format.
If a hashing seed is also set via rhai::config::hashing::set_ahash_seed
,
this environment variable has no effect.
RHAI_AHASH_SEED="[123, 456, 789, 42]" cargo build ...
The seed specified in RHAI_AHASH_SEED
is always used to initialize the hasher.
If the environment variable is missing, or it contains all zeros (i.e. [0, 0, 0, 0]
),
then static hashing is disabled.
TL;DR
For static hashing seed, ahash
requires:
default-features = false
runtime-rng
feature is not set (default on)compile-time-rng
feature is not set
The bane of additive Cargo features
However, ahash
is also extremely popular, used by many many other crates,
most notably hashbrown
.
Chances are that there are dependency crates that in turn depend on ahash
with
default features. Since cargo features are additive, it is almost certain that ahash
will use a runtime-generated seed for large projects.
Hence, there exists a need to tell ahash
to use a fixed seed, even when its feature flags
say otherwise.
Serialize an AST
In many situations, it is tempting to serialize an AST
, so that it can be loaded and recreated
later on.
In Rhai, there is usually little reason to do so.
Serialize the AST
into some format for storage.
Storing the original script text, preferably compressed (via gzip
etc.) usually yields much smaller data size.
Plus, is it possibly faster to recompile the original script than to recreate the AST
via
deserialization.
That is because the deserialization processing is essentially a form of parsing, in this case
parsing the serialized data into an AST
– an equivalent process to what Rhai does, which
is parsing the script text into the same AST
.
Illustration
The following script occupies only 42 bytes, possibly less if compressed. That is only slightly more than 5 words on a 64-bit CPU!
fn sum(x, y) { x + y }
print(sum(42, 1));
The AST
would be much more complicated and looks something like this:
FnDef {
Name: "sum",
ThisType: None,
Parameters: [
"x",
"y"
],
Body: Block [
Return {
Expression {
FnCall {
Name: "+",
Arguments: [
Variable "x",
Variable "y"
]
}
}
}
]
}
Block [
FnCall {
Name: "print",
Arguments: [
Expression {
FnCall {
Name: "sum",
Arguments: [
Constant 42,
Constant 1
]
}
}
]
}
]
which would take much more space to serialize.
For instance, the constant 1 alone would take up 8 bytes, while the script text takes up only one byte!
Domain-Specific Tools
-
A system has a domain-specific API, requiring custom types and/or Rust functions to be registered and exposed to scripting.
-
The system’s behavior is controlled by Rhai script functions, such as in the Scriptable Event Handler with State, Control Layer or Singleton Command Object patterns.
-
It is desirable to be able to interactively test the system with different scripts, and/or debug them.
Implementation
Copy necessary tool source files
Each bin tool is a single source file.
Download the necessary one(s) into the project’s bin
or example
subdirectory.
Select this source file | To make |
---|---|
rhai-run.rs | a simple script runner for the system |
rhai-repl.rs | an interactive REPL for the system |
rhai-dbg.rs | a script debugger for the system |
Example
rhai-run.rs -> /path/to/my_project/examples/test.rs
rhai-repl.rs -> /path/to/my_project/examples/repl.rs
rhai-dbg.rs -> /path/to/my_project/examples/db.rs
Leverage Engine
configuration code in the project
Assume the project already contains configuration code for a customized Engine
.
use rhai::Engine;
use rhai::plugin::*;
// Domain-specific data types
use my_project::*;
#[export_module]
mod my_domain_api {
:
:
// plugin module
:
:
}
// This function creates a new Rhai scripting engine and
// configures it properly
fn create_scripting_engine(config: MySystemConfig) -> Engine {
let mut engine = Engine::new();
// Register domain-specific API into the Engine
engine.register_type_with_name::<MyObject>("MyObject")
.register_type_with_name::<MyOtherObject>("MyOtherObject")
.register_fn(...)
.register_fn(...)
.register_fn(...)
:
:
// register API functions
:
:
.register_get_set(...)
.register_index_get_set(...)
.register_fn(...);
// Plugin modules can be used to easily and quickly
// register an entire API
engine.register_global_module(exported_module!(my_domain_api).into());
// Configuration options in 'MySystemConfig' may be used
// to control the Engine's behavior
if config.strict_mode {
engine.set_strict_variables(true);
}
// Return the scripting engine
engine
}
Modify Engine
creation
Each bin tool contains a line that creates the main script Engine
.
Modify it to call the project’s creation function.
// Initialize scripting engine
let mut engine = Engine::new();
// Modify to this:
let mut engine = create_scripting_engine(my_config);
Make sure that Rhai has the appropriate feature(s)
In the project’s Cargo.toml
, specify the Rhai dependency with bin-features
.
[dependencies]
rhai = { version = "1.20.0", features = ["bin-features"] }
Rebuild
Rebuild the project, which should automatically build all the customized tools.
Run tools
Each customized tool now has access to the entire domain-specific API. Functions and custom types can be used in REPL, debugging etc.
Multiple Instantiation
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, however, is more complex.
Rhai Language Features Are Not Additive
Language features cannot be easily made additive.
That is because the lack of a language feature is a feature by itself.
Assume an additive feature called floating-point
that adds floating-point support.
Assume also that the application omits the floating-point
feature (why? perhaps integers are all
that make sense within the project domain). Floating-point numbers do not even parse under this
configuration and will generate syntax errors.
Now, assume that a dependent crate also depends on Rhai, but a new version suddenly decides to
require floating-point support. That dependent crate would, naturally, specify the
floating-point
feature.
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 floating-point
feature turned on because features are additive.
This will in turn break the application, which by itself specifically omits floating-point
and
expects floating-point numbers to be rejected, in unexpected ways. Suddenly and without warning,
floating-point numbers show up in data which the application is not prepared to handle.
There is no way out of this dilemma, because the lack of a language feature can be depended upon as a feature.
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:
-
Different versions from
crates.io
– The official crate. -
Different releases from
GitHub
– Crate source on GitHub. -
Forked copy of https://github.com/rhaiscript/rhai on GitHub.
-
Local copy of https://github.com/rhaiscript/rhai downloaded form GitHub.
Use the following configuration in Cargo.toml
to pull in multiple copies of Rhai within the same project:
[dependencies]
rhai = { version = "1.20.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.
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.
Rhai Language Reference
This is a stand-alone reference for the Rhai scripting language.
Comments
Comments are C-style, including /*
… */
pairs for block comments and //
for comments to the
end of the line.
Block comments can be nested.
let /* intruder comment */ name = "Bob";
// This is a very important one-line comment
/* This comment spans
multiple lines, so it
only makes sense that
it is even more important */
/* Fear not, Rhai satisfies all nesting needs with nested comments:
/*/*/*/*/**/*/*/*/*/
*/
Doc-Comments
Comments starting with ///
(three slashes) or /**
(two asterisks) are doc-comments.
Doc-comments can only appear in front of function definitions, not any other elements.
/// 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;
}
Long streams of //////
… and /*****
… do NOT form doc-comments.
This is consistent with popular comment block styles for C-like languages.
/////////////////////////////// <- this is not a doc-comment
// This is not a doc-comment // <- this is not a doc-comment
/////////////////////////////// <- this is not a doc-comment
// However, watch out for comment lines starting with '///'
////////////////////////////////////////// <- this is not a doc-comment
/// This, however, IS a doc-comment!!! /// <- doc-comment!
////////////////////////////////////////// <- this is not a doc-comment
/****************************************
* *
* This is also not a doc-comment block *
* so we don't have to put this in *
* front of a function. *
* *
****************************************/
Module Documentation
Comment lines starting with //!
make up the module documentation.
They are used to document the containing module – or for a Rhai script file, to document the file itself.
//! Documentation for this script file.
//! This script is used to calculate something and display the result.
fn calculate(x) {
...
}
fn display(msg) {
//! Module documentation can be placed anywhere within the file.
...
}
//! All module documentation lines will be collected into a single block.
For the example above, the module documentation block is:
//! Documentation for this script file.
//! This script is used to calculate something and display the result.
//! Module documentation can be placed anywhere within the file.
//! All module documentation lines will be collected into a single block.
Value Types
The following primitive value types are supported natively.
Category | type_of() | to_string() |
---|---|---|
System integer | "i32" or "i64" | "42" , "123" etc. |
Other integer number | "i32" , "u64" etc. | "42" , "123" etc. |
Integer numeric range | "range" , "range=" | "2..7" , "0..=15" etc. |
Floating-point number | "f32" or "f64" | "123.4567" etc. |
Fixed precision decimal number | "decimal" | "42" , "123.4567" etc. |
Boolean value | "bool" | "true" or "false" |
Unicode character | "char" | "A" , "x" etc. |
Immutable Unicode string | "string" | "hello" etc. |
Array | "array" | "[ 1, 2, 3 ]" etc. |
Byte array – BLOB | "blob" | "[01020304abcd]" etc. |
Object map | "map" | "#{ "a": 1, "b": true }" etc. |
Timestamp | "timestamp" | "<timestamp>" |
Function pointer | "Fn" | "Fn(foo)" etc. |
Dynamic value (i.e. can be anything) | the actual type | actual value |
Shared value (a reference-counted, shared dynamic value, created via closures | the actual type | actual value |
Nothing/void/nil/null/Unit (or whatever it is called) | "()" | "" (empty string) |
All types are treated strictly distinct by Rhai, meaning that i32
and i64
and u32
are
completely different. They cannot even be added together.
This is very similar to Rust.
Dynamic Values
A dynamic value can be any type.
let x = 42; // value is an integer
x = 123.456; // value is now a floating-point number
x = "hello"; // value is now a string
x = x.len > 0; // value is now a boolean
x = [x]; // value is now an array
x = #{x: x}; // value is now an object map
Use type_of()
to Get Value Type
Because type_of()
a dynamic value returns the type of the actual value,
it is usually used to perform type-specific actions based on the actual value’s type.
let mystery = get_some_dynamic_value();
switch type_of(mystery) {
"()" => print("Hey, I got the unit () here!"),
"i64" => print("Hey, I got an integer here!"),
"f64" => print("Hey, I got a float here!"),
"decimal" => print("Hey, I got a decimal here!"),
"range" => print("Hey, I got an exclusive range here!"),
"range=" => print("Hey, I got an inclusive range here!"),
"string" => print("Hey, I got a string here!"),
"bool" => print("Hey, I got a boolean here!"),
"array" => print("Hey, I got an array here!"),
"blob" => print("Hey, I got a BLOB here!"),
"map" => print("Hey, I got an object map here!"),
"Fn" => print("Hey, I got a function pointer here!"),
"timestamp" => print("Hey, I got a time-stamp here!"),
"TestStruct" => print("Hey, I got the TestStruct custom type here!"),
_ => print(`I don't know what this is: ${type_of(mystery)}`)
}
type_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)}!!!`
}
See here for the type_of
output of standard types.
Dynamic Value Tag
Each dynamic value can contain a tag that is i32
and can contain any arbitrary 32-bit signed data.
On 32-bit targets, however, the tag is only i16
(16-bit signed).
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
let x = 42;
x.tag == 0; // tag defaults to zero
x.tag = 123; // set tag value
set_tag(x, 123); // 'set_tag' function also works
x.tag == 123; // get updated tag value
x.tag() == 123; // method also works
tag(x) == 123; // function call style also works
x.tag[3..5] = 2; // tag can be used as a bit-field
x.tag[3..5] == 2;
let y = x;
y.tag == 123; // the tag is copied across assignment
y.tag = 3000000000; // runtime error: 3000000000 is too large for 'i32'
Practical Applications
Attaching arbitrary information together with a value has a lot of practical uses.
Identify code path
For example, it is easy to attach an ID number to a value to indicate how or why that value is originally set.
This is tremendously convenient for debugging purposes where it is necessary to figure out which code path a particular value went through.
After the script is verified, all tag assignment statements can simply be removed.
const ROUTE1 = 1;
const ROUTE2 = 2;
const ROUTE3 = 3;
const ERROR_ROUTE = 9;
fn some_complex_calculation(x) {
let result;
if some_complex_condition(x) {
result = 42;
result.tag = ROUTE1; // record route #1
} else if some_other_very_complex_condition(x) == 1 {
result = 123;
result.tag = ROUTE2; // record route #2
} else if some_non_understandable_calculation(x) > 0 {
result = 0;
result.tag = ROUTE3; // record route #3
} else {
result = -1;
result.tag = ERROR_ROUTE; // record error
}
result // this value now contains the tag
}
let my_result = some_complex_calculation(key);
// The code path that 'my_result' went through is now in its tag.
// It is now easy to trace how 'my_result' gets its final value.
print(`Result = ${my_result} and reason = ${my_result.tag}`);
Identify data source
It is convenient to use the tag value to record the source of a piece of data.
let x = [0, 1, 2, 3, 42, 99, 123];
// Store the index number of each value into its tag before
// filtering out all even numbers, leaving only odd numbers
let filtered = x.map(|v, i| { v.tag = i; v }).filter(|v| v.is_odd());
// The tag now contains the original index position
for (data, i) in filtered {
print(`${i + 1}: Value ${data} from position #${data.tag + 1}`);
}
Identify code conditions
The tag value may also contain a bit-field of up to 32 (16 under 32-bit targets) individual bits, recording up to 32 (or 16 under 32-bit targets) logic conditions that contributed to the value.
Again, after the script is verified, all tag assignment statements can simply be removed.
fn some_complex_calculation(x) {
let result = x;
// Check first condition
if some_complex_condition() {
result += 1;
result.tag[0] = true; // Set first bit in bit-field
}
// Check second condition
if some_other_very_complex_condition(x) == 1 {
result *= 10;
result.tag[1] = true; // Set second bit in bit-field
}
// Check third condition
if some_non_understandable_calculation(x) > 0 {
result -= 42;
result.tag[2] = true; // Set third bit in bit-field
}
// Check result
if result > 100 {
result = 0;
result.tag[3] = true; // Set forth bit in bit-field
}
result
}
let my_result = some_complex_calculation(42);
// The tag of 'my_result' now contains a bit-field indicating
// the result of each condition.
// It is now easy to trace how 'my_result' gets its final value.
// Use indexing on the tag to get at individual bits.
print(`Result = ${my_result}`);
print(`First condition = ${my_result.tag[0]}`);
print(`Second condition = ${my_result.tag[1]}`);
print(`Third condition = ${my_result.tag[2]}`);
print(`Result check = ${my_result.tag[3]}`);
Return auxillary info
Sometimes it is useful to return auxillary info from a function.
// Verify Bell's Inequality by calculating a norm
// and comparing it with a hypotenuse.
// https://en.wikipedia.org/wiki/Bell%27s_theorem
//
// Returns the smaller of the norm or hypotenuse.
// Tag is 1 if norm <= hypo, 0 if otherwise.
fn bells_inequality(x, y, z) {
let norm = sqrt(x ** 2 + y ** 2);
let result;
if norm <= z {
result = norm;
result.tag = 1;
} else {
result = z;
result.tag = 0;
}
result
}
let dist = bells_inequality(x, y, z);
print(`Value = ${dist}`);
if dist.tag == 1 {
print("Local realism maintained! Einstein rules!");
} else {
print("Spooky action at a distance detected! Einstein will hate this...");
}
Poor-man’s tuples
Rhai does not have tuples (nor does JavaScript in this sense).
Similar to the JavaScript situation, practical alternatives using Rhai include returning an object map or an array.
Both of these alternatives, however, incur overhead that may be wasteful when the amount of
additional information is small – e.g. in many cases, a single bool
, or a small number.
To return a number of small values from functions, the tag value as a bit-field is an ideal container without resorting to a full-blown object map or array.
// This function essentially returns a tuple of four numbers:
// (result, a, b, c)
fn complex_calc(x, y, z) {
let a = x + y;
let b = x - y + z;
let c = (a + b) * z / y;
let r = do_complex_calculation(a, b, c);
// Store 'a', 'b' and 'c' into tag if they are small
r.tag[0..8] = a;
r.tag[8..16] = b;
r.tag[16..32] = c;
r
}
// Deconstruct the tuple
let result = complex_calc(x, y, z);
let a = r.tag[0..8];
let b = r.tag[8..16];
let c = r.tag[16..32];
Numbers
Integers
Integer numbers follow C-style format with support for decimal, binary (0b
), octal (0o
) and hex (0x
) notations.
Integers can also be conveniently manipulated as bit-fields.
Floating-Point Numbers
Both decimal and scientific notations can be used to represent floating-point numbers.
Decimal Numbers
When rounding errors cannot be accepted, such as in financial calculations, use 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 (.
).
Sample | Format |
---|---|
_123 | improper separator |
123_345 , -42 | decimal |
0o07_76 | octal |
0xab_cd_ef | hex |
0b0101_1001 | binary |
123._456 | improper separator |
123_456.78_9 | normal floating-point |
-42. | ending with decimal point |
123_456_.789e-10 | scientific notation |
.456 | missing leading 0 |
123.456e_10 | improper separator |
123.e-10 | missing decimal 0 |
Floating-Point vs. Decimal
Decimal numbers 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 numbers take up more space (16 bytes each) than a standard floating-point number (4-8 bytes) and is much slower in calculations due to the lack of CPU hardware support. Use it only when necessary.
Numeric Operators
Numeric operators generally follow C styles.
Unary Operators
Operator | Description |
---|---|
+ | positive |
- | negative |
let number = +42;
number = -5;
number = -5 - +5;
-(-42) == +42; // two '-' equals '+'
// beware: '++' and '--' are reserved symbols
Binary Operators
Operator | Description | Result type | Integer | Floating-point | Decimal |
---|---|---|---|---|---|
+ , += | plus | numeric | yes | yes, also integer | yes, also integer |
- , -= | minus | numeric | yes | yes, also integer | yes, also integer |
* , *= | multiply | numeric | yes | yes, also integer | yes, also integer |
/ , /= | divide (integer division if acting on integer types) | numeric | yes | yes, also integer | yes, also integer |
% , %= | modulo (remainder) | numeric | yes | yes, also integer | yes, also integer |
** , **= | power/exponentiation | numeric | yes | yes, also FLOAT**INT | no |
<< , <<= | left bit-shift (if negative number of bits, shift right instead) | numeric | yes | no | no |
>> , >>= | right bit-shift (if negative number of bits, shift left instead) | numeric | yes | no | no |
& , &= | bit-wise And | numeric | yes | no | no |
| , |= | bit-wise Or | numeric | yes | no | no |
^ , ^= | bit-wise Xor | numeric | yes | no | no |
== | equals to | bool | yes | yes, also integer | yes, also integer |
!= | not equals to | bool | yes | yes, also integer | yes, also integer |
> | greater than | bool | yes | yes, also integer | yes, also integer |
>= | greater than or equals to | bool | yes | yes, also integer | yes, also integer |
< | less than | bool | yes | yes, also integer | yes, also integer |
<= | less than or equals to | bool | yes | yes, also integer | yes, also integer |
.. | exclusive range | range | yes | no | no |
..= | inclusive range | range | yes | no | no |
Examples
let x = (1 + 2) * (6 - 4) / 2; // arithmetic, with parentheses
let reminder = 42 % 10; // modulo
let power = 42 ** 2; // power
let left_shifted = 42 << 3; // left shift
let right_shifted = 42 >> 3; // right shift
let bit_op = 42 | 99; // bit masking
Floating-Point Interoperates with Integers
When one of the operands to a binary arithmetic operator is floating-point, it works with integer for the other operand and the result is floating-point.
let x = 41.0 + 1; // float + integer
type_of(x) == "f64"; // result is float
let x = 21 * 2.0; // float * integer
type_of(x) == "f64";
(x == 42) == true; // float == integer
(10 < x) == true; // integer < float
Decimal Interoperates with Integers
When one of the operands to a binary arithmetic operator is decimal, it works with integer for the other operand and the result is decimal.
let d = parse_decimal("2");
let x = d + 1; // decimal + integer
type_of(x) == "decimal"; // result is decimal
let x = 21 * d; // decimal * integer
type_of(x) == "decimal";
(x == 42) == true; // decimal == integer
(10 < x) == true; // integer < decimal
Unary Before Binary
In Rhai, unary operators take [precedence] over binary operators. This is especially important to
remember when handling operators such as **
which in some languages bind tighter than the unary
-
operator.
-2 + 2 == 0;
-2 - 2 == -4;
-2 * 2 == -4;
-2 / 2 == -1;
-2 % 2 == 0;
-2 ** 2 = 4; // means: (-2) ** 2
// in some languages this means: -(2 ** 2)
Numeric Functions
Integer Functions
The following standard functions operate on integers only.
Function | Description |
---|---|
is_odd method and property | returns true if the value is an odd number, otherwise false |
is_even method and property | returns true if the value is an even number, otherwise false |
min | returns the smaller of two numbers, the first number if equal |
max | returns the larger of two numbers, the first number if equal |
to_float | convert the value into f64 (f32 under 32-bit) |
to_decimal | convert the value into decimal |
Signed Numeric Functions
The following standard functions operate on signed numbers (including floating-point and decimal) only.
Function | Description |
---|---|
abs | absolute value |
sign | returns −1 if negative, +1 if positive, 0 if zero |
is_zero method and property | returns true if the value is zero, otherwise false |
Floating-Point Functions
The following standard functions operate on floating-point and decimal numbers only.
Category | Decimal? | Functions |
---|---|---|
Trigonometry | yes | sin , cos , tan |
Trigonometry | no | sinh , cosh , tanh in radians, hypot( x, y) |
Arc-trigonometry | no | asin , acos , atan( v) , atan( x, y) , asinh , acosh , atanh in radians |
Square root | yes | sqrt |
Exponential | yes | exp (base e) |
Logarithmic | yes | ln (base e), log (base 10) |
Logarithmic | no | log( x, base) |
Rounding | yes | floor , ceiling , round , int , fraction methods and properties |
Conversion | yes | to_int , to_decimal , to_float |
Conversion | no | to_degrees , to_radians |
Comparison | yes | min , max (also inter-operates with integers) |
Testing | no | is_nan , is_finite , is_infinite methods and properties |
Decimal Rounding Functions
The following rounding methods operate on decimal numbers only.
Rounding type | Behavior | Methods |
---|---|---|
None | floor , ceiling , int , fraction methods and properties | |
Banker’s rounding | round to integer | round method and property |
Banker’s rounding | round to specified number of decimal points | round( decimal points) |
Round up | away from zero | round_up( decimal points) |
Round down | towards zero | round_down( decimal points) |
Round half-up | mid-point away from zero | round_half_up( decimal points) |
Round half-down | mid-point towards zero | round_half_down( decimal points) |
Parsing Functions
The following standard functions parse numbers.
Function | Description |
---|---|
parse_int | converts a string to integer with an optional radix |
parse_float | converts a string to floating-point |
parse_decimal | converts a string to decimal |
Formatting Functions
The following standard functions convert integer numbers into a string of hex, octal or binary representations.
Function | Description |
---|---|
to_binary | converts an integer number to binary |
to_octal | converts an integer number to octal |
to_hex | converts an integer number to hex |
Floating-point Constants
The following functions return standard mathematical constants.
Function | Description |
---|---|
PI | returns the value of π |
E | returns the value of e |
Value Conversions
Convert Between Integer and Floating-Point
Function | From type | To type |
---|---|---|
to_int | floating-point, decimal | integer |
to_float | integer, decimal | floating-point |
to_decimal | integer, floating-point | 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
Function | From type | To type |
---|---|---|
parse_int | string | integer |
parse_int with radix 2-36 | string | integer (specified radix) |
parse_float | string | floating-point |
parse_decimal | string | decimal |
let x = parse_float("123.4"); // parse as floating-point
x == 123.4;
type_of(x) == "f64";
let x = parse_decimal("123.4"); // parse as Decimal value
type_of(x) == "decimal";
let x = 1234.to_decimal() / 10; // alternate method to create a Decimal value
type_of(x) == "decimal";
let dec = parse_int("42"); // parse as integer
dec == 42;
type_of(dec) == "i64";
let dec = parse_int("42", 10); // radix = 10 is the default
dec == 42;
type_of(dec) == "i64";
let bin = parse_int("110", 2); // parse as binary (radix = 2)
bin == 0b110;
type_of(bin) == "i64";
let hex = parse_int("ab", 16); // parse as hex (radix = 16)
hex == 0xab;
type_of(hex) == "i64";
Format Numbers
Function | From type | To type | Format |
---|---|---|---|
to_binary | integer | string | binary (i.e. only 1 and 0 ) |
to_octal | integer | string | octal (i.e. 0 … 7 ) |
to_hex | integer | string | hex (i.e. 0 … f ) |
let x = 0x1234abcd;
x == 305441741;
x.to_string() == "305441741";
x.to_binary() == "10010001101001010101111001101";
x.to_octal() == "2215125715";
x.to_hex() == "1234abcd";
Ranges
Syntax
Numeric ranges can be constructed by the ..
(exclusive) or ..=
(inclusive) operators.
Exclusive range
start
..
end
An exclusive range does not include the last (i.e. “end”) value.
type_of()
an exclusive range returns "range"
.
Inclusive range
start
..=
end
An inclusive range includes the last (i.e. “end”) value.
type_of()
an inclusive range returns "range="
.
Usage Scenarios
Ranges are commonly used in the following scenarios.
Scenario | Example |
---|---|
for statements | for n in 0..100 { ... } |
in expressions | if n in 0..100 { ... } |
switch expressions | switch n { 0..100 => ... } |
Bit-fields access | let x = n[2..6]; |
Bits iteration | for bit in n.bits(2..=9) { ... } |
Array range-based API’s | array.extract(2..8) |
BLOB range-based API’s | blob.parse_le_int(4..8) |
String range-based API’s | string.sub_string(4..=12) |
Characters iteration | for ch in string.bits(4..=12) { ... } |
Built-in Functions
The following methods operate on ranges.
Function | Parameter(s) | Description |
---|---|---|
start method and property | beginning of the range | |
end method and property | end of the range | |
contains , in operator | number to check | does this range contain the specified number? |
is_empty method and property | returns true if the range contains no items | |
is_inclusive method and property | is the range inclusive? | |
is_exclusive method and property | is the range exclusive? |
TL;DR
Rust has open-ended ranges, such as start..
, ..end
and ..=end
. They are not available in Rhai.
They are not needed because Rhai can overload functions.
Typically, an API accepting ranges as parameters would have equivalent versions that accept a
starting position and a length (the standard start + len
pair), as well as a versions that accept
only the starting position (the length assuming to the end).
In fact, usually all versions redirect to a call to one single version.
Therefore, there should always be a function that can do what open-ended ranges are intended for.
The left-open form (i.e. ..end
and ..=end
) is trivially replaced by using zero as the starting
position with a length that corresponds to the end position (for ..end
).
The right-open form (i.e. start..
) is trivially replaced by the version taking a single starting position.
let x = [1, 2, 3, 4, 5];
x.extract(0..3); // normal range argument
// copies 'x' from positions 0-2
x.extract(2); // copies 'x' from position 2 onwards
// equivalent to '2..'
x.extract(0, 2); // copies 'x' from beginning for 2 items
// equivalent to '..2'
Integer as Bit-Fields
Nothing here cannot be done via standard bit-manipulation (i.e. shifting and masking).
Built-in support is more elegant and performant since it usually replaces a sequence of multiple steps.
Since bit-wise operators are defined on integer numbers, individual bits can also be accessed and manipulated via an indexing syntax.
If a bit is set (i.e. 1
), the index access returns true
.
If a bit is not set (i.e. 0
), the index access returns false
.
When a range is used, the bits within the range are shifted and extracted as an integer value.
Bit-fields are very commonly used in embedded systems which must squeeze data into limited memory.
Built-in support makes handling them efficient.
Syntax
From Least-Significant Bit (LSB)
Bits in a bit-field are accessed with zero-based, non-negative integer indices:
integer
[
index from 0 to 63 or 31]
integer
[
index from 0 to 63 or 31] =
true
orfalse
;
[Ranges] can also be used:
integer
[
start..
end]
integer[
start..=
end]
integer
[
start..
end] =
new integer value ;
integer[
start..=
end] =
new integer value ;
The maximum bit number that can be accessed is one less than the number of bits for the system integer type (usually 63).
Bits outside of the range are ignored.
From Most-Significant Bit (MSB)
A negative index accesses a bit in the bit-field counting from the end, or from the most-significant bit, with −1 being the highest bit.
integer
[
index from −1 to −64 or −32]
integer
[
index from −1 to −64 or −32] =
true
orfalse
;
Ranges always count from the least-significant bit (LSB) and has no support for negative positions.
The maximum bit number that can be accessed is negative the number of bits for the system integer type (usually −64).
Bit-Field Functions
The following standard functions operate on bit-fields.
Function | Parameter(s) | Description |
---|---|---|
get_bit | bit number, counting from MSB if < 0 | returns the state of a bit: true if 1 , false if 0 |
set_bit |
| sets the state of a bit |
get_bits |
| extracts a number of bits, shifted towards LSB |
get_bits | range of bits | extracts a number of bits, shifted towards LSB |
set_bits |
| sets a number of bits from the new value |
set_bits |
| sets a number of bits from the new value |
bits method and property |
| allows iteration over the bits of a bit-field |
bits | range of bits | allows iteration over the bits of a bit-field |
Example
// Assume the following bits fields in a single 16-bit word:
// ┌─────────┬────────────┬──────┬─────────┐
// │ 15-12 │ 11-4 │ 3 │ 2-0 │
// ├─────────┼────────────┼──────┼─────────┤
// │ 0 │ 0-255 data │ flag │ command │
// └─────────┴────────────┴──────┴─────────┘
let value = read_start_hw_register(42);
let command = value.get_bits(0, 3); // Command = bits 0-2
let flag = value[3]; // Flag = bit 3
let data = value[4..=11]; // Data = bits 4-11
let data = value.get_bits(4..=11); // <- above is the same as this
let reserved = value.get_bits(-4); // Reserved = last 4 bits
if reserved != 0 {
throw reserved;
}
switch command {
0 => print(`Data = ${data}`),
1 => value[4..=11] = data / 2,
2 => value[3] = !flag,
_ => print(`Unknown: ${command}`)
}
Strings and Characters
String in Rhai contain any text sequence of valid Unicode characters.
type_of()
a string returns "string"
.
String and Character Literals
String and character literals follow JavaScript-style syntax.
Type | Quotes | Escapes? | Continuation? | Interpolation? |
---|---|---|---|---|
Normal string | "..." | yes | with \ | no |
Raw string | #..#"..."#..# | no | no | no |
Multi-line literal string | `...` | no | no | with ${...} |
Character | '...' | yes | no | no |
Strings can be built up from other strings and types via the +
or +=
operators.
Standard Escape Sequences
Use the to_int
method to convert a Unicode character into its 32-bit Unicode encoding.
There is built-in support for Unicode (\u
xxxx or \U
xxxxxxxx) and hex (\x
xx) 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 sequence | Meaning |
---|---|
\\ | back-slash (\ ) |
\t | tab |
\r | carriage-return (CR ) |
\n | line-feed (LF ) |
\" or "" | double-quote (" ) |
\' | single-quote (' ) |
\x xx | ASCII character in 2-digit hex |
\u xxxx | Unicode character in 4-digit hex |
\U xxxxxxxx | Unicode character in 8-digit hex |
Line Continuation
For a normal string wrapped by double-quotes ("
), a back-slash (\
) character at the end of a
line indicates that the string continues onto the next line without any line-break.
Whitespace up to the indentation of the opening double-quote is ignored in order to enable lining up blocks of text.
Spaces are not added, so to separate one line with the next with a space, put a space before the
ending back-slash (\
) character.
let x = "hello, world!\
hello world again! \
this is the ""last"" time!!!";
// ^^^^^^ these whitespaces are ignored
// The above is the same as:
let x = "hello, world!hello world again! this is the \"last\" time!!!";
A string with continuation does not open up a new line. To do so, a new-line character must be manually inserted at the appropriate position.
let x = "hello, world!\n\
hello world again!\n\
this is the last time!!!";
// The above is the same as:
let x = "hello, world!\nhello world again!\nthis is the last time!!!";
If the ending double-quote is omitted, it is a syntax error.
let x = "hello
";
// ^ syntax error: unterminated string literal
Technically speaking, there is no difficulty in allowing strings to run for multiple lines without the continuation back-slash.
Rhai forces you to manually mark a continuation with a back-slash because the ending quote is easy to omit. Once it happens, the entire remainder of the script would become one giant, multi-line string.
This behavior is different from Rust, where string literals can run for multiple lines.
Raw Strings
A raw string is any text enclosed by a pair of double-quotes ("
), wrapped by hash (#
) characters.
The number of hash (#
) on each side must be the same.
Any text inside the double-quotes, as long as it is not a double-quote ("
) followed by the same
number of hash (#
) characters, is simply copied verbatim, including control codes and/or
line-breaks.
Raw strings are very useful for embedded regular expressions, file paths, and program code etc.
let x = #"Hello, I am a raw string! which means that I can contain
line-breaks, \ slashes (not escapes), "quotes" and even # characters!"#
// Use more than one '#' if you happen to have '"###...' inside the string...
let x = ###"In Rhai, you can write ##"hello"## as a raw string."###;
// ^^^ this is not the end of the raw string
Multi-Line Literal Strings
A string wrapped by a pair of back-tick (`
) characters is interpreted literally,
meaning that every single character that lies between the two back-ticks is taken verbatim.
This include new-lines, whitespaces, escape characters etc.
let x = `hello, world! "\t\x42"
hello world again! 'x'
this is the last time!!! `;
// The above is the same as:
let x = "hello, world! \"\\t\\x42\"\n hello world again! 'x'\n this is the last time!!! ";
If a back-tick (`
) appears at the end of a line, then it is understood that the entire text
block starts from the next line; the starting new-line character is stripped.
let x = `
hello, world! "\t\x42"
hello world again! 'x'
this is the last time!!!
`;
// The above is the same as:
let x = " hello, world! \"\\t\\x42\"\n hello world again! 'x'\n this is the last time!!!\n";
To actually put a back-tick (`
) character inside a multi-line literal string, use two
back-ticks together (i.e. ``
).
let x = `I have a quote " as well as a back-tick `` here.`;
// The above is the same as:
let x = "I have a quote \" as well as a back-tick ` here.";
String Interpolation
🤦 Well, you just have to ask for the impossible, don’t you?
Currently there is no way to escape ${
. Build the string in three pieces:
`Interpolations start with "`
+ "${"
+ `" and end with }.`
Multi-line literal strings support string interpolation wrapped in ${
… }
.
Interpolation is not supported for normal string or character literals.
${
… }
acts as a statements block and can contain anything that is allowed within a
statements block, including another interpolated string!
The last result of the block is taken as the value for interpolation.
Rhai uses to_string
to convert any value into a string, then physically joins all
the sub-strings together.
For convenience, if any interpolated value is a BLOB, however, it is automatically treated as a UTF-8 encoded string. That is because it is rarely useful to interpolate a BLOB into a string, but extremely useful to be able to directly manipulate UTF-8 encoded text.
let x = 42;
let y = 123;
let s = `x = ${x} and y = ${y}.`; // <- interpolated string
let s = ("x = " + {x} + " and y = " + {y} + "."); // <- de-sugars to this
s == "x = 42 and y = 123.";
let s = `
Undeniable logic:
1) Hello, ${let w = `${x} world`; if x > 1 { w += "s" } w}!
2) If ${y} > ${x} then it is ${y > x}!
`;
s == "Undeniable logic:\n1) Hello, 42 worlds!\n2) If 123 > 42 then it is true!\n";
let blob = blob(3, 0x21);
print(blob); // prints [212121]
print(`Data: ${blob}`); // prints "Data: !!!"
// BLOB is treated as UTF-8 encoded string
print(`Data: ${blob.to_string()}`); // prints "Data: [212121]"
Indexing
Strings can be indexed into to get access to any individual character. This is similar to many modern languages but different from Rust.
From beginning
Individual characters within a string can be accessed with zero-based, non-negative integer indices:
string
[
index from 0 to (total number of characters − 1)]
From end
A negative index accesses a character in the string counting from the end, with −1 being the last character.
string
[
index from −1 to −(total number of characters)]
Internally, a Rhai string is still stored compactly as a Rust UTF-8 string in order to save memory.
Therefore, getting the character at a particular index involves walking through the entire UTF-8 encoded bytes stream to extract individual Unicode characters, counting them on the way.
Because of this, indexing can be a slow procedure, especially for long strings. Along the same lines, getting the length of a string (which returns the number of characters, not bytes) can also be slow.
Sub-Strings
Sub-strings, or slices in some programming languages, are parts of strings.
In Rhai, a sub-string can be specified by indexing with a range of characters:
string
[
first character (starting from zero)..
last character (exclusive)]
string
[
first character (starting from zero)..=
last character (inclusive)]
Sub-string ranges always start from zero counting towards the end of the string. Negative ranges are not supported.
Examples
let name = "Bob";
let middle_initial = 'C';
let last = "Davis";
let full_name = `${name} ${middle_initial}. ${last}`;
full_name == "Bob C. Davis";
// String building with different types
let age = 42;
let record = `${full_name}: age ${age}`;
record == "Bob C. Davis: age 42";
// Unlike Rust, Rhai strings can be indexed to get a character
// (disabled with 'no_index')
let c = record[4];
c == 'C'; // single character
let slice = record[4..8]; // sub-string slice
slice == " C. D";
ts.s = record; // custom type properties can take strings
let c = ts.s[4];
c == 'C';
let c = ts.s[-4]; // negative index counts from the end
c == 'e';
let c = "foo"[0]; // indexing also works on string literals...
c == 'f';
let c = ("foo" + "bar")[5]; // ... and expressions returning strings
c == 'r';
let text = "hello, world!";
text[0] = 'H'; // modify a single character
text == "Hello, world!";
text[7..=11] = "Earth"; // modify a sub-string slice
text == "Hello, Earth!";
// Escape sequences in strings
record += " \u2764\n"; // escape sequence of '❤' in Unicode
record == "Bob C. Davis: age 42 ❤\n"; // '\n' = new-line
// Unlike Rust, Rhai strings can be directly modified character-by-character
// (disabled with 'no_index')
record[4] = '\x58'; // 0x58 = 'X'
record == "Bob X. Davis: age 42 ❤\n";
// Use 'in' to test if a substring (or character) exists in a string
"Davis" in record == true;
'X' in record == true;
'C' in record == false;
// Strings can be iterated with a 'for' statement, yielding characters
for ch in record {
print(ch);
}
Standard String Functions
The following standard methods operate on strings (and possibly characters).
Function | Parameter(s) | Description |
---|---|---|
len method and property | none | returns the number of characters (not number of bytes) in the string |
bytes method and property | none | returns the number of bytes making up the UTF-8 string; for strings containing only ASCII characters, this is much faster than len |
is_empty method and property | none | returns true if the string is empty |
to_blob | none | converts the string into an UTF-8 encoded byte-stream and returns it as a BLOB. |
to_chars | none | splits the string by individual characters, returning them as an array |
get | position, counting from end if < 0 | gets the character at a certain position (() if the position is not valid) |
set |
| sets a certain position to a new character (no effect if the position is not valid) |
pad |
| pads the string with a character or a string to at least a specified length |
append , += operator | item to append | adds the display text of an item to the end of the string |
remove | character/string to remove | removes a character or a string from the string |
pop | (optional) number of characters to remove, none if ≤ 0, entire string if ≥ length | removes the last character (if no parameter) and returns it (() if empty); otherwise, removes the last number of characters and returns them as a string |
clear | none | empties the string |
truncate | target length | cuts off the string at exactly a specified number of characters |
to_upper | none | converts the string/character into upper-case as a new string/character and returns it |
to_lower | none | converts the string/character into lower-case as a new string/character and returns it |
make_upper | none | converts the string/character into upper-case |
make_lower | none | converts the string/character into lower-case |
trim | none | trims the string of whitespace at the beginning and end |
contains | character/sub-string to search for | checks if a certain character or sub-string occurs in the string |
starts_with | string | returns true if the string starts with a certain string |
ends_with | string | returns true if the string ends with a certain string |
min |
| returns the smaller of two characters/strings |
max |
| returns the larger of two characters/strings |
index_of |
| returns the position that a certain character or sub-string occurs in the string, or −1 if not found |
sub_string |
| extracts a sub-string |
sub_string | range of characters to extract, from beginning if ≤ 0, to end if ≥ length | extracts a sub-string |
split | none | splits the string by whitespaces, returning an array of string segments |
split | position to split at (in number of characters), counting from end if < 0, end if ≥ length | splits the string into two segments at the specified character position, returning an array of two string segments |
split |
| splits the string by the specified delimiter, returning an array of string segments |
split_rev |
| splits the string by the specified delimiter in reverse order, returning an array of string segments |
crop |
| retains only a portion of the string |
crop | range of characters to retain, from beginning if ≤ 0, to end if ≥ length | retains only a portion of the string |
replace |
| replaces a sub-string with another |
chars method and property |
| allows iteration of the characters inside the string |
Beware that functions that involve indexing into a string to get at individual
characters, e.g. sub_string
, require walking through the entire UTF-8 encoded
bytes stream to extract individual Unicode characters and counting them, which can be slow for long
strings.
Building Strings
Strings can be built from segments via the +
operator.
Operator | Description |
---|---|
string += item | convert the item into a string, then append it to the first string |
string + item | convert the item into a string, then concatenate them as a new string |
item + string | convert the item into a string, then concatenate them as a new string |
let x = 42;
// Build string with '+'
let s = "The answer is: " + x + "!!!";
// Prints: "The answer is: 42!!!"
print(s);
Standard Operators Between Strings and/or Characters
The following standard operators inter-operate between strings and/or characters.
When one (or both) of the operands is a character, it is first converted into a one-character string before running the operator.
Operator | Description |
---|---|
+ , += | character/string concatenation |
- , -= | remove [character](strings-chars.md/sub-string from string |
== | equals to |
!= | not equals to |
> | greater than |
>= | greater than or equals to |
< | less than |
<= | less than or equals to |
Interop with BLOB’s
For convenience, when a BLOB is appended to a string, or vice versa, it is treated as a UTF-8 encoded byte stream and automatically first converted into the appropriate string value.
That is because it is rarely useful to append a BLOB into a string, but extremely useful to be able to directly manipulate UTF-8 encoded text.
Operator | Description |
---|---|
+ , += | append a BLOB (as a UTF-8 encoded byte stream) to the end of the string |
+ | concatenate a BLOB (as a UTF-8 encoded byte stream) with a string |
Examples
let full_name == " Bob C. Davis ";
full_name.len == 14;
full_name.trim();
full_name.len == 12;
full_name == "Bob C. Davis";
full_name.pad(15, '$');
full_name.len == 15;
full_name == "Bob C. Davis$$$";
let n = full_name.index_of('$');
n == 12;
full_name.index_of("$$", n + 1) == 13;
full_name.sub_string(n, 3) == "$$$";
full_name.sub_string(n..n+3) == "$$$";
full_name.truncate(6);
full_name.len == 6;
full_name == "Bob C.";
full_name.replace("Bob", "John");
full_name.len == 7;
full_name == "John C.";
full_name.contains('C') == true;
full_name.contains("John") == true;
full_name.crop(5);
full_name == "C.";
full_name.crop(0, 1);
full_name == "C";
full_name.clear();
full_name.len == 0;
Arrays
Arrays are first-class citizens in Rhai.
All elements stored in an array are dynamic, and the array can freely grow or shrink with elements added or removed.
type_of()
an array returns "array"
.
Literal Syntax
Array literals are built within square brackets [
… ]
and separated by commas ,
:
[
value,
value,
…,
value]
[
value,
value,
…,
value,
]
// trailing comma is OK
Element Access Syntax
From beginning
Like C, arrays are accessed with zero-based, non-negative integer indices:
array
[
index position from 0 to length−1]
From end
A negative position accesses an element in the array counting from the end, with −1 being the last element.
array
[
index position from −1 to −length]
Built-in Functions
The following methods operate on arrays.
Function | Parameter(s) | Description |
---|---|---|
get | position, counting from end if < 0 | gets a copy of the element at a certain position (() if the position is not valid) |
set |
| sets a certain position to a new value (no effect if the position is not valid) |
push , += operator | element to append (not an array) | appends an element to the end |
append , += operator | array to append | concatenates the second array to the end of the first |
+ operator |
| concatenates the first array with the second |
== operator |
| are two arrays the same (elements compared with the == operator, if defined)? |
!= operator |
| are two arrays different (elements compared with the == operator, if defined)? |
insert |
| inserts an element at a certain position |
pop | none | removes the last element and returns it (() if empty) |
shift | none | removes the first element and returns it (() if empty) |
extract |
| extracts a portion of the array into a new array |
extract | range of elements to extract, from beginning if ≤ 0, to end if ≥ length | extracts a portion of the array into a new array |
remove | position, counting from end if < 0 | removes an element at a particular position and returns it (() if the position is not valid) |
reverse | none | reverses the array |
len method and property | none | returns the number of elements |
is_empty method and property | none | returns true if the array is empty |
pad |
| pads the array with an element to at least a specified length |
clear | none | empties the array |
truncate | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) |
chop | target length | cuts off the head of the array, leaving the tail at exactly a specified length |
split |
| splits the array into two arrays, starting from a specified position |
for_each | function pointer for processing elements | run through each element in the array in order, binding each to this and calling the processing function taking the following parameters:
|
drain | function pointer to predicate (usually a closure) | removes all elements (returning them) that return true when called with the predicate function taking the following parameters (if none, the array element is bound to this ):
|
drain |
| removes a portion of the array, returning the removed elements as a new array |
drain | range of elements to remove, from beginning if ≤ 0, to end if ≥ length | removes a portion of the array, returning the removed elements as a new array |
retain | function pointer to predicate (usually a closure) | removes all elements (returning them) that do not return true when called with the predicate function taking the following parameters (if none, the array element is bound to this ):
|
retain |
| retains a portion of the array, removes all other elements and returning them as a new array |
retain | range of elements to retain, from beginning if ≤ 0, to end if ≥ length | retains a portion of the array, removes all other bytes and returning them as a new array |
splice |
| replaces a portion of the array with another (not necessarily of the same length as the replaced portion) |
splice |
| replaces a portion of the array with another (not necessarily of the same length as the replaced portion) |
filter | function pointer to predicate (usually a closure) | constructs a new array with all elements that return true when called with the predicate function taking the following parameters (if none, the array element is bound to this ):
|
contains , in operator | element to find | does the array contain an element? The == operator is used for comparison |
index_of |
| returns the position of the first element in the array that equals the supplied element (using the == operator, if defined), or −1 if not found |
index_of |
| returns the position of the first element in the array that returns true when called with the predicate function, or −1 if not found:
|
find |
| returns the first element in the array that returns true when called with the predicate function, or () if not found:
|
find_map |
| returns the first non-() value of the first element in the array when called with the predicate function, or () if not found:
|
dedup | (optional) function pointer to predicate (usually a closure); if omitted, the == operator is used, if defined | removes all but the first of consecutive elements in the array that return true when called with the predicate function (non-consecutive duplicates are not removed):1st & 2nd parameters: two elements in the array |
map | function pointer to conversion function (usually a closure) | constructs a new array with all elements mapped to the result of applying the conversion function taking the following parameters (if none, the array element is bound to this ):
|
reduce |
| reduces the array into a single value via the accumulator function taking the following parameters (if the second parameter is omitted, the array element is bound to this ):
|
reduce_rev |
| reduces the array (in reverse order) into a single value via the accumulator function taking the following parameters (if the second parameter is omitted, the array element is bound to this ):
|
some | function pointer to predicate (usually a closure) | returns true if any element returns true when called with the predicate function taking the following parameters (if none, the array element is bound to this ):
|
all | function pointer to predicate (usually a closure) | returns true if all elements return true when called with the predicate function taking the following parameters (if none, the array element is bound to this ):
|
sort | function pointer to a comparison function (usually a closure) | sorts the array with a comparison function taking the following parameters:
|
sort | none | sorts a homogeneous array containing only elements of the same comparable built-in type (integers, floating-point, decimal, string, character, bool , () ) |
Examples
let y = [2, 3]; // y == [2, 3]
let y = [2, 3,]; // y == [2, 3]
y.insert(0, 1); // y == [1, 2, 3]
y.insert(999, 4); // y == [1, 2, 3, 4]
y.len == 4;
y[0] == 1;
y[1] == 2;
y[2] == 3;
y[3] == 4;
(1 in y) == true; // use 'in' to test if an element exists in the array
(42 in y) == false; // 'in' uses the 'contains' function, which uses the
// '==' operator (that users can override)
// to check if the target element exists in the array
y.contains(1) == true; // the above de-sugars to this
y[1] = 42; // y == [1, 42, 3, 4]
(42 in y) == true;
y.remove(2) == 3; // y == [1, 42, 4]
y.len == 3;
y[2] == 4; // elements after the removed element are shifted
ts.list = y; // arrays can be assigned completely (by value copy)
ts.list[1] == 42;
[1, 2, 3][0] == 1; // indexing on array literal
[1, 2, 3][-1] == 3; // negative position counts from the end
fn abc() {
[42, 43, 44] // a function returning an array
}
abc()[0] == 42;
y.push(4); // y == [1, 42, 4, 4]
y += 5; // y == [1, 42, 4, 4, 5]
y.len == 5;
y.shift() == 1; // y == [42, 4, 4, 5]
y.chop(3); // y == [4, 4, 5]
y.len == 3;
y.pop() == 5; // y == [4, 4]
y.len == 2;
for element in y { // arrays can be iterated with a 'for' statement
print(element);
}
y.pad(6, "hello"); // y == [4, 4, "hello", "hello", "hello", "hello"]
y.len == 6;
y.truncate(4); // y == [4, 4, "hello", "hello"]
y.len == 4;
y.clear(); // y == []
y.len == 0;
// The examples below use 'a' as the master array
let a = [42, 123, 99];
a.map(|v| v + 1); // returns [43, 124, 100]
a.map(|| this + 1); // returns [43, 124, 100]
a.map(|v, i| v + i); // returns [42, 124, 101]
a.filter(|v| v > 50); // returns [123, 99]
a.filter(|| this > 50); // returns [123, 99]
a.filter(|v, i| i == 1); // returns [123]
a.filter("is_odd"); // returns [123, 99]
a.filter(Fn("is_odd")); // <- previous statement is equivalent to this...
a.filter(|v| is_odd(v)); // <- or this
a.some(|v| v > 50); // returns true
a.some(|| this > 50); // returns true
a.some(|v, i| v < i); // returns false
a.all(|v| v > 50); // returns false
a.all(|| this > 50); // returns false
a.all(|v, i| v > i); // returns true
// Reducing - initial value provided directly
a.reduce(|sum| sum + this, 0) == 264;
// Reducing - initial value provided directly
a.reduce(|sum, v| sum + v, 0) == 264;
// Reducing - initial value is '()'
a.reduce(
|sum, v| if sum.type_of() == "()" { v } else { sum + v }
) == 264;
// Reducing - initial value has index position == 0
a.reduce(|sum, v, i|
if i == 0 { v } else { sum + v }
) == 264;
// Reducing in reverse - initial value provided directly
a.reduce_rev(|sum| sum + this, 0) == 264;
// Reducing in reverse - initial value provided directly
a.reduce_rev(|sum, v| sum + v, 0) == 264;
// Reducing in reverse - initial value is '()'
a.reduce_rev(
|sum, v| if sum.type_of() == "()" { v } else { sum + v }
) == 264;
// Reducing in reverse - initial value has index position == 0
a.reduce_rev(|sum, v, i|
if i == 2 { v } else { sum + v }
) == 264;
// In-place modification
a.splice(1..=1, [1, 3, 2]); // a == [42, 1, 3, 2, 99]
a.extract(1..=3); // returns [1, 3, 2]
a.sort(|x, y| y - x); // a == [99, 42, 3, 2, 1]
a.sort(); // a == [1, 2, 3, 42, 99]
a.drain(|v| v <= 1); // a == [2, 3, 42, 99]
a.drain(|v, i| i ≥ 3); // a == [2, 3, 42]
a.retain(|v| v > 10); // a == [42]
a.retain(|v, i| i > 0); // a == []
BLOB’s
BLOB’s (Binary Large OBjects), used to hold packed arrays of bytes, have built-in support in Rhai.
A BLOB has no literal representation, but is created via the blob
function, or simply returned as
the result of a function call (e.g. generate_thumbnail_image
that generates a thumbnail version of
a large image as a BLOB).
All items stored in a BLOB are bytes (i.e. u8
) and the BLOB can freely grow or shrink with bytes
added or removed.
type_of()
a BLOB returns "blob"
.
Element Access Syntax
From beginning
Like arrays, BLOB’s are accessed with zero-based, non-negative integer indices:
blob
[
index position from 0 to length−1]
From end
A negative position accesses an element in the BLOB counting from the end, with −1 being the last element.
blob
[
index position from −1 to −length]
The value of a particular byte in a BLOB is mapped to an integer.
Only the lowest 8 bits are significant, all other bits are ignored.
Create a BLOB
The function blob
allows creating an empty BLOB, optionally filling it to a required size with a
particular value (default zero).
let x = blob(); // empty BLOB
let x = blob(10); // BLOB with ten zeros
let x = blob(50, 42); // BLOB with 50x 42's
To quickly initialize a BLOB with a particular byte stream, the write_be
method can be used to
write eight bytes at a time (four under 32-bit) in big-endian byte order.
If fewer than eight bytes are needed, remember to right-pad the number as big-endian byte order is used.
let buf = blob(12, 0); // BLOB with 12x zeros
// Write eight bytes at a time, in big-endian order
buf.write_be(0, 8, 0xab_cd_ef_12_34_56_78_90);
buf.write_be(8, 8, 0x0a_0b_0c_0d_00_00_00_00);
// ^^^^^^^^^^^ remember to pad unused bytes
print(buf); // prints "[abcdef1234567890 0a0b0c0d]"
buf[3] == 0x12;
buf[10] == 0x0c;
// Under 'only_i32', write four bytes at a time:
buf.write_be(0, 4, 0xab_cd_ef_12);
buf.write_be(4, 4, 0x34_56_78_90);
buf.write_be(8, 4, 0x0a_0b_0c_0d);
Writing ASCII Bytes
For many embedded applications, it is necessary to encode an ASCII string as a byte stream.
Use the write_ascii
method to write ASCII strings into any specific
range within a BLOB.
The following is an example of a building a 16-byte command to send to an embedded device.
// Assume the following 16-byte command for an embedded device:
// ┌─────────┬───────────────┬──────────────────────────────────┬───────┐
// │ 0 │ 1 │ 2-13 │ 14-15 │
// ├─────────┼───────────────┼──────────────────────────────────┼───────┤
// │ command │ string length │ ASCII string, max. 12 characters │ CRC │
// └─────────┴───────────────┴──────────────────────────────────┴───────┘
let buf = blob(16, 0); // initialize command buffer
let text = "foo & bar"; // text string to send to device
buf[0] = 0x42; // command code
buf[1] = s.len(); // length of string
buf.write_ascii(2..14, text); // write the string
let crc = buf.calc_crc(); // calculate CRC
buf.write_le(14, 2, crc); // write CRC
print(buf); // prints "[4209666f6f202620 626172000000abcd]"
// ^^ command code ^^^^ CRC
// ^^ string length
// ^^^^^^^^^^^^^^^^^^^ foo & bar
device.send(buf); // send command to device
The write_utf8
function writes a string in UTF-8 encoding.
UTF-8, however, is not very common for embedded applications.
Built-in Functions
The following functions operate on BLOB’s.
Functions | Parameter(s) | Description |
---|---|---|
blob constructor function |
| creates a new BLOB, optionally of a particular length filled with an initial byte value (default = 0) |
to_array | none | converts the BLOB into an array of integers |
as_string | none | converts the BLOB into a string (the byte stream is interpreted as UTF-8) |
get | position, counting from end if < 0 | gets a copy of the byte at a certain position (0 if the position is not valid) |
set |
| sets a certain position to a new value (no effect if the position is not valid) |
push , append , += operator |
| appends a byte to the end |
append , += operator |
| concatenates the second BLOB to the end of the first |
append , += operator |
| concatenates a string/character (as UTF-8 encoded byte-stream) to the end of the BLOB |
+ operator |
| creates a new string by concatenating the BLOB (as UTF-8 encoded byte-stream) with the the string |
+ operator |
| creates a new string by concatenating the BLOB (as UTF-8 encoded byte-stream) to the end of the string |
+ operator |
| concatenates the first BLOB with the second |
== operator |
| are two BLOB’s the same? |
!= operator |
| are two BLOB’s different? |
insert |
| inserts a byte at a certain position |
pop | none | removes the last byte and returns it (0 if empty) |
shift | none | removes the first byte and returns it (0 if empty) |
extract |
| extracts a portion of the BLOB into a new BLOB |
extract | range of bytes to extract, from beginning if ≤ 0, to end if ≥ length | extracts a portion of the BLOB into a new BLOB |
remove | position, counting from end if < 0 | removes a byte at a particular position and returns it (0 if the position is not valid) |
reverse | none | reverses the BLOB byte by byte |
len method and property | none | returns the number of bytes in the BLOB |
is_empty method and property | none | returns true if the BLOB is empty |
pad |
| pads the BLOB with a byte value to at least a specified length |
clear | none | empties the BLOB |
truncate | target length | cuts off the BLOB at exactly a specified length (discarding all subsequent bytes) |
chop | target length | cuts off the head of the BLOB, leaving the tail at exactly a specified length |
contains , in operator | byte value to find | does the BLOB contain a particular byte value? |
split |
| splits the BLOB into two BLOB’s, starting from a specified position |
drain |
| removes a portion of the BLOB, returning the removed bytes as a new BLOB |
drain | range of bytes to remove, from beginning if ≤ 0, to end if ≥ length | removes a portion of the BLOB, returning the removed bytes as a new BLOB |
retain |
| retains a portion of the BLOB, removes all other bytes and returning them as a new BLOB |
retain | range of bytes to retain, from beginning if ≤ 0, to end if ≥ length | retains a portion of the BLOB, removes all other bytes and returning them as a new BLOB |
splice |
| replaces a portion of the BLOB with another (not necessarily of the same length as the replaced portion) |
splice |
| replaces a portion of the BLOB with another (not necessarily of the same length as the replaced portion) |
parse_le_int |
| parses an integer at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_le_int | range of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under 32-bit) | parses an integer at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_be_int |
| parses an integer at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_be_int | range of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under 32-bit) | parses an integer at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_le_float |
| parses a floating-point number at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_le_float | range of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under 32-bit) | parses a floating-point number at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_be_float |
| parses a floating-point number at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
parse_be_float | range of bytes to parse, from beginning if ≤ 0, to end if ≥ length (up to 8 bytes, 4 under 32-bit) | parses a floating-point number at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
write_le |
| writes a value at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
write_le |
| writes a value at the particular offset in little-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
write_be |
| writes a value at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
write_be |
| writes a value at the particular offset in big-endian byte order (if not enough bytes, zeros are padded; extra bytes are ignored) |
write_utf8 |
| writes a string to the particular offset in UTF-8 encoding |
write_utf8 | writes a string to the particular offset in UTF-8 encoding | |
write_ascii |
| writes a string to the particular offset in 7-bit ASCII encoding (non-ASCII characters are skipped) |
write_ascii | writes a string to the particular offset in 7-bit ASCII encoding (non-ASCII characters are skipped) |
Object Maps
Object maps are hash dictionaries. Properties are all dynamic values and can be freely added and retrieved.
type_of()
an object map returns "map"
.
Normally, when properties are accessed, copies of the data values are made. This is normally slow.
Object maps have special treatment – properties are accessed via references, meaning that no copies of data values are made.
This makes object map access fast, especially when deep within a properties chain.
// 'obj' is a normal custom type
let x = obj.a.b.c.d;
// The above is equivalent to:
let a_value = obj.a; // temp copy of 'a'
let b_value = a_value.b; // temp copy of 'b'
let c_value = b_value.c; // temp copy of 'c'
let d_value = c_value.d; // temp copy of 'd'
let x = d_value;
// 'map' is an object map
let x = map.a.b.c.d; // direct access to 'd'
// 'a', 'b' and 'c' are not copied
map.a.b.c.d = 42; // directly modifies 'd' in 'a', 'b' and 'c'
// no copy of any property value is made
map.a.b.c.d.calc(); // directly calls 'calc' on 'd'
// no copy of any property value is made
Literal Syntax
Object map literals are built within braces #{
… }
with name:
value pairs separated by
commas ,
:
#{
property:
value,
…,
property:
value}
#{
property:
value,
…,
property:
value,
}
// trailing comma is OK
The property name can be a simple identifier following the same naming rules as variables, or a string literal without interpolation.
Property Access Syntax
Dot notation
The dot notation allows only property names that follow the same naming rules as variables.
object
.
property
Elvis notation
The Elvis notation is similar to the dot
notation except that it returns ()
if the object itself is ()
.
// returns () if object is ()
object?.
property
// no action if object is ()
object?.
property=
value;
Index notation
The index notation allows setting/getting properties of arbitrary names (even the empty string).
object
[
property]
Handle Non-Existent Properties
Trying to read a non-existent property returns ()
instead of causing an error.
This is similar to JavaScript where accessing a non-existent property returns undefined
.
let map = #{ foo: 42 };
// Regular property access
let x = map.foo; // x == 42
// Non-existent property
let x = map.bar; // x == ()
Check for property existence
Use the in
operator to check whether a property exists in an object-map.
let map = #{ foo: 42 };
"foo" in map == true;
"bar" in map == false;
Short-circuit non-existent property access
Use the Elvis operator (?.
) to short-circuit
further processing if the object is ()
.
x.a.b.foo(); // <- error if 'x', 'x.a' or 'x.a.b' is ()
x.a.b = 42; // <- error if 'x' or 'x.a' is ()
x?.a?.b?.foo(); // <- ok! returns () if 'x', 'x.a' or 'x.a.b' is ()
x?.a?.b = 42; // <- ok even if 'x' or 'x.a' is ()
Default property value
Using the null-coalescing operator to give non-existent properties default values.
let map = #{ foo: 42 };
// Regular property access
let x = map.foo; // x == 42
// Non-existent property
let x = map.bar; // x == ()
// Default value for property
let x = map.bar ?? 42; // x == 42
Built-in Functions
The following methods operate on object maps.
Function | Parameter(s) | Description |
---|---|---|
get | property name | gets a copy of the value of a certain property (() if the property does not exist) |
set |
| sets a certain property to a new value (property is added if not already exists) |
len | none | returns the number of properties |
is_empty | none | returns true if the object map is empty |
clear | none | empties the object map |
remove | property name | removes a certain property and returns it (() if the property does not exist) |
+= operator, mixin | second object map | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) |
+ operator |
| merges the first object map with the second |
== operator |
| are the two object maps the same (elements compared with the == operator, if defined)? |
!= operator |
| are the two object maps different (elements compared with the == operator, if defined)? |
fill_with | second object map | adds in all properties of the second object map that do not exist in the object map |
contains , in operator | property name | does the object map contain a property of a particular name? |
drain | function pointer to predicate (usually a closure) | removes all elements (returning them) that return true when called with the predicate function taking the following parameters:
|
retain | function pointer to predicate (usually a closure) | removes all elements (returning them) that do not return true when called with the predicate function taking the following parameters:
|
filter | function pointer to predicate (usually a closure) | constructs a object map with all elements that return true when called with the predicate function taking the following parameters:
|
keys | none | returns an array of all the property names (in random order) |
values | none | returns an array of all the property values (in random order) |
to_json | none | returns a JSON representation of the object map (() is mapped to null , all other data types must be supported by JSON) |
Examples
let y = #{ // object map literal with 3 properties
a: 1,
bar: "hello",
"baz!$@": 123.456, // like JavaScript, you can use any string as property names...
"": false, // even the empty string!
`hello`: 999, // literal strings are also OK
a: 42, // <- syntax error: duplicated property name
`a${2}`: 42, // <- syntax error: property name cannot have string interpolation
};
y.a = 42; // access via dot notation
y.a == 42;
y.baz!$@ = 42; // <- syntax error: only proper variable names allowed in dot notation
y."baz!$@" = 42; // <- syntax error: strings not allowed in dot notation
y["baz!$@"] = 42; // access via index notation is OK
"baz!$@" in y == true; // use 'in' to test if a property exists in the object map
("z" in y) == false;
ts.obj = y; // object maps can be assigned completely (by value copy)
let foo = ts.list.a;
foo == 42;
let foo = #{ a:1, }; // trailing comma is OK
let foo = #{ a:1, b:2, c:3 }["a"];
let foo = #{ a:1, b:2, c:3 }.a;
foo == 1;
fn abc() {
{ a:1, b:2, c:3 } // a function returning an object map
}
let foo = abc().b;
foo == 2;
let foo = y["a"];
foo == 42;
y.contains("a") == true;
y.contains("xyz") == false;
y.xyz == (); // a non-existent property returns '()'
y["xyz"] == ();
y.len == (); // an object map has no property getter function
y.len() == 3; // method calls are OK
y.remove("a") == 1; // remove property
y.len() == 2;
y.contains("a") == false;
for name in y.keys() { // get an array of all the property names via 'keys'
print(name);
}
for val in y.values() { // get an array of all the property values via 'values'
print(val);
}
y.clear(); // empty the object map
y.len() == 0;
Special Support for OOP
Object maps can be used to simulate object-oriented programming (OOP) by storing data as properties and methods as properties holding function pointers.
If an object map’s property holds a function pointer, the property can simply be called like a normal method in method-call syntax.
This is a short-hand to avoid the more verbose syntax of using the call
function keyword.
When a property holding a function pointer or a closure is called like a method, it is replaced as a method call on the object map itself.
let obj = #{
data: 40,
action: || this.data += x // 'action' holds a closure
};
obj.action(2); // calls the function pointer with 'this' bound to 'obj'
obj.call(obj.action, 2); // <- the above de-sugars to this
obj.data == 42;
// To achieve the above with normal function pointer call will fail.
fn do_action(map, x) { map.data += x; } // 'map' is a copy
obj.action = do_action; // <- de-sugars to 'Fn("do_action")'
obj.action.call(obj, 2); // a copy of 'obj' is passed by value
obj.data == 42; // 'obj.data' is not changed
Timestamps
Timestamps are provided by the via the timestamp
function.
type_of()
a timestamp returns "timestamp"
.
Built-in Functions
The following methods operate on timestamps.
Function | Parameter(s) | Description |
---|---|---|
elapsed method and property | none | returns the number of seconds since the timestamp |
+ operator | number of seconds to add | returns a new timestamp with a specified number of seconds added |
+= operator | number of seconds to add | adds a specified number of seconds to the timestamp |
- operator | number of seconds to subtract | returns a new timestamp with a specified number of seconds subtracted |
-= operator | number of seconds to subtract | subtracts a specified number of seconds from the timestamp |
- operator |
| returns the number of seconds between the two timestamps |
The following time-related functions are also available.
Function | Parameter(s) | Description |
---|---|---|
sleep | number of seconds to sleep | blocks the current thread for a specified number of seconds |
Examples
let now = timestamp();
// Do some lengthy operation...
if now.elapsed > 30.0 {
print("takes too long (over 30 seconds)!")
}
Keywords
The following are reserved keywords in Rhai.
Active keywords | Reserved keywords | Usage |
---|---|---|
true , false | constants | |
let , const | var , static | variables |
is_shared | shared values | |
is | type checking | |
if , else | goto | control flow |
switch | match , case | switching and matching |
do , while , loop , until , for , in , continue , break | looping | |
fn , private , is_def_fn , this | public , protected , new | functions |
return | return values | |
throw , try , catch | throw/catch exceptions | |
import , export , as | use , with , module , package , super | modules |
global | automatic global module | |
Fn , call , curry | function pointers | |
spawn , thread , go , sync , async , await , yield | threading/async | |
type_of , print , debug , eval , is_def_var | special functions | |
default , void , null , nil | special values |
Statements
Statements are terminated by semicolons ;
and they are mandatory, except for the last statement
in a block (enclosed by {
… }
pairs) where it can be omitted.
Semicolons can also be omitted for statement types that always end in a block – for example
the if
, while
, for
, loop
and
switch
statements.
let a = 42; // normal assignment statement
let a = foo(42); // normal function call statement
foo < 42; // normal expression as statement
let a = { 40 + 2 }; // 'a' is set to the value of the statements block, which is the value of the last statement
// ^ the last statement does not require a terminating semicolon (but also works with it)
// ^ semicolon required here to terminate the 'let' statement
// it is a syntax error without it, even though it ends with '}'
// that is because the 'let' statement doesn't end in a block
if foo { a = 42 }
// ^ no need to terminate an if-statement with a semicolon
// that is because the 'if' statement ends in a block
4 * 10 + 2 // a statement which is just one expression - no ending semicolon is OK
// because it is the last statement of the whole block
Statements Block
Syntax
Statements blocks in Rhai are formed by enclosing zero or more statements within braces {
…}
.
{
statement;
statement;
… statement}
{
statement;
statement;
… statement;
}
// trailing semi-colon is optional
Closed scope
A statements block forms a closed scope.
Any variable and/or constant defined within the block are removed outside the block, so are modules imported within the block.
let x = 42;
let y = 18;
{
import "hello" as h;
const HELLO = 99;
let y = 0;
h::greet(); // ok
print(y + HELLO); // prints 99 (y is zero)
:
:
} // <- 'HELLO' and 'y' go away here...
print(x + y); // prints 60 (y is still 18)
print(HELLO); // <- error: 'HELLO' not found
h::greet(); // <- error: module 'h' not found
Statement Expression
A statement can be used anywhere where an expression is expected.
These are called, for lack of a more creative name, “statement expressions.”
The last statement of a statements block is always the block’s return value when used as a statement, regardless of whether it is terminated by a semicolon or not.
If the last statement has no return value (e.g. variable definitions, assignments) then it is
assumed to be ()
.
let x = {
let foo = calc_something();
let bar = foo + baz;
bar.further_processing(); // <- this is the return value
}; // <- semicolon is needed here...
// The above is equivalent to:
let result;
{
let foo = calc_something();
let bar = foo + baz;
result = bar.further_processing();
}
let x = result;
// Statement expressions can be inserted inside normal expressions
// to avoid duplicated calculations
let x = foo(bar) + { let v = calc(); process(v, v.len, v.abs) } + baz;
// The above is equivalent to:
let foo_result = foo(bar);
let calc_result;
{
let v = calc();
result = process(v, v.len, v.abs); // <- avoid calculating 'v'
}
let x = foo_result + calc_result + baz;
// Statement expressions are also useful as function call arguments
// when side effects are desired
do_work(x, y, { let z = foo(x, y); print(z); z });
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// statement expression
Variables
Valid Names
Variables in Rhai follow normal C naming rules – must contain only ASCII letters, digits and underscores _
.
Character set | Description |
---|---|
A … Z | Upper-case ASCII letters |
a … z | Lower-case ASCII letters |
0 … 9 | Digit characters |
_ | Underscore character |
However, a variable name must also contain at least one ASCII letter, and an ASCII
letter must come before any digits. In other words, the first character that is not an underscore _
must be an ASCII letter and not a digit.
To reduce confusion (and subtle bugs) because, for instance, _1
can easily be misread (or mistyped)
as -1
.
Rhai is dynamic without type checking, so there is no compiler to catch these typos.
Therefore, some names, e.g. _
, _42foo
, _1
etc., are not valid in Rhai.
For example: c3po
and _r2d2_
are valid variable names, but 3abc
and ____49steps
are not.
Variable names are case sensitive.
Variable names also cannot be the same as a keyword (active or reserved).
Rhai inlines a string, which avoids allocations unless it is over its internal limit (23 ASCII characters on 64-bit, but only 11 ASCII characters on 32-bit).
On 64-bit systems, most variable names are shorter than 23 letters, so this is unlikely to become an issue.
However, on 32-bit systems, take care to limit, where possible, variable names to within 11 letters. This is particularly true for local variables inside a hot loop, where they are created and destroyed in rapid succession.
// The following is SLOW on 32-bit
for my_super_loop_variable in array {
print(`Super! ${my_super_loop_variable}`);
}
// Suggested revision:
for loop_var in array {
print(`Super! ${loop_var}`);
}
Declare a Variable
Variables are declared using the let
keyword.
Variables do not have to be given an initial value.
If none is provided, it defaults to ()
.
A variable defined within a statements block is local to that block.
let x; // ok - value is '()'
let x = 3; // ok
let _x = 42; // ok
let x_ = 42; // also ok
let _x_ = 42; // still ok
let _ = 123; // <- syntax error: illegal variable name
let _9 = 9; // <- syntax error: illegal variable name
let x = 42; // variable is 'x', lower case
let X = 123; // variable is 'X', upper case
print(x); // prints 42
print(X); // prints 123
{
let x = 999; // local variable 'x' shadows the 'x' in parent block
print(x); // prints 999
}
print(x); // prints 42 - the parent block's 'x' is not changed
let x = 0; // new variable 'x' shadows the old 'x'
print(x); // prints 0
is_def_var("x") == true;
is_def_var("_x") == true;
is_def_var("y") == false;
Shadowing
New variables automatically shadow existing ones of the same name. There is no error.
let x = 42;
let y = 123;
print(x); // prints 42
let x = 88; // <- 'x' is shadowed here
// At this point, it is no longer possible to access the
// original 'x' on the first line...
print(x); // prints 88
let x = 0; // <- 'x' is shadowed again
// At this point, it is no longer possible to access both
// previously-defined 'x'...
print(x); // prints 0
{
let x = 999; // <- 'x' is shadowed in a block
print(x); // prints 999
}
print(x); // prints 0 - shadowing within the block goes away
print(y); // prints 123 - 'y' is not shadowed
Constants
Constants can be defined using the const
keyword and are immutable.
const X; // 'X' is a constant '()'
const X = 40 + 2; // 'X' is a constant 42
print(X * 2); // prints 84
X = 123; // <- syntax error: constant modified
Constants follow the same naming rules as variables, but as a convention are often named with all-capital letters.
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.
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'
}
Assignments
Value assignments to variables use the =
symbol.
let foo = 42;
bar = 123 * 456 - 789;
x[1][2].prop = do_calculation();
Valid Assignment Targets
The left-hand-side (LHS) of an assignment statement must be a valid l-value, which must be rooted in a variable, potentially extended via indexing or properties.
Expressions that are not valid l-values cannot be assigned to.
x = 42; // variable is an l-value
x[1][2][3] = 42 // variable indexing is an l-value
x.prop1.prop2 = 42; // variable property is an l-value
foo(x) = 42; // syntax error: function call is not an l-value
x.foo() = 42; // syntax error: method call is not an l-value
(x + y) = 42; // syntax error: binary op is not an l-value
Values are Cloned
Values assigned are always cloned. So care must be taken when assigning large data types (such as arrays).
x = y; // value of 'y' is cloned
x == y; // both 'x' and 'y' hold different copies
// of the same value
Moving Data
When assigning large data types, sometimes it is desirable to move the data instead of cloning it.
Use the take
function to move data.
The original variable is left with ()
x = take(y); // value of 'y' is moved to 'x'
y == (); // 'y' now holds '()'
x != y; // 'x' holds the original value of 'y'
Return large data types from functions
take
is convenient when returning large data types from a function.
fn get_large_value_naive() {
let large_result = do_complex_calculation();
large_result.done = true;
// Return a cloned copy of the result, then the
// local variable 'large_result' is thrown away!
large_result
}
fn get_large_value_smart() {
let large_result = do_complex_calculation();
large_result.done = true;
// Return the result without cloning!
// Method style call is also OK.
large_result.take()
}
Assigning large data types to object map properties
take
is useful when assigning large data types to object map properties.
let x = [];
// Build a large array
for n in 0..1000000 { x += n; }
// The following clones the large array from 'x'.
// Both 'my_object.my_property' and 'x' now hold exact copies
// of the same large array!
my_object.my_property = x;
// Move it to object map property via 'take' without cloning.
// 'x' now holds '()'.
my_object.my_property = x.take();
// Without 'take', the following must be done to avoid cloning:
my_object.my_property = [];
for n in 0..1000000 { my_object.my_property += n; }
Compound Assignments
Compound assignments are assignments with a binary operator attached.
number += 8; // number = number + 8
number -= 7; // number = number - 7
number *= 6; // number = number * 6
number /= 5; // number = number / 5
number %= 4; // number = number % 4
number **= 3; // number = number ** 3
number <<= 2; // number = number << 2
number >>= 1; // number = number >> 1
number &= 0x00ff; // number = number & 0x00ff;
number |= 0x00ff; // number = number | 0x00ff;
number ^= 0x00ff; // number = number ^ 0x00ff;
The Flexible +=
The the +
and +=
operators are often overloaded to perform build-up
operations for different data types.
Build strings
let my_str = "abc";
my_str += "ABC";
my_str += 12345;
my_str == "abcABC12345"
Concatenate arrays
let my_array = [1, 2, 3];
my_array += [4, 5];
my_array == [1, 2, 3, 4, 5];
Concatenate BLOB’s
let my_blob = blob(3, 0x42);
my_blob += blob(5, 0x89);
my_blob.to_string() == "[4242428989898989]";
Mix two object maps together
let my_obj = #{ a:1, b:2 };
my_obj += #{ c:3, d:4, e:5 };
my_obj == #{ a:1, b:2, c:3, d:4, e:5 };
Add seconds to timestamps
let now = timestamp();
now += 42.0;
(now - timestamp()).round() == 42.0;
Comparison Operators
Operator | Description ( x operator y ) | x , y same type or are numeric | x , y different types |
---|---|---|---|
== | x is equals to y | error if not defined | false if not defined |
!= | x is not equals to y | error if not defined | true if not defined |
> | x is greater than y | error if not defined | false if not defined |
>= | x is greater than or equals to y | error if not defined | false if not defined |
< | x is less than y | error if not defined | false if not defined |
<= | x is less than or equals to y | error if not defined | false if not defined |
Comparison operators between most values of the same type are built in for all standard types.
Floating-point numbers interoperate with integers
Comparing a floating-point number with an integer is also supported.
42 == 42.0; // true
42.0 == 42; // true
42.0 > 42; // false
42 >= 42.0; // true
42.0 < 42; // false
Decimal numbers interoperate with integers
Comparing a decimal number with an integer is also supported.
let d = parse_decimal("42");
42 == d; // true
d == 42; // true
d > 42; // false
42 >= d; // true
d < 42; // false
Strings interoperate with characters
Comparing a string with a character is also supported, with the character first turned into a string before performing the comparison.
'x' == "x"; // true
"" < 'a'; // true
'x' > "hello"; // false
Comparing different types defaults to false
Comparing two values of different data types defaults to false
unless the appropriate operator
functions have been registered.
The exception is !=
(not equals) which defaults to true
. This is in line with intuition.
42 > "42"; // false: i64 cannot be compared with string
42 <= "42"; // false: i64 cannot be compared with string
let ts = new_ts(); // custom type
ts == 42; // false: different types cannot be compared
ts != 42; // true: different types cannot be compared
ts == ts; // error: '==' not defined for the custom type
Safety valve: Comparing different numeric types has no default
Beware that the above default does NOT apply to numeric values of different types
(e.g. comparison between i64
and u16
, i32
and f64
) – when multiple numeric types are
used it is too easy to mess up and for subtle errors to creep in.
// Assume variable 'x' = 42_u16, 'y' = 42_u16 (both types of u16)
x == y; // true: '==' operator for u16 is built-in
x == "hello"; // false: different non-numeric operand types default to false
x == 42; // error: ==(u16, i64) not defined, no default for numeric types
42 == y; // error: ==(i64, u16) not defined, no default for numeric types
Boolean Operators
All boolean operators are built in for the bool
data type.
Operator | Description | Arity | Short-circuits? |
---|---|---|---|
! (prefix) | NOT | unary | no |
&& | AND | binary | yes |
& | AND | binary | no |
|| | OR | binary | yes |
| | OR | binary | no |
Double boolean operators &&
and ||
short-circuit – meaning that the second operand will not be evaluated
if the first one already proves the condition wrong.
Single boolean operators &
and |
always evaluate both operands.
a() || b(); // b() is not evaluated if a() is true
a() && b(); // b() is not evaluated if a() is false
a() | b(); // both a() and b() are evaluated
a() & b(); // both a() and b() are evaluated
Null-Coalescing Operator
Operator | Description | Arity | Short-circuits? |
---|---|---|---|
?? | Null-coalesce | binary | yes |
The null-coalescing operator (??
) returns the first operand if it is not ()
, or the second
operand if the first operand is ()
.
This operator short-circuits – meaning that the second operand will not be evaluated if the
first operand is not ()
.
a ?? b // returns 'a' if it is not (), otherwise 'b'
a() ?? b(); // b() is only evaluated if a() is ()
Use the null-coalescing operator to implement default values for non-existent object map properties.
let map = #{ foo: 42 };
// Regular property access
let x = map.foo; // x == 42
// Non-existent property
let x = map.bar; // x == ()
// Default value for property
let x = map.bar ?? 42; // x == 42
Short-circuit loops and early returns
The following statements are allowed to follow the null-coalescing operator:
This means that you can use the null-coalescing operator to short-circuit loops and/or
early-return from functions when the value tested is ()
.
let total = 0;
for value in list {
// Whenever 'calculate' returns '()', the loop stops
total += calculate(value) ?? break;
}
In Operator
The in
operator is simply syntactic sugar for a call to the contains
function.
Similarly, !in
is a call to !contains
.
The in
operator is used to check for containment – i.e. whether a particular collection
data type contains a particular item.
Similarly, !in
is used to check for non-existence – i.e. it is true
if a particular
collection data type does not contain a particular item.
42 in array;
array.contains(42); // <- the above is equivalent to this
123 !in array;
!array.contains(123); // <- the above is equivalent to this
Built-in support for standard data types
Data type | Check for |
---|---|
Numeric range | integer number |
Array | contained item |
Object map | property name |
String | sub-string or character |
Examples
let array = [1, "abc", 42, ()];
42 in array == true; // check array for item
let map = #{
foo: 42,
bar: true,
baz: "hello"
};
"foo" in map == true; // check object map for property name
'w' in "hello, world!" == true; // check string for character
'w' !in "hello, world!" == false;
"wor" in "hello, world" == true; // check string for sub-string
42 in -100..100 == true; // check range for number
Indexing
Some data types take an index that is not an integer. For example, object map indices are strings.
Some data types, such as arrays, can be indexed via a Rust-like syntax:
object
[
index]
object
[
index]
=
value;
Usually, a runtime error is raised if the index value is out of bounds or does not exist for the object’s data type.
Elvis Notation
The Elvis notation is similar except that it
returns ()
if the object itself is ()
.
// returns () if object is ()
object?[
index]
// no action if object is ()
object?[
index]
=
value;
Properties
Data types typically expose properties, which can be accessed in a Rust-like syntax:
object
.
propertyobject
.
property=
value;
A runtime error is raised if the property does not exist for the object’s data type.
Elvis Operator
The Elvis operator can be used to short-circuit
processing if the object itself is ()
.
// returns () if object is ()
object?.
property
// no action if object is ()
object?.
property=
value;
Methods
Data types may have methods that can be called:
object
.
method(
parameters …)
A runtime error is raised if the appropriate method does not exist for the object’s data type.
Elvis Operator
The Elvis operator can be used to short-circuit
the method call when the object itself is ()
.
// method is not called if object is ()
object?.
method(
parameters …)
If Statement
if
statements follow C syntax.
if foo(x) {
print("It's true!");
} else if bar == baz {
print("It's true again!");
} else if baz.is_foo() {
print("Yet again true.");
} else if foo(bar - baz) {
print("True again... this is getting boring.");
} else {
print("It's finally false!");
}
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.
There is no ambiguity regarding which if
clause a branch belongs to.
// Rhai is not C!
if (decision) print(42);
// ^ syntax error, expecting '{'
If Expression
if
statements can also be used as expressions, replacing the ? :
conditional operators in
other C-like languages.
// 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 == ();
Beware that, like Rust, if
is parsed primarily as a statement where it makes sense.
This is to avoid surprises.
fn index_of(x) {
// 'if' is parsed primarily as a statement
if this.contains(x) {
return this.find_index(x)
}
-1
}
The above will not be parsed as a single expression:
fn index_of(x) {
if this.contains(x) { return this.find_index(x) } - 1
// error due to '() - 1' ^
}
To force parsing as an expression, parentheses are required:
fn calc_index(b, offset) {
(if b { 1 } else { 0 }) + offset
// ^---------------------^ parentheses
}
Switch Statement
The switch
statement allows matching on literal values.
switch calc_secret_value(x) {
1 => print("It's one!"),
2 => {
// A statements block instead of a one-line statement
print("It's two!");
print("Again!");
}
3 => print("Go!"),
// A list of alternatives
4 | 5 | 6 => print("Some small number!"),
// _ is the default when no case matches. It must be the last case.
_ => print(`Oops! Something's wrong: ${x}`)
}
Default Case
A default case (i.e. when no other cases match) can be specified with _
.
switch wrong_default {
1 => 2,
_ => 9, // <- syntax error: default case not the last
2 => 3,
3 => 4, // <- ending with extra comma is OK
}
switch wrong_default {
1 => 2,
2 => 3,
3 => 4,
_ => 8, // <- syntax error: default case not the last
_ => 9
}
Array and Object Map Literals Also Work
The switch
expression can match against any literal, including
array and object map literals.
// Match on arrays
switch [foo, bar, baz] {
["hello", 42, true] => ...,
["hello", 123, false] => ...,
["world", 1, true] => ...,
_ => ...
}
// Match on object maps
switch map {
#{ a: 1, b: 2, c: true } => ...,
#{ a: 42, d: "hello" } => ...,
_ => ...
}
Case Conditions
Similar to Rust, each case (except the default case at the end) can provide an optional condition
that must evaluate to true
in order for the case to match.
All cases are checked in order, so an earlier case that matches will override all later cases.
let result = switch calc_secret_value(x) {
1 if some_external_condition(x, y, z) => 100,
1 | 2 | 3 if x < foo => 200, // <- all alternatives share the same condition
2 if bar() => 999,
2 => "two", // <- fallback value for 2
2 => "dead code", // <- this case is a duplicate and will never match
// because the previous case matches first
5 if CONDITION => 123, // <- value for 5 matching condition
5 => "five", // <- fallback value for 5
_ if CONDITION => 8888 // <- syntax error: default case cannot have condition
};
Case conditions, together with type_of()
, makes it extremely easy to work with
values which may be of several different types (like properties in a JSON object).
switch value.type_of() {
// if 'value' is a string...
"string" if value.len() < 5 => ...,
"string" => ...,
// if 'value' is an array...
"array" => ...,
// if 'value' is an object map...
"map" if value.prop == 42 => ...,
"map" => ...,
// if 'value' is a number...
"i64" if value > 0 => ...,
"i64" => ...,
// anything else: probably an error...
_ => ...
}
Range Cases
Because of their popularity, literal integer ranges can also
be used as switch
cases.
Numeric ranges are only searched when the switch
value is itself a number (including
floating-point and decimal). They never match any other data types.
let x = 42;
switch x {
'x' => ..., // no match: wrong data type
1 => ..., // <- specific numeric cases are checked first
2 => ..., // <- but these do not match
0..50 if x > 45 => ..., // no match: condition is 'false'
-10..20 => ..., // no match: not in range
0..50 => ..., // <- MATCH!!! duplicated range cases are OK
30..100 => ..., // no match: even though it is within range,
// the previous case matches first
42 => ..., // <- syntax error: numeric cases cannot follow range cases
}
Switch Expression
Like if
, switch
also works as 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);
}
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.
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.
let x = 10;
while x > 0 {
x -= 1;
if x < 6 { continue; } // skip to the next iteration
print(x);
if x == 5 { break; } // break out of while loop
}
While Expression
while
statements can also be used as expressions.
The break
statement takes an optional expression that provides the return value.
The default return value of a while
expression is ()
.
let x = 0;
// 'while' can be used just like an expression
let result = while x < 100 {
if is_magic_number(x) {
// if the 'while' loop breaks here, return a specific value
break get_magic_result(x);
}
x += 1;
// ... if the 'while' loop exits here, the return value is ()
};
if result == () {
print("Magic number not found!");
} else {
print(`Magic result = ${result}!`);
}
Do Loop
do
loops have two opposite variants: do
… while
and do
… until
.
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.
let x = 10;
do {
x -= 1;
if x < 6 { continue; } // skip to the next iteration
print(x);
if x == 5 { break; } // break out of do loop
} while x > 0;
do {
x -= 1;
if x < 6 { continue; } // skip to the next iteration
print(x);
if x == 5 { break; } // break out of do loop
} until x == 0;
Do Expression
do
statements can also be used as expressions.
The break
statement takes an optional expression that provides the return value.
The default return value of a do
expression is ()
.
let x = 0;
// 'do' can be used just like an expression
let result = do {
if is_magic_number(x) {
// if the 'do' loop breaks here, return a specific value
break get_magic_result(x);
}
x += 1;
// ... if the 'do' loop exits here, the return value is ()
} until x >= 100;
if result == () {
print("Magic number not found!");
} else {
print(`Magic result = ${result}!`);
}
Infinite Loop
Infinite loops follow Rust syntax.
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.
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
}
A loop
statement without a break
statement inside its loop block is infinite.
There is no way for the loop to stop iterating.
Loop Expression
loop
statements can also be used as expressions.
The break
statement takes an optional expression that provides the return value.
The default return value of a loop
expression is ()
.
let x = 0;
// 'loop' can be used just like an expression
let result = loop {
if is_magic_number(x) {
// if the loop breaks here, return a specific value
break get_magic_result(x);
}
x += 1;
// ... if the loop exits here, the return value is ()
};
if result == () {
print("Magic number not found!");
} else {
print(`Magic result = ${result}!`);
}
For Loop
Iterating through a numeric range or an array, or any iterable type,
is provided by the for
… in
loop.
There are two alternative syntaxes, one including a counter variable:
for
variablein
expression{
…}
for (
variable,
counter)
in
expression{
…}
Break or Continue
continue
can be used to skip to the next iteration, by-passing all following statements;
break
can be used to break out of the loop unconditionally.
For Expression
for
statements can also be used as expressions.
The break
statement takes an optional expression that provides the return value.
The default return value of a for
expression is ()
.
let a = [42, 123, 999, 0, true, "hello", "world!", 987.6543];
// 'for' can be used just like an expression
let index = for (item, count) in a {
// if the 'for' loop breaks here, return a specific value
switch item.type_of() {
"i64" if item.is_even => break count,
"f64" if item.to_int().is_even => break count,
}
// ... if the 'for' loop exits here, the return value is ()
};
if index == () {
print("Magic number not found!");
} else {
print(`Magic number found at index ${index}!`);
}
Counter Variable
The counter variable, if specified, starts from zero, incrementing upwards.
let a = [42, 123, 999, 0, true, "hello", "world!", 987.6543];
// Loop through the array
for (item, count) in a {
if x.type_of() == "string" {
continue; // skip to the next iteration
}
// 'item' contains a copy of each element during each iteration
// 'count' increments (starting from zero) for each iteration
print(`Item #${count + 1} = ${item}`);
if x == 42 { break; } // break out of for loop
}
Iterate Through Arrays
Iterating through an array yields cloned copies of each element.
let a = [1, 3, 5, 7, 9, 42];
// Loop through the array
for x in a {
if x > 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
Iterate Through Strings
Iterating through a string yields individual characters.
The chars
method also allow iterating through characters in a string,
optionally accepting the character position to start from (counting from the end if negative), as
well as the number of characters to iterate (defaults to all).
char
also accepts a range which can be created via the ..
(exclusive) and ..=
(inclusive) operators.
let s = "hello, world!";
// Iterate through all the characters.
for ch in s {
print(ch);
}
// Iterate starting from the 3rd character and stopping at the 7th.
for ch in s.chars(2, 5) {
if ch > 'z' { continue; } // skip to the next iteration
print(ch);
if x == '@' { break; } // break out of for loop
}
// Iterate starting from the 3rd character and stopping at the end.
for ch in s.chars(2..s.len) {
if ch > 'z' { continue; } // skip to the next iteration
print(ch);
if x == '@' { break; } // break out of for loop
}
Iterate Through Numeric Ranges
Ranges are created via the ..
(exclusive) and ..=
(inclusive) operators.
The range
function similarly creates exclusive ranges, plus allowing optional step values.
// Iterate starting from 0 and stopping at 49
// The step is assumed to be 1 when omitted for integers
for x in 0..50 {
if x > 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
// The 'range' function is just the same
for x in range(0, 50) {
if x > 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
// The 'range' function also takes a step
for x in range(0, 50, 3) { // step by 3
if x > 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
// The 'range' function can also step backwards
for x in range(50..0, -3) { // step down by -3
if x < 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
// It works also for floating-point numbers
for x in range(5.0, 0.0, -2.0) { // step down by -2.0
if x < 10 { continue; } // skip to the next iteration
print(x);
if x == 4.2 { break; } // break out of for loop
}
Iterate Through Bit-Fields
The bits
function allows iterating through an integer as a bit-field.
bits
optionally accepts the bit number to start from (counting from the most-significant-bit if
negative), as well as the number of bits to iterate (defaults all).
bits
also accepts a range which can be created via the ..
(exclusive) and ..=
(inclusive) operators.
let x = 0b_1001110010_1101100010_1100010100;
let num_on = 0;
// Iterate through all the bits
for bit in x.bits() {
if bit { num_on += 1; }
}
print(`There are ${num_on} bits turned on!`);
const START = 3;
// Iterate through all the bits from 3 through 12
for (bit, index) in x.bits(START, 10) {
print(`Bit #${index} is ${if bit { "ON" } else { "OFF" }}!`);
if index >= 7 { break; } // break out of for loop
}
// Iterate through all the bits from 3 through 12
for (bit, index) in x.bits(3..=12) {
print(`Bit #${index} is ${if bit { "ON" } else { "OFF" }}!`);
if index >= 7 { break; } // break out of for loop
}
Iterate Through Object Maps
Two methods, keys
and values
, return arrays containing cloned copies
of all property names and values of an object map, respectively.
These arrays can be iterated.
let map = #{a:1, b:3, c:5, d:7, e:9};
// Property names are returned in unsorted, random order
for x in map.keys() {
if x > 10 { continue; } // skip to the next iteration
print(x);
if x == 42 { break; } // break out of for loop
}
// Property values are returned in unsorted, random order
for val in map.values() {
print(val);
}
Return Value
return
The return
statement is used to immediately stop evaluation and exist the current context
(typically a function call) yielding a return value.
return; // equivalent to return ();
return 123 + 456; // returns 579
A return
statement at global level exits the script with the return value as the result.
A return
statement inside a function call exits with a return value to the caller.
exit
Similar to the return
statement, the exit
function is used to immediately stop evaluation,
but it does so regardless of where it is called from, even deep inside nested function calls.
The result value of exit
, when omitted, defaults to ()
.
fn foo() {
exit(42); // exit with result value 42
}
fn bar() {
foo();
}
fn baz() {
bar();
}
let x = baz(); // exits with result value 42
print(x); // <- this is never run
Throw Exception on Error
To deliberately return an error, use the throw
keyword.
if some_bad_condition_has_happened {
throw error; // 'throw' any value as the exception
}
throw; // defaults to '()'
Catch a Thrown Exception
It is possible to catch an exception, instead of having it abort the script run, via the
try
… catch
statement common to many C-like languages.
fn code_that_throws() {
throw 42;
}
try
{
code_that_throws();
}
catch (err) // 'err' captures the thrown exception value
{
print(err); // prints 42
}
Catch Exceptions
When an exception is thrown via a throw
statement, the script halts with
the exception value.
It is possible, via the try
… catch
statement, to catch exceptions, optionally with an
error variable.
try
{
…}
catch
{
…}
try
{
…}
catch
(
error variable)
{
…}
// Catch an exception and capturing its value
try
{
throw 42;
}
catch (err) // 'err' captures the thrown exception value
{
print(err); // prints 42
}
// Catch an exception without capturing its value
try
{
print(42/0); // deliberate divide-by-zero exception
}
catch // no error variable - exception value is discarded
{
print("Ouch!");
}
// Exception in the 'catch' block
try
{
print(42/0); // throw divide-by-zero exception
}
catch
{
print("You seem to be dividing by zero here...");
throw "die"; // a 'throw' statement inside a 'catch' block
// throws a new exception
}
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.
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
}
Many script-oriented exceptions can be caught via try
… catch
.
Error type | Error value |
---|---|
Runtime error thrown by a throw statement | value in throw statement |
Arithmetic error | object map |
Variable not found | object map |
Function not found | object map |
Module not found | object map |
Unbound this | object map |
Data type mismatch | object map |
Assignment to a calculated/constant value | object map |
Array/string/bit-field indexing out-of-bounds | object map |
Indexing with an inappropriate data type | object map |
Error in property access | object map |
for statement on a type that is not iterable | object map |
Data race detected | object map |
Other runtime error | object 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.
Some system exceptions cannot be caught.
Error type | Notes |
---|---|
System error – e.g. script file not found | system errors are not recoverable |
Syntax error during parsing | invalid script |
[Custom syntax] mismatch error | incompatible [Engine ] instance |
Script evaluation metrics exceeding [limits][safety] | [safety] protection |
Script evaluation manually terminated | [safety] protection |
Functions
Rhai supports defining functions in script via the fn
keyword.
Valid function names are the same as valid variable names.
fn add(x, y) {
x + y
}
fn sub(x, y,) { // trailing comma in parameters list is OK
x - y
}
add(2, 3) == 5;
sub(2, 3,) == -1; // trailing comma in arguments list is OK
Use is_def_fn
to detect if a Rhai function is defined (and therefore callable)
based on its name and the number of parameters (arity).
fn foo(x) { x + 1 }
is_def_fn("foo", 1) == true;
is_def_fn("foo", 0) == false;
is_def_fn("foo", 2) == false;
is_def_fn("bar", 1) == false;
Implicit Return
The last statement of a block is always the block’s return value regardless of whether it is
terminated with a semicolon ;
.
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.
// Global level is OK
fn add(x, y) {
x + y
}
// The following will not compile
fn do_addition(x) {
fn add_y(n) { // <- syntax error: cannot define inside another function
n + y
}
add_y(x)
}
No Access to External Scope
Functions are not closures. They do not capture the calling environment and can only access their own parameters.
They cannot access variables external to the function itself.
let x = 42;
fn foo() {
x // <- error: variable 'x' not found
}
But Can Call Other Functions and Access Modules
All functions can call each other.
fn foo(x) { // function defined in the global namespace
x + 1
}
fn bar(x) {
foo(x) // ok! function 'foo' can be called
}
In addition, modules imported at global level can be accessed.
import "hello" as hey;
import "world" as woo;
{
import "x" as xyz; // <- this module is not at global level
} // <- it goes away here
fn foo(x) {
hey::process(x); // ok! imported module 'hey' can be accessed
print(woo::value); // ok! imported module 'woo' can be accessed
xyz::do_work(); // <- error: module 'xyz' not found
}
Automatic Global Module
When a constant is declared at global scope, it is added to a special
module called global
.
Functions can access those constants via the special global
module.
const CONSTANT = 42; // this constant is automatically added to 'global'
let hello = 1; // variables are not added to 'global'
{
const INNER = 0; // this constant is not at global level
} // <- it goes away here
fn foo(x) {
x * global::hello // <- error: variable 'hello' not found in 'global'
x * global::CONSTANT // ok! 'CONSTANT' exists in 'global'
x * global::INNER // <- error: constant 'INNER' not found in 'global'
}
Use Before Definition Allowed
Unlike C/C++, functions in Rhai can be defined anywhere at global level.
A function does not need to be defined prior to being used in a script; a statement in the script can freely call a function defined afterwards.
This is similar to Rust and many other modern languages, such as JavaScript’s function
keyword.
let x = foo(41); // <- I can do this!
fn foo(x) { // <- define 'foo' after use
x + 1
}
Arguments are Passed by Value
Functions with the same name and same number of parameters are equivalent.
All arguments are passed by value, so all Rhai script-defined functions are pure (i.e. they never modify their arguments).
Any update to an argument will not be reflected back to the caller.
fn change(s) { // 's' is passed by value
s = 42; // only a COPY of 's' is changed
}
let x = 500;
change(x);
x == 500; // 'x' is NOT changed!
The only possibility for a Rhai script-defined function to modify an external variable is
via the this
pointer.
this
– Simulating an Object Method
The only way for a script-defined function to change an external value is via this
.
Arguments passed to script-defined functions are always by value because functions are pure.
However, functions can also be called in method-call style:
object
.
method(
parameters …)
When a function is called this way, the keyword this
binds to the object in the
method call and can be changed.
fn change() { // note that the method does not need a parameter
this = 42; // 'this' binds to the object in method-call
}
let x = 500;
x.change(); // call 'change' in method-call style, 'this' binds to 'x'
x == 42; // 'x' is changed!
change(); // <- error: 'this' is unbound
Elvis Operator
The Elvis operator can be used to short-circuit
the method call when the object itself is ()
.
object
?.
method(
parameters …)
In the above, the method is never called if object is ()
.
Restrict the Type of this
in Function Definitions
Methods defined this way are automatically exposed to the global namespace.
In many cases it may be desirable to implement methods for different custom types using script-defined functions.
The Problem
Doing so is brittle and requires a lot of type checking code because there can only be one function definition for the same name and arity:
// Really painful way to define a method called 'do_update' on various data types
fn do_update(x) {
switch type_of(this) {
"i64" => this *= x,
"string" => this.len += x,
"bool" if this => this *= x,
"bool" => this *= 42,
"MyType" => this.update(x),
"Strange-Type#Name::with_!@#symbols" => this.update(x),
_ => throw `I don't know how to handle ${type_of(this)}`!`
}
}
The Solution
With a special syntax, it is possible to restrict a function to be callable only
when the object pointed to by this
is of a certain type:
fn
type name.
method(
parameters …) {
…}
or in quotes if the type name is not a valid identifier itself:
fn
"
type name string"
.
method(
parameters …) {
…}
int
can be used in place of the system integer type (usually i64
or i32
).
float
can be used in place of the system floating-point type (usually f64
or f32
).
Using these make scripts more portable.
Examples
/// This 'do_update' can only be called on objects of type 'MyType' in method style
fn MyType.do_update(x, y) {
this.update(x * y);
}
/// This 'do_update' can only be called on objects of type 'Strange-Type#Name::with_!@#symbols'
/// (which can be specified via 'Engine::register_type_with_name') in method style
fn "Strange-Type#Name::with_!@#symbols".do_update(x, y) {
this.update(x * y);
}
/// Define a blanket version
fn do_update(x, y) {
this = `${this}, ${x}, ${y}`;
}
/// This 'do_update' can only be called on integers in method style
fn int.do_update(x, y) {
this += x * y
}
let obj = create_my_type(); // 'x' is 'MyType'
obj.type_of() == "MyType";
obj.do_update(42, 123); // ok!
let x = 42; // 'x' is an integer
x.type_of() == "i64";
x.do_update(42, 123); // ok!
let x = true; // 'x' is a boolean
x.type_of() == "bool";
x.do_update(42, 123); // <- this works because there is a blanket version
// Use 'is_def_fn' with three parameters to test for typed methods
is_def_fn("MyType", "do_update", 2) == true;
is_def_fn("int", "do_update", 2) == true;
Bind to this
for Module Functions
The Problem
The method-call syntax is not possible for functions imported from modules.
import "my_module" as foo;
let x = 42;
x.foo::change_value(1); // <- syntax error
The Solution
In order to call a module function as a method, it must be
defined with a restriction on the type of object pointed to by this
:
┌────────────────┐
│ my_module.rhai │
└────────────────┘
// This is a typed method function requiring 'this' to be an integer.
// Typed methods are automatically marked global when importing this module.
fn int.change_value(offset) {
// 'this' is guaranteed to be an integer
this += offset;
}
┌───────────┐
│ main.rhai │
└───────────┘
import "my_module";
let x = 42;
x.change_value(1); // ok!
x == 43;
Function Overloading
Functions defined in script can be overloaded by arity (i.e. they are resolved purely upon the function’s name and number of parameters, but not parameter types since all parameters are dynamic).
New definitions overwrite previous definitions of the same name and number of parameters.
fn foo(x, y, z) {
print(`Three!!! ${x}, ${y}, ${z}`);
}
fn foo(x) {
print(`One! ${x}`);
}
fn foo(x, y) {
print(`Two! ${x}, ${y}`);
}
fn foo() {
print("None.");
}
fn foo(x) { // <- overwrites previous definition
print(`HA! NEW ONE! ${x}`);
}
foo(1,2,3); // prints "Three!!! 1,2,3"
foo(42); // prints "HA! NEW ONE! 42"
foo(1,2); // prints "Two!! 1,2"
foo(); // prints "None."
Function Pointers
It is possible to store a function pointer in a variable just like a normal value.
A function pointer is created via the Fn
function, which takes a string parameter.
Call a function pointer via the call
method.
Short-Hand Notation
Having to write Fn("foo")
in order to create a function pointer to the function
foo
is a chore, so there is a short-hand available.
A function pointer to any script-defined function within the same script can be obtained simply by referring to the function’s name.
fn foo() { ... } // function definition
let f = foo; // function pointer to 'foo'
let f = Fn("foo"); // <- the above is equivalent to this
let g = bar; // error: variable 'bar' not found
The short-hand notation is particularly useful when passing functions as closure arguments.
fn is_even(n) { n % 2 == 0 }
let array = [1, 2, 3, 4, 5];
array.filter(is_even);
array.filter(Fn("is_even")); // <- the above is equivalent to this
array.filter(|n| n % 2 == 0); // <- ... or this
Built-in Functions
The following standard methods operate on function pointers.
Function | Parameter(s) | Description |
---|---|---|
name method and property | none | returns the name of the function encapsulated by the function pointer |
is_anonymous method and property | none | does the function pointer refer to an anonymous function? |
call | arguments | calls the function matching the function pointer’s name with the arguments |
Examples
fn foo(x) { 41 + x }
let func = Fn("foo"); // use the 'Fn' function to create a function pointer
let func = foo; // <- short-hand: equivalent to 'Fn("foo")'
print(func); // prints 'Fn(foo)'
let func = fn_name.Fn(); // <- error: 'Fn' cannot be called in method-call style
func.type_of() == "Fn"; // type_of() as function pointer is 'Fn'
func.name == "foo";
func.call(1) == 42; // call a function pointer with the 'call' method
foo(1) == 42; // <- the above de-sugars to this
call(func, 1); // normal function call style also works for 'call'
let len = Fn("len"); // 'Fn' also works with registered native Rust functions
len.call("hello") == 5;
let fn_name = "hello"; // the function name does not have to exist yet
let hello = Fn(fn_name + "_world");
hello.call(0); // error: function not found - 'hello_world (i64)'
Because of their dynamic nature, function pointers cannot refer to functions in
import
-ed modules.
They can only refer to functions defined globally within the script or a built-in function.
import "foo" as f; // assume there is 'f::do_work()'
f::do_work(); // works!
let p = Fn("f::do_work"); // error: invalid function name
fn do_work_now() { // call it from a local function
f::do_work();
}
let p = Fn("do_work_now");
p.call(); // works!
Dynamic Dispatch
The purpose of function pointers is to enable rudimentary dynamic dispatch, meaning to determine, at runtime, which function to call among a group.
Although it is possible to simulate dynamic dispatch via a number and a large
if-then-else-if
statement, using function pointers significantly simplifies the code.
let x = some_calculation();
// These are the functions to call depending on the value of 'x'
fn method1(x) { ... }
fn method2(x) { ... }
fn method3(x) { ... }
// Traditional - using decision variable
let func = sign(x);
// Dispatch with if-statement
if func == -1 {
method1(42);
} else if func == 0 {
method2(42);
} else if func == 1 {
method3(42);
}
// Using pure function pointer
let func = if x < 0 {
method1
} else if x == 0 {
method2
} else if x > 0 {
method3
};
// Dynamic dispatch
func.call(42);
// Using functions map
let map = [ method1, method2, method3 ];
let func = sign(x) + 1;
// Dynamic dispatch
map[func].call(42);
Bind the this
Pointer
When call
is called as a method but not on a function pointer, it is possible to dynamically dispatch
to a function call while binding the object in the method call to the this
pointer of the function.
To achieve this, pass the function pointer as the first argument to call
:
fn add(x) { // define function which uses 'this'
this += x;
}
let func = add; // function pointer to 'add'
func.call(1); // error: 'this' pointer is not bound
let x = 41;
func.call(x, 1); // error: function 'add (i64, i64)' not found
call(func, x, 1); // error: function 'add (i64, i64)' not found
x.call(func, 1); // 'this' is bound to 'x', dispatched to 'func'
x == 42;
Beware that this only works for method-call style.
Normal function-call style cannot bind the this
pointer (for syntactic reasons).
Currying
It is possible to curry a function pointer by providing partial (or all) arguments.
Currying is done via the curry
keyword and produces a new function pointer which carries the
curried arguments.
When the curried function pointer is called, the curried arguments are inserted starting from the left.
The actual call arguments should be reduced by the number of curried arguments.
fn mul(x, y) { // function with two parameters
x * y
}
let func = mul; // <- de-sugars to 'Fn("mul")'
func.call(21, 2) == 42; // two arguments are required for 'mul'
let curried = func.curry(21); // currying produces a new function pointer which
// carries 21 as the first argument
let curried = curry(func, 21); // function-call style also works
curried.call(2) == 42; // <- de-sugars to 'func.call(21, 2)'
// only one argument is now required
Closures
Many functions in the standard API expect function pointer as parameters.
For example:
// Function 'double' defined here - used only once
fn double(x) { 2 * x }
// Function 'square' defined here - again used only once
fn square(x) { x * x }
let x = [1, 2, 3, 4, 5];
// Pass a function pointer to 'double'
let y = x.map(double);
// Pass a function pointer to 'square' using Fn(...) notation
let z = y.map(Fn("square"));
Sometimes it gets tedious to define separate functions only to dispatch them via single function pointers – essentially, those functions are only ever called in one place.
This scenario is especially common when simulating object-oriented programming ([OOP]).
// Define functions one-by-one
fn obj_inc(x, y) { this.data += x * y; }
fn obj_dec(x) { this.data -= x; }
fn obj_print() { print(this.data); }
// Define object
let obj = #{
data: 42,
increment: obj_inc, // use function pointers to
decrement: obj_dec, // refer to method functions
print: obj_print
};
Syntax
Closures have a syntax similar to Rust’s closures (they are not the same).
|
param 1,
param 2,
…,
param n|
statement
|
param 1,
param 2,
…,
param n| {
statements…}
No parameters:
||
statement
|| {
statements…}
Rewrite Using Closures
The above can be rewritten using closures.
let x = [1, 2, 3, 4, 5];
let y = x.map(|x| 2 * x);
let z = y.map(|x| x * x);
let obj = #{
data: 42,
increment: |x, y| this.data += x * y, // one statement
decrement: |x| this.data -= x, // one statement
print_obj: || {
print(this.data); // full function body
}
};
This de-sugars to:
// Automatically generated...
fn anon_fn_0001(x) { 2 * x }
fn anon_fn_0002(x) { x * x }
fn anon_fn_0003(x, y) { this.data += x * y; }
fn anon_fn_0004(x) { this.data -= x; }
fn anon_fn_0005() { print(this.data); }
let x = [1, 2, 3, 4, 5];
let y = x.map(anon_fn_0001);
let z = y.map(anon_fn_0002);
let obj = #{
data: 42,
increment: anon_fn_0003,
decrement: anon_fn_0004,
print: anon_fn_0005
};
Capture External Variables
Closures differ from standard functions because they can captures variables that are not defined within the current scope, but are instead defined in an external scope – i.e. where the it is created.
All variables that are accessible during the time the closure is created are automatically captured when they are used, as long as they are not shadowed by local variables defined within the function’s.
The captured variables are automatically converted into reference-counted shared values.
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.
let x = 1; // a normal variable
x.is_shared() == false;
let f = |y| x + y; // variable 'x' is auto-curried (captured) into 'f'
x.is_shared() == true; // 'x' is now a shared value!
f.call(2) == 3; // 1 + 2 == 3
x = 40; // changing 'x'...
f.call(2) == 42; // the value of 'x' is 40 because 'x' is shared
// The above de-sugars into something like this:
fn anon_0001(x, y) { x + y } // parameter 'x' is inserted
make_shared(x); // convert variable 'x' into a shared value
let f = anon_0001.curry(x); // shared 'x' is curried
Data races are possible in Rhai scripts.
Avoid performing a method call on a captured shared variable (which essentially takes a mutable reference to the shared object) while using that same variable as a parameter in the method call – this is a sure-fire way to generate a data race error.
If a shared value is used as the this
pointer in a method call to a closure function,
then the same shared value must not be captured inside that function, or a data race
will occur and the script will terminate with an error.
let x = 20;
x.is_shared() == false; // 'x' not shared, so no data races
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'
Functions Metadata
The metadata of a function means all relevant information related to a function’s definition including:
-
Its callable name
-
Its access mode (public or private)
-
Its parameter names (if any)
-
Its purpose, in the form of doc-comments
-
Usage notes, warnings, examples etc., in the form of doc-comments
A function’s signature encapsulates the first three pieces of information in a single concise line of definition:
[private]
name(
param 1,
param 2,
…,
param n)
Get Functions Metadata
The built-in function get_fn_metadata_list
returns an array of object
maps, each containing the metadata of one script-defined function
in scope.
get_fn_metadata_list
has a few versions taking different parameters:
Signature | Description |
---|---|
get_fn_metadata_list() | returns an array for all script-defined functions |
get_fn_metadata_list(name) | returns an array containing all script-defined functions matching a specified name |
get_fn_metadata_list(name, params) | returns an array containing all script-defined functions matching a specified name and accepting the specified number of parameters |
The return value is an array of object maps containing the following fields.
Field | Type | Optional? | Description |
---|---|---|---|
namespace | string | yes | the module namespace if the function is defined within a module |
access | string | no | "public" if the function is public,"private" if it is private |
name | string | no | function name |
params | array of strings | no | parameter names |
this_type | string | yes | restrict the type of this if the function is a method |
is_anonymous | bool | no | is this function an anonymous function? |
comments | array of strings | yes | doc-comments, if any, one per line |
print
and debug
The print
and debug
functions can be used to output values.
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
Modules
Rhai allows organizing code into modules.
A module holds a collection of functions, constants and sub-modules.
It may encapsulates a Rhai script together with the functions and constants defined by that script.
Other scripts can then load this module and use the functions and constants exported as if they were defined inside the same script.
Export Variables, Functions and Sub-Modules From a Script
The easiest way to expose a collection of functions as a self-contained module is to do it via a Rhai script itself.
The script text is evaluated.
Variables are then selectively exposed via the export
statement.
Functions defined by the script are automatically exported, unless marked as private
.
Modules loaded within this module at the global level become sub-modules and are also automatically exported.
Export Global Constants
The export
statement, which can only be at global level, exposes a selected
variable as member of a module.
Variables not exported are private and hidden. They are merely used to initialize the module, but cannot be accessed from outside.
Everything exported from a module is constant (i.e. read-only).
// This is a module script.
let hidden = 123; // variable not exported - default hidden
let x = 42; // this will be exported below
export x; // the variable 'x' is exported under its own name
export const x = 42; // convenient short-hand to declare a constant and export it
// under its own name
export let x = 123; // variables can be exported as well, though it'll still be constant
export x as answer; // the variable 'x' is exported under the alias 'answer'
// another script can load this module and access 'x' as 'module::answer'
{
let inner = 0; // local variable - it disappears when the statements block ends,
// therefore it is not 'global' and cannot be exported
export inner; // <- syntax error: cannot export a local variable
}
[Variables] can be exported under multiple names. For example, the following exports three [variables]:
x
asx
andhello
y
asfoo
andbar
z
asz
export x;
export x as hello;
export y as foo;
export x as world;
export y as bar;
export 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.
// This is a module script.
fn inc(x) { x + 1 } // script-defined function - default public
private fn foo() {} // private function - hidden
Sub-Modules
All loaded modules are automatically exported as sub-modules.
To prevent a module from being exported, load it inside a block statement so that it goes away at the end of the block.
// This is a module script.
import "hello" as foo; // <- exported
{
import "world" as bar; // <- not exported
}
Import a Module
import
Statement
A module that is only import
-ed but not given any name is simply run.
This is a very simple way to run another script file from within a script.
A module can be imported via the import
statement, and be given a name.
Its members can be accessed via ::
similar to C++.
import "crypto_banner"; // run the script file 'crypto_banner.rhai' without creating an imported module
import "crypto" as lock; // run the script file 'crypto.rhai' and import it as a module named 'lock'
const SECRET_NUMBER = 42;
let mod_file = `crypto_${SECRET_NUMBER}`;
import mod_file as my_mod; // load the script file "crypto_42.rhai" and import it as a module named 'my_mod'
// notice that module path names can be dynamically constructed!
// any expression that evaluates to a string is acceptable after the 'import' keyword
lock::encrypt(secret); // use functions defined under the module via '::'
lock::hash::sha256(key); // sub-modules are also supported
print(lock::status); // module variables are constants
lock::status = "off"; // <- runtime error: cannot modify a constant
Modules imported via import
statements are only accessible inside the relevant block scope.
import "hacker" as h; // import module - visible globally
if secured { // <- new block scope
let mod = "crypt";
import mod + "o" as c; // import module (the path needs not be a constant string)
let x = c::encrypt(key); // use a function in the module
h::hack(x); // global module 'h' is visible here
} // <- module 'c' disappears at the end of the block scope
h::hack(something); // this works as 'h' is visible
c::encrypt(something); // <- this causes a run-time error because
// module 'c' is no longer available!
fn foo(something) {
h::hack(something); // <- this also works as 'h' is visible
}
for x in 0..1000 {
import "crypto" as c; // <- importing a module inside a loop is a Very Bad Idea™
c.encrypt(something);
}
import
statements can appear anywhere a normal statement can be, but in the vast majority of cases they are
usually grouped at the top (beginning) of a script for manageability and visibility.
It is not advised to deviate from this common practice unless there is a Very Good Reason™.
Especially, do not place an import
statement within a loop; doing so will repeatedly re-load the
same module during every iteration of the loop!
Beware of import cycles – i.e. recursively loading the same module. This is a sure-fire way to cause a stack overflow error.
For instance, importing itself always causes an infinite recursion:
┌────────────┐
│ hello.rhai │
└────────────┘
import "hello" as foo; // import itself - infinite recursion!
foo::do_something();
Modules cross-referencing also cause infinite recursion:
┌────────────┐
│ hello.rhai │
└────────────┘
import "world" as foo;
foo::do_something();
┌────────────┐
│ world.rhai │
└────────────┘
import "hello" as bar;
bar::do_something_else();
eval
Function
Or “How to Shoot Yourself in the Foot even Easier”
Saving the best for last, there is the ever-dreaded… eval
function!
let x = 10;
fn foo(x) { x += 12; x }
let script =
"
let y = x;
y += foo(y);
x + y
";
let result = eval(script); // <- look, JavaScript, we can also do this!
result == 42;
x == 10; // prints 10 - arguments are passed by value
y == 32; // prints 32 - variables defined in 'eval' persist!
eval("{ let z = y }"); // to keep a variable local, use a statements block
print(z); // <- error: variable 'z' not found
"print(42)".eval(); // <- nope... method-call style doesn't work with 'eval'
Script segments passed to eval
execute inside the current scope, so they can access and modify
everything, including all variables that are visible at that position in code!
let script = "x += 32";
let x = 10;
eval(script); // variable 'x' is visible!
print(x); // prints 42
// The above is equivalent to:
let script = "x += 32";
let x = 10;
x += 32;
print(x);
eval
can also be used to define new variables and do other things normally forbidden inside
a function call.
let script = "let x = 42";
eval(script);
print(x); // prints 42
Treat it as if the script segments are physically pasted in at the position of the eval
call.
External Tools
External tools available to work with Rhai.
Tool | Description |
---|---|
Online Playground | edit and run Rhai scripts in a browser |
LSP Server | Rhai Language Server |
rhai-doc | generate documentation for Rhai functions |
rhai-dylib | create dynamically loadable Rhai libraries |
rhai-autodocs | generate MarkDown/MDX API documentation from an Engine instance |
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 Language Server
Rhai provides a Language Server Protocol (LSP) server to work with IDE tools.
Author : @tamasfe
Repo : on GitHub
URL : link to LSP Server
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-formatted 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/Option | Parameter | Default | Description |
---|---|---|---|
-h , --help | print help | ||
-V , --version | print version | ||
-a , --all | generate documentation for all functions, including private ones (default false) | ||
-v | use multiple to set verbosity: 1=silent, 2,3 (default)=full | ||
-c , --config | <file> | rhai.toml | set configuration file |
-D , --dest | <directory> | dist | set destination directory for documentation output |
-d , --dir | <directory> | current directory | set source directory for Rhai scripts |
-p , --pages | <directory> | pages | set source directory for additional MarkDown page files to include |
Commands
Command | Description | Example |
---|---|---|
none | generate documentation | rhai-doc |
new | create a skeleton rhai.toml in the source directory | rhai-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
Option | Value type | Default | Description |
---|---|---|---|
name | string | none | name of project – used as titles on documentation pages |
color | RGB values (0-255) array | [246, 119, 2] | theme color for generated documentation |
root | URL string | none | root URL generated as part of documentation |
index | file path | none | main MarkDown file – becomes index.html |
icon | file path | Rhai icon | project icon |
stylesheet | file path | none | custom stylesheet |
code_theme | theme string | default | highlight.js theme for syntax highlighting in code blocks |
code_lang | language string | ts | default language for code blocks |
extension | extension string | .rhai | script files extension (default .rhai ) |
google_analytics | ID string | none | Google Analytics ID |
[[links]] | table | none | external links |
• name | string | none | • title of external link |
• link | URL string | none | • URL of external link |
By default, rhai-doc
will generate documentation pages from a pages
sub-directory
under the scripts directory. The pages are assumed to be in MarkDown.
Alternatively, you can specify another location via the --pages
option.
Create Dynamically Loadable Rhai Libraries
rhai-dylib
is an independent crate that demonstrates an API to register Rhai functionalities via
dynamic shared libraries (i.e. .so
in Linux or .dll
in Windows).
In other words, functions and modules can be defined in external libraries that are loaded dynamically at runtime, allowing for great flexibility at the cost of depending on the unstable Rust ABI.
A module resolver is also included.
On
crates.io
:rhai-dylib
On
GitHub
:rhaiscript/rhai-dylib
API trait name:
rhai_dylib::Plugin
rhai-autodocs
: Generate API Documentation
rhai-autodocs
helps generate API documentation, in MarkDown or MDX format, for functions
registered inside an Engine
instance.
It is typically imported as a build dependency into the build script.
The MarkDown/MDX files can then be used to create a static documentation site using generators
such as mdbook
or Docusaurus.
On
crates.io
:rhai-autodocs
On
GitHub
:rhaiscript/rhai-autodocs
Usage
Cargo.toml
:
[dev-dependencies]
rhai = "1.20.0"
rhai-autodocs = "0.4" # use rhai-autodocs crate
build.rs
:
fn main() {
// Specify an environment variable that points to the directory
// where the documentation will be generated.
if let Ok(docs_path) = std::env::var("DOCS_DIR") {
let mut engine = rhai::Engine::new();
// register custom functions and types...
// or load packages...
let docs = rhai_autodocs::options()
.include_standard_packages(false)
.generate(&engine)
.expect("failed to generate documentation");
// Write the documentation to a file etc.
}
}
Keywords List
Keyword | Description | Inactive under | Is function? |
---|---|---|---|
true | boolean true literal | no | |
false | boolean false literal | no | |
let | variable declaration | no | |
const | constant declaration | no | |
if | if statement | no | |
else | else block of if statement | no | |
switch | matching | no | |
do | looping | no | |
while | no | ||
until | do loop | no | |
loop | infinite loop | no | |
for | for loop | no | |
in |
| no | |
!in | negated containment test | no | |
continue | continue a loop at the next iteration | no | |
break | break out of loop iteration | no | |
return | return value | no | |
throw | throw exception | no | |
try | trap exception | no | |
catch | catch exception | no | |
import | import module | no_module | no |
export | export variable | no_module | no |
as | alias for variable export | no_module | no |
global | automatic global module | no_module , no_function | no |
private | mark function private | no_function | no |
fn (lower-case f ) | function definition | no_function | no |
Fn (capital F ) | create a function pointer | yes | |
call | call a function pointer | yes | |
curry | curry a function pointer | yes | |
is_shared | is a variable shared? | no_function , no_closure | yes |
is_def_fn | is function defined? | no_function | yes |
is_def_var | is variable defined? | yes | |
this | reference to base object for method call | no_function | no |
type_of | get type name of value | yes | |
print | print value | yes | |
debug | print value in debug format | yes | |
eval | evaluate script | yes |
Reserved Keywords
Keyword | Potential usage |
---|---|
var | variable declaration |
static | variable declaration |
shared | share value |
goto | control flow |
match | matching |
case | matching |
public | function/field access |
protected | function/field access |
new | constructor |
use | import namespace |
with | scope |
is | type check |
module | module |
package | package |
super | base class/module |
thread | threading |
spawn | threading |
go | threading |
await | async |
async | async |
sync | async |
yield | async |
default | special value |
void | special value |
null | special value |
nil | special value |
Operators and Symbols
Operators
Operator | Description | Binary? | Binding direction |
---|---|---|---|
+ | add | yes | left |
- | 1) subtract 2) negative (prefix) | yes no | left right |
* | multiply | yes | left |
/ | divide | yes | left |
% | modulo | yes | left |
** | power/exponentiation | yes | right |
>> | right bit-shift | yes | left |
<< | left bit-shift | yes | left |
& | 1) bit-wise AND 2) boolean AND | yes | left |
| | 1) bit-wise OR 2) boolean OR | yes | left |
^ | 1) bit-wise XOR 2) boolean XOR | yes | left |
= , += , -= , *= , /= ,**= , %= , <<= , >>= , &= ,|= , ^= | assignments | yes | n/a |
== | equals to | yes | left |
!= | not equals to | yes | left |
> | greater than | yes | left |
>= | greater than or equals to | yes | left |
< | less than | yes | left |
<= | less than or equals to | yes | left |
&& | boolean AND (short-circuits) | yes | left |
|| | boolean OR (short-circuits) | yes | left |
?? | null-coalesce (short-circuits) | yes | left |
! | boolean NOT | no | right |
[ … ] , ?[ … ] | indexing | yes | left |
. , ?. | 1) property access 2) method call | yes | left |
.. | exclusive range | yes | left |
..= | inclusive range | yes | left |
Symbols and Patterns
Symbol | Name | Description |
---|---|---|
_ | underscore | default switch case |
; | semicolon | statement separator |
, | comma | list separator |
: | colon | object map property value separator |
:: | path | module path separator |
#{ … } | hash map | object map literal |
" … " | double quote | string |
` … ` | back-tick | multi-line literal string |
' … ' | single quote | character |
\ | 1) escape 2) line continuation | escape character literal |
() | unit | null value |
( … ) | parentheses | expression grouping |
{ … } | braces | block statement |
| … | | pipes | closure |
[ … ] | brackets | array literal |
! | bang | function call in calling scope |
=> | double arrow | switch expression case separator |
// | comment | line comment |
/// | doc-comment | line [doc-comment] |
//! | module doc | module documentation |
/* … */ | comment | block comment |
/** … */ | doc-comment | block [doc-comment] |
(* … *) | comment | reserved |
#! | shebang | reserved |
++ | increment | reserved |
-- | decrement | reserved |
... | rest | reserved |
~ | tilde | reserved |
!. | reserved | |
? | question | reserved |
# | hash | reserved |
@ | at | reserved |
$ | dollar | reserved |
-> | arrow | reserved |
<- | left arrow | reserved |
<| | left triangle | reserved |
|> | right triangle | reserved |
=== | strict equals to | reserved |
!== | strict not equals to | reserved |
:= | assignment | reserved |
:; | typo to :: | reserved |
::< … > | turbofish | reserved |
Literals Syntax
Type | Literal syntax |
---|---|
INT | decimal: 42 , -123 , 0 hex: 0x????.. binary: 0b????.. octal: 0o????.. |
FLOAT ,Decimal (requires no_float +decimal ) | 42.0 , -123.456 , 123. , 123.456e-10 |
Ranges in switch cases | -10..10 (exclusive), 0..=50 (inclusive) |
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}` |
Character | single: '?' ASCII hex: '\x??' Unicode: '\u????' , '\U????????' |
Array | [ ???, ???, ??? ] |
Object map | #{ a: ???, b: ???, c: ???, "def": ??? } |
Boolean true | true |
Boolean false | false |
Nothing /null /nil /void /Unit | () |