...
This commit is contained in:
62
rhai_engine/rhaibook/patterns/blocking.md
Normal file
62
rhai_engine/rhaibook/patterns/blocking.md
Normal file
@@ -0,0 +1,62 @@
|
||||
Blocking/Async Function Calls
|
||||
=============================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish danger.small "Warning: Async and scripting don't mix well"
|
||||
|
||||
Otherwise, you reinvent the [_Callback Hell_](https://en.wiktionary.org/wiki/callback_hell)
|
||||
which is JavaScript before all the async extensions.
|
||||
```
|
||||
|
||||
```admonish info "Usage scenarios"
|
||||
|
||||
* A system's API contains async functions.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* This pattern is based upon the _[Multi-Threaded Synchronization](multi-threading.md)_ 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
|
||||
--------------
|
||||
|
||||
```admonish info.side "See also"
|
||||
|
||||
See the _[Multi-Threaded Synchronization](multi-threading.md)_ pattern.
|
||||
```
|
||||
|
||||
1. Spawn a thread to run the scripting [`Engine`]. Usually the [`sync`] feature is
|
||||
_NOT_ used for this pattern.
|
||||
|
||||
2. 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.
|
||||
|
||||
3. Create a pair of MPSC channels (named `command` and `reply` below) for full-duplex
|
||||
communications between the two threads.
|
||||
|
||||
4. Register async API function to the [`Engine`] with a closure that captures the MPSC end-points.
|
||||
|
||||
5. 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 the `command` channel can be wrapped in an `Arc<Mutex<Channel>>` for shared access.
|
||||
|
||||
6. 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 the `worker` thread and the appropriate async function called.
|
||||
|
||||
7. The [`Engine`] blocks on the function call, waiting for a reply message on the `reply` channel.
|
||||
|
||||
8. When the async function call complete on the `worker` thread, the result is sent back to
|
||||
the [`Engine`] thread via the `reply` channel.
|
||||
|
||||
9. After the result is obtained from the `reply` channel, the [`Engine`] returns it as the return value
|
||||
of the function call, ending the block and continuing evaluation.
|
307
rhai_engine/rhaibook/patterns/builder.md
Normal file
307
rhai_engine/rhaibook/patterns/builder.md
Normal file
@@ -0,0 +1,307 @@
|
||||
Builder Pattern / Fluent API
|
||||
============================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* An API uses the [Builder Pattern](https://en.wikipedia.org/wiki/Builder_pattern) or a [fluent API](https://en.wikipedia.org/wiki/Fluent_interface).
|
||||
|
||||
* The builder type is not necessarily `Clone`.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* Wrap the builder type in shared interior mutability (aka `Rc<RefCell<T>>` or `Arc<RwLock<T>>`).
|
||||
```
|
||||
|
||||
```admonish tip.small "Tip: Fluent API"
|
||||
|
||||
This same pattern can be used to implement any [_fluent API_](https://en.wikipedia.org/wiki/Fluent_interface).
|
||||
```
|
||||
|
||||
|
||||
Implementation With Clonable Builder Type
|
||||
-----------------------------------------
|
||||
|
||||
This assumes that the builder type implements `Clone`.
|
||||
This is the most common scenario.
|
||||
|
||||
```rust
|
||||
/// 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`.
|
||||
|
||||
```rust
|
||||
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!
|
||||
|
||||
```rust
|
||||
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
|
||||
┌──────┐
|
||||
│ 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();
|
||||
```
|
171
rhai_engine/rhaibook/patterns/config.md
Normal file
171
rhai_engine/rhaibook/patterns/config.md
Normal file
@@ -0,0 +1,171 @@
|
||||
Loadable Configuration
|
||||
======================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* A system where settings and configurations are complex and logic-driven.
|
||||
|
||||
* Where said system is too complex to configure via standard configuration file formats such as
|
||||
`JSON`, `TOML` or `YAML`.
|
||||
|
||||
* The system is complex enough to require a full programming language to configure.
|
||||
Essentially _configuration by code_.
|
||||
|
||||
* Yet the configuration must be flexible, late-bound and dynamically loadable, just like a
|
||||
configuration file.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* Leverage the loadable [modules] of Rhai. The [`no_module`] feature must not be on.
|
||||
|
||||
* Expose the configuration API. Use separate scripts to configure that API.
|
||||
Dynamically load scripts via the `import` statement.
|
||||
|
||||
* Leverage [function overloading] to simplify the API design.
|
||||
|
||||
* Since Rhai is _sand-boxed_, it cannot mutate the environment. To modify the external
|
||||
configuration object via an API, it must be wrapped in a `RefCell` (or `RwLock`/`Mutex` for
|
||||
[`sync`]) and shared to the [`Engine`].
|
||||
```
|
||||
|
||||
|
||||
Implementation
|
||||
--------------
|
||||
|
||||
### Configuration type
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct Config {
|
||||
id: String,
|
||||
some_field: i64,
|
||||
some_list: Vec<String>,
|
||||
some_map: HashMap<String, bool>,
|
||||
}
|
||||
```
|
||||
|
||||
### Make shared object
|
||||
|
||||
```rust
|
||||
type SharedConfig = Rc<RefCell<Config>>;
|
||||
|
||||
let config = SharedConfig::default();
|
||||
```
|
||||
|
||||
or in multi-threaded environments with the [`sync`] feature, use one of the following:
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```rust
|
||||
// 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
|
||||
|
||||
```rust
|
||||
┌────────────────┐
|
||||
│ 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
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
┌────────────────┐
|
||||
│ 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].
|
168
rhai_engine/rhaibook/patterns/constants.md
Normal file
168
rhai_engine/rhaibook/patterns/constants.md
Normal file
@@ -0,0 +1,168 @@
|
||||
Global Constants
|
||||
================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* 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`][`global`] throughout the script makes
|
||||
it slow and cumbersome.
|
||||
|
||||
* Using [`global`] or a [variable resolver] defeats
|
||||
[constants propagation]({{rootUrl}}/engine/optimize/constants.md) in [script optimization].
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* The key to global [constants] is to use them to [optimize][script optimization] 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][script optimization] the [`AST`].
|
||||
|
||||
* This pattern also works under [_Strict Variables Mode_][strict variables].
|
||||
```
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Assume that the following Rhai script needs to work (but it doesn't).
|
||||
|
||||
```rust
|
||||
// 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][script optimization] at this stage since
|
||||
the [`AST`] will be re-optimized later.
|
||||
|
||||
[_Strict Variables Mode_][strict variables] must be OFF for this to work.
|
||||
|
||||
```rust
|
||||
// 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`](https://docs.rs/rhai/{{version}}/rhai/struct.AST.html#method.iter_literal_variables)
|
||||
to extract top-level [constants] from the [`AST`].
|
||||
|
||||
```rust
|
||||
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][script optimization] the [`AST`] using the new constants.
|
||||
|
||||
```rust
|
||||
// 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_][strict variables] is used, however, it is necessary to re-compile the
|
||||
script in order to detect undefined [variable] usages.
|
||||
|
||||
```rust
|
||||
// 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:
|
||||
|
||||
```rust
|
||||
// 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`.
|
||||
|
||||
```rust
|
||||
// The 'scope' is no longer necessary
|
||||
engine.run_ast(&ast)?;
|
||||
```
|
145
rhai_engine/rhaibook/patterns/control.md
Normal file
145
rhai_engine/rhaibook/patterns/control.md
Normal file
@@ -0,0 +1,145 @@
|
||||
Scriptable Control Layer Over Rust Backend
|
||||
==========================================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* A system provides core functionalities, but no driving logic.
|
||||
|
||||
* The driving logic must be dynamic and hot-loadable.
|
||||
|
||||
* A script is used to drive the system and provide control intelligence.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* Expose a Control API.
|
||||
|
||||
* Leverage [function overloading] to simplify the API design.
|
||||
|
||||
* Since Rhai is _[sand-boxed]_, it cannot mutate anything outside of its internal environment.
|
||||
To perform external actions via an API, the actual system must be wrapped in a `RefCell`
|
||||
(or `RwLock`/`Mutex` for [`sync`]) and shared to the [`Engine`].
|
||||
```
|
||||
|
||||
```admonish danger "Using Rhai for games"
|
||||
|
||||
Although this usage pattern appears a perfect fit for _game_ logic, avoid writing the _entire game_
|
||||
in Rhai. Performance will not be acceptable.
|
||||
|
||||
Implement as much functionalities of the game engine in Rust as possible. Rhai integrates well with
|
||||
Rust so this is usually not a hinderance.
|
||||
|
||||
Lift as much out of Rhai as possible. Use Rhai only for the logic that _must_ be dynamic or
|
||||
hot-loadable.
|
||||
```
|
||||
|
||||
|
||||
Implementation
|
||||
--------------
|
||||
|
||||
There are two broad ways for Rhai to control an external system, both of which involve wrapping the
|
||||
system in a shared, interior-mutated object.
|
||||
|
||||
This is one way which does not involve exposing the data structures of the external system,
|
||||
but only through exposing an abstract API primarily made up of functions.
|
||||
|
||||
Use this when the API is relatively simple and clean, and the number of functions is small enough.
|
||||
|
||||
For a complex API involving lots of functions, or an API that has a clear object structure, use the
|
||||
[Singleton Command Object]({{rootUrl}}/patterns/singleton.md) pattern instead.
|
||||
|
||||
|
||||
### Functional API
|
||||
|
||||
Assume that a system provides the following functional API:
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
pub type SharedBunny = Rc<RefCell<EnergizerBunny>>;
|
||||
```
|
||||
|
||||
or in multi-threaded environments with the [`sync`] feature, use one of the following:
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```rust
|
||||
// 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
|
||||
|
||||
```rust
|
||||
if !bunny_is_going() { bunny_power(true); }
|
||||
|
||||
if bunny_get_speed() > 50 { bunny_set_speed(50); }
|
||||
```
|
139
rhai_engine/rhaibook/patterns/domain-tools.md
Normal file
139
rhai_engine/rhaibook/patterns/domain-tools.md
Normal file
@@ -0,0 +1,139 @@
|
||||
Domain-Specific Tools
|
||||
=====================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
[bin tool]: {{rootUrl}}/start/bin.md
|
||||
[bin tools]: {{rootUrl}}/start/bin.md
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* A system has a _domain-specific_ API, requiring [custom types] and/or
|
||||
Rust [functions]({{rootUrl}}/rust/functions.md) 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_](events.md), [_Control Layer_](control.md) or
|
||||
[_Singleton Command Object_](singleton.md) patterns.
|
||||
|
||||
* It is desirable to be able to _interactively_ test the system with different scripts,
|
||||
and/or [debug][debugger] them.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* Leverage the pre-packaged [bin tools] – it is easy because each of them is
|
||||
one single source file.
|
||||
|
||||
* Modify the [`Engine`] creation code to include domain-specific registrations.
|
||||
```
|
||||
|
||||
|
||||
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`]({{repoHome}}/src/bin/rhai-run.rs) | a simple script runner for the system |
|
||||
| [`rhai-repl.rs`]({{repoHome}}/src/bin/rhai-repl.rs) | an interactive REPL for the system |
|
||||
| [`rhai-dbg.rs`]({{repoHome}}/src/bin/rhai-dbg.rs) | a script debugger for the system |
|
||||
|
||||
#### Example
|
||||
|
||||
```sh
|
||||
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`].
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```rust
|
||||
// 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`.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rhai = { version = "{{version}}", 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][debugger] etc.
|
55
rhai_engine/rhaibook/patterns/dynamic-const.md
Normal file
55
rhai_engine/rhaibook/patterns/dynamic-const.md
Normal file
@@ -0,0 +1,55 @@
|
||||
Dynamic Constants Provider
|
||||
==========================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* A system has a _large_ number of constants, but only a minor set will be used by any script.
|
||||
|
||||
* The system constants are expensive to load.
|
||||
|
||||
* The system constants set is too massive to push into a custom [`Scope`].
|
||||
|
||||
* The values of system constants are volatile and call-dependent.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* Use a [variable resolver] to intercept variable access.
|
||||
|
||||
* Only load a variable when it is being used.
|
||||
|
||||
* Perform a lookup based on variable name, and provide the correct data value.
|
||||
|
||||
* May even perform back-end network access or look up the latest value from a database.
|
||||
```
|
||||
|
||||
|
||||
Implementation
|
||||
--------------
|
||||
|
||||
```rust
|
||||
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)));
|
||||
```
|
||||
|
||||
|
||||
```admonish note.small "Values are constants"
|
||||
|
||||
All values provided by a [variable resolver] are _[constants]_ due to their dynamic nature.
|
||||
They cannot be assigned to.
|
||||
|
||||
In order to change values in an external system, register a dedicated API for that purpose.
|
||||
```
|
281
rhai_engine/rhaibook/patterns/enums.md
Normal file
281
rhai_engine/rhaibook/patterns/enums.md
Normal file
@@ -0,0 +1,281 @@
|
||||
Working With Rust Enums
|
||||
=======================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
```admonish question.side.wide "Why enums are hard"
|
||||
|
||||
Rust enum variants are not considered separate types.
|
||||
|
||||
Although Rhai integrates fine with Rust enums (treated transparently as [custom types]),
|
||||
it is impossible (short of registering a complete API) to distinguish between individual
|
||||
variants and to extract internal data from them.
|
||||
```
|
||||
|
||||
Enums in Rust can hold data and are typically used with _pattern matching_.
|
||||
|
||||
Unlike Rust, Rhai does not have built-in pattern matching capabilities, so working with enum
|
||||
variants that contain embedded data is not an easy proposition.
|
||||
|
||||
Since Rhai is dynamic and [variables] can hold any type of data, they are essentially enums
|
||||
by nature.
|
||||
|
||||
Multiple distinct types can be stored in a single [`Dynamic`] without merging them into an enum
|
||||
as variants.
|
||||
|
||||
This section outlines a number of possible solutions to work with Rust enums.
|
||||
|
||||
|
||||
Simulate an Enum API
|
||||
--------------------
|
||||
|
||||
A [plugin module] is extremely handy in creating an entire API for a custom enum type.
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```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;
|
||||
```
|
||||
|
||||
~~~admonish tip.small "Tip: Use a macro"
|
||||
|
||||
For enums containing only variants with no inner data, it is convenient to use a simple macro to create
|
||||
such a [plugin module].
|
||||
|
||||
```rust
|
||||
// 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:
|
||||
|
||||
```c , no_run
|
||||
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:
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```c , no_run
|
||||
// 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.
|
148
rhai_engine/rhaibook/patterns/events-1.md
Normal file
148
rhai_engine/rhaibook/patterns/events-1.md
Normal file
@@ -0,0 +1,148 @@
|
||||
Scriptable Event Handler with State<br/>Main Style
|
||||
==================================================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish example
|
||||
|
||||
A runnable example of this implementation is included.
|
||||
|
||||
See the [_Examples_]({{rootUrl}}/start/examples/rust.md) 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.
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```js
|
||||
/// 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.
|
||||
|
||||
```rust
|
||||
// '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
|
||||
}
|
||||
|
||||
:
|
||||
:
|
||||
}
|
||||
```
|
190
rhai_engine/rhaibook/patterns/events-2.md
Normal file
190
rhai_engine/rhaibook/patterns/events-2.md
Normal file
@@ -0,0 +1,190 @@
|
||||
Scriptable Event Handler with State<br/>JS Style
|
||||
================================================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish example
|
||||
|
||||
A runnable example of this implementation is included.
|
||||
|
||||
See the [_Examples_]({{rootUrl}}/start/examples/rust.md) 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`].
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```rust
|
||||
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`.
|
||||
|
||||
```rust
|
||||
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
|
||||
-----------------------
|
||||
|
||||
```admonish note.side "No shadowing"
|
||||
|
||||
Notice that `this` can never be [shadowed][shadowing] because it is not a valid [variable] name.
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
```js
|
||||
/// 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);
|
||||
}
|
||||
```
|
163
rhai_engine/rhaibook/patterns/events-3.md
Normal file
163
rhai_engine/rhaibook/patterns/events-3.md
Normal file
@@ -0,0 +1,163 @@
|
||||
Scriptable Event Handler with State<br/>Map Style
|
||||
=================================================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish example
|
||||
|
||||
A runnable example of this implementation is included.
|
||||
|
||||
See the [_Examples_]({{rootUrl}}/start/examples/rust.md) 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_](events-2.md), a slight modification of the
|
||||
[_Main Style_](events-1.md) 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_](events-2.md)), you'd write `state.variable_name` instead.
|
||||
|
||||
It is up to you to decide whether this is an improvement!
|
||||
|
||||
|
||||
Handler Initialization
|
||||
----------------------
|
||||
|
||||
```admonish note.side "No shadowing 'state'"
|
||||
|
||||
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_](events-1.md), 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].
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```js
|
||||
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
|
||||
|
||||
```js
|
||||
/// 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);
|
||||
}
|
||||
```
|
232
rhai_engine/rhaibook/patterns/events.md
Normal file
232
rhai_engine/rhaibook/patterns/events.md
Normal file
@@ -0,0 +1,232 @@
|
||||
Scriptable Event Handler with State
|
||||
===================================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish tip "IMPORTANT PATTERN"
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
```admonish example "Examples"
|
||||
|
||||
Because of the importance of this pattern, runnable examples are included.
|
||||
|
||||
See the [_Examples_](../start/examples/rust.md) section for details.
|
||||
```
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* A system sends _events_ that must be handled.
|
||||
|
||||
* Flexibility in event handling must be provided, through user-side scripting.
|
||||
|
||||
* State must be kept between invocations of event handlers.
|
||||
|
||||
* State may be provided by the system or the user, or both.
|
||||
|
||||
* Default implementations of event handlers can be provided.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* An _event handler_ object is declared that holds the following items:
|
||||
* [`Engine`] with registered functions serving as an API,
|
||||
* [`AST`] of the user script,
|
||||
* [`Scope`] containing system-provided default state.
|
||||
|
||||
* User-provided state is initialized by a [function] called via [`Engine::call_fn_with_options`][`call_fn`].
|
||||
|
||||
* Upon an event, the appropriate event handler [function] in the script is called via
|
||||
[`Engine::call_fn`][`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_](parallel.md) for more details.
|
||||
|
||||
```rust
|
||||
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].
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
1. Register an API with the [`Engine`],
|
||||
2. Create a custom [`Scope`] to serve as the stored state,
|
||||
3. Add default state [variables] into the custom [`Scope`],
|
||||
4. Optionally, call an initiation [function] to create new state [variables];
|
||||
`Engine::call_fn_with_options` is used instead of `Engine::call_fn` so that [variables] created
|
||||
inside the [function] will not be removed from the custom [`Scope`] upon exit,
|
||||
5. Get the handler script and [compile][`AST`] it,
|
||||
6. Store the compiled [`AST`] for future evaluations,
|
||||
7. Run the [`AST`] to initialize event handler state [variables].
|
||||
|
||||
```rust
|
||||
impl Handler {
|
||||
// Create a new 'Handler'.
|
||||
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register custom types and APIs
|
||||
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`.
|
||||
|
||||
```rust
|
||||
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](events-1.md) | [JS style](events-2.md) | [Map style](events-3.md) |
|
||||
| ------------------------------------------------- | :---------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------: | :--------------------------------------------------------------------------: |
|
||||
| States store | custom [`Scope`] | [object map] bound to `this` | [object map] in custom [`Scope`] |
|
||||
| Access state [variable] | normal [variable] | [property][object map] of `this` | [property][object map] 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 | <ul><li>no new variables in [functions] (apart from `init`)</li><li>easy variable name collisions</li></ul> | <ul><li>`this.xxx` all over the place</li><li>more complex implementation</li></ul> | <ul><li>`state.xxx` all over the place</li><li>inconsistent syntax</li></ul> |
|
155
rhai_engine/rhaibook/patterns/global-mutable-state.md
Normal file
155
rhai_engine/rhaibook/patterns/global-mutable-state.md
Normal file
@@ -0,0 +1,155 @@
|
||||
Mutable Global State
|
||||
====================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
Don't Do It™
|
||||
------------
|
||||
|
||||
```admonish question.side "Consider JavaScript"
|
||||
|
||||
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](constants.md), _mutable_ global states are **strongly
|
||||
discouraged** because:
|
||||
|
||||
1) It is a sure-fire way to create race conditions – that is why Rust does not support it;
|
||||
|
||||
2) It adds considerably to debug complexity – it is difficult to reason, in large code bases,
|
||||
where/when a state value is being modified;
|
||||
|
||||
3) It forces hard (but obscure) dependencies between separate pieces of code that are difficult to
|
||||
break when the need arises;
|
||||
|
||||
4) It is almost impossible to add new layers of redirection and/or abstraction afterwards without
|
||||
major surgery.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
```rust
|
||||
// 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();
|
||||
```
|
||||
|
||||
```admonish question.small "Why is this better?"
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
```admonish danger.small "I don't care! I want it! Just tell me how to do it! Now!"
|
||||
|
||||
This is not something that Rhai encourages. _You Have Been Warned™_.
|
||||
|
||||
There are two ways...
|
||||
```
|
||||
|
||||
|
||||
Option 1 – Get/Set Functions
|
||||
----------------------------------
|
||||
|
||||
This is similar to the [Control Layer](control.md) pattern.
|
||||
|
||||
Use get/set functions to read/write the global mutable state.
|
||||
|
||||
```rust
|
||||
// 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.
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```rust
|
||||
// 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.
|
||||
|
||||
```rust
|
||||
fn foo() {
|
||||
value = value * 2;
|
||||
// ^ global variable can be read
|
||||
// ^ global variable can also be modified
|
||||
}
|
||||
```
|
||||
|
||||
```admonish danger.small "Anti-Pattern"
|
||||
|
||||
This option makes mutable global state so easy to implement that it should actually be
|
||||
considered an _Anti-Pattern_.
|
||||
```
|
107
rhai_engine/rhaibook/patterns/hot-reload.md
Normal file
107
rhai_engine/rhaibook/patterns/hot-reload.md
Normal file
@@ -0,0 +1,107 @@
|
||||
Hot Reloading
|
||||
=============
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* A system where scripts are used for behavioral control.
|
||||
|
||||
* All or parts of the control scripts need to be modified dynamically without re-initializing the
|
||||
host system.
|
||||
|
||||
* New scripts must be active as soon as possible after modifications are detected.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* The Rhai [`Engine`] is _re-entrant_, meaning that it is decoupled from scripts.
|
||||
|
||||
* A new script only needs to be recompiled and the new [`AST`] replaces the old for new behaviors
|
||||
to be active.
|
||||
|
||||
* Surgically _patch_ scripts when only parts of the scripts are modified.
|
||||
```
|
||||
|
||||
|
||||
Implementation
|
||||
--------------
|
||||
|
||||
### Embed scripting engine and script into system
|
||||
|
||||
Say, a system has a Rhai [`Engine`] plus a compiled script (in [`AST`] form), with the [`AST`] kept
|
||||
with interior mutability...
|
||||
|
||||
```rust
|
||||
// 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.
|
||||
|
||||
```rust
|
||||
// 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].
|
||||
|
||||
```rust
|
||||
// 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;
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
```admonish tip.small "Tip: Multi-threaded considerations"
|
||||
|
||||
For a multi-threaded environments, replace `Rc` with `Arc`, `RefCell` with `RwLock` or `Mutex`, and
|
||||
turn on the [`sync`] feature.
|
||||
```
|
119
rhai_engine/rhaibook/patterns/macros.md
Normal file
119
rhai_engine/rhaibook/patterns/macros.md
Normal file
@@ -0,0 +1,119 @@
|
||||
Simulate Macros to Simplify Scripts
|
||||
===================================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* Scripts need to access existing data in [variables].
|
||||
|
||||
* The particular fields to access correspond to long/complex expressions (e.g. long
|
||||
[indexing][indexers] and/or [property][getters/setters] 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].
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* Pick a _macro_ syntax that is unlikely to conflict with content in literal [strings].
|
||||
|
||||
* Before script evaluation/compilation, globally replace macros with their corresponding expansions.
|
||||
```
|
||||
|
||||
|
||||
Pick a Macro Syntax
|
||||
-------------------
|
||||
|
||||
```admonish danger.side "Warning: Not real macros"
|
||||
|
||||
The technique described here is to _simulate_ macros.
|
||||
They are not _REAL_ macros.
|
||||
```
|
||||
|
||||
Pick a syntax that is intuitive for the domain but **unlikely to occur naturally inside
|
||||
[string] literals**.
|
||||
|
||||
| Sample Syntax | Sample usage |
|
||||
| ------------- | ------------------ |
|
||||
| `#FOO` | `#FOO = 42;` |
|
||||
| `$Bar` | `$Bar.work();` |
|
||||
| `<Baz>` | `print(<Baz>);` |
|
||||
| `#HELLO#` | `let x = #HELLO#;` |
|
||||
| `%HEY%` | `%HEY% += 1;` |
|
||||
|
||||
~~~admonish tip.small "Tip: Avoid normal words"
|
||||
|
||||
Avoid normal syntax that may show up inside a [string] literal.
|
||||
|
||||
For example, if using `Target` as a macro:
|
||||
|
||||
```rust
|
||||
// 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
|
||||
---------------------
|
||||
|
||||
```rust
|
||||
// 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)?;
|
||||
```
|
||||
|
||||
~~~admonish example.small "Example script"
|
||||
|
||||
```js
|
||||
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() })`);
|
||||
```
|
||||
~~~
|
||||
|
||||
```admonish bug.small "Character positions"
|
||||
|
||||
After macro expansion, the _character positions_ of different script
|
||||
elements will be shifted based on the length of the expanded text.
|
||||
|
||||
Therefore, error positions may no longer point to the correct locations
|
||||
in the original, unexpanded scripts.
|
||||
|
||||
Line numbers are not affected.
|
||||
```
|
136
rhai_engine/rhaibook/patterns/multi-layer.md
Normal file
136
rhai_engine/rhaibook/patterns/multi-layer.md
Normal file
@@ -0,0 +1,136 @@
|
||||
Multi-Layered Functions
|
||||
=======================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* A system is divided into separate _layers_, each providing logic in terms of scripted [functions].
|
||||
|
||||
* A lower layer provides _default implementations_ of certain [functions].
|
||||
|
||||
* Higher layers each provide progressively more specific implementations of the same [functions].
|
||||
|
||||
* A more specific [function], if defined in a higher layer, always overrides the implementation
|
||||
in a lower layer.
|
||||
|
||||
* This is akin to object-oriented programming but with [functions].
|
||||
|
||||
* This type of system is extremely convenient for dynamic business rules configuration, setting
|
||||
corporate-wide policies, granting permissions for specific roles etc. where specific, local rules
|
||||
need to override corporate-wide defaults.
|
||||
```
|
||||
|
||||
```admonish tip "Practical scenario"
|
||||
|
||||
Assuming a LOB (line-of-business) system for a large MNC (multi-national corporation) with branches,
|
||||
facilities and offices across the globe.
|
||||
|
||||
The system needs to provide basic, corporate-wide policies to be enforced through the worldwide
|
||||
organization, but also cater for country- or region-specific rules, practices and regulations.
|
||||
|
||||
| Layer | Description |
|
||||
| :---------: | --------------------------------------------------- |
|
||||
| `corporate` | corporate-wide policies |
|
||||
| `regional` | regional policy overrides |
|
||||
| `country` | country-specific modifications for legal compliance |
|
||||
| `office` | special treatments for individual office locations |
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* Each layer is a separate script.
|
||||
|
||||
* The lowest layer script is compiled into a base [`AST`].
|
||||
|
||||
* Higher layer scripts are also compiled into [`AST`] and _combined_ into the base using
|
||||
[`AST::combine`]({{rootUrl}}/engine/ast.md) (or the `+=` operator), overriding any existing [functions].
|
||||
```
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Assume the following four scripts, one for each layer:
|
||||
|
||||
```rust
|
||||
┌────────────────┐
|
||||
│ 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:
|
||||
|
||||
```rust
|
||||
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'
|
||||
```
|
||||
|
||||
```admonish failure.small "No super call"
|
||||
|
||||
Unfortunately, there is no `super` call that calls the base implementation (i.e. no way for a
|
||||
higher-layer function to call an equivalent lower-layer function).
|
||||
```
|
116
rhai_engine/rhaibook/patterns/multi-threading.md
Normal file
116
rhai_engine/rhaibook/patterns/multi-threading.md
Normal file
@@ -0,0 +1,116 @@
|
||||
Multi-Threaded Synchronization
|
||||
==============================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenarios"
|
||||
|
||||
* A system needs to communicate with an [`Engine`] running in a separate thread.
|
||||
|
||||
* Multiple [`Engine`]s running in separate threads need to coordinate/synchronize with each other.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* An MPSC channel (or any other appropriate synchronization primitive) is used to send/receive
|
||||
messages to/from an [`Engine`] running in a separate thread.
|
||||
|
||||
* An API is registered with the [`Engine`] that is essentially _blocking_ until synchronization is achieved.
|
||||
```
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
|
||||
```admonish note "Async"
|
||||
|
||||
The example above highlights the fact that Rhai scripts can call any Rust function, including ones
|
||||
that are _blocking_.
|
||||
|
||||
However, Rhai is essentially a blocking, single-threaded engine. Therefore it does _not_ provide an
|
||||
async API.
|
||||
|
||||
That means, although it is simple to use Rhai within a multi-threading environment where blocking a
|
||||
thread is acceptable or even expected, it is currently not possible to call _async_ functions within
|
||||
Rhai scripts because there is no mechanism in [`Engine`] to wrap the state of the call stack inside
|
||||
a future.
|
||||
|
||||
Fortunately an [`Engine`] is re-entrant so it can be shared among many async tasks. It is usually
|
||||
possible to split a script into multiple parts to avoid having to call async functions.
|
||||
|
||||
Creating an [`Engine`] is also relatively cheap (extremely cheap if creating a [raw `Engine`]),
|
||||
so it is also a valid pattern to spawn a new [`Engine`] instance for each task.
|
||||
```
|
95
rhai_engine/rhaibook/patterns/multiple.md
Normal file
95
rhai_engine/rhaibook/patterns/multiple.md
Normal file
@@ -0,0 +1,95 @@
|
||||
Multiple Instantiation
|
||||
======================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
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.
|
||||
|
||||
```admonish question "A practical illustration"
|
||||
|
||||
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`](https://crates.io/crates/rhai/) – The official crate.
|
||||
|
||||
* Different releases from [`GitHub`](https://github.com/rhaiscript/rhai) – Crate source on GitHub.
|
||||
|
||||
* Forked copy of [https://github.com/rhaiscript/rhai](https://github.com/rhaiscript/rhai) on GitHub.
|
||||
|
||||
* Local copy of [https://github.com/rhaiscript/rhai](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:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
rhai = { version = "{{version}}", 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.
|
||||
|
||||
|
||||
```admonish danger.small "No way To avoid dependency conflicts"
|
||||
|
||||
Unfortunately, pulling in Rhai from different sources do not resolve the problem of [features]
|
||||
conflict between dependencies. Even overriding `crates.io` via the `[patch]` manifest section
|
||||
doesn't work – all dependencies will eventually find the only one copy.
|
||||
|
||||
What is necessary – multiple copies of Rhai, one for each dependent crate that requires it,
|
||||
together with their _unique_ [features] set intact. In other words, turning off Cargo's crate
|
||||
merging feature _just for Rhai_.
|
||||
|
||||
Unfortunately, as of this writing, there is no known method to achieve it.
|
||||
|
||||
Therefore, moral of the story: avoid pulling in multiple crates that depend on Rhai.
|
||||
```
|
84
rhai_engine/rhaibook/patterns/objects.md
Normal file
84
rhai_engine/rhaibook/patterns/objects.md
Normal file
@@ -0,0 +1,84 @@
|
||||
Objects with Defined Behaviors
|
||||
==============================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* To simulate specific _object types_ coded in Rust with fields and methods.
|
||||
|
||||
* Native Rust methods (not scripted) are pre-defined for these types.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* Use an [object map] as the main container of the type.
|
||||
|
||||
* Create [function pointers] binding to _native Rust functions_ and store them as properties
|
||||
of the [object map].
|
||||
```
|
||||
|
||||
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 via [object maps].
|
||||
|
||||
When using [object maps] to simulate objects (See [here](oop.md) for more details), a property that
|
||||
holds a [function pointer] can be called like a method function with the [object map] being the
|
||||
object of the method call.
|
||||
|
||||
It is also possible to create [function pointers] that bind to native Rust functions/closures so
|
||||
that those are called when the [function pointers] are called.
|
||||
|
||||
|
||||
Implementation
|
||||
--------------
|
||||
|
||||
A [function pointer] can be created that binds to a specific Rust function or closure.
|
||||
|
||||
(See [here]({{rootUrl}}/language/fn-ptr.md#bind-to-a-native-rust-function) for details on using
|
||||
`FnPtr::from_fn` and `FnPtr::from_dyn_fn`).
|
||||
|
||||
```rust
|
||||
// This is the pre-defined behavior for the 'Awesome' object type.
|
||||
fn my_awesome_fn(ctx: NativeCallContext, args: &mut[&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
// Check number of arguments
|
||||
if args.len() != 2 {
|
||||
return Err("one argument is required, plus the object".into());
|
||||
}
|
||||
|
||||
// Get call arguments
|
||||
let x = args[1].try_cast::<i64>().map_err(|_| "argument must be an integer".into())?;
|
||||
|
||||
// Get mutable reference to the object map, which is passed as the first argument
|
||||
let map = &mut *args[0].as_map_mut().map_err(|_| "object must be a map".into())?;
|
||||
|
||||
// Do something awesome here ...
|
||||
let result = ...
|
||||
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
// Register a function to create a pre-defined object
|
||||
engine.register_fn("create_awesome_object", || {
|
||||
// Use an object map as base
|
||||
let mut map = Map::new();
|
||||
|
||||
// Create a function pointer that binds to 'my_awesome_fn'
|
||||
let fp = FnPtr::from_fn("awesome", my_awesome_fn)?;
|
||||
// ^ name of method
|
||||
// ^ native function
|
||||
|
||||
// Store the function pointer in the object map
|
||||
map.insert("awesome".into(), fp.into());
|
||||
|
||||
Ok(Dynamic::from_map(map))
|
||||
});
|
||||
```
|
||||
|
||||
The [object map] can then be used just like any object-oriented object.
|
||||
|
||||
```rust
|
||||
let obj = create_awesome_object();
|
||||
|
||||
let result = obj.awesome(42); // calls 'my_awesome_fn' with '42' as argument
|
||||
```
|
196
rhai_engine/rhaibook/patterns/oop.md
Normal file
196
rhai_engine/rhaibook/patterns/oop.md
Normal file
@@ -0,0 +1,196 @@
|
||||
Object-Oriented Programming (OOP)
|
||||
=================================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
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.
|
||||
|
||||
```admonish question.small "To OOP or not to OOP, that is the question."
|
||||
|
||||
Regardless of whether object-oriented programming (OOP) should be treated as a pattern or
|
||||
an _anti-pattern_ (the programming world is split 50-50 on this), there are always users who
|
||||
would like to write Rhai in "the OOP way."
|
||||
|
||||
Rust itself is not object-oriented in the traditional sense; JavaScript also isn't, but that didn't
|
||||
prevent generations of programmers trying to shoehorn a class-based inheritance system onto it.
|
||||
|
||||
So... as soon as Rhai gained in usage, way way before version 1.0, PR's started coming in to make
|
||||
it possible to write Rhai in "the OOP way."
|
||||
```
|
||||
|
||||
|
||||
Use Object Maps to Simulate OOP
|
||||
-------------------------------
|
||||
|
||||
Rhai's [object maps] has [special support for OOP]({{rootUrl}}/language/object-maps-oop.md).
|
||||
|
||||
| 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.
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
```rust
|
||||
// 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.
|
||||
|
||||
```rust
|
||||
// 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'
|
||||
```
|
86
rhai_engine/rhaibook/patterns/parallel.md
Normal file
86
rhai_engine/rhaibook/patterns/parallel.md
Normal file
@@ -0,0 +1,86 @@
|
||||
One `Engine` Instance Per Call
|
||||
==============================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* A system where scripts are called a _lot_, in tight loops or in parallel.
|
||||
|
||||
* Keeping a global [`Engine`] instance is sub-optimal due to contention and locking.
|
||||
|
||||
* Scripts need to be executed independently from each other, perhaps concurrently.
|
||||
|
||||
* Scripts are used to [create Rust closures][`Func`] 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.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* Rhai's [`AST`] structure is sharable – meaning that one copy of the [`AST`] can be run on
|
||||
multiple instances of [`Engine`] simultaneously.
|
||||
|
||||
* Rhai's [packages] and [modules] are also sharable.
|
||||
|
||||
* This means that [`Engine`] instances can be _decoupled_ from the base system ([packages] and
|
||||
[modules]) as well as the scripts ([`AST`]) so they can be created very cheaply.
|
||||
```
|
||||
|
||||
|
||||
Procedure
|
||||
---------
|
||||
|
||||
* 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 a [`StandardPackage`]({{rootUrl}}/rust/packages/builtin.md).
|
||||
|
||||
* [Packages] are sharable, so using a [custom package] is _much cheaper_ than registering all the
|
||||
functions one by one.
|
||||
|
||||
* Store a global [`AST`] for use with all [`Engine`] instances.
|
||||
|
||||
* Always use `Engine::new_raw` to create a [raw `Engine`], instead of `Engine::new` which is _much_
|
||||
more expensive. A [raw `Engine`] is _extremely_ cheap to create.
|
||||
|
||||
* Register the [custom package] with the [raw `Engine`] via `Package::register_into_engine`.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
```rust
|
||||
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)?;
|
||||
}
|
||||
```
|
164
rhai_engine/rhaibook/patterns/references.md
Normal file
164
rhai_engine/rhaibook/patterns/references.md
Normal file
@@ -0,0 +1,164 @@
|
||||
Passing External References to Rhai (Unsafe)
|
||||
============================================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
[`transmute`]: https://doc.rust-lang.org/std/mem/fn.transmute.html
|
||||
[`Engine::set_default_tag`]: https://docs.rs/rhai/{{version}}/rhai/struct.Engine.html#method.set_default_tag
|
||||
|
||||
|
||||
```admonish danger "Don't Do It™"
|
||||
|
||||
As with anything `unsafe`, don't do this unless you have exhausted all possible alternatives.
|
||||
|
||||
There are usually alternatives.
|
||||
```
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* 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.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* 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 requires `unsafe` code.
|
||||
|
||||
* Use the newtype as a _handle_ for all registered API functions, [`transmute`] the integer back
|
||||
to a reference before use. This also requires `unsafe` code.
|
||||
```
|
||||
|
||||
```admonish bug "Here be dragons..."
|
||||
|
||||
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
|
||||
-------
|
||||
|
||||
```rust
|
||||
/// 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!
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
```admonish bug "Here be Many Dragons!"
|
||||
|
||||
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`][`NativeCallContext`]). 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`].
|
||||
```
|
86
rhai_engine/rhaibook/patterns/serialize-ast.md
Normal file
86
rhai_engine/rhaibook/patterns/serialize-ast.md
Normal file
@@ -0,0 +1,86 @@
|
||||
Serialize an AST
|
||||
================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
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.
|
||||
|
||||
```admonish failure.small "Don't Do This"
|
||||
|
||||
Serialize the [`AST`] into some format for storage.
|
||||
```
|
||||
|
||||
```admonish success.small "Do This"
|
||||
|
||||
Store a copy of the original script, preferably compressed.
|
||||
```
|
||||
|
||||
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!
|
||||
|
||||
```rust
|
||||
fn sum(x, y) { x + y }
|
||||
print(sum(42, 1));
|
||||
```
|
||||
|
||||
The [`AST`] would be _much_ more complicated and looks something like this:
|
||||
|
||||
```json
|
||||
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!
|
221
rhai_engine/rhaibook/patterns/singleton.md
Normal file
221
rhai_engine/rhaibook/patterns/singleton.md
Normal file
@@ -0,0 +1,221 @@
|
||||
Singleton Command Object
|
||||
========================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
|
||||
```admonish info "Usage scenario"
|
||||
|
||||
* A system provides core functionalities, but no driving logic.
|
||||
|
||||
* The driving logic must be dynamic and hot-loadable.
|
||||
|
||||
* A script is used to drive the system and provide control intelligence.
|
||||
|
||||
* The API is multiplexed, meaning that it can act on multiple system-provided entities, or
|
||||
|
||||
* The API lends itself readily to an object-oriented (OO) representation.
|
||||
```
|
||||
|
||||
```admonish abstract "Key concepts"
|
||||
|
||||
* Expose a Command type with an API. The [`no_object`] feature must not be on.
|
||||
|
||||
* Leverage [function overloading] to simplify the API design.
|
||||
|
||||
* Since Rhai is _[sand-boxed]_, it cannot mutate anything outside of its internal environment.
|
||||
To perform external actions via an API, the command object type must be wrapped in a `RefCell`
|
||||
(or `RwLock`/`Mutex` for [`sync`]) and shared to the [`Engine`].
|
||||
|
||||
* Load each command object into a custom [`Scope`] as constant variables.
|
||||
|
||||
* Control each command object in script via the constants.
|
||||
```
|
||||
|
||||
|
||||
Implementation
|
||||
--------------
|
||||
|
||||
There are two broad ways for Rhai to control an external system, both of which involve wrapping the
|
||||
system in a shared, interior-mutated object.
|
||||
|
||||
This is the other way which involves directly exposing the data structures of the external system as
|
||||
a name singleton object in the scripting space.
|
||||
|
||||
Use this when the API is complex but has a clear object structure.
|
||||
|
||||
For a relatively simple API that is action-based and not object-based, use the
|
||||
[Control Layer]({{rootUrl}}/patterns/control.md) pattern instead.
|
||||
|
||||
|
||||
### Functional API
|
||||
|
||||
Assume the following command object type:
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
pub type SharedBunny = Rc<RefCell<EnergizerBunny>>;
|
||||
```
|
||||
|
||||
or in multi-threaded environments with the [`sync`] feature, use one of the following:
|
||||
|
||||
```rust
|
||||
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][getters/setters] to operate
|
||||
on a [constant] without raising an error. Therefore, it is needed on _all_ functions.
|
||||
|
||||
```rust
|
||||
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());
|
||||
```
|
||||
|
||||
```admonish tip.small "Tip: Friendly name for custom type"
|
||||
|
||||
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
|
||||
|
||||
```rust
|
||||
let ast = engine.compile(script)?;
|
||||
```
|
||||
|
||||
### Push constant command object into custom scope and run AST
|
||||
|
||||
```rust
|
||||
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)?;
|
||||
```
|
||||
|
||||
~~~admonish tip.small "Tip: Prevent shadowing"
|
||||
|
||||
It is usually desirable to prevent [shadowing] of the singleton command object.
|
||||
|
||||
This can be easily achieved via a [variable definition filter].
|
||||
|
||||
```rust
|
||||
// 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
|
||||
|
||||
```rust
|
||||
// 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
|
||||
```
|
104
rhai_engine/rhaibook/patterns/static-hash.md
Normal file
104
rhai_engine/rhaibook/patterns/static-hash.md
Normal file
@@ -0,0 +1,104 @@
|
||||
Static Hashing
|
||||
==============
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
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.
|
||||
|
||||
```admonish abstract.small "Hashing seed"
|
||||
|
||||
A hashing seed requires four 64-bit numbers (i.e. `u64`).
|
||||
```
|
||||
|
||||
```admonish tip.small "Tip: Static hashes are consistent and predictable"
|
||||
|
||||
Under static hashing, the same function signature _always_ generate the same hash value,
|
||||
given the same seed.
|
||||
```
|
||||
|
||||
```admonish warning.small "Warning: Safety considerations"
|
||||
|
||||
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.
|
||||
|
||||
```rust
|
||||
// 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();
|
||||
```
|
||||
|
||||
```admonish danger.small "Warning: Only call once"
|
||||
|
||||
`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.
|
||||
|
||||
```admonish warning.small "Warning"
|
||||
|
||||
If a hashing seed is also set via `rhai::config::hashing::set_ahash_seed`,
|
||||
this environment variable has no effect.
|
||||
```
|
||||
|
||||
```sh
|
||||
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
|
||||
-----
|
||||
|
||||
~~~admonish question "Why can't we tell `ahash` to use a static (fixed) seed?"
|
||||
|
||||
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`](https://crates.io/crates/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.
|
||||
~~~
|
Reference in New Issue
Block a user