From d134b72473f391773a0e49abf61c4f32e9e3b896 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 23 Aug 2020 14:50:53 +0800 Subject: [PATCH 1/5] Add link from parse_json to serde. --- doc/src/language/json.md | 14 ++++++++++++++ doc/src/rust/serde.md | 21 ++++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/doc/src/language/json.md b/doc/src/language/json.md index d76877c9..951f0cbe 100644 --- a/doc/src/language/json.md +++ b/doc/src/language/json.md @@ -75,3 +75,17 @@ let map = engine.parse_json(&new_json, false)?; map.len() == 2; // 'map' contains two properties: 'a' and 'b' ``` + + +Use `serde` to Serialize/Deserialize to/from JSON +------------------------------------------------ + +Remember, `Engine::parse_json` is nothing more than a _cheap_ alternative to true JSON parsing. + +If correctness is needed, or for more configuration possibilities, turn on the [`serde`][features] +feature to pull in the [`serde`](https://crates.io/crates/serde) crate which enables +serialization and deserialization to/from multiple formats, including JSON. + +Beware, though... the [`serde`](https://crates.io/crates/serde) crate is quite heavy. + +See _[Serialization/Deserialization of `Dynamic` with `serde`][`serde`]_ for more details. diff --git a/doc/src/rust/serde.md b/doc/src/rust/serde.md index c28eccfd..19c3a22e 100644 --- a/doc/src/rust/serde.md +++ b/doc/src/rust/serde.md @@ -6,15 +6,16 @@ Serialization and Deserialization of `Dynamic` with `serde` Rhai's [`Dynamic`] type supports serialization and deserialization by [`serde`](https://crates.io/crates/serde) via the [`serde`][features] feature. -A [`Dynamic`] can be seamlessly converted to and from a type that implements `serde::Serialize` and/or -`serde::Deserialize`. +A [`Dynamic`] can be seamlessly converted to and from a type that implements +[`serde::Serialize`](https://docs.serde.rs/serde/trait.Serialize.html) and/or +[`serde::Deserialize`](https://docs.serde.rs/serde/trait.Deserialize.html). Serialization ------------- -The function `rhai::ser::to_dynamic` automatically converts any Rust type that implements `serde::Serialize` -into a [`Dynamic`]. +The function `rhai::ser::to_dynamic` automatically converts any Rust type that implements +[`serde::Serialize`](https://docs.serde.rs/serde/trait.Serialize.html) into a [`Dynamic`]. This is usually not necessary because using [`Dynamic::from`][`Dynamic`] is much easier and is essentially the same thing. The only difference is treatment for integer values. `Dynamic::from` will keep the different @@ -64,7 +65,7 @@ Deserialization --------------- The function `rhai::de::from_dynamic` automatically converts a [`Dynamic`] value into any Rust type -that implements `serde::Deserialize`. +that implements [`serde::Deserialize`](https://docs.serde.rs/serde/trait.Deserialize.html). In particular, [object maps] are converted into Rust `struct`'s (or any type that is marked as a `serde` map) while [arrays] are converted into Rust `Vec`'s (or any type that is marked @@ -102,3 +103,13 @@ let result: Dynamic = engine.eval(r#" // Convert the 'Dynamic' object map into 'MyStruct' let x: MyStruct = from_dynamic(&result)?; ``` + + +Lighter Alternative +------------------- + +The [`serde`](https://crates.io/crates/serde) crate is quite heavy. + +If only _simple_ JSON parsing (i.e. only deserialization) of a hash object into a Rhai [object map] is required, +the [`Engine::parse_json`]({{rootUrl}}/language/json.md}}) method is available as a _cheap_ alternative, +but it does not provide the same level of correctness, nor are there any configurable options. From f4c74cc03b05f43d4014942c940c33d58f62f9fd Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 23 Aug 2020 16:29:15 +0800 Subject: [PATCH 2/5] Add external closure test. --- tests/closures.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/closures.rs b/tests/closures.rs index 348aa162..6f5de9e7 100644 --- a/tests/closures.rs +++ b/tests/closures.rs @@ -200,7 +200,7 @@ type MyType = Rc>; #[test] #[cfg(not(feature = "no_object"))] #[cfg(not(feature = "sync"))] -fn test_closure_shared_obj() -> Result<(), Box> { +fn test_closures_shared_obj() -> Result<(), Box> { let mut engine = Engine::new(); // Register API on MyType @@ -250,3 +250,30 @@ fn test_closure_shared_obj() -> Result<(), Box> { Ok(()) } + +#[test] +#[cfg(not(feature = "no_closure"))] +fn test_closures_external() -> Result<(), Box> { + let engine = Engine::new(); + + let mut ast = engine.compile( + r#" + let test = "hello"; + + |x| test + x + "#, + )?; + + // Save the function pointer together with captured variables + let fn_ptr = engine.eval_ast::(&ast)?; + + // Get rid of the script, retaining only functions + ast.retain_functions(|_, _, _| true); + + // Closure 'f' captures: the engine, the AST, and the curried function pointer + let f = move |x: INT| fn_ptr.call_dynamic(&engine, ast, None, [x.into()]); + + assert_eq!(f(42)?.as_str(), Ok("hello42")); + + Ok(()) +} From 196e145c960611737e6d229199255d65ebc712d5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 23 Aug 2020 16:29:32 +0800 Subject: [PATCH 3/5] Derive standard traits for Limits. --- src/engine.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/engine.rs b/src/engine.rs index 5c4fdaee..70e93568 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -340,6 +340,7 @@ pub fn get_script_function_by_signature<'a>( /// /// This type is volatile and may change. #[cfg(not(feature = "unchecked"))] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Limits { /// Maximum levels of call-stack to prevent infinite recursion. /// @@ -377,6 +378,7 @@ pub struct Limits { /// ``` /// /// Currently, `Engine` is neither `Send` nor `Sync`. Use the `sync` feature to make it `Send + Sync`. +#[derive(Clone)] pub struct Engine { /// A unique ID identifying this scripting `Engine`. pub id: Option, From e2f271644a4ba7512bb9df798ff29fe60bf1b607 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 23 Aug 2020 17:22:39 +0800 Subject: [PATCH 4/5] Expand packages and raw Engine write-up. --- doc/src/SUMMARY.md | 3 +- doc/src/engine/func.md | 8 ++-- doc/src/language/fn-closure.md | 6 ++- doc/src/links.md | 2 + doc/src/patterns/parallel.md | 70 +++++++++++++++++++++++++++++++++ doc/src/rust/packages/create.md | 8 ++++ doc/src/rust/traits.md | 2 +- src/engine.rs | 1 - src/fn_func.rs | 6 +-- tests/packages.rs | 33 ++++++++++++++++ 10 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 doc/src/patterns/parallel.md create mode 100644 tests/packages.rs diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 7a4fd884..59584d28 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -23,7 +23,7 @@ The Rhai Scripting Language 1. [Hello World in Rhai - Evaluate a Script](engine/hello-world.md) 2. [Compile to AST for Repeated Evaluations](engine/compile.md) 3. [Call a Rhai Function from Rust](engine/call-fn.md) - 4. [Create a Rust Anonymous Function from a Rhai Function](engine/func.md) + 4. [Create a Rust Closure from a Rhai Function](engine/func.md) 5. [Evaluate Expressions Only](engine/expressions.md) 6. [Raw Engine](engine/raw.md) 4. [Extend Rhai with Rust](rust/index.md) @@ -106,6 +106,7 @@ The Rhai Scripting Language 2. [Loadable Configuration](patterns/config.md) 3. [Control Layer](patterns/control.md) 4. [Singleton Command](patterns/singleton.md) + 5. [One Engine Instance Per Call](patterns/parallel.md) 2. [Capture Scope for Function Call](language/fn-capture.md) 3. [Serialization/Deserialization of `Dynamic` with `serde`](rust/serde.md) 4. [Script Optimization](engine/optimize/index.md) diff --git a/doc/src/engine/func.md b/doc/src/engine/func.md index 551c4e48..f545b926 100644 --- a/doc/src/engine/func.md +++ b/doc/src/engine/func.md @@ -1,11 +1,11 @@ -Create a Rust Anonymous Function from a Rhai Function -=================================================== +Create a Rust Closure from a Rhai Function +========================================= {{#include ../links.md}} It is possible to further encapsulate a script in Rust such that it becomes a normal Rust function. -Such an _anonymous function_ is basically a boxed closure, very useful as call-back functions. +Such a _closure_ is very useful as call-back functions. Creating them is accomplished via the `Func` trait which contains `create_from_script` (as well as its companion method `create_from_ast`): @@ -30,7 +30,7 @@ let func = Func::<(i64, String), bool>::create_from_script( "calc" // the entry-point function name )?; -func(123, "hello".to_string())? == false; // call the anonymous function +func(123, "hello".to_string())? == false; // call the closure schedule_callback(func); // pass it as a callback to another function diff --git a/doc/src/language/fn-closure.md b/doc/src/language/fn-closure.md index 2f6ce2c5..7f04dbac 100644 --- a/doc/src/language/fn-closure.md +++ b/doc/src/language/fn-closure.md @@ -35,7 +35,8 @@ The actual implementation de-sugars to: 1. Keeping track of what variables are accessed inside the anonymous function, -2. If a variable is not defined within the anonymous function's scope, it is looked up _outside_ the function and in the current execution scope - where the anonymous function is created. +2. If a variable is not defined within the anonymous function's scope, it is looked up _outside_ the function and + in the current execution scope - where the anonymous function is created. 3. The variable is added to the parameters list of the anonymous function, at the front. @@ -43,7 +44,8 @@ The actual implementation de-sugars to: An [anonymous function] which captures an external variable is the only way to create a reference-counted shared value in Rhai. -5. The shared value is then [curried][currying] into the [function pointer] itself, essentially carrying a reference to that shared value and inserting it into future calls of the function. +5. The shared value is then [curried][currying] into the [function pointer] itself, essentially carrying a reference to that shared value + and inserting it into future calls of the function. This process is called _Automatic Currying_, and is the mechanism through which Rhai simulates normal closures. diff --git a/doc/src/links.md b/doc/src/links.md index ee7f05e4..953c1164 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -31,6 +31,8 @@ [built-in operators]: {{rootUrl}}/engine/raw.md#built-in-operators [package]: {{rootUrl}}/rust/packages/index.md [packages]: {{rootUrl}}/rust/packages/index.md +[custom package]: {{rootUrl}}/rust/packages/create.md +[custom packages]: {{rootUrl}}/rust/packages/create.md [`Scope`]: {{rootUrl}}/rust/scope.md [`serde`]: {{rootUrl}}/rust/serde.md diff --git a/doc/src/patterns/parallel.md b/doc/src/patterns/parallel.md new file mode 100644 index 00000000..e070b78d --- /dev/null +++ b/doc/src/patterns/parallel.md @@ -0,0 +1,70 @@ +One Engine Instance Per Call +=========================== + +{{#include ../links.md}} + + +Usage Scenario +-------------- + +* A system where scripts are called a _lot_, in tight loops or in parallel. + +* Keeping a global [`Engine`] instance is sub-optimal due to contention and locking. + +* Scripts need to be executed independently from each other, perhaps concurrently. + +* Scripts are used to [create Rust closure][`Func`] that are stored and may be called at any time, perhaps concurrently. + In this case, the [`Engine`] instance is usually moved into the closure itself. + + +Key Concepts +------------ + +* Create a single instance of each standard [package] required. To duplicate `Engine::new`, create a [`StandardPackage`]({{rootUrl}}/rust/packages/builtin.md). + +* Gather up all common custom functions into a [custom package]. + +* Store a global `AST` for use with all engines. + +* Always use `Engine::new_raw` to create a [raw `Engine`], instead of `Engine::new` which is _much_ more expensive. + A [raw `Engine`] is _extremely_ cheap to create. + + Loading the [`StandardPackage`]({{rootUrl}}/rust/packages/builtin.md) into a [raw `Engine`] via `Engine::load_package` is essentially the same as `Engine::new`. + But because packages are shared, loading an existing package is _much cheaper_ than registering all the functions one by one. + +* Load the required packages into the [raw `Engine`] via `Engine::load_package`, using `Package::get` to obtain a shared copy. + + +Examples +-------- + +```rust +use rhai::packages::{Package, StandardPackage}; + +let ast = /* ... some AST ... */; +let std_pkg = StandardPackage::new(); +let custom_pkg = MyCustomPackage::new(); + +let make_call = |x: i64| -> Result<(), Box> { + // Create a raw Engine - extremely cheap. + let mut engine = Engine::new_raw(); + + // Load packages - cheap. + engine.load_package(std_pkg.get()); + engine.load_package(custom_pkg.get()); + + // Create custom scope - cheap. + let mut scope = Scope::new(); + + // Push variable into scope - relatively cheap. + scope.push("x", x); + + // Evaluate script. + engine.consume_ast_with_scope(&mut scope, &ast) +}; + +// The following loop creates 10,000 Engine instances! +for x in 0..10_000 { + make_call(x)?; +} +``` diff --git a/doc/src/rust/packages/create.md b/doc/src/rust/packages/create.md index cdff10b8..89f0e306 100644 --- a/doc/src/rust/packages/create.md +++ b/doc/src/rust/packages/create.md @@ -5,6 +5,14 @@ Create a Custom Package Sometimes specific functionalities are needed, so custom packages can be created. +A custom package is a convenient means to gather up a number of functions for later use. +An [`Engine`] only needs to `Engine::load_package` the custom package once to gain access +to the entire set of functions within. + +Loading a package into an [`Engine`] is functionally equivalent to calling `Engine::register_fn` etc. +on _each_ of the functions inside the package. But because packages are _shared_, loading an existing +package is _much_ cheaper than registering all the functions one by one. + The macro `rhai::def_package!` is used to create a new custom package. diff --git a/doc/src/rust/traits.md b/doc/src/rust/traits.md index 12ea514e..88231146 100644 --- a/doc/src/rust/traits.md +++ b/doc/src/rust/traits.md @@ -9,5 +9,5 @@ A number of traits, under the `rhai::` module namespace, provide additional func | ------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------- | | `RegisterFn` | Trait for registering functions | `register_fn` | | `RegisterResultFn` | Trait for registering fallible functions returning `Result>` | `register_result_fn` | -| `Func` | Trait for creating anonymous functions from script | `create_from_ast`, `create_from_script` | +| `Func` | Trait for creating Rust closures from script | `create_from_ast`, `create_from_script` | | `ModuleResolver` | Trait implemented by module resolution services | `resolve` | diff --git a/src/engine.rs b/src/engine.rs index 70e93568..66c4de23 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -378,7 +378,6 @@ pub struct Limits { /// ``` /// /// Currently, `Engine` is neither `Send` nor `Sync`. Use the `sync` feature to make it `Send + Sync`. -#[derive(Clone)] pub struct Engine { /// A unique ID identifying this scripting `Engine`. pub id: Option, diff --git a/src/fn_func.rs b/src/fn_func.rs index 5eaee7f1..751ade9d 100644 --- a/src/fn_func.rs +++ b/src/fn_func.rs @@ -12,11 +12,11 @@ use crate::scope::Scope; use crate::stdlib::{boxed::Box, string::ToString}; -/// Trait to create a Rust anonymous function from a script. +/// Trait to create a Rust closure from a script. pub trait Func { type Output; - /// Create a Rust anonymous function from an `AST`. + /// Create a Rust closure from an `AST`. /// The `Engine` and `AST` are consumed and basically embedded into the closure. /// /// # Examples @@ -47,7 +47,7 @@ pub trait Func { /// # } fn create_from_ast(self, ast: AST, entry_point: &str) -> Self::Output; - /// Create a Rust anonymous function from a script. + /// Create a Rust closure from a script. /// The `Engine` is consumed and basically embedded into the closure. /// /// # Examples diff --git a/tests/packages.rs b/tests/packages.rs new file mode 100644 index 00000000..c08234fe --- /dev/null +++ b/tests/packages.rs @@ -0,0 +1,33 @@ +use rhai::{Engine, EvalAltResult, INT, Scope}; +use rhai::packages::{Package, StandardPackage}; + +#[test] +fn test_packages() -> Result<(), Box> { + let e = Engine::new(); + let ast = e.compile("x")?; + let std_pkg = StandardPackage::new(); + + let make_call = |x: INT| -> Result> { + // Create a raw Engine - extremely cheap. + let mut engine = Engine::new_raw(); + + // Load packages - cheap. + engine.load_package(std_pkg.get()); + + // Create custom scope - cheap. + let mut scope = Scope::new(); + + // Push variable into scope - relatively cheap. + scope.push("x", x); + + // Evaluate script. + engine.eval_ast_with_scope::(&mut scope, &ast) + }; + + // The following loop creates 10,000 Engine instances! + for x in 0..10_000 { + assert_eq!(make_call(x)?, x); + } + + Ok(()) +} From 7cd345b128f0cf913b1416fa058089efc0ac5678 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 23 Aug 2020 17:46:39 +0800 Subject: [PATCH 5/5] Fix bug in closure capture for no_object. --- RELEASES.md | 1 + src/parser.rs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index db26c661..7e9bb8b2 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,6 +9,7 @@ Bug fixes * `Engine::compile_expression`, `Engine::eval_expression` etc. no longer parse anonymous functions and closures. * Imported modules now work inside closures. +* Closures that capture now work under `no_object`. Version 0.18.2 diff --git a/src/parser.rs b/src/parser.rs index 01be7715..eaaf559d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3167,6 +3167,8 @@ fn make_curry_from_externals( let num_externals = externals.len(); let mut args: StaticVec<_> = Default::default(); + args.push(fn_expr); + #[cfg(not(feature = "no_closure"))] externals.iter().for_each(|(var_name, pos)| { args.push(Expr::Variable(Box::new(( @@ -3182,9 +3184,9 @@ fn make_curry_from_externals( args.push(Expr::Variable(Box::new(((var_name, pos), None, 0, None)))); }); - let hash = calc_fn_hash(empty(), KEYWORD_FN_PTR_CURRY, num_externals, empty()); + let hash = calc_fn_hash(empty(), KEYWORD_FN_PTR_CURRY, num_externals + 1, empty()); - let fn_call = Expr::FnCall(Box::new(( + let expr = Expr::FnCall(Box::new(( (KEYWORD_FN_PTR_CURRY.into(), false, false, pos), None, hash, @@ -3192,8 +3194,6 @@ fn make_curry_from_externals( None, ))); - let expr = Expr::Dot(Box::new((fn_expr, fn_call, pos))); - // If there are captured variables, convert the entire expression into a statement block, // then insert the relevant `Share` statements. #[cfg(not(feature = "no_closure"))]