This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
rhaj/rhai_engine/rhaibook/engine/scope.md
2025-04-03 09:18:05 +02:00

216 lines
11 KiB
Markdown

`Scope` – Maintaining State
=================================
{{#include ../links.md}}
By default, Rhai treats each [`Engine`] invocation as a fresh one, persisting only the functions
that have been registered but no global state.
This gives each evaluation a clean starting slate.
In order to continue using the same global state from one invocation to the next, such a state
(a `Scope`) must be manually created and passed in.
All `Scope` [variables] and [constants] have values that are [`Dynamic`], meaning they can store
values of any type.
Under [`sync`], however, only types that are `Send + Sync` are supported, and the entire `Scope`
itself will also be `Send + Sync`. This is extremely useful in multi-threaded applications.
```admonish info.small "Shadowing"
A newly-added [variable] or [constant] _[shadows][shadow]_ previous ones of the same name.
In other words, all versions are kept for [variables] and [constants], but only the latest ones can
be accessed via `get_value<T>`, `get_mut<T>` and `set_value<T>`.
Essentially, a `Scope` is always searched in _reverse order_.
```
```admonish tip.small "Tip: The lifetime parameter"
`Scope` has a _lifetime_ parameter, in the vast majority of cases it can be omitted and
automatically inferred to be `'static`.
Currently, that lifetime parameter is not used. It is there to maintain backwards compatibility
as well as for possible future expansion when references can also be put into the `Scope`.
The lifetime parameter is not guaranteed to remain unused for future versions.
In order to put a `Scope` into a `struct`, use `Scope<'static>`.
```
~~~admonish tip.small "Tip: The `const` generic parameter"
`Scope` also has a _`const` generic_ parameter, which is a number that defaults to 8.
It indicates the number of entries that the `Scope` can keep _inline_ without allocations.
The larger this number, the larger the `Scope` type gets, but allocations will happen far
less frequently.
A smaller number makes `Scope` smaller, but allocation costs will be incurred when the
number of entries exceed the _inline_ capacity.
~~~
`Scope` API
-----------
| Method | Description |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `new` _instance method_ | create a new empty `Scope` |
| `with_capacity` _instance method_ | create a new empty `Scope` with a specified initial capacity |
| `len` | number of [variables]/[constants] currently within the `Scope` |
| `rewind` | _rewind_ (i.e. reset) the `Scope` to a particular number of [variables]/[constants] |
| `clear` | remove all [variables]/[constants] from the `Scope`, making it empty |
| `is_empty` | is the `Scope` empty? |
| `is_constant` | is the particular [variable]/[constant] in the `Scope` a [constant]? |
| `push`, `push_constant` | add a new [variable]/[constant] into the `Scope` with a specified value |
| `push_dynamic`, `push_constant_dynamic` | add a new [variable]/[constant] into the `Scope` with a [`Dynamic`] value |
| `set_or_push<T>` | set the value of the last [variable] within the `Scope` by name if it exists and is not [constant]; add a new [variable] into the `Scope` otherwise |
| `contains` | does the particular [variable] or [constant] exist in the `Scope`? |
| `get_value<T>` | get the value of the last [variable]/[constant] within the `Scope` by name |
| `set_value<T>` | set the value of the last [variable] within the `Scope` by name, panics if it is [constant] |
| `remove<T>` | remove the last [variable]/[constant] from the `Scope` by name, returning its value |
| `get` | get a reference to the value of the last [variable]/[constant] within the `Scope` by name |
| `get_mut` | get a reference to the value of the last [variable] within the `Scope` by name, `None` if it is [constant] |
| `set_alias` | [exported][`export`] the last [variable]/[constant] within the `Scope` by name |
| `iter`, `iter_raw`, `IntoIterator::into_iter` | get an iterator to the [variables]/[constants] within the `Scope` |
| `Extend::extend` | add [variables]/[constants] to the `Scope` |
~~~admonish info.small "`Scope` public API"
For details on the `Scope` API, refer to the
[documentation](https://docs.rs/rhai/{{version}}/rhai/struct.Scope.html) online.
~~~
Serializing/Deserializing
-------------------------
With the [`serde`] feature, `Scope` is serializable and deserializable via
[`serde`](https://crates.io/crates/serde).
[Custom types] stored in the `Scope`, however, are serialized as full type-name strings.
Data in [custom types] are not serialized.
Example
-------
In the following example, a `Scope` is created with a few initialized variables, then it is threaded
through multiple evaluations.
```rust
use rhai::{Engine, Scope, EvalAltResult};
let engine = Engine::new();
// First create the state
let mut scope = Scope::new();
// Then push (i.e. add) some initialized variables into the state.
// Remember the system number types in Rhai are i64 (i32 if 'only_i32')
// and f64 (f32 if 'f32_float').
// Better stick to them or it gets hard working with the script.
scope.push("y", 42_i64)
.push("z", 999_i64)
.push_constant("MY_NUMBER", 123_i64) // constants can also be added
.set_value("s", "hello, world!"); // 'set_value' adds a new variable when one doesn't exist
// First invocation
engine.run_with_scope(&mut scope,
"
let x = 4 + 5 - y + z + MY_NUMBER + s.len;
y = 1;
")?;
// Second invocation using the same state.
// Notice that the new variable 'x', defined previously, is still here.
let result = engine.eval_with_scope::<i64>(&mut scope, "x + y")?;
println!("result: {result}"); // prints 1103
// Variable y is changed in the script - read it with 'get_value'
assert_eq!(scope.get_value::<i64>("y").expect("variable y should exist"), 1);
// We can modify scope variables directly with 'set_value'
scope.set_value("y", 42_i64);
assert_eq!(scope.get_value::<i64>("y").expect("variable y should exist"), 42);
```
`Engine` API Using `Scope`
--------------------------
[`Engine`] API methods that accept a `Scope` parameter all end in `_with_scope`, making that
`Scope` (and everything inside it) available to the script:
| `Engine` API | Not available under |
| --------------------------------------- | :-----------------: |
| `Engine::eval_with_scope` | |
| `Engine::eval_ast_with_scope` | |
| `Engine::eval_file_with_scope` | [`no_std`] |
| `Engine::eval_expression_with_scope` | |
| `Engine::run_with_scope` | |
| `Engine::run_ast_with_scope` | |
| `Engine::run_file_with_scope` | [`no_std`] |
| `Engine::compile_file_with_scope` | [`no_std`] |
| `Engine::compile_expression_with_scope` | |
~~~admonish danger "Don't forget to `rewind`"
[Variables] or [constants] defined at the global level of a script persist inside the custom `Scope`
even after the script ends.
```rust
let mut scope = Scope::new();
engine.run_with_scope(&mut scope, "let x = 42;")?;
// Variable 'x' stays inside the custom scope!
engine.run_with_scope(&mut scope, "print(x);")?; // prints 42
```
Due to [variable shadowing][shadowing], new [variables]/[constants] are simply added on top of
existing ones (even when they already exist), so care must be taken that new [variables]/[constants]
inside the custom `Scope` do not grow without bounds.
```rust
let mut scope = Scope::new();
// Don't do this - this creates 1 million variables named 'x'
// inside 'scope'!!!
for _ in 0..1_000_000 {
engine.run_with_scope(&mut scope, "let x = 42;")?;
}
// The 'scope' contains a LOT of variables...
assert_eq!(scope.len(), 1_000_000);
// Variable 'x' stays inside the custom scope!
engine.run_with_scope(&mut scope, "print(x);")?; // prints 42
```
In order to remove [variables] or [constants] introduced by a script, use the `rewind` method.
```rust
// Run a million times
for _ in 0..1_000_000 {
// Save the current size of the 'scope'
let orig_scope_size = scope.len();
engine.run_with_scope(&mut scope, "let x = 42;")?;
// Rewind the 'scope' to the original size
scope.rewind(orig_scope_size);
}
// The 'scope' is empty
assert_eq!(scope.len(), 0);
// Variable 'x' is no longer inside 'scope'!
engine.run_with_scope(&mut scope, "print(x);")?; // error: variable 'x' not found
```
~~~