Add patterns section.
This commit is contained in:
parent
5e6d5e8e80
commit
0b21d80641
@ -100,8 +100,12 @@ The Rhai Scripting Language
|
|||||||
8. [Maximum Call Stack Depth](safety/max-call-stack.md)
|
8. [Maximum Call Stack Depth](safety/max-call-stack.md)
|
||||||
9. [Maximum Statement Depth](safety/max-stmt-depth.md)
|
9. [Maximum Statement Depth](safety/max-stmt-depth.md)
|
||||||
7. [Advanced Topics](advanced.md)
|
7. [Advanced Topics](advanced.md)
|
||||||
1. [Capture Scope for Function Call](language/fn-capture.md)
|
1. [Advanced Patterns](patterns/index.md)
|
||||||
2. [Object-Oriented Programming (OOP)](language/oop.md)
|
1. [Loadable Configuration](patterns/config.md)
|
||||||
|
2. [Control Layer](patterns/control.md)
|
||||||
|
3. [Singleton Command](patterns/singleton.md)
|
||||||
|
4. [Object-Oriented Programming (OOP)](patterns/oop.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)
|
||||||
1. [Optimization Levels](engine/optimize/optimize-levels.md)
|
1. [Optimization Levels](engine/optimize/optimize-levels.md)
|
||||||
|
@ -5,9 +5,9 @@ Advanced Topics
|
|||||||
|
|
||||||
This section covers advanced features such as:
|
This section covers advanced features such as:
|
||||||
|
|
||||||
* [Capture the calling scope]({{rootUrl}}/language/fn-capture.md) in a function call.
|
* [Advanced patterns]({{rootUrl}}/patterns/index.md) in using Rhai.
|
||||||
|
|
||||||
* Simulated [Object Oriented Programming (OOP)][OOP].
|
* [Capture the calling scope]({{rootUrl}}/language/fn-capture.md) in a function call.
|
||||||
|
|
||||||
* [`serde`] integration.
|
* [`serde`] integration.
|
||||||
|
|
||||||
|
@ -10,24 +10,23 @@ If an [object map]'s property holds a [function pointer], the property can simpl
|
|||||||
a normal method in method-call syntax. This is a _short-hand_ to avoid the more verbose syntax
|
a normal method in method-call syntax. This is a _short-hand_ to avoid the more verbose syntax
|
||||||
of using the `call` function keyword.
|
of using the `call` function keyword.
|
||||||
|
|
||||||
When a property holding a [function pointer] is called like a method, what happens next depends
|
When a property holding a [function pointer] (which incudes [closures]) is called like a method,
|
||||||
on whether the target function is a native Rust function or a script-defined function.
|
what happens next depends on whether the target function is a native Rust function or
|
||||||
|
a script-defined function.
|
||||||
|
|
||||||
If it is a registered native Rust method function, then it is called directly.
|
If it is a registered native Rust method function, it is called directly.
|
||||||
|
|
||||||
If it is a script-defined function, the `this` variable within the function body is bound
|
If it is a script-defined function, the `this` variable within the function body is bound
|
||||||
to the [object map] before the function is called. There is no way to simulate this behavior
|
to the [object map] before the function is called. There is no way to simulate this behavior
|
||||||
via a normal function-call syntax because all scripted function arguments are passed by value.
|
via a normal function-call syntax because all scripted function arguments are passed by value.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn do_action(x) { this.data += x; } // 'this' binds to the object when called
|
|
||||||
|
|
||||||
let obj = #{
|
let obj = #{
|
||||||
data: 40,
|
data: 40,
|
||||||
action: Fn("do_action") // 'action' holds a function pointer to 'do_action'
|
action: || this.data += x // 'action' holds a function pointer which is a closure
|
||||||
};
|
};
|
||||||
|
|
||||||
obj.action(2); // Calls 'do_action' with `this` bound to 'obj'
|
obj.action(2); // Calls the function pointer with `this` bound to 'obj'
|
||||||
|
|
||||||
obj.call(obj.action, 2); // The above de-sugars to this
|
obj.call(obj.action, 2); // The above de-sugars to this
|
||||||
|
|
||||||
@ -36,5 +35,7 @@ obj.data == 42;
|
|||||||
// To achieve the above with normal function pointer call will fail.
|
// To achieve the above with normal function pointer call will fail.
|
||||||
fn do_action(map, x) { map.data += x; } // 'map' is a copy
|
fn do_action(map, x) { map.data += x; } // 'map' is a copy
|
||||||
|
|
||||||
|
obj.action = Fn("do_action");
|
||||||
|
|
||||||
obj.action.call(obj, 2); // 'obj' is passed as a copy by value
|
obj.action.call(obj, 2); // 'obj' is passed as a copy by value
|
||||||
```
|
```
|
||||||
|
@ -97,7 +97,7 @@
|
|||||||
|
|
||||||
[`eval`]: {{rootUrl}}/language/eval.md
|
[`eval`]: {{rootUrl}}/language/eval.md
|
||||||
|
|
||||||
[OOP]: {{rootUrl}}/language/oop.md
|
[OOP]: {{rootUrl}}/patterns/oop.md
|
||||||
[DSL]: {{rootUrl}}/engine/dsl.md
|
[DSL]: {{rootUrl}}/engine/dsl.md
|
||||||
|
|
||||||
[maximum statement depth]: {{rootUrl}}/safety/max-stmt-depth.md
|
[maximum statement depth]: {{rootUrl}}/safety/max-stmt-depth.md
|
||||||
|
103
doc/src/patterns/config.md
Normal file
103
doc/src/patterns/config.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
Loadable Configuration
|
||||||
|
======================
|
||||||
|
|
||||||
|
{{#include ../links.md}}
|
||||||
|
|
||||||
|
|
||||||
|
Usage Scenario
|
||||||
|
--------------
|
||||||
|
|
||||||
|
* A system where settings and configurations are complex and logic-driven.
|
||||||
|
|
||||||
|
* Where it is not possible to configure said system via standard configuration file formats such as `TOML` or `YAML`.
|
||||||
|
|
||||||
|
* The system configuration is complex enough that it requires a full programming language. Essentially _configuration by code_.
|
||||||
|
|
||||||
|
* Yet the configurations must be flexible, late-bound and dynamically loadable, just like a configuration file.
|
||||||
|
|
||||||
|
|
||||||
|
Key Concepts
|
||||||
|
------------
|
||||||
|
|
||||||
|
* Leverage the loadable [modules] of Rhai. The [`no_module`] feature must not be on.
|
||||||
|
|
||||||
|
* Expose the configuration API. Use separate scripts to configure that API. Dynamically load scripts via the `import` statement.
|
||||||
|
|
||||||
|
* Since Rhai is _sand-boxed_, it cannot mutate the environment. To modify the external configuration object via an API, it must be wrapped in a `RefCell` (or `RwLock`/`Mutex` for [`sync`]) and shared to the [`Engine`].
|
||||||
|
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
--------------
|
||||||
|
|
||||||
|
### Configuration Type
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct Config {
|
||||||
|
pub id: String;
|
||||||
|
pub some_field: i64;
|
||||||
|
pub some_list: Vec<String>;
|
||||||
|
pub some_map: HashMap<String, bool>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Make Shared Object
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let config: Rc<RefCell<Config>> = Rc::new(RefCell::(Default::default()));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register Config API
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Notice 'move' is used to move the shared configuration object into the closure.
|
||||||
|
let cfg = config.clone();
|
||||||
|
engine.register_fn("config_set_id", move |id: String| *cfg.borrow_mut().id = id);
|
||||||
|
|
||||||
|
let cfg = config.clone();
|
||||||
|
engine.register_fn("config_get_id", move || cfg.borrow().id.clone());
|
||||||
|
|
||||||
|
let cfg = config.clone();
|
||||||
|
engine.register_fn("config_set", move |value: i64| *cfg.borrow_mut().some_field = value);
|
||||||
|
|
||||||
|
// Remember Rhai functions can be overloaded when designing the API.
|
||||||
|
|
||||||
|
let cfg = config.clone();
|
||||||
|
engine.register_fn("config_add", move |value: String|
|
||||||
|
cfg.borrow_mut().some_list.push(value)
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg = config.clone();
|
||||||
|
engine.register_fn("config_add", move |values: &mut Array|
|
||||||
|
cfg.borrow_mut().some_list.extend(values.into_iter().map(|v| v.to_string()))
|
||||||
|
);
|
||||||
|
|
||||||
|
let cfg = config.clone();
|
||||||
|
engine.register_fn("config_add", move |key: String, value: bool|
|
||||||
|
cfg.borrow_mut().som_map.insert(key, value)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Script
|
||||||
|
|
||||||
|
```rust
|
||||||
|
------------------
|
||||||
|
| my_config.rhai |
|
||||||
|
------------------
|
||||||
|
|
||||||
|
config_set_id("hello");
|
||||||
|
|
||||||
|
config_add("foo"); // add to list
|
||||||
|
config_add("bar", true); // add to map
|
||||||
|
config_add("baz", false); // add to map
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load the Configuration
|
||||||
|
|
||||||
|
```rust
|
||||||
|
import "my_config"; // run configuration script without creating a module
|
||||||
|
|
||||||
|
let id = config_get_id();
|
||||||
|
|
||||||
|
id == "hello";
|
||||||
|
```
|
117
doc/src/patterns/control.md
Normal file
117
doc/src/patterns/control.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
Scriptable Control Layer
|
||||||
|
========================
|
||||||
|
|
||||||
|
{{#include ../links.md}}
|
||||||
|
|
||||||
|
|
||||||
|
Usage Scenario
|
||||||
|
--------------
|
||||||
|
|
||||||
|
* A system provides core functionalities, but no driving logic.
|
||||||
|
|
||||||
|
* The driving logic must be dynamic and hot-loadable.
|
||||||
|
|
||||||
|
* A script is used to drive the system and provide control intelligence.
|
||||||
|
|
||||||
|
|
||||||
|
Key Concepts
|
||||||
|
------------
|
||||||
|
|
||||||
|
* Expose a Control API.
|
||||||
|
|
||||||
|
* Since Rhai is _sand-boxed_, it cannot mutate the environment. To perform external actions via an API, the actual system must be wrapped in a `RefCell` (or `RwLock`/`Mutex` for [`sync`]) and shared to the [`Engine`].
|
||||||
|
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
--------------
|
||||||
|
|
||||||
|
### Functional API
|
||||||
|
|
||||||
|
Assume that a system provides the following functional API:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct EnergizerBunny;
|
||||||
|
|
||||||
|
impl EnergizerBunny {
|
||||||
|
pub fn new () -> Self { ... }
|
||||||
|
pub fn go (&mut self) { ... }
|
||||||
|
pub fn stop (&mut self) { ... }
|
||||||
|
pub fn is_going (&self) { ... }
|
||||||
|
pub fn get_speed (&self) -> i64 { ... }
|
||||||
|
pub fn set_speed (&mut self, speed: i64) { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wrap API in Shared Object
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let bunny: Rc<RefCell<EnergizerBunny>> = Rc::new(RefCell::(EnergizerBunny::new()));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register Control API
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Notice 'move' is used to move the shared API object into the closure.
|
||||||
|
let b = bunny.clone();
|
||||||
|
engine.register_fn("bunny_power", move |on: bool| {
|
||||||
|
if on {
|
||||||
|
if b.borrow().is_going() {
|
||||||
|
println!("Still going...");
|
||||||
|
} else {
|
||||||
|
b.borrow_mut().go();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if b.borrow().is_going() {
|
||||||
|
b.borrow_mut().stop();
|
||||||
|
} else {
|
||||||
|
println!("Already out of battery!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let b = bunny.clone();
|
||||||
|
engine.register_fn("bunny_is_going", move || b.borrow().is_going());
|
||||||
|
|
||||||
|
let b = bunny.clone();
|
||||||
|
engine.register_fn("bunny_get_speed", move ||
|
||||||
|
if b.borrow().is_going() { b.borrow().get_speed() } else { 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
let b = bunny.clone();
|
||||||
|
engine.register_result_fn("bunny_set_speed", move |speed: i64|
|
||||||
|
if speed <= 0 {
|
||||||
|
return Err("Speed must be positive!".into());
|
||||||
|
} else if speed > 100 {
|
||||||
|
return Err("Bunny will be going too fast!".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.borrow().is_going() {
|
||||||
|
b.borrow_mut().set_speed(speed)
|
||||||
|
} else {
|
||||||
|
return Err("Bunny is not yet going!".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(().into())
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use the API
|
||||||
|
|
||||||
|
```rust
|
||||||
|
if !bunny_is_going() { bunny_power(true); }
|
||||||
|
|
||||||
|
if bunny_get_speed() > 50 { bunny_set_speed(50); }
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Caveat
|
||||||
|
------
|
||||||
|
|
||||||
|
Although this usage pattern appears a perfect fit for _game_ logic, avoid writing the
|
||||||
|
_entire game_ in Rhai. Performance will not be acceptable.
|
||||||
|
|
||||||
|
Implement as much functionalities of the game engine in Rust as possible.
|
||||||
|
Rhai integrates well with Rust so this is usually not a hinderance.
|
||||||
|
|
||||||
|
Lift as much out of Rhai as possible.
|
||||||
|
Use Rhai only for the logic that _must_ be dynamic or hot-loadable.
|
9
doc/src/patterns/index.md
Normal file
9
doc/src/patterns/index.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
Advanced Patterns
|
||||||
|
=================
|
||||||
|
|
||||||
|
{{#include ../links.md}}
|
||||||
|
|
||||||
|
|
||||||
|
Use Rhai in different scenarios other than simply evaluating a user script.
|
||||||
|
|
||||||
|
These patterns are useful when Rhai needs to affect/control the external environment.
|
136
doc/src/patterns/singleton.md
Normal file
136
doc/src/patterns/singleton.md
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
Singleton Command Objects
|
||||||
|
========================
|
||||||
|
|
||||||
|
{{#include ../links.md}}
|
||||||
|
|
||||||
|
|
||||||
|
Usage Scenario
|
||||||
|
--------------
|
||||||
|
|
||||||
|
* A system provides core functionalities, but no driving logic.
|
||||||
|
|
||||||
|
* The driving logic must be dynamic and hot-loadable.
|
||||||
|
|
||||||
|
* A script is used to drive the system and provide control intelligence.
|
||||||
|
|
||||||
|
* The API is multiplexed, meaning that it can act on multiple system-provided entities, or
|
||||||
|
|
||||||
|
* The API lends itself readily to an object-oriented (OO) representation.
|
||||||
|
|
||||||
|
|
||||||
|
Key Concepts
|
||||||
|
------------
|
||||||
|
|
||||||
|
* Expose a Command type with an API.
|
||||||
|
|
||||||
|
* Since Rhai is _sand-boxed_, it cannot mutate the environment. To perform external actions via an API, the command object type must be wrapped in a `RefCell` (or `RwLock`/`Mutex` for [`sync`]) and shared to the [`Engine`].
|
||||||
|
|
||||||
|
* Load each command object into a custom [`Scope`] as constant variables.
|
||||||
|
|
||||||
|
* Control each command object in script via the constants.
|
||||||
|
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
--------------
|
||||||
|
|
||||||
|
### Functional API
|
||||||
|
|
||||||
|
Assume the following command object type:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct EnergizerBunny { ... }
|
||||||
|
|
||||||
|
impl EnergizerBunny {
|
||||||
|
pub fn new () -> Self { ... }
|
||||||
|
pub fn go (&mut self) { ... }
|
||||||
|
pub fn stop (&mut self) { ... }
|
||||||
|
pub fn is_going (&self) { ... }
|
||||||
|
pub fn get_speed (&self) -> i64 { ... }
|
||||||
|
pub fn set_speed (&mut self, speed: i64) { ... }
|
||||||
|
pub fn turn (&mut self, left_turn: bool) { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wrap Command Object Type as Shared
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let SharedBunnyType = Rc<RefCell<EnergizerBunny>>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register the Custom Type
|
||||||
|
|
||||||
|
```rust
|
||||||
|
engine.register_type_with_name::<SharedBunnyType>("EnergizerBunny");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Register Methods and Getters/Setters
|
||||||
|
|
||||||
|
```rust
|
||||||
|
engine
|
||||||
|
.register_get_set("power",
|
||||||
|
|bunny: &mut SharedBunnyType| bunny.borrow().is_going(),
|
||||||
|
|bunny: &mut SharedBunnyType, on: bool| {
|
||||||
|
if on {
|
||||||
|
if bunny.borrow().is_going() {
|
||||||
|
println!("Still going...");
|
||||||
|
} else {
|
||||||
|
bunny.borrow_mut().go();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if bunny.borrow().is_going() {
|
||||||
|
bunny.borrow_mut().stop();
|
||||||
|
} else {
|
||||||
|
println!("Already out of battery!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).register_get("speed", |bunny: &mut SharedBunnyType| {
|
||||||
|
if bunny.borrow().is_going() {
|
||||||
|
bunny.borrow().get_speed()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}).register_set_result("speed", |bunny: &mut SharedBunnyType, speed: i64| {
|
||||||
|
if speed <= 0 {
|
||||||
|
Err("Speed must be positive!".into())
|
||||||
|
} else if speed > 100 {
|
||||||
|
Err("Bunny will be going too fast!".into())
|
||||||
|
} else if !bunny.borrow().is_going() {
|
||||||
|
Err("Bunny is not yet going!".into())
|
||||||
|
} else {
|
||||||
|
b.borrow_mut().set_speed(speed);
|
||||||
|
Ok(().into())
|
||||||
|
}
|
||||||
|
}).register_fn("turn_left", |bunny: &mut SharedBunnyType| {
|
||||||
|
if bunny.borrow().is_going() {
|
||||||
|
bunny.borrow_mut().turn(true);
|
||||||
|
}
|
||||||
|
}).register_fn("turn_right", |bunny: &mut SharedBunnyType| {
|
||||||
|
if bunny.borrow().is_going() {
|
||||||
|
bunny.borrow_mut().turn(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Push Constant Command Object into Custom Scope
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let bunny: SharedBunnyType = Rc::new(RefCell::(EnergizerBunny::new()));
|
||||||
|
|
||||||
|
let mut scope = Scope::new();
|
||||||
|
scope.push_constant("BUNNY", bunny.clone());
|
||||||
|
|
||||||
|
engine.consume_with_scope(&mut scope, script)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use the Command API in Script
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Access the command object via constant variable 'BUNNY'.
|
||||||
|
|
||||||
|
if !BUNNY.power { BUNNY.power = true; }
|
||||||
|
|
||||||
|
if BUNNY.speed > 50 { BUNNY.speed = 50; }
|
||||||
|
|
||||||
|
BUNNY.turn_left();
|
||||||
|
```
|
Loading…
Reference in New Issue
Block a user