Merge branch 'plugins' into plugins_dev

This commit is contained in:
Stephen Chung 2020-08-23 17:54:37 +08:00
commit a72f797da1
15 changed files with 191 additions and 21 deletions

View File

@ -9,6 +9,7 @@ Bug fixes
* `Engine::compile_expression`, `Engine::eval_expression` etc. no longer parse anonymous functions and closures. * `Engine::compile_expression`, `Engine::eval_expression` etc. no longer parse anonymous functions and closures.
* Imported modules now work inside closures. * Imported modules now work inside closures.
* Closures that capture now work under `no_object`.
Version 0.18.2 Version 0.18.2

View File

@ -23,7 +23,7 @@ The Rhai Scripting Language
1. [Hello World in Rhai - Evaluate a Script](engine/hello-world.md) 1. [Hello World in Rhai - Evaluate a Script](engine/hello-world.md)
2. [Compile to AST for Repeated Evaluations](engine/compile.md) 2. [Compile to AST for Repeated Evaluations](engine/compile.md)
3. [Call a Rhai Function from Rust](engine/call-fn.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) 5. [Evaluate Expressions Only](engine/expressions.md)
6. [Raw Engine](engine/raw.md) 6. [Raw Engine](engine/raw.md)
4. [Extend Rhai with Rust](rust/index.md) 4. [Extend Rhai with Rust](rust/index.md)
@ -106,6 +106,7 @@ The Rhai Scripting Language
2. [Loadable Configuration](patterns/config.md) 2. [Loadable Configuration](patterns/config.md)
3. [Control Layer](patterns/control.md) 3. [Control Layer](patterns/control.md)
4. [Singleton Command](patterns/singleton.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) 2. [Capture Scope for Function Call](language/fn-capture.md)
3. [Serialization/Deserialization of `Dynamic` with `serde`](rust/serde.md) 3. [Serialization/Deserialization of `Dynamic` with `serde`](rust/serde.md)
4. [Script Optimization](engine/optimize/index.md) 4. [Script Optimization](engine/optimize/index.md)

View File

@ -1,11 +1,11 @@
Create a Rust Anonymous Function from a Rhai Function Create a Rust Closure from a Rhai Function
=================================================== =========================================
{{#include ../links.md}} {{#include ../links.md}}
It is possible to further encapsulate a script in Rust such that it becomes a normal Rust function. 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` Creating them is accomplished via the `Func` trait which contains `create_from_script`
(as well as its companion method `create_from_ast`): (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 "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 schedule_callback(func); // pass it as a callback to another function

View File

@ -35,7 +35,8 @@ The actual implementation de-sugars to:
1. Keeping track of what variables are accessed inside the anonymous function, 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. 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. 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. This process is called _Automatic Currying_, and is the mechanism through which Rhai simulates normal closures.

View File

@ -75,3 +75,17 @@ let map = engine.parse_json(&new_json, false)?;
map.len() == 2; // 'map' contains two properties: 'a' and 'b' 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.

View File

@ -31,6 +31,8 @@
[built-in operators]: {{rootUrl}}/engine/raw.md#built-in-operators [built-in operators]: {{rootUrl}}/engine/raw.md#built-in-operators
[package]: {{rootUrl}}/rust/packages/index.md [package]: {{rootUrl}}/rust/packages/index.md
[packages]: {{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 [`Scope`]: {{rootUrl}}/rust/scope.md
[`serde`]: {{rootUrl}}/rust/serde.md [`serde`]: {{rootUrl}}/rust/serde.md

View File

@ -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<EvalAltResult>> {
// 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)?;
}
```

View File

@ -5,6 +5,14 @@ Create a Custom Package
Sometimes specific functionalities are needed, so custom packages can be created. 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. The macro `rhai::def_package!` is used to create a new custom package.

View File

@ -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) Rhai's [`Dynamic`] type supports serialization and deserialization by [`serde`](https://crates.io/crates/serde)
via the [`serde`][features] feature. via the [`serde`][features] feature.
A [`Dynamic`] can be seamlessly converted to and from a type that implements `serde::Serialize` and/or A [`Dynamic`] can be seamlessly converted to and from a type that implements
`serde::Deserialize`. [`serde::Serialize`](https://docs.serde.rs/serde/trait.Serialize.html) and/or
[`serde::Deserialize`](https://docs.serde.rs/serde/trait.Deserialize.html).
Serialization Serialization
------------- -------------
The function `rhai::ser::to_dynamic` automatically converts any Rust type that implements `serde::Serialize` The function `rhai::ser::to_dynamic` automatically converts any Rust type that implements
into a [`Dynamic`]. [`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 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 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 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 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 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' // Convert the 'Dynamic' object map into 'MyStruct'
let x: MyStruct = from_dynamic(&result)?; 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.

View File

@ -9,5 +9,5 @@ A number of traits, under the `rhai::` module namespace, provide additional func
| ------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------- | | ------------------ | ---------------------------------------------------------------------------------------- | --------------------------------------- |
| `RegisterFn` | Trait for registering functions | `register_fn` | | `RegisterFn` | Trait for registering functions | `register_fn` |
| `RegisterResultFn` | Trait for registering fallible functions returning `Result<Dynamic, Box<EvalAltResult>>` | `register_result_fn` | | `RegisterResultFn` | Trait for registering fallible functions returning `Result<Dynamic, Box<EvalAltResult>>` | `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` | | `ModuleResolver` | Trait implemented by module resolution services | `resolve` |

View File

@ -340,6 +340,7 @@ pub fn get_script_function_by_signature<'a>(
/// ///
/// This type is volatile and may change. /// This type is volatile and may change.
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Limits { pub struct Limits {
/// Maximum levels of call-stack to prevent infinite recursion. /// Maximum levels of call-stack to prevent infinite recursion.
/// ///

View File

@ -12,11 +12,11 @@ use crate::scope::Scope;
use crate::stdlib::{boxed::Box, string::ToString}; 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<ARGS, RET> { pub trait Func<ARGS, RET> {
type Output; 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. /// The `Engine` and `AST` are consumed and basically embedded into the closure.
/// ///
/// # Examples /// # Examples
@ -47,7 +47,7 @@ pub trait Func<ARGS, RET> {
/// # } /// # }
fn create_from_ast(self, ast: AST, entry_point: &str) -> Self::Output; 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. /// The `Engine` is consumed and basically embedded into the closure.
/// ///
/// # Examples /// # Examples

View File

@ -3167,6 +3167,8 @@ fn make_curry_from_externals(
let num_externals = externals.len(); let num_externals = externals.len();
let mut args: StaticVec<_> = Default::default(); let mut args: StaticVec<_> = Default::default();
args.push(fn_expr);
#[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_closure"))]
externals.iter().for_each(|(var_name, pos)| { externals.iter().for_each(|(var_name, pos)| {
args.push(Expr::Variable(Box::new(( 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)))); 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), (KEYWORD_FN_PTR_CURRY.into(), false, false, pos),
None, None,
hash, hash,
@ -3192,8 +3194,6 @@ fn make_curry_from_externals(
None, 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, // If there are captured variables, convert the entire expression into a statement block,
// then insert the relevant `Share` statements. // then insert the relevant `Share` statements.
#[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_closure"))]

View File

@ -200,7 +200,7 @@ type MyType = Rc<RefCell<INT>>;
#[test] #[test]
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
#[cfg(not(feature = "sync"))] #[cfg(not(feature = "sync"))]
fn test_closure_shared_obj() -> Result<(), Box<EvalAltResult>> { fn test_closures_shared_obj() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new(); let mut engine = Engine::new();
// Register API on MyType // Register API on MyType
@ -250,3 +250,30 @@ fn test_closure_shared_obj() -> Result<(), Box<EvalAltResult>> {
Ok(()) Ok(())
} }
#[test]
#[cfg(not(feature = "no_closure"))]
fn test_closures_external() -> Result<(), Box<EvalAltResult>> {
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::<FnPtr>(&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(())
}

33
tests/packages.rs Normal file
View File

@ -0,0 +1,33 @@
use rhai::{Engine, EvalAltResult, INT, Scope};
use rhai::packages::{Package, StandardPackage};
#[test]
fn test_packages() -> Result<(), Box<EvalAltResult>> {
let e = Engine::new();
let ast = e.compile("x")?;
let std_pkg = StandardPackage::new();
let make_call = |x: INT| -> Result<INT, Box<EvalAltResult>> {
// 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::<INT>(&mut scope, &ast)
};
// The following loop creates 10,000 Engine instances!
for x in 0..10_000 {
assert_eq!(make_call(x)?, x);
}
Ok(())
}