Scriptable Event Handler with State
In many usage scenarios, a scripting engine is used to provide flexibility in event handling.
That means to execute certain actions in response to certain events that occur at run-time, and scripts are used to provide flexibility for coding those actions.
You’d be surprised how many applications fit under this pattern – they are all essentially event handling systems.
Because of the importance of this pattern, runnable examples are included.
See the Examples section for details.
-
A system sends events that must be handled.
-
Flexibility in event handling must be provided, through user-side scripting.
-
State must be kept between invocations of event handlers.
-
State may be provided by the system or the user, or both.
-
Default implementations of event handlers can be provided.
-
An event handler object is declared that holds the following items:
-
User-provided state is initialized by a function called via
Engine::call_fn_with_options
. -
Upon an event, the appropriate event handler function in the script is called via
Engine::call_fn
. -
Optionally, trap the
EvalAltResult::ErrorFunctionNotFound
error to provide a default implementation.
Basic Infrastructure
Declare handler object
In most cases, it would be simpler to store an Engine
instance together with the handler object
because it only requires registering all API functions only once.
In rare cases where handlers are created and destroyed in a tight loop, a new Engine
instance
can be created for each event. See One Engine Instance Per Call for more details.
use rhai::{Engine, Scope, AST};
// Event handler
struct Handler {
// Scripting engine
pub engine: Engine,
// Use a custom 'Scope' to keep stored state
pub scope: Scope<'static>,
// Program script
pub ast: AST
}
Register API for custom types
Custom types are often used to hold state. The easiest way to register an entire API is via a plugin module.
use rhai::plugin::*;
// A custom type to a hold state value.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct TestStruct {
data: i64
}
// Plugin module containing API to TestStruct
#[export_module]
mod test_struct_api {
#[rhai_fn(global)]
pub fn new_state(value: i64) -> TestStruct {
TestStruct { data: value }
}
#[rhai_fn(global)]
pub fn func1(obj: &mut TestStruct) -> bool {
:
}
#[rhai_fn(global)]
pub fn func2(obj: &mut TestStruct) -> i64 {
:
}
pub fn process(data: i64) -> i64 {
:
}
#[rhai_fn(get = "value", pure)]
pub fn get_value(obj: &mut TestStruct) -> i64 {
obj.data
}
#[rhai_fn(set = "value")]
pub fn set_value(obj: &mut TestStruct, value: i64) {
obj.data = value;
}
}
Initialize handler object
Steps to initialize the event handler:
- Register an API with the
Engine
, - Create a custom
Scope
to serve as the stored state, - Add default state variables into the custom
Scope
, - Optionally, call an initiation function to create new state variables;
Engine::call_fn_with_options
is used instead ofEngine::call_fn
so that variables created inside the function will not be removed from the customScope
upon exit, - Get the handler script and compile it,
- Store the compiled
AST
for future evaluations, - Run the
AST
to initialize event handler state variables.
impl Handler {
// Create a new 'Handler'.
pub fn new(path: impl Into<PathBuf>) -> Self {
let mut engine = Engine::new();
// Register custom types and API's
engine.register_type_with_name::<TestStruct>("TestStruct")
.register_global_module(exported_module!(test_struct_api).into());
// Create a custom 'Scope' to hold state
let mut scope = Scope::new();
// Add any system-provided state into the custom 'Scope'.
// Constants can be used to optimize the script.
scope.push_constant("MY_CONSTANT", 42_i64);
:
:
// Initialize state variables
:
:
// Compile the handler script.
// In a real application you'd be handling errors...
let ast = engine.compile_file_with_scope(&mut scope, path).unwrap();
// The event handler is essentially these three items:
Self { engine, scope, ast }
}
}
Hook up events
There is usually an interface or trait that gets called when an event comes from the system.
Mapping an event from the system into a scripted handler is straight-forward, via Engine::call_fn
.
impl Handler {
// Say there are three events: 'start', 'end', 'update'.
// In a real application you'd be handling errors...
pub fn on_event(&mut self, event_name: &str, event_data: i64) -> Dynamic {
let engine = &self.engine;
let scope = &mut self.scope;
let ast = &self.ast;
match event_name {
// The 'start' event maps to function 'start'.
// In a real application you'd be handling errors...
"start" => engine.call_fn(scope, ast, "start", (event_data,)).unwrap(),
// The 'end' event maps to function 'end'.
// In a real application you'd be handling errors...
"end" => engine.call_fn(scope, ast, "end", (event_data,)).unwrap(),
// The 'update' event maps to function 'update'.
// This event provides a default implementation when the scripted function is not found.
"update" =>
engine.call_fn(scope, ast, "update", (event_data,))
.or_else(|err| match *err {
EvalAltResult::ErrorFunctionNotFound(fn_name, _)
if fn_name.starts_with("update") =>
{
// Default implementation of 'update' event handler.
self.scope.set_value("obj_state", TestStruct::new(42));
// Turn function-not-found into a success.
Ok(Dynamic::UNIT)
}
_ => Err(err)
}).unwrap(),
// In a real application you'd be handling unknown events...
_ => panic!("unknown event: {}", event_name)
}
}
}
Scripting Styles
Depending on needs and scripting style, there are three different ways to implement this pattern.
Main style | JS style | Map style | |
---|---|---|---|
States store | custom Scope | object map bound to this | object map in custom Scope |
Access state variable | normal variable | property of this | property of state variable |
Access global constants? | yes | yes | yes |
Add new state variable? | init function only | all functions | all functions |
Add new global constants? | yes | no | no |
OOP-style functions on states? | no | yes | yes |
Detect system-provided initial states? | no | yes | yes |
Local variable may shadow state variable? | yes | no | no |
Benefits | simple | fewer surprises | versatile |
Disadvantages |
|
|
|