165 lines
5.2 KiB
Markdown
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`].
|
|
```
|