Add Engine::load_module.

This commit is contained in:
Stephen Chung 2020-11-15 23:14:16 +08:00
parent fbe9425794
commit 937b45a187
6 changed files with 251 additions and 34 deletions

View File

@ -9,48 +9,143 @@ it is impossible (short of registering a complete API) to distinguish between in
enum variants or to extract internal data from them. enum variants or to extract internal data from them.
Switch Through Arrays Simulate an Enum API
--------------------- --------------------
An easy way to work with Rust enums is through exposing the internal data of each enum variant
as an [array], usually with the name of the variant as the first item:
```rust ```rust
use rhai::{Engine, Array}; use rhai::{Engine, RegisterFn, Dynamic, EvalAltResult};
use rhai::plugin::*;
#[derive(Debug, Clone)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum MyEnum { enum MyEnum {
Foo, Foo,
Bar(i64), Bar(i64),
Baz(String, bool) Baz(String, bool)
} }
impl MyEnum { // Create a plugin module with functions constructing the 'MyEnum' variants
fn get_enum_data(&mut self) -> Array { #[export_module]
match self { pub mod MyEnumModule {
Self::Foo => vec![ // 'MyEnum' variants
"Foo".into() pub const Foo: &MyEnum = MyEnum::Foo;
] as Array, pub fn Bar(value: i64) -> MyEnum { MyEnum::Bar(value) }
Self::Bar(num) => vec![ pub fn Baz(val1: String, val2: bool) -> MyEnum { MyEnum::Baz(val1, val2) }
"Bar".into(), (*num).into()
] as Array,
Self::Baz(name, option) => vec![
"Baz".into(), name.clone().into(), (*option).into()
] as Array
}
}
} }
let mut engine = Engine::new();
// Register API for 'MyEnum'
engine engine
// Register enum custom type
.register_type_with_name::<MyEnum>("MyEnum") .register_type_with_name::<MyEnum>("MyEnum")
.register_get("enum_data", MyEnum::get_enum_data); // Register access to fields
.register_get("type", |a: &mut MyEnum| match a {
MyEnum::Foo => "Foo".to_string(),
MyEnum::Bar(_) => "Bar".to_string(),
MyEnum::Baz(_, _) => "Baz".to_string()
})
.register_get("field_0", |a: &mut MyEnum| match a {
MyEnum::Foo => Dynamic::UNIT,
MyEnum::Bar(x) => Dynamic::from(x),
MyEnum::Baz(x, _) => Dynamic::from(x)
})
.register_get("field_1", |a: &mut MyEnum| match a {
MyEnum::Foo | MyEnum::Bar(_) => Dynamic::UNIT,
MyEnum::Baz(_, x) => Dynamic::from(x)
})
// Register printing
.register_fn("to_string", |a: &mut MyEnum| format!("{:?}", a))
.register_fn("print", |a: &mut MyEnum| format!("{:?}", a))
.register_fn("debug", |a: &mut MyEnum| format!("{:?}", a))
.register_fn("+", |s: &str, a: MyEnum| format!("{}{:?}", s, a))
.register_fn("+", |a: &mut MyEnum, s: &str| format!("{:?}", a).push_str(s))
.register_fn("+=", |s: &mut ImmutableString, a: MyEnum| s += a.to_string())
// Register '==' and '!=' operators
.register_fn("==", |a: &mut MyEnum, b: MyEnum| a == &b)
.register_fn("!=", |a: &mut MyEnum, b: MyEnum| a != &b)
// Register array functions
.register_fn("push", |list: &mut Array, item: MyEnum| list.push(Dynamic::from(item)))
.register_fn("+=", |list: &mut Array, item: MyEnum| list.push(Dynamic::from(item)))
.register_fn("insert", |list: &mut Array, position: i64, item: MyEnum| {
if position <= 0 {
list.insert(0, Dynamic::from(item));
} else if (position as usize) >= list.len() - 1 {
list.push(item);
} else {
list.insert(position as usize, Dynamic::from(item));
}
}).register_fn("pad", |list: &mut Array, len: i64, item: MyEnum| {
if len as usize > list.len() { list.resize(len as usize, item); }
})
// Load the module as the module namespace "MyEnum"
.register_module("MyEnum", exported_module!(MyEnumModule));
```
Instead of registering all these manually, it is often convenient to wrap them up into
a [custom package] that can be loaded into any [`Engine`].
With this API in place, working with enums will be almost the same as in Rust:
```rust
let x = MyEnum::Foo;
let y = MyEnum::Bar(42);
let z = MyEnum::Baz("hello", true);
x == MyEnum::Foo;
y != MyEnum::Bar(0);
// Detect enum types
x.type == "Foo";
y.type == "Bar";
z.type == "Baz";
// Extract enum fields
y.field_0 == 42;
y.field_1 == ();
z.field_0 == "hello";
z.field_1 == true;
```
Use `switch` Through Arrays
---------------------------
Since enums are internally treated as [custom types], they are not _literals_ and cannot be
used as a match case in `switch` expressions. This is quite a limitation because the equivalent
`match` statement is commonly used in Rust to work with enums.
One way to work with Rust enums in a `switch` expression is through exposing the internal data
of each enum variant as an [array], usually with the name of the variant as the first item:
```rust
use rhai::Array;
engine.register_get("enum_data", |x: &mut Enum} {
match x {
Enum::Foo => vec!["Foo".into()] as Array,
Enum::Bar(value) => vec!["Bar".into(), (*value).into()] as Array,
Enum::Baz(val1, val2) => vec![
"Baz".into(), val1.clone().into(), (*val2).into()
] as Array
}
});
``` ```
Then it is a simple matter to match an enum via the `switch` expression: Then it is a simple matter to match an enum via the `switch` expression:
```c ```c
// Assume 'value' = 'MyEnum::Baz("hello", true)' // Assume 'value' = 'MyEnum::Baz("hello", true)'
// 'get_data' creates a variable-length array with 'MyEnum' data // 'enum_data' creates a variable-length array with 'MyEnum' data
let x = switch value.enum_data { let x = switch value.enum_data {
["Foo"] => 1, ["Foo"] => 1,
["Bar", 42] => 2, ["Bar", 42] => 2,
@ -61,4 +156,14 @@ let x = switch value.enum_data {
}; };
x == 5; x == 5;
// Which is essentially the same as:
let x = switch [value.type, value.field_0, value.field_1] {
["Foo", (), ()] => 1,
["Bar", 42, ()] => 2,
["Bar", 123, ()] => 3,
["Baz", "hello", false] => 4,
["Baz", "hello", true] => 5,
_ => 9
}
``` ```

View File

@ -123,6 +123,42 @@ x == 43;
Notice that, when using a [module] as a [package], only functions registered at the _top level_ Notice that, when using a [module] as a [package], only functions registered at the _top level_
can be accessed. Variables as well as sub-modules are ignored. can be accessed. Variables as well as sub-modules are ignored.
### Use `Engine::load_module`
Another simple way to load this into an [`Engine`] is, again, to use the `exported_module!` macro
to turn it into a normal Rhai [module], then use the `Engine::load_module` method on it:
```rust
fn main() {
let mut engine = Engine::new();
// The macro call creates a Rhai module from the plugin module.
let module = exported_module!(my_module);
// A module can simply be loaded as a globally-available module.
engine.load_module("service", module);
}
```
The functions contained within the module definition (i.e. `greet`, `get_num` and `increment`),
plus the constant `MY_NUMBER`, are automatically loaded under the module namespace `service`:
```rust
let x = service::greet("world");
x == "hello, world!";
service::MY_NUMBER == 42;
let x = service::greet(service::get_num().to_string());
x == "hello, 42!";
let x = service::get_num();
x == 42;
service::increment(x);
x == 43;
```
### Use as loadable `Module` ### Use as loadable `Module`
Using this directly as a dynamically-loadable Rhai [module] is almost the same, except that a Using this directly as a dynamically-loadable Rhai [module] is almost the same, except that a

View File

@ -25,7 +25,8 @@ Make the `Module` Available to the `Engine`
`Engine::load_package` supports loading a [module] as a [package]. `Engine::load_package` supports loading a [module] as a [package].
Since it acts as a [package], all functions will be registered into the _global_ namespace Since it acts as a [package], all functions will be registered into the _global_ namespace
and can be accessed without _module qualifiers_. and can be accessed without _namespace qualifiers_. This is by far the easiest way to expose
a module's functionalities to Rhai.
```rust ```rust
use rhai::{Engine, Module}; use rhai::{Engine, Module};
@ -41,6 +42,25 @@ engine.eval::<i64>("inc(41)")? == 42; // no need to import module
``` ```
Make the `Module` a Global Module
------------------------------------
`Engine::load_module` loads a [module] and makes it available globally under a specific namespace.
```rust
use rhai::{Engine, Module};
let mut module = Module::new(); // new module
module.set_fn_1("inc", |x: i64| Ok(x+1)); // use the 'set_fn_XXX' API to add functions
// Load the module into the Engine as a sub-module named 'calc'
let mut engine = Engine::new();
engine.load_module("calc", module);
engine.eval::<i64>("calc::inc(41)")? == 42; // refer to the 'Calc' module
```
Make the `Module` Dynamically Loadable Make the `Module` Dynamically Loadable
------------------------------------- -------------------------------------

View File

@ -596,6 +596,8 @@ pub struct Engine {
pub(crate) global_module: Module, pub(crate) global_module: Module,
/// A collection of all library packages loaded into the Engine. /// A collection of all library packages loaded into the Engine.
pub(crate) packages: PackagesCollection, pub(crate) packages: PackagesCollection,
/// A collection of all sub-modules directly loaded into the Engine.
pub(crate) global_sub_modules: Imports,
/// A module resolution service. /// A module resolution service.
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
@ -711,6 +713,7 @@ impl Engine {
packages: Default::default(), packages: Default::default(),
global_module: Default::default(), global_module: Default::default(),
global_sub_modules: Default::default(),
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_std"))] #[cfg(not(feature = "no_std"))]
@ -773,6 +776,7 @@ impl Engine {
packages: Default::default(), packages: Default::default(),
global_module: Default::default(), global_module: Default::default(),
global_sub_modules: Default::default(),
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
module_resolver: None, module_resolver: None,
@ -1814,7 +1818,7 @@ impl Engine {
Expr::True(_) => Ok(true.into()), Expr::True(_) => Ok(true.into()),
Expr::False(_) => Ok(false.into()), Expr::False(_) => Ok(false.into()),
Expr::Unit(_) => Ok(().into()), Expr::Unit(_) => Ok(Dynamic::UNIT),
Expr::Custom(custom, _) => { Expr::Custom(custom, _) => {
let expressions = custom let expressions = custom
@ -2090,7 +2094,7 @@ impl Engine {
} else if let Some(def_stmt) = def_stmt { } else if let Some(def_stmt) = def_stmt {
self.eval_stmt(scope, mods, state, lib, this_ptr, def_stmt, level) self.eval_stmt(scope, mods, state, lib, this_ptr, def_stmt, level)
} else { } else {
Ok(().into()) Ok(Dynamic::UNIT)
} }
} }

View File

@ -30,6 +30,9 @@ use crate::fn_register::{RegisterFn, RegisterResultFn};
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
use crate::{fn_args::FuncArgs, fn_call::ensure_no_data_race, module::Module, StaticVec}; use crate::{fn_args::FuncArgs, fn_call::ensure_no_data_race, module::Module, StaticVec};
#[cfg(not(feature = "no_module"))]
use crate::fn_native::{shared_take_or_clone, Shared};
#[cfg(not(feature = "no_optimize"))] #[cfg(not(feature = "no_optimize"))]
use crate::optimize::optimize_into_ast; use crate::optimize::optimize_into_ast;
@ -772,6 +775,45 @@ impl Engine {
self.register_indexer_get(getter) self.register_indexer_get(getter)
.register_indexer_set(setter) .register_indexer_set(setter)
} }
/// Register a `Module` as a sub-module with the `Engine`.
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
/// use rhai::{Engine, Module};
///
/// let mut engine = Engine::new();
///
/// // Create the module
/// let mut module = Module::new();
/// module.set_fn_1("calc", |x: i64| Ok(x + 1));
///
/// // Register the module as a sub-module
/// engine.register_module("CalcService", module);
///
/// assert_eq!(engine.eval::<i64>("CalcService::calc(41)")?, 42);
/// # Ok(())
/// # }
/// ```
#[cfg(not(feature = "no_module"))]
pub fn register_module(
&mut self,
name: impl Into<ImmutableString>,
module: impl Into<Shared<Module>>,
) -> &mut Self {
let module = module.into();
if !module.is_indexed() {
// Index the module (making a clone copy if necessary) if it is not indexed
let mut module = shared_take_or_clone(module);
module.build_index();
self.global_sub_modules.push(name, module);
} else {
self.global_sub_modules.push(name, module);
}
self
}
/// Compile a string into an `AST`, which can be used later for evaluation. /// Compile a string into an `AST`, which can be used later for evaluation.
/// ///
/// # Example /// # Example
@ -1368,7 +1410,8 @@ impl Engine {
scope: &mut Scope, scope: &mut Scope,
ast: &AST, ast: &AST,
) -> Result<T, Box<EvalAltResult>> { ) -> Result<T, Box<EvalAltResult>> {
let mut mods = Default::default(); let mut mods = self.global_sub_modules.clone();
let (result, _) = self.eval_ast_with_scope_raw(scope, &mut mods, ast)?; let (result, _) = self.eval_ast_with_scope_raw(scope, &mut mods, ast)?;
let typ = self.map_type_name(result.type_name()); let typ = self.map_type_name(result.type_name());
@ -1446,7 +1489,8 @@ impl Engine {
scope: &mut Scope, scope: &mut Scope,
ast: &AST, ast: &AST,
) -> Result<(), Box<EvalAltResult>> { ) -> Result<(), Box<EvalAltResult>> {
let mut mods = Default::default(); let mut mods = self.global_sub_modules.clone();
self.eval_statements_raw(scope, &mut mods, ast.statements(), &[ast.lib()]) self.eval_statements_raw(scope, &mut mods, ast.statements(), &[ast.lib()])
.map(|_| ()) .map(|_| ())
} }
@ -1599,7 +1643,7 @@ impl Engine {
.ok_or_else(|| EvalAltResult::ErrorFunctionNotFound(name.into(), NO_POS))?; .ok_or_else(|| EvalAltResult::ErrorFunctionNotFound(name.into(), NO_POS))?;
let mut state = Default::default(); let mut state = Default::default();
let mut mods = Default::default(); let mut mods = self.global_sub_modules.clone();
// Check for data race. // Check for data race.
if cfg!(not(feature = "no_closure")) { if cfg!(not(feature = "no_closure")) {

View File

@ -16,7 +16,11 @@ use crate::{calc_native_fn_hash, calc_script_fn_hash, StaticVec};
use crate::ast::ScriptFnDef; use crate::ast::ScriptFnDef;
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
use crate::{ast::AST, engine::Engine, scope::Scope}; use crate::{
ast::AST,
engine::{Engine, Imports},
scope::Scope,
};
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
use crate::engine::{Array, FN_IDX_GET, FN_IDX_SET}; use crate::engine::{Array, FN_IDX_GET, FN_IDX_SET};
@ -1361,7 +1365,8 @@ impl Module {
ast: &AST, ast: &AST,
engine: &Engine, engine: &Engine,
) -> Result<Self, Box<EvalAltResult>> { ) -> Result<Self, Box<EvalAltResult>> {
let mut mods = Default::default(); let mut mods = engine.global_sub_modules.clone();
let orig_mods_len = mods.len();
// Run the script // Run the script
engine.eval_ast_with_scope_raw(&mut scope, &mut mods, &ast)?; engine.eval_ast_with_scope_raw(&mut scope, &mut mods, &ast)?;
@ -1380,8 +1385,11 @@ impl Module {
} }
}); });
// Modules left in the scope become sub-modules // Extra modules left in the scope become sub-modules
mods.iter().for_each(|(alias, m)| { let mut func_mods: Imports = Default::default();
mods.into_iter().skip(orig_mods_len).for_each(|(alias, m)| {
func_mods.push(alias.clone(), m.clone());
module.set_sub_module(alias, m); module.set_sub_module(alias, m);
}); });
@ -1396,7 +1404,7 @@ impl Module {
// Encapsulate AST environment // Encapsulate AST environment
let mut func = func.as_ref().clone(); let mut func = func.as_ref().clone();
func.lib = Some(ast_lib.clone()); func.lib = Some(ast_lib.clone());
func.mods = mods.clone(); func.mods = func_mods.clone();
module.set_script_fn(func.into()); module.set_script_fn(func.into());
}); });
} }