This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
rhaj/_archive/rhai_engine/rhaibook/patterns/events.md
2025-04-04 08:28:07 +02:00

233 lines
11 KiB
Markdown

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