4.4 KiB
Scriptable Event Handler with State
Main Style
{{#include ../links.md}}
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.
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
/// 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.
// '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
}
:
:
}