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/_archive/rhai_engine/rhaibook/patterns/references.md
2025-04-04 08:28:07 +02:00

165 lines
5.2 KiB
Markdown

Passing External References to Rhai (Unsafe)
============================================
{{#include ../links.md}}
[`transmute`]: https://doc.rust-lang.org/std/mem/fn.transmute.html
[`Engine::set_default_tag`]: https://docs.rs/rhai/{{version}}/rhai/struct.Engine.html#method.set_default_tag
```admonish danger "Don't Do It™"
As with anything `unsafe`, don't do this unless you have exhausted all possible alternatives.
There are usually alternatives.
```
```admonish info "Usage scenario"
* A system where an embedded [`Engine`] is used to call scripts within a callback function/closure
from some external system.
This is extremely common when working with an ECS (Entity Component System) or a GUI library where the script
must draw via the provided graphics context.
* Said external system provides only _references_ (mutable or immutable) to work with their internal states.
* Such states are not available outside of references (and therefore cannot be made shared).
* Said external system's API require such states to function.
* It is impossible to pass references into Rhai because the [`Engine`] does not track lifetimes.
```
```admonish abstract "Key concepts"
* Make a newtype that wraps an integer which is set to the CPU's pointer size (typically `usize`).
* [`transmute`] the reference provided by the system into an integer and store it within the newtype.
This requires `unsafe` code.
* Use the newtype as a _handle_ for all registered API functions, [`transmute`] the integer back
to a reference before use. This also requires `unsafe` code.
```
```admonish bug "Here be dragons..."
Make **absolutely sure** that the newtype is never stored anywhere permanent (e.g. in a [`Scope`])
nor does it ever live outside of the reference's scope!
Otherwise, a crash is the system being nice to you...
```
Example
-------
```rust
/// Newtype wrapping a reference (pointer) cast into 'usize'
/// together with a unique ID for protection.
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub struct WorldHandle(usize, i64);
/// Create handle from reference
impl From<&mut World> for WorldHandle {
fn from(world: &mut World) -> Self {
Self::new(world)
}
}
/// Recover reference from handle
impl AsMut<World> for WorldHandle {
fn as_mut(&mut self) -> &mut World {
unsafe { std::mem::transmute(self.0) }
}
}
impl WorldHandle {
/// Create handle from reference, using a random number as unique ID
pub fn new(world: &mut World) -> Self {
let handle =unsafe { std::mem::transmute(world) };
let unique_id = rand::random();
Self(handle, unique_id)
}
/// Get the unique ID of this instance
pub fn unique_id(&self) -> i64 {
self.1
}
}
/// API for handle to 'World'
#[export_module]
pub mod handle_module {
pub type Handle = WorldHandle;
/// Draw a bunch of pretty shapes.
#[rhai_fn(return_raw)]
pub fn draw(context: NativeCallContext, handle: &mut Handle, shapes: Array) -> Result<(), Box<EvalAltResult>> {
// Double check the pointer is still fresh
// by comparing the handle's unique ID with
// the version stored in the engine's tag!
if handle.unique_id() != context.tag() {
return "Ouch! The handle is stale!".into();
}
// Get the reference to 'World'
let world: &mut World = handle.as_mut();
// ... work with reference
world.draw_really_pretty_shapes(shapes);
Ok(())
}
}
// Register API for handle type
engine.register_global_module(exported_module!(handle_module).into());
// Successfully pass a reference to 'World' into Rhai
super_ecs_system.query(...).for_each(|world: &mut World| {
// Create handle from reference to 'World'
let handle: WorldHandle = world.into();
// Set the handle's ID into the engine's tag.
// Alternatively, use 'Engine::call_fn_with_options'.
engine.set_default_tag(handle.unique_id());
// Add handle into scope
let mut scope = Scope::new();
scope.push("world", handle);
// Work with handle
engine.run_with_scope(&mut scope, "world.draw([1, 2, 3])")
// Make sure no instance of 'handle' leaks outside this scope!
});
```
```admonish bug "Here be Many Dragons!"
It is of utmost importance that no instance of the handle newtype ever **_leaks_** outside of the
appropriate scope where the wrapped reference may no longer be valid.
For example, do not allow the script to store a copy of the handle anywhere that can
potentially be persistent (e.g. within an [object map]).
#### Safety check via unique ID
One solution, as illustrated in the example, is to always tag each handle instance together with
a unique random ID. That same ID can then be set into the [`Engine`] (via [`Engine::set_default_tag`])
before running scripts.
Before executing any API function, first check whether the handle's ID matches that of the current [`Engine`]
(via [`NativeCallContext::tag`][`NativeCallContext`]). If they differ, the handle is stale and should never be used;
an error should be returned instead.
#### Alternative to `Engine::set_default_tag`
Alternatively, if the [`Engine`] cannot be made mutable, use `Engine::call_fn_with_options`
to set the ID before directly calling a script [function] in a compiled [`AST`].
```