11 KiB
Scriptable Event Handler with State
{{#include ../links.md}}
In many usage scenarios, a scripting engine is used to provide flexibility in event handling.
That means to execute certain **actions** in response to certain **_events_** that occur at run-time,
and scripts are used to provide flexibility for coding those actions.
You'd be surprised how many applications fit under this pattern – they are all essentially
event handling systems.
Because of the importance of this pattern, runnable examples are included.
See the [_Examples_](../start/examples/rust.md) section for details.
* A system sends _events_ that must be handled.
* Flexibility in event handling must be provided, through user-side scripting.
* State must be kept between invocations of event handlers.
* State may be provided by the system or the user, or both.
* Default implementations of event handlers can be provided.
* An _event handler_ object is declared that holds the following items:
* [`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 for more details.
use rhai::{Engine, Scope, AST};
// Event handler
struct Handler {
// Scripting engine
pub engine: Engine,
// Use a custom 'Scope' to keep stored state
pub scope: Scope<'static>,
// Program script
pub ast: AST
}
Register API for custom types
[Custom types] are often used to hold state. The easiest way to register an entire API is via a [plugin module].
use rhai::plugin::*;
// A custom type to a hold state value.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct TestStruct {
data: i64
}
// Plugin module containing API to TestStruct
#[export_module]
mod test_struct_api {
#[rhai_fn(global)]
pub fn new_state(value: i64) -> TestStruct {
TestStruct { data: value }
}
#[rhai_fn(global)]
pub fn func1(obj: &mut TestStruct) -> bool {
:
}
#[rhai_fn(global)]
pub fn func2(obj: &mut TestStruct) -> i64 {
:
}
pub fn process(data: i64) -> i64 {
:
}
#[rhai_fn(get = "value", pure)]
pub fn get_value(obj: &mut TestStruct) -> i64 {
obj.data
}
#[rhai_fn(set = "value")]
pub fn set_value(obj: &mut TestStruct, value: i64) {
obj.data = value;
}
}
Initialize handler object
Steps to initialize the event handler:
- Register an API with the [
Engine
], - Create a custom [
Scope
] to serve as the stored state, - Add default state [variables] into the custom [
Scope
], - Optionally, call an initiation [function] to create new state [variables];
Engine::call_fn_with_options
is used instead ofEngine::call_fn
so that [variables] created inside the [function] will not be removed from the custom [Scope
] upon exit, - Get the handler script and [compile][
AST
] it, - Store the compiled [
AST
] for future evaluations, - Run the [
AST
] to initialize event handler state [variables].
impl Handler {
// Create a new 'Handler'.
pub fn new(path: impl Into<PathBuf>) -> Self {
let mut engine = Engine::new();
// Register custom types and 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
.
impl Handler {
// Say there are three events: 'start', 'end', 'update'.
// In a real application you'd be handling errors...
pub fn on_event(&mut self, event_name: &str, event_data: i64) -> Dynamic {
let engine = &self.engine;
let scope = &mut self.scope;
let ast = &self.ast;
match event_name {
// The 'start' event maps to function 'start'.
// In a real application you'd be handling errors...
"start" => engine.call_fn(scope, ast, "start", (event_data,)).unwrap(),
// The 'end' event maps to function 'end'.
// In a real application you'd be handling errors...
"end" => engine.call_fn(scope, ast, "end", (event_data,)).unwrap(),
// The 'update' event maps to function 'update'.
// This event provides a default implementation when the scripted function is not found.
"update" =>
engine.call_fn(scope, ast, "update", (event_data,))
.or_else(|err| match *err {
EvalAltResult::ErrorFunctionNotFound(fn_name, _)
if fn_name.starts_with("update") =>
{
// Default implementation of 'update' event handler.
self.scope.set_value("obj_state", TestStruct::new(42));
// Turn function-not-found into a success.
Ok(Dynamic::UNIT)
}
_ => Err(err)
}).unwrap(),
// In a real application you'd be handling unknown events...
_ => panic!("unknown event: {}", event_name)
}
}
}
Scripting Styles
Depending on needs and scripting style, there are three different ways to implement this pattern.
Main style | JS style | Map style | |
---|---|---|---|
States store | custom [Scope ] |
[object map] bound to this |
[object map] in custom [Scope ] |
Access state [variable] | normal [variable] | [property][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 |
|
|
|