diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 3b23b321..a705b015 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -100,8 +100,12 @@ The Rhai Scripting Language 8. [Maximum Call Stack Depth](safety/max-call-stack.md) 9. [Maximum Statement Depth](safety/max-stmt-depth.md) 7. [Advanced Topics](advanced.md) - 1. [Capture Scope for Function Call](language/fn-capture.md) - 2. [Object-Oriented Programming (OOP)](language/oop.md) + 1. [Advanced Patterns](patterns/index.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) 4. [Script Optimization](engine/optimize/index.md) 1. [Optimization Levels](engine/optimize/optimize-levels.md) diff --git a/doc/src/advanced.md b/doc/src/advanced.md index 6b79fe70..0b9e170e 100644 --- a/doc/src/advanced.md +++ b/doc/src/advanced.md @@ -5,9 +5,9 @@ Advanced Topics 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. diff --git a/doc/src/language/object-maps-oop.md b/doc/src/language/object-maps-oop.md index c93f6847..0c2b840d 100644 --- a/doc/src/language/object-maps-oop.md +++ b/doc/src/language/object-maps-oop.md @@ -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 of using the `call` function keyword. -When a property holding a [function pointer] is called like a method, what happens next depends -on whether the target function is a native Rust function or a script-defined function. +When a property holding a [function pointer] (which incudes [closures]) is called like a method, +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 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. ```rust -fn do_action(x) { this.data += x; } // 'this' binds to the object when called - let obj = #{ 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 @@ -36,5 +35,7 @@ obj.data == 42; // To achieve the above with normal function pointer call will fail. 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 ``` diff --git a/doc/src/links.md b/doc/src/links.md index 11b16796..d2df1d5b 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -97,7 +97,7 @@ [`eval`]: {{rootUrl}}/language/eval.md -[OOP]: {{rootUrl}}/language/oop.md +[OOP]: {{rootUrl}}/patterns/oop.md [DSL]: {{rootUrl}}/engine/dsl.md [maximum statement depth]: {{rootUrl}}/safety/max-stmt-depth.md diff --git a/doc/src/patterns/config.md b/doc/src/patterns/config.md new file mode 100644 index 00000000..640191fa --- /dev/null +++ b/doc/src/patterns/config.md @@ -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; + pub some_map: HashMap; +} +``` + +### Make Shared Object + +```rust +let config: Rc> = 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"; +``` diff --git a/doc/src/patterns/control.md b/doc/src/patterns/control.md new file mode 100644 index 00000000..2f5aa961 --- /dev/null +++ b/doc/src/patterns/control.md @@ -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> = 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. diff --git a/doc/src/patterns/index.md b/doc/src/patterns/index.md new file mode 100644 index 00000000..522d3403 --- /dev/null +++ b/doc/src/patterns/index.md @@ -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. diff --git a/doc/src/language/oop.md b/doc/src/patterns/oop.md similarity index 100% rename from doc/src/language/oop.md rename to doc/src/patterns/oop.md diff --git a/doc/src/patterns/singleton.md b/doc/src/patterns/singleton.md new file mode 100644 index 00000000..7172e745 --- /dev/null +++ b/doc/src/patterns/singleton.md @@ -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>; +``` + +### Register the Custom Type + +```rust +engine.register_type_with_name::("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(); +```