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(())
}