Scriptable Event Handler with State
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.
-
Default implementations of event handlers can be provided.
Key Concepts
-
An event handler object is declared that holds the following items:
-
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.
Implementation
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.
#![allow(unused)] fn main() { use rhai::{Engine, Scope, AST, EvalAltResult}; // 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 Any Custom Type
Custom types are often used to hold state. The easiest way to register an entire API is via a plugin module.
#![allow(unused)] fn main() { use rhai::plugin::*; // A custom type to a hold state value. #[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] pub struct SomeType { data: i64; } #[export_module] mod SomeTypeAPI { #[rhai_fn(global)] pub func1(obj: &mut SomeType) -> bool { ... } #[rhai_fn(global)] pub func2(obj: &mut SomeType) -> bool { ... } pub process(data: i64) -> i64 { ... } #[rhai_fn(get = "value")] pub get_value(obj: &mut SomeType) -> i64 { obj.data } #[rhai_fn(set = "value")] pub set_value(obj: &mut SomeType, 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
, - Get the handler script and compile it,
- Store the compiled
AST
for future evaluations, - Run the
AST
to initialize event handler state variables.
#![allow(unused)] fn main() { impl Handler { pub new(path: impl Into<PathBuf>) -> Self { let mut engine = Engine::new(); // Register custom types and API's engine .register_type_with_name::<SomeType>("SomeType") .register_global_module(exported_module!(SomeTypeAPI)); // Create a custom 'Scope' to hold state let mut scope = Scope::new(); // Add initialized state into the custom 'Scope' scope.push("state1", false); scope.push("state2", SomeType::new(42)); // Compile the handler script. // In a real application you'd be handling errors... let ast = engine.compile_file(path).unwrap(); // Evaluate the script to initialize it and other state variables. // In a real application you'd again be handling errors... engine.consume_ast_with_scope(&mut scope, &ast).unwrap(); // The event handler is essentially these three items: Handler { 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:
#![allow(unused)] fn main() { 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) -> Result<(), Error> { 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,))?, // 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,))?, // 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 == "update" => { // Default implementation of 'update' event handler self.scope.set_value("state2", SomeType::new(42)); // Turn function-not-found into a success Ok(Dynamic::UNIT) } _ => Err(err.into()) })? } } } }
Sample Handler Script
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.
#![allow(unused)] fn main() { fn start(data) { if state1 { throw "Already started!"; } if state2.func1() || state2.func2() { throw "Conditions not yet ready to start!"; } state1 = true; state2.value = data; } fn end(data) { if !state1 { throw "Not yet started!"; } if state2.func1() || state2.func2() { throw "Conditions not yet ready to start!"; } state1 = false; state2.value = data; } fn update(data) { state2.value += process(data); } }