From 937b45a18786e9ee89179b211b0f6c1d252ac4d2 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 15 Nov 2020 23:14:16 +0800 Subject: [PATCH] Add Engine::load_module. --- doc/src/patterns/enums.md | 151 ++++++++++++++++++++++++++++----- doc/src/plugins/module.md | 36 ++++++++ doc/src/rust/modules/create.md | 22 ++++- src/engine.rs | 8 +- src/engine_api.rs | 50 ++++++++++- src/module/mod.rs | 18 ++-- 6 files changed, 251 insertions(+), 34 deletions(-) diff --git a/doc/src/patterns/enums.md b/doc/src/patterns/enums.md index c5663b44..b27eba08 100644 --- a/doc/src/patterns/enums.md +++ b/doc/src/patterns/enums.md @@ -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. -Switch Through Arrays ---------------------- - -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: +Simulate an Enum API +-------------------- ```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 { Foo, Bar(i64), Baz(String, bool) } -impl MyEnum { - fn get_enum_data(&mut self) -> Array { - match self { - Self::Foo => vec![ - "Foo".into() - ] as Array, - Self::Bar(num) => vec![ - "Bar".into(), (*num).into() - ] as Array, - Self::Baz(name, option) => vec![ - "Baz".into(), name.clone().into(), (*option).into() - ] as Array - } - } +// Create a plugin module with functions constructing the 'MyEnum' variants +#[export_module] +pub mod MyEnumModule { + // 'MyEnum' variants + pub const Foo: &MyEnum = MyEnum::Foo; + pub fn Bar(value: i64) -> MyEnum { MyEnum::Bar(value) } + pub fn Baz(val1: String, val2: bool) -> MyEnum { MyEnum::Baz(val1, val2) } } +let mut engine = Engine::new(); + +// Register API for 'MyEnum' engine + // Register enum custom type .register_type_with_name::("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: ```c // 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 { ["Foo"] => 1, ["Bar", 42] => 2, @@ -61,4 +156,14 @@ let x = switch value.enum_data { }; 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 +} ``` diff --git a/doc/src/plugins/module.md b/doc/src/plugins/module.md index e51f5103..29bac196 100644 --- a/doc/src/plugins/module.md +++ b/doc/src/plugins/module.md @@ -123,6 +123,42 @@ x == 43; 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. +### 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` Using this directly as a dynamically-loadable Rhai [module] is almost the same, except that a diff --git a/doc/src/rust/modules/create.md b/doc/src/rust/modules/create.md index 069e5896..0a163837 100644 --- a/doc/src/rust/modules/create.md +++ b/doc/src/rust/modules/create.md @@ -25,7 +25,8 @@ Make the `Module` Available to the `Engine` `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 -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 use rhai::{Engine, Module}; @@ -41,6 +42,25 @@ engine.eval::("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::("calc::inc(41)")? == 42; // refer to the 'Calc' module +``` + + Make the `Module` Dynamically Loadable ------------------------------------- diff --git a/src/engine.rs b/src/engine.rs index 99a717be..af9b5b82 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -596,6 +596,8 @@ pub struct Engine { pub(crate) global_module: Module, /// A collection of all library packages loaded into the Engine. 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. #[cfg(not(feature = "no_module"))] @@ -711,6 +713,7 @@ impl Engine { packages: Default::default(), global_module: Default::default(), + global_sub_modules: Default::default(), #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] @@ -773,6 +776,7 @@ impl Engine { packages: Default::default(), global_module: Default::default(), + global_sub_modules: Default::default(), #[cfg(not(feature = "no_module"))] module_resolver: None, @@ -1814,7 +1818,7 @@ impl Engine { Expr::True(_) => Ok(true.into()), Expr::False(_) => Ok(false.into()), - Expr::Unit(_) => Ok(().into()), + Expr::Unit(_) => Ok(Dynamic::UNIT), Expr::Custom(custom, _) => { let expressions = custom @@ -2090,7 +2094,7 @@ impl Engine { } else if let Some(def_stmt) = def_stmt { self.eval_stmt(scope, mods, state, lib, this_ptr, def_stmt, level) } else { - Ok(().into()) + Ok(Dynamic::UNIT) } } diff --git a/src/engine_api.rs b/src/engine_api.rs index 3fc8891e..b526f545 100644 --- a/src/engine_api.rs +++ b/src/engine_api.rs @@ -30,6 +30,9 @@ use crate::fn_register::{RegisterFn, RegisterResultFn}; #[cfg(not(feature = "no_function"))] 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"))] use crate::optimize::optimize_into_ast; @@ -772,6 +775,45 @@ impl Engine { self.register_indexer_get(getter) .register_indexer_set(setter) } + /// Register a `Module` as a sub-module with the `Engine`. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), Box> { + /// 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::("CalcService::calc(41)")?, 42); + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_module"))] + pub fn register_module( + &mut self, + name: impl Into, + module: impl Into>, + ) -> &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. /// /// # Example @@ -1368,7 +1410,8 @@ impl Engine { scope: &mut Scope, ast: &AST, ) -> Result> { - 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 typ = self.map_type_name(result.type_name()); @@ -1446,7 +1489,8 @@ impl Engine { scope: &mut Scope, ast: &AST, ) -> Result<(), Box> { - let mut mods = Default::default(); + let mut mods = self.global_sub_modules.clone(); + self.eval_statements_raw(scope, &mut mods, ast.statements(), &[ast.lib()]) .map(|_| ()) } @@ -1599,7 +1643,7 @@ impl Engine { .ok_or_else(|| EvalAltResult::ErrorFunctionNotFound(name.into(), NO_POS))?; let mut state = Default::default(); - let mut mods = Default::default(); + let mut mods = self.global_sub_modules.clone(); // Check for data race. if cfg!(not(feature = "no_closure")) { diff --git a/src/module/mod.rs b/src/module/mod.rs index 7d798f70..ebd15a3a 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -16,7 +16,11 @@ use crate::{calc_native_fn_hash, calc_script_fn_hash, StaticVec}; use crate::ast::ScriptFnDef; #[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"))] use crate::engine::{Array, FN_IDX_GET, FN_IDX_SET}; @@ -1361,7 +1365,8 @@ impl Module { ast: &AST, engine: &Engine, ) -> Result> { - let mut mods = Default::default(); + let mut mods = engine.global_sub_modules.clone(); + let orig_mods_len = mods.len(); // Run the script 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 - mods.iter().for_each(|(alias, m)| { + // Extra modules left in the scope become sub-modules + 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); }); @@ -1396,7 +1404,7 @@ impl Module { // Encapsulate AST environment let mut func = func.as_ref().clone(); func.lib = Some(ast_lib.clone()); - func.mods = mods.clone(); + func.mods = func_mods.clone(); module.set_script_fn(func.into()); }); }