Passing External References to Rhai (Unsafe)

Don’t Do It™

As with anything unsafe, don’t do this unless you have exhausted all possible alternatives.

There are usually alternatives.

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.

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.

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

/// 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!
});

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). 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.