-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[fud2] Refactor plugin script loading (#2083)
* [fud2] Make plugins optional Very minor fix: #2078 added plugins, controlled by a `plugins` key in `fud2.toml`. However, it inadvertently made this option required, i.e., fud2 would crash if this option was not present. Now it's optional! So we just silently proceed with no plugins if none are configured. * Fix a doc typo * Start refactoring plugin loading * Start refactoring builder functions * Refactor the rest of the functions * Gather up a `register` function * Use a fresh engine for each plugin?? Maybe this is wasteful, but it simplifies things a lot. * Run one script at a time * Refector script contexts * Refactor runner struct * Further beef up the runner * Further simplify interface * Fix a borrow that lasts too long * Fix a method name Clippy was thrown for a loop because of the `to_*` name. * Move plugin loading to DriverBuilder? Now the top-level thing is in the builder itself... no need for an extra trait? Not sure this is a good idea.
- Loading branch information
Showing
5 changed files
with
186 additions
and
130 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,3 @@ pub mod script; | |
pub mod utils; | ||
|
||
pub use exec::{Driver, DriverBuilder}; | ||
pub use script::LoadPlugins; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,4 @@ mod exec_scripts; | |
mod plugin; | ||
mod report; | ||
|
||
pub use plugin::LoadPlugins; | ||
pub use plugin::ScriptRunner; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,156 +1,191 @@ | ||
use crate::{ | ||
config, | ||
exec::{OpRef, SetupRef, StateRef}, | ||
DriverBuilder, | ||
}; | ||
use std::{cell::RefCell, path::PathBuf, rc::Rc}; | ||
use std::{ | ||
cell::RefCell, | ||
path::{Path, PathBuf}, | ||
rc::Rc, | ||
}; | ||
|
||
use super::{ | ||
error::RhaiSystemError, | ||
exec_scripts::{to_rhai_err, to_str_slice, RhaiResult, RhaiSetupCtx}, | ||
report::RhaiReport, | ||
}; | ||
|
||
fn to_setup_refs( | ||
ctx: &rhai::NativeCallContext, | ||
setups: rhai::Array, | ||
struct ScriptContext { | ||
builder: DriverBuilder, | ||
path: PathBuf, | ||
ast: rhai::AST, | ||
this: Rc<RefCell<DriverBuilder>>, | ||
) -> RhaiResult<Vec<SetupRef>> { | ||
setups | ||
.into_iter() | ||
.map(|s| match s.clone().try_cast::<rhai::FnPtr>() { | ||
Some(fnptr) => Ok(this.borrow_mut().add_setup( | ||
&format!("{} (plugin)", fnptr.fn_name()), | ||
RhaiSetupCtx { | ||
path: path.clone(), | ||
ast: ast.clone(), | ||
name: fnptr.fn_name().to_string(), | ||
}, | ||
)), | ||
// if we can't cast as a FnPtr, try casting as a SetupRef directly | ||
None => s.clone().try_cast::<SetupRef>().ok_or_else(move || { | ||
RhaiSystemError::setup_ref(s) | ||
.with_pos(ctx.position()) | ||
.into() | ||
}), | ||
}) | ||
.collect::<RhaiResult<Vec<_>>>() | ||
} | ||
|
||
pub trait LoadPlugins { | ||
fn load_plugins(self) -> Self; | ||
impl ScriptContext { | ||
/// Take a Rhai array value that is supposed to contain setups and produce | ||
/// an array of actual references to setups. The array might contain string names | ||
/// for the setups, or it might be function references that define those setups. | ||
fn setups_array( | ||
&mut self, | ||
ctx: &rhai::NativeCallContext, | ||
setups: rhai::Array, | ||
) -> RhaiResult<Vec<SetupRef>> { | ||
setups | ||
.into_iter() | ||
.map(|s| match s.clone().try_cast::<rhai::FnPtr>() { | ||
Some(fnptr) => { | ||
// TODO: Do we really need to clone stuff here, or can we continue to thread through | ||
// the `Rc`? | ||
let rctx = RhaiSetupCtx { | ||
path: self.path.clone(), | ||
ast: self.ast.clone(), | ||
name: fnptr.fn_name().to_string(), | ||
}; | ||
Ok(self.builder.add_setup( | ||
&format!("{} (plugin)", fnptr.fn_name()), | ||
rctx, | ||
)) | ||
} | ||
// if we can't cast as a FnPtr, try casting as a SetupRef directly | ||
None => { | ||
s.clone().try_cast::<SetupRef>().ok_or_else(move || { | ||
RhaiSystemError::setup_ref(s) | ||
.with_pos(ctx.position()) | ||
.into() | ||
}) | ||
} | ||
}) | ||
.collect::<RhaiResult<Vec<_>>>() | ||
} | ||
} | ||
|
||
impl LoadPlugins for DriverBuilder { | ||
fn load_plugins(self) -> Self { | ||
// get list of plugins | ||
let config = config::load_config(&self.name); | ||
let plugin_files = match config.extract_inner::<Vec<PathBuf>>("plugins") | ||
{ | ||
Ok(v) => v, | ||
Err(_) => { | ||
// No plugins to load. | ||
return self; | ||
} | ||
}; | ||
pub struct ScriptRunner { | ||
ctx: Rc<RefCell<ScriptContext>>, | ||
engine: rhai::Engine, | ||
} | ||
|
||
// wrap driver in a ref cell, so that we can call it from a | ||
// Rhai context | ||
let this = Rc::new(RefCell::new(self)); | ||
impl ScriptRunner { | ||
fn from_file(builder: DriverBuilder, path: &Path) -> Self { | ||
// Compile the script's source code. | ||
let engine = rhai::Engine::new(); | ||
let ast = engine.compile_file(path.into()).unwrap(); | ||
|
||
// scope rhai engine code so that all references to `this` | ||
// are dropped before the end of the function | ||
{ | ||
let mut engine = rhai::Engine::new(); | ||
// TODO: Consider removing the clones here. We can probably just recover the stuff. | ||
Self { | ||
ctx: Rc::new(RefCell::new(ScriptContext { | ||
builder, | ||
path: path.into(), | ||
ast: ast.clone(), | ||
})), | ||
engine, | ||
} | ||
} | ||
|
||
/// Obtain the wrapped `DriverBuilder`. Panic if other references (from the | ||
/// script, for example) still exist. | ||
fn unwrap_builder(self) -> DriverBuilder { | ||
std::mem::drop(self.engine); // Drop references to the context. | ||
Rc::into_inner(self.ctx) | ||
.expect("script references still live") | ||
.into_inner() | ||
.builder | ||
} | ||
|
||
// register AST independent functions | ||
let t = this.clone(); | ||
engine.register_fn( | ||
"state", | ||
move |name: &str, extensions: rhai::Array| { | ||
let v = to_str_slice(&extensions); | ||
let v = v.iter().map(|x| &**x).collect::<Vec<_>>(); | ||
t.borrow_mut().state(name, &v) | ||
}, | ||
); | ||
fn reg_state(&mut self) { | ||
let sctx = self.ctx.clone(); | ||
self.engine.register_fn( | ||
"state", | ||
move |name: &str, extensions: rhai::Array| { | ||
let v = to_str_slice(&extensions); | ||
let v = v.iter().map(|x| &**x).collect::<Vec<_>>(); | ||
sctx.borrow_mut().builder.state(name, &v) | ||
}, | ||
); | ||
} | ||
|
||
let t = Rc::clone(&this); | ||
engine.register_fn("get_state", move |state_name: &str| { | ||
t.borrow().find_state(state_name).map_err(to_rhai_err) | ||
fn reg_get_state(&mut self) { | ||
let sctx = self.ctx.clone(); | ||
self.engine | ||
.register_fn("get_state", move |state_name: &str| { | ||
sctx.borrow() | ||
.builder | ||
.find_state(state_name) | ||
.map_err(to_rhai_err) | ||
}); | ||
} | ||
|
||
let t = Rc::clone(&this); | ||
engine.register_fn("get_setup", move |setup_name: &str| { | ||
t.borrow().find_setup(setup_name).map_err(to_rhai_err) | ||
fn reg_get_setup(&mut self) { | ||
let sctx = self.ctx.clone(); | ||
self.engine | ||
.register_fn("get_setup", move |setup_name: &str| { | ||
sctx.borrow() | ||
.builder | ||
.find_setup(setup_name) | ||
.map_err(to_rhai_err) | ||
}); | ||
} | ||
|
||
// go through each plugin file, and execute the script which adds a plugin | ||
// we need to define the following two functions in the loop because they | ||
// need the ast of the current file | ||
for path in plugin_files { | ||
// compile the file into an Ast | ||
let ast = engine.compile_file(path.clone()).unwrap(); | ||
fn reg_rule(&mut self) { | ||
let sctx = self.ctx.clone(); | ||
self.engine.register_fn::<_, 4, true, OpRef, true, _>( | ||
"rule", | ||
move |ctx: rhai::NativeCallContext, | ||
setups: rhai::Array, | ||
input: StateRef, | ||
output: StateRef, | ||
rule_name: &str| { | ||
let mut sctx = sctx.borrow_mut(); | ||
let setups = sctx.setups_array(&ctx, setups)?; | ||
Ok(sctx.builder.rule(&setups, input, output, rule_name)) | ||
}, | ||
); | ||
} | ||
|
||
let t = Rc::clone(&this); | ||
let rule_ast = ast.clone_functions_only(); | ||
let path_copy = path.clone(); | ||
engine.register_fn::<_, 4, true, OpRef, true, _>( | ||
"rule", | ||
move |ctx: rhai::NativeCallContext, | ||
setups: rhai::Array, | ||
input: StateRef, | ||
output: StateRef, | ||
rule_name: &str| { | ||
let setups = to_setup_refs( | ||
&ctx, | ||
setups, | ||
path_copy.clone(), | ||
rule_ast.clone(), | ||
Rc::clone(&t), | ||
)?; | ||
Ok(t.borrow_mut() | ||
.rule(&setups, input, output, rule_name)) | ||
}, | ||
); | ||
fn reg_op(&mut self) { | ||
let sctx = self.ctx.clone(); | ||
self.engine.register_fn::<_, 5, true, OpRef, true, _>( | ||
"op", | ||
move |ctx: rhai::NativeCallContext, | ||
name: &str, | ||
setups: rhai::Array, | ||
input: StateRef, | ||
output: StateRef, | ||
build: rhai::FnPtr| { | ||
let mut sctx = sctx.borrow_mut(); | ||
let setups = sctx.setups_array(&ctx, setups)?; | ||
let rctx = RhaiSetupCtx { | ||
path: sctx.path.clone(), | ||
ast: sctx.ast.clone(), | ||
name: build.fn_name().to_string(), | ||
}; | ||
Ok(sctx.builder.add_op(name, &setups, input, output, rctx)) | ||
}, | ||
); | ||
} | ||
|
||
let t = Rc::clone(&this); | ||
let rule_ast = ast.clone_functions_only(); | ||
let path_copy = path.clone(); | ||
engine.register_fn::<_, 5, true, OpRef, true, _>( | ||
"op", | ||
move |ctx: rhai::NativeCallContext, | ||
name: &str, | ||
setups: rhai::Array, | ||
input: StateRef, | ||
output: StateRef, | ||
build: rhai::FnPtr| { | ||
let setups = to_setup_refs( | ||
&ctx, | ||
setups, | ||
path_copy.clone(), | ||
rule_ast.clone(), | ||
Rc::clone(&t), | ||
)?; | ||
Ok(t.borrow_mut().add_op( | ||
name, | ||
&setups, | ||
input, | ||
output, | ||
RhaiSetupCtx { | ||
path: path_copy.clone(), | ||
ast: rule_ast.clone(), | ||
name: build.fn_name().to_string(), | ||
}, | ||
)) | ||
}, | ||
); | ||
/// Register all the builder functions in the engine. | ||
fn register(&mut self) { | ||
self.reg_state(); | ||
self.reg_get_state(); | ||
self.reg_get_setup(); | ||
self.reg_rule(); | ||
self.reg_op(); | ||
} | ||
|
||
engine.run_ast(&ast).report(&path); | ||
} | ||
} | ||
/// Run the script. | ||
fn run(&self) { | ||
// TODO this whole dance feels unnecessary... | ||
let (ast, path) = { | ||
let sctx = self.ctx.borrow(); | ||
(sctx.ast.clone(), sctx.path.clone()) | ||
}; | ||
self.engine.run_ast(&ast).report(path); | ||
} | ||
|
||
Rc::into_inner(this).expect("Back into inner").into_inner() | ||
/// Execute a script from a file, adding to the builder. | ||
pub fn run_file(builder: DriverBuilder, path: &Path) -> DriverBuilder { | ||
let mut runner = ScriptRunner::from_file(builder, path); | ||
runner.register(); | ||
runner.run(); | ||
runner.unwrap_builder() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters