luau 0.732.0

Safe lifetime-bound Rust embedding API for the Luau runtime
use std::cell::Cell;
use std::rc::Rc;

use luau::{AnyUserdata, Error, Lua, ObjectLike, Userdata, UserdataFields, UserdataMethods};

struct Counter(i32);

impl Userdata for Counter {
    fn add_fields<F: UserdataFields<Self>>(fields: &mut F) {
        fields.add_field("kind", "counter");
        fields.add_field_method_get("value", luau::callback!(|_lua, counter| { Ok(counter.0) }));
        fields.add_field_method_set(
            "value",
            luau::callback!(|_lua, counter, value: i32| {
                counter.0 = value;
                Ok(())
            }),
        );
    }

    fn add_methods<M: UserdataMethods<Self>>(methods: &mut M) {
        methods.add_function(
            "new",
            luau::callback!(|_lua, value: i32| { Ok(Self(value)) }),
        );
        methods.add_method_mut(
            "add",
            luau::callback!(|_lua, counter, ..values| {
                counter.0 += values.iter_as::<i32>().sum::<Result<i32, _>>()?;
                Ok(counter.0)
            }),
        );
    }
}

struct Other;

impl Userdata for Other {}

struct Tracked(Rc<Cell<usize>>);

impl Userdata for Tracked {}

impl Drop for Tracked {
    fn drop(&mut self) {
        self.0.set(self.0.get() + 1);
    }
}

#[test]
fn userdata_borrows_validate_type_and_exclusivity() -> Result<(), Error> {
    let lua = Lua::new()?;
    let userdata = lua.create_userdata(Counter(42))?;

    assert!(userdata.is::<Counter>());
    assert!(!userdata.is::<Other>());
    assert!(matches!(
        userdata.borrow::<Other>(),
        Err(Error::UserdataTypeMismatch)
    ));

    {
        let first = userdata.borrow::<Counter>()?;
        let second = userdata.borrow::<Counter>()?;
        assert!(matches!(
            userdata.borrow_mut::<Counter>(),
            Err(Error::UserdataBorrowMutError)
        ));
        assert_eq!((first.0, second.0), (42, 42));
    }

    {
        let mut exclusive = userdata.borrow_mut::<Counter>()?;
        assert!(matches!(
            userdata.borrow::<Counter>(),
            Err(Error::UserdataBorrowError)
        ));
        exclusive.0 = 43;
    }
    assert_eq!(userdata.borrow::<Counter>()?.0, 43);

    Ok(())
}

#[test]
fn userdata_registration_dispatches_fields_methods_and_varargs() -> Result<(), Error> {
    let lua = Lua::new()?;
    let counter = lua.create_userdata(Counter(1))?;
    lua.globals()?.set("counter", &counter)?;

    assert_eq!(counter.get::<String>("kind")?, "counter");
    assert_eq!(
        lua.load(
            r#"
            counter.value = 10
            return counter:add(1, 5, -2)
            "#,
        )
        .eval::<i32>()?,
        14
    );
    assert_eq!(counter.borrow::<Counter>()?.0, 14);

    Ok(())
}

#[test]
fn mutable_userdata_borrows_invalidate_cached_imports() -> Result<(), Error> {
    let lua = Lua::new()?;
    let counter = lua.create_userdata(Counter(1))?;
    lua.globals()?.set("counter", &counter)?;
    lua.sandbox(true)?;

    let value = lua.load("return counter.value").into_function()?;
    assert_eq!(value.call::<i32>(())?, 1);

    {
        let mut counter = counter.borrow_mut::<Counter>()?;
        counter.0 = 2;
    }
    assert_eq!(value.call::<i32>(())?, 2);
    Ok(())
}

#[test]
fn userdata_take_and_destroy_require_exclusive_payload_access() -> Result<(), Error> {
    let lua = Lua::new()?;

    let taken = lua.create_userdata(Counter(18))?;
    let taken_alias = taken.try_clone()?;
    taken.set_user_value("attached")?;
    {
        let borrow = taken.borrow::<Counter>()?;
        assert!(matches!(
            taken.take::<Counter>(),
            Err(Error::UserdataBorrowMutError)
        ));
        assert_eq!(borrow.0, 18);
    }
    assert_eq!(taken.take::<Counter>()?.0, 18);
    assert!(matches!(
        taken.borrow::<Counter>(),
        Err(Error::UserdataDestructed)
    ));
    assert!(taken.equals(&taken_alias)?);
    assert_eq!(taken.user_value::<String>()?, "attached");

    let drops = Rc::new(Cell::new(0));
    let destroyed = lua.create_userdata(Tracked(Rc::clone(&drops)))?;
    {
        let _borrow = destroyed.borrow::<Tracked>()?;
        assert!(matches!(
            destroyed.destroy(),
            Err(Error::UserdataBorrowMutError)
        ));
    }
    destroyed.destroy()?;
    assert_eq!(drops.get(), 1);
    assert!(matches!(
        destroyed.borrow::<Tracked>(),
        Err(Error::UserdataDestructed)
    ));

    Ok(())
}

#[test]
fn userdata_user_value_cycles_are_collectable() -> Result<(), Error> {
    let lua = Lua::new()?;
    let drops = Rc::new(Cell::new(0));
    {
        let userdata = lua.create_userdata(Tracked(Rc::clone(&drops)))?;
        userdata.set_user_value(userdata.try_clone()?)?;
    }

    lua.gc_collect()?;
    lua.gc_collect()?;
    assert_eq!(drops.get(), 1);

    Ok(())
}

#[test]
fn userdata_proxy_exposes_static_members_but_not_instance_members() -> Result<(), Error> {
    let lua = Lua::new()?;
    let proxy = lua.create_proxy::<Counter>()?;

    assert!(proxy.is_proxy::<Counter>());
    assert_eq!(proxy.get::<String>("kind")?, "counter");
    assert!(proxy.get::<i32>("value").is_err());
    assert!(proxy.call_method::<i32>("add", 1).is_err());

    lua.globals()?.set("Counter", &proxy)?;
    let counter = lua
        .load("return Counter.new(321)")
        .eval::<AnyUserdata<'_>>()?;
    assert!(counter.is::<Counter>());
    assert_eq!(counter.get::<i32>("value")?, 321);
    assert_eq!(counter.call_method::<i32>("add", (1, 2))?, 324);

    Ok(())
}

#[test]
fn userdata_proxy_metamethods_preserve_relocated_stack_positions() -> Result<(), Error> {
    let lua = Lua::new()?;
    let proxy = lua.create_proxy::<Counter>()?;

    assert!(proxy.get::<i32>("value").is_err());
    assert!(proxy.set("value", 1).is_err());
    assert!(proxy.call_method::<i32>("add", 0).is_err());

    Ok(())
}