use core::alloc::Layout;
use core::mem::size_of;
use luau_common::{BStr, ByteSlice, flags};
use luau_compiler::CompileOptions;
use luau_vm::internal::RawHandle;
use luau_vm::internal::debug::DebugRuntime;
use luau_vm::internal::gc::GcRuntime;
use luau_vm::lua::Lua;
use luau_vm::state::{LUA_OK, LuaAllocator, SystemLuaAllocator};
use luau_vm::thread::{
LUA_ENVIRON_INDEX, LUA_GLOBALS_INDEX, LUA_MULTRET, LUA_REGISTRY_INDEX, LUA_TBUFFER, LUA_TNIL,
LUA_TNONE, LUA_TNUMBER, LUA_TSTRING, LUA_TTABLE, LUA_TUSERDATA, LUAI_MAX_C_STACK, Thread,
};
use luau_vm::{
LUA_GC_COLLECT, LUA_GC_COUNT, LUA_GC_IS_RUNNING, LUA_GC_RESTART, LUA_GC_SET_GOAL,
LUA_GC_SET_STEP_MUL, LUA_GC_SET_STEP_SIZE, LUA_GC_STEP, LUA_GC_STOP, LuaDebug,
NativeCallContext, NativeCallResult, NativeFunction,
};
use luau_vm::{VmError, VmExit, VmResult};
use std::path::PathBuf;
use std::ptr::{self, NonNull};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering};
const TEST_ALLOC_ALIGN: usize = 16;
const FIXTURE_DIR: &str = "../luau-vm/tests/fixtures/conformance";
const GC_OPTIONS: &[&str] = &[
"stop",
"restart",
"collect",
"count",
"isrunning",
"step",
"setgoal",
"setstepmul",
"setstepsize",
];
const API_HELPERS: &[NativeFunction] = &[
NativeFunction {
name: "collectgarbage",
function: collect_garbage,
},
NativeFunction {
name: "loadstring",
function: load_string,
},
];
const SILENT_PRINT_HELPER: &[NativeFunction] = &[NativeFunction {
name: "print",
function: silence,
}];
static REFERENCE_DTOR_HITS: AtomicUsize = AtomicUsize::new(0);
static BLOCKABLE_REALLOC_ALLOWED: AtomicBool = AtomicBool::new(true);
static USERDATA_DTOR_HITS: AtomicI64 = AtomicI64::new(0);
fn opaque<T>(value: &mut T) -> *mut () {
(value as *mut T).cast()
}
#[derive(Clone, Copy)]
struct LimitedAllocator;
unsafe impl LuaAllocator for LimitedAllocator {
unsafe fn allocate(&self, layout: Layout) -> Option<NonNull<u8>> {
if layout.size() > 8 * 1024 * 1024 {
None
} else {
unsafe { SystemLuaAllocator.allocate(layout) }
}
}
unsafe fn reallocate(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Option<NonNull<u8>> {
if new_layout.size() > 8 * 1024 * 1024 {
None
} else {
unsafe { SystemLuaAllocator.reallocate(ptr, old_layout, new_layout) }
}
}
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
unsafe { SystemLuaAllocator.deallocate(ptr, layout) };
}
}
#[derive(Clone, Copy)]
struct AlignedAllocator;
impl AlignedAllocator {
fn layout(layout: Layout) -> Layout {
Layout::from_size_align(layout.size(), layout.align().max(TEST_ALLOC_ALIGN))
.expect("valid aligned test allocation layout")
}
}
unsafe impl LuaAllocator for AlignedAllocator {
unsafe fn allocate(&self, layout: Layout) -> Option<NonNull<u8>> {
unsafe { SystemLuaAllocator.allocate(Self::layout(layout)) }
}
unsafe fn reallocate(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Option<NonNull<u8>> {
if old_layout.align().max(TEST_ALLOC_ALIGN) == new_layout.align().max(TEST_ALLOC_ALIGN) {
unsafe {
SystemLuaAllocator.reallocate(
ptr,
Self::layout(old_layout),
Self::layout(new_layout),
)
}
} else {
let new_ptr = unsafe { self.allocate(new_layout)? };
unsafe {
ptr::copy_nonoverlapping(
ptr.as_ptr(),
new_ptr.as_ptr(),
old_layout.size().min(new_layout.size()),
);
self.deallocate(ptr, old_layout);
}
Some(new_ptr)
}
}
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
unsafe { SystemLuaAllocator.deallocate(ptr, Self::layout(layout)) };
}
}
#[derive(Clone, Copy)]
struct BlockableAllocator;
unsafe impl LuaAllocator for BlockableAllocator {
unsafe fn allocate(&self, layout: Layout) -> Option<NonNull<u8>> {
if BLOCKABLE_REALLOC_ALLOWED.load(Ordering::SeqCst) {
unsafe { SystemLuaAllocator.allocate(layout) }
} else {
None
}
}
unsafe fn reallocate(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Option<NonNull<u8>> {
if new_layout.size() <= old_layout.size()
|| BLOCKABLE_REALLOC_ALLOWED.load(Ordering::SeqCst)
{
unsafe { SystemLuaAllocator.reallocate(ptr, old_layout, new_layout) }
} else {
None
}
}
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
unsafe { SystemLuaAllocator.deallocate(ptr, layout) };
}
}
#[derive(Clone)]
struct CountingAllocator {
allocations: Arc<AtomicUsize>,
}
unsafe impl LuaAllocator for CountingAllocator {
unsafe fn allocate(&self, layout: Layout) -> Option<NonNull<u8>> {
let result = unsafe { SystemLuaAllocator.allocate(layout) };
if result.is_some() {
self.allocations.fetch_add(1, Ordering::SeqCst);
}
result
}
unsafe fn reallocate(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Option<NonNull<u8>> {
unsafe { SystemLuaAllocator.reallocate(ptr, old_layout, new_layout) }
}
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
unsafe { SystemLuaAllocator.deallocate(ptr, layout) };
}
}
fn reference_dtor(_: *mut ()) {
REFERENCE_DTOR_HITS.fetch_add(1, Ordering::SeqCst);
}
fn overflow_dtor(_: *mut ()) {}
fn userdata_tag_dtor(_: &Thread, data: *mut ()) {
USERDATA_DTOR_HITS.fetch_add(unsafe { *data.cast::<i32>() as i64 }, Ordering::SeqCst);
}
fn userdata_inline_i32_dtor(data: *mut ()) {
USERDATA_DTOR_HITS.fetch_add(unsafe { *data.cast::<i32>() as i64 }, Ordering::SeqCst);
}
fn userdata_inline_i8_dtor(data: *mut ()) {
USERDATA_DTOR_HITS.fetch_add(unsafe { *data.cast::<i8>() as i64 }, Ordering::SeqCst);
}
fn trigger_new_userdata_overflow(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe { thread.new_userdata_dtor(usize::MAX, overflow_dtor)? };
let _ = unsafe { thread.get_metatable(-1) };
Ok(0)
}
fn test_user_atom(_: &Thread, bytes: &BStr) -> i16 {
if bytes.as_bytes() == b"string" {
0
} else if bytes.as_bytes() == b"important" {
1
} else {
-1
}
}
fn collect_garbage(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
let (what, result) = unsafe {
let option = thread.check_option(1, Some("collect"), GC_OPTIONS)?;
let extra = thread.opt_integer(2, 0)?;
let what = match option {
0 => LUA_GC_STOP,
1 => LUA_GC_RESTART,
2 => LUA_GC_COLLECT,
3 => LUA_GC_COUNT,
4 => LUA_GC_IS_RUNNING,
5 => LUA_GC_STEP,
6 => LUA_GC_SET_GOAL,
7 => LUA_GC_SET_STEP_MUL,
8 => LUA_GC_SET_STEP_SIZE,
_ => unreachable!(),
};
(what, thread.gc(what, extra)?)
};
unsafe {
match what {
LUA_GC_STEP | LUA_GC_IS_RUNNING => thread.push_boolean(result)?,
_ => thread.push_number(result as f64)?,
}
}
Ok(1)
}
fn load_string(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
let source = thread.check_string(1)?;
let chunk_name = thread.opt_string(2)?.unwrap_or(source);
thread.set_safe_env(LUA_ENVIRON_INDEX, 0);
let bytecode = luau_compiler::compile_bytes(source, CompileOptions::default());
let result = thread.load(chunk_name, &bytecode, 0);
if result.is_ok() {
return Ok(1);
}
thread.push_nil()?;
thread.insert(-2);
Ok(2)
}
}
fn silence(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
let count = thread.get_top();
let mut sink = std::io::sink();
for index in 1..=count {
let string = thread.lua_to_string(index)?;
if index > 1 {
let _ = std::io::Write::write_all(&mut sink, b"\t");
}
let _ = std::io::Write::write_all(&mut sink, string.as_bytes());
thread.pop(1);
}
let _ = std::io::Write::write_all(&mut sink, b"\n");
Ok(0)
}
}
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(FIXTURE_DIR)
.join(name)
}
fn read_fixture(name: &str) -> Vec<u8> {
std::fs::read(fixture_path(name)).expect("conformance fixture should exist")
}
fn thread_bstr(thread: &Thread, index: i32) -> Option<&BStr> {
unsafe { thread.to_string(index) }.expect("test string conversion should not fail")
}
fn thread_message(thread: &Thread, index: i32) -> String {
thread_bstr(thread, index)
.map(|bytes| String::from_utf8_lossy(bytes.as_bytes()).into_owned())
.unwrap_or_else(|| "<non-string error>".to_owned())
}
fn register_helpers(thread: &Thread) {
unsafe { thread.push_value(LUA_GLOBALS_INDEX) }.expect("globals table should push");
unsafe { thread.register(None, API_HELPERS) }.expect("api helpers should register");
unsafe { thread.register(None, SILENT_PRINT_HELPER) }.expect("print helper should register");
unsafe { thread.pop(1) };
}
fn run_state(name: &str, allocator: impl LuaAllocator + 'static) -> Lua {
let source = read_fixture(name);
let compile_options = CompileOptions {
debug_level: 1,
optimization_level: 1,
type_info_level: 1,
..CompileOptions::default()
};
let bytecode = luau_compiler::compile_bytes(&source, compile_options);
let lua = Lua::new_with_allocator(allocator).expect("Lua::new_with_allocator should succeed");
let thread = lua.main_thread();
unsafe { thread.open_libs() }.expect("api libraries should open");
register_helpers(thread);
if cfg!(debug_assertions) {
unsafe {
thread
.push_boolean(1)
.expect("limitedstack flag should push");
thread
.set_field(LUA_GLOBALS_INDEX, "limitedstack")
.expect("limitedstack flag should set");
}
}
unsafe {
thread.sandbox().expect("api state should sandbox");
thread.sandbox_thread().expect("api thread should sandbox");
thread
.push_value(LUA_GLOBALS_INDEX)
.expect("globals table should push");
thread.set_field(-1, "_G").expect("_G should set");
}
let load_result = unsafe { thread.load(name, &bytecode, 0) };
let status = if load_result.is_ok() {
unsafe { thread.resume(None, 0) }
} else {
Err(VmExit::Error(VmError::Syntax))
};
unsafe { thread.validate() };
assert_eq!(status, Ok(()), "{}", thread_message(thread, -1));
assert_eq!(unsafe { thread.type_of(-1) }, LUA_TSTRING);
assert_eq!(thread_bstr(thread, -1).unwrap().as_bytes(), b"OK");
unsafe { thread.pop(1) };
lua
}
fn slowly_overflow_stack(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
for _ in 0..LUAI_MAX_C_STACK * 2 {
unsafe { thread.lua_check_stack(1, Some("test"))? };
unsafe { thread.push_number(1.0)? };
}
Ok(0)
}
fn cpcall_test(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
unsafe {
let should_fail = *thread.to_light_userdata(1).cast::<bool>();
if should_fail {
luau_vm::run_error!(thread, "Failed").map_err(Into::into)
} else {
thread.push_integer(123)?;
thread.set_field(LUA_GLOBALS_INDEX, "cpcallvalue")?;
Ok(0)
}
}
}
fn assert_not_yieldable(ctx: NativeCallContext) -> NativeCallResult {
let thread = ctx.raw_thread();
if unsafe { thread.is_yieldable() } != 0 {
return unsafe { luau_vm::run_error!(thread, "lua_resume made thread yieldable") }
.map_err(Into::into);
}
Ok(0)
}
#[test]
fn reference() {
REFERENCE_DTOR_HITS.store(0, Ordering::SeqCst);
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.new_userdata_dtor(0, reference_dtor) }.expect("userdata should allocate");
unsafe { thread.new_userdata_dtor(0, reference_dtor) }.expect("userdata should allocate");
assert_eq!(unsafe { thread.gc(LUA_GC_COLLECT, 0) }, Ok(0));
assert_eq!(REFERENCE_DTOR_HITS.load(Ordering::SeqCst), 0);
let reference = unsafe { thread.ref_value(-2) }.expect("reference should create");
unsafe { thread.pop(2) };
assert_eq!(unsafe { thread.gc(LUA_GC_COLLECT, 0) }, Ok(0));
assert_eq!(REFERENCE_DTOR_HITS.load(Ordering::SeqCst), 1);
assert_eq!(
unsafe { thread.get_ref(reference) }.expect("reference should read"),
LUA_TUSERDATA
);
unsafe { thread.pop(1) };
assert_eq!(unsafe { thread.gc(LUA_GC_COLLECT, 0) }, Ok(0));
assert_eq!(REFERENCE_DTOR_HITS.load(Ordering::SeqCst), 1);
unsafe { thread.unref_value(reference) };
assert_eq!(unsafe { thread.gc(LUA_GC_COLLECT, 0) }, Ok(0));
assert_eq!(REFERENCE_DTOR_HITS.load(Ordering::SeqCst), 2);
}
#[test]
fn sandbox_without_libs() {
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
assert_eq!(
unsafe { thread.open_base() }.expect("base library should open"),
1
);
unsafe { thread.pop(1) };
unsafe { thread.sandbox() }.expect("state should sandbox");
assert_eq!(unsafe { thread.get_readonly(LUA_GLOBALS_INDEX) }, 1);
}
#[test]
fn api_tables() {
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
let mut lu1 = 1i32;
let mut lu2 = 2i32;
unsafe { thread.create_table(0, 0) }.expect("table should allocate");
unsafe { thread.push_number(123.0) }.expect("number should push");
unsafe { thread.set_field(-2, "key") }.expect("field should set");
unsafe { thread.push_number(456.0) }.expect("number should push");
unsafe { thread.raw_set_field(-2, "key2") }.expect("raw field should set");
unsafe { thread.push_optional_string(Some("key3")) }.expect("string should push");
unsafe { thread.raw_seti(-2, 5) }.expect("raw index should set");
unsafe { thread.push_optional_string(Some("key4")) }.expect("string should push");
unsafe { thread.raw_setp_tagged(-2, opaque(&mut lu1), 0) }.expect("raw pointer should set");
unsafe { thread.push_optional_string(Some("key5")) }.expect("string should push");
unsafe { thread.raw_setp_tagged(-2, opaque(&mut lu2), 1) }.expect("raw pointer should set");
unsafe { thread.push_optional_string(Some("key6")) }.expect("string should push");
unsafe { thread.raw_setp_tagged(-2, opaque(&mut lu2), 2) }.expect("raw pointer should set");
unsafe { thread.push_optional_string(Some("key")) }.expect("string should push");
assert_eq!(
unsafe { thread.get_table(-2) }.expect("table read should succeed"),
LUA_TNUMBER
);
assert_eq!(unsafe { thread.to_number(-1) }, Some(123.0));
unsafe { thread.pop(1) };
assert_eq!(
unsafe { thread.get_field(-1, "key") }.expect("field read should succeed"),
LUA_TNUMBER
);
assert_eq!(unsafe { thread.to_number(-1) }, Some(123.0));
unsafe { thread.pop(1) };
assert_eq!(
unsafe { thread.raw_get_field(-1, "key2") }.expect("raw field read should succeed"),
LUA_TNUMBER
);
assert_eq!(unsafe { thread.to_number(-1) }, Some(456.0));
unsafe { thread.pop(1) };
unsafe { thread.push_optional_string(Some("key")) }.expect("string should push");
assert_eq!(unsafe { thread.raw_get(-2) }, LUA_TNUMBER);
assert_eq!(unsafe { thread.to_number(-1) }, Some(123.0));
unsafe { thread.pop(1) };
assert_eq!(
unsafe { thread.raw_geti(-1, 5) }.expect("raw index read should succeed"),
LUA_TSTRING
);
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"key3"
);
unsafe { thread.pop(1) };
assert_eq!(
unsafe { thread.raw_getp_tagged(-1, opaque(&mut lu1), 0) }
.expect("raw pointer read should succeed"),
LUA_TSTRING
);
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"key4"
);
unsafe { thread.pop(1) };
assert_eq!(
unsafe { thread.raw_getp_tagged(-1, opaque(&mut lu2), 1) }
.expect("raw pointer read should succeed"),
LUA_TSTRING
);
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"key5"
);
unsafe { thread.pop(1) };
assert_eq!(
unsafe { thread.raw_getp_tagged(-1, opaque(&mut lu2), 2) }
.expect("raw pointer read should succeed"),
LUA_TSTRING
);
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"key6"
);
unsafe { thread.pop(1) };
assert_eq!(
unsafe { thread.raw_getp_tagged(-1, opaque(&mut lu2), 0) }
.expect("raw pointer read should succeed"),
LUA_TNIL
);
unsafe { thread.pop(1) };
unsafe { thread.clone_table(-1) }.expect("table should clone");
assert_eq!(
unsafe { thread.get_field(-1, "key") }.expect("field read should succeed"),
LUA_TNUMBER
);
assert_eq!(unsafe { thread.to_number(-1) }, Some(123.0));
unsafe { thread.pop(1) };
unsafe { thread.push_number(456.0) }.expect("number should push");
unsafe { thread.raw_set_field(-2, "key") }.expect("raw field should set");
unsafe { thread.pop(1) };
assert_eq!(
unsafe { thread.get_field(-1, "key") }.expect("field read should succeed"),
LUA_TNUMBER
);
assert_eq!(unsafe { thread.to_number(-1) }, Some(123.0));
unsafe { thread.pop(1) };
unsafe { thread.clear_table(-1) }.expect("table should clear");
unsafe { thread.push_nil() }.expect("nil should push");
assert_eq!(
unsafe { thread.next(-2) }.expect("table iteration should succeed"),
0
);
unsafe { thread.pop(1) };
}
#[test]
fn get_global_uses_table_lookup() {
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.create_table(0, 1) }.expect("fallback table should allocate");
unsafe { thread.push_integer(42) }.expect("integer should push");
unsafe { thread.set_field(-2, "answer") }.expect("fallback field should set");
unsafe { thread.create_table(0, 1) }.expect("metatable should allocate");
unsafe { thread.push_value(-2) }.expect("fallback table should push");
unsafe { thread.set_field(-2, "__index") }.expect("index metamethod should set");
unsafe { thread.push_value(LUA_GLOBALS_INDEX) }.expect("globals table should push");
unsafe { thread.push_value(-2) }.expect("metatable should push");
assert_eq!(
unsafe { thread.set_metatable(-2) }.expect("globals metatable should set"),
1
);
unsafe { thread.pop(3) };
assert_eq!(
unsafe { thread.get_global("answer") }.expect("global lookup should succeed"),
LUA_TNUMBER
);
assert_eq!(unsafe { thread.to_integer(-1) }, Some(42));
unsafe { thread.pop(1) };
}
#[test]
fn new_userdata_overflow() {
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.push_native_closure_k(trigger_new_userdata_overflow, None, 0, None) }
.expect("overflow trigger should push");
assert_eq!(
unsafe { thread.protected_call(0, 0, 0) },
Err(VmExit::Error(VmError::Runtime))
);
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"memory allocation error: block too big"
);
}
#[test]
fn api_iter() {
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.create_table(0, 0) }.expect("table should allocate");
unsafe { thread.push_number(123.0) }.expect("number should push");
unsafe { thread.set_field(-2, "key") }.expect("field should set");
unsafe { thread.push_number(456.0) }.expect("number should push");
unsafe { thread.raw_set_field(-2, "key2") }.expect("raw field should set");
unsafe { thread.push_optional_string(Some("test")) }.expect("string should push");
unsafe { thread.raw_seti(-2, 1) }.expect("raw index should set");
let mut sum1 = 0.0;
unsafe { thread.push_nil() }.expect("nil should push");
while unsafe { thread.next(-2) }.expect("table iteration should succeed") != 0 {
sum1 += unsafe { thread.to_number(-2) }.unwrap_or(0.0);
sum1 += unsafe { thread.to_number(-1) }.unwrap_or(0.0);
unsafe { thread.pop(1) };
}
assert_eq!(sum1, 580.0);
let mut sum2 = 0.0;
let mut index = 0;
loop {
index = unsafe { thread.raw_iter(-1, index) }.expect("raw iteration should succeed");
if index < 0 {
break;
}
sum2 += unsafe { thread.to_number(-2) }.unwrap_or(0.0);
sum2 += unsafe { thread.to_number(-1) }.unwrap_or(0.0);
unsafe { thread.pop(2) };
}
assert_eq!(sum2, 580.0);
unsafe { thread.set_top(18) }.expect("stack top should grow");
unsafe { thread.push_value(1) }.expect("stack value should push");
assert_eq!(unsafe { thread.get_top() }, 19);
assert_eq!(unsafe { thread.check_stack(2) }, 1);
let mut sum3 = 0.0;
let mut index = 0;
loop {
index = unsafe { thread.raw_iter(-1, index) }.expect("raw iteration should succeed");
if index < 0 {
break;
}
sum3 += unsafe { thread.to_number(-2) }.unwrap_or(0.0);
sum3 += unsafe { thread.to_number(-1) }.unwrap_or(0.0);
unsafe { thread.pop(2) };
}
assert_eq!(sum3, 580.0);
unsafe { thread.pop(19) };
}
#[test]
fn api_atoms() {
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { (*thread.global().callbacks()).user_atom = Some(test_user_atom) };
unsafe { thread.push_optional_string(Some("string")) }.expect("string should push");
unsafe { thread.push_optional_string(Some("import")) }.expect("string should push");
unsafe { thread.push_optional_string(Some("ant")) }.expect("string should push");
unsafe { thread.concat(2) }.expect("strings should concatenate");
unsafe { thread.push_optional_string(Some("unimportant")) }.expect("string should push");
let (s1, a1) = unsafe { thread.to_string_atom(-3) }.unwrap();
let (s2, a2) = unsafe { thread.to_string_atom(-2) }.unwrap();
let (s3, a3) = unsafe { thread.to_string_atom(-1) }.unwrap();
assert_eq!(s1.as_bytes(), b"string");
assert_eq!(a1, 0);
assert_eq!(s2.as_bytes(), b"important");
assert_eq!(a2, 1);
assert_eq!(s3.as_bytes(), b"unimportant");
assert_eq!(a3, -1);
}
#[test]
fn api_type() {
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.push_number(2.0) }.expect("number should push");
assert_eq!(unsafe { thread.lua_type_name(-1) }.as_bytes(), b"number");
assert_eq!(unsafe { thread.lua_type_name(1) }.as_bytes(), b"number");
assert_eq!(unsafe { thread.type_of(-1) }, LUA_TNUMBER);
assert_eq!(unsafe { thread.type_of(1) }, LUA_TNUMBER);
assert_eq!(unsafe { thread.lua_type_name(2) }.as_bytes(), b"no value");
assert_eq!(unsafe { thread.type_of(2) }, LUA_TNONE);
assert_eq!(
unsafe { thread.type_name(thread.type_of(2)) }.as_bytes(),
b"no value"
);
unsafe { thread.new_userdata_tagged(0, 0) }.expect("userdata should allocate");
assert_eq!(unsafe { thread.lua_type_name(-1) }.as_bytes(), b"userdata");
assert_eq!(unsafe { thread.type_of(-1) }, LUA_TUSERDATA);
unsafe { thread.create_table(0, 0) }.expect("table should allocate");
unsafe { thread.push_optional_string(Some("hello")) }.expect("string should push");
unsafe { thread.set_field(-2, "__type") }.expect("field should set");
assert_eq!(
unsafe { thread.set_metatable(-2) }.expect("metatable should set"),
1
);
assert_eq!(unsafe { thread.lua_type_name(-1) }.as_bytes(), b"hello");
assert_eq!(unsafe { thread.type_of(-1) }, LUA_TUSERDATA);
}
#[test]
fn api_buffer() {
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.new_buffer(1000) }.expect("buffer should allocate");
assert_eq!(unsafe { thread.type_of(-1) }, LUA_TBUFFER);
assert!(unsafe { thread.to_buffer(-1) }.is_some());
assert_eq!(unsafe { thread.obj_len(-1) }, 1000);
assert_eq!(
unsafe { thread.type_name(LUA_TBUFFER) }.as_bytes(),
b"buffer"
);
assert_eq!(unsafe { thread.lua_type_name(-1) }.as_bytes(), b"buffer");
let (p1, len) = unsafe { thread.to_buffer(-1) }.unwrap();
assert_eq!(len, 1000);
let (p2, len) = unsafe { thread.to_buffer(-1) }.unwrap();
assert_eq!(len, 1000);
assert_eq!(p1, p2);
let (p3, len) = unsafe { thread.check_buffer(-1) }.expect("buffer should check");
assert_eq!(len, 1000);
assert_eq!(p1, p3);
unsafe { ptr::write_bytes(p1, 0xab, 1000) };
assert!(!unsafe { thread.to_pointer(-1) }.is_null());
unsafe { thread.new_buffer(0) }.expect("buffer should allocate");
unsafe { thread.push_value(-2) }.expect("buffer should push");
assert_eq!(
unsafe { thread.equal(-3, -1) }.expect("buffers should compare"),
1
);
assert_eq!(
unsafe { thread.equal(-2, -1) }.expect("buffers should compare"),
0
);
unsafe { thread.pop(1) };
}
#[test]
fn api_stack() {
BLOCKABLE_REALLOC_ALLOWED.store(true, Ordering::SeqCst);
let lua = Lua::new_with_allocator(BlockableAllocator)
.expect("Lua::new_with_allocator should succeed");
let global = lua.main_thread();
{
let thread = unsafe { global.new_thread() }.expect("thread should allocate");
unsafe { thread.push_native_closure_k(slowly_overflow_stack, None, 0, None) }
.expect("overflow closure should push");
let result = unsafe { thread.protected_call(0, 0, 0) };
assert_eq!(result, Err(VmExit::Error(VmError::Runtime)));
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"stack overflow (test)"
);
}
{
let thread = unsafe { global.new_thread() }.expect("thread should allocate");
assert_eq!(unsafe { thread.check_stack(100) }, 1);
BLOCKABLE_REALLOC_ALLOWED.store(false, Ordering::SeqCst);
assert_eq!(unsafe { thread.check_stack(1000) }, 0);
BLOCKABLE_REALLOC_ALLOWED.store(true, Ordering::SeqCst);
assert_eq!(unsafe { thread.check_stack(1000) }, 1);
assert_eq!(unsafe { thread.check_stack(LUAI_MAX_C_STACK * 2) }, 0);
}
}
#[test]
fn api_alloc() {
let allocations = Arc::new(AtomicUsize::new(0));
let lua = Lua::new_with_allocator(CountingAllocator {
allocations: Arc::clone(&allocations),
})
.expect("Lua::new_with_allocator should succeed");
assert!(allocations.load(Ordering::SeqCst) > 0);
drop(lua);
}
#[test]
#[allow(clippy::approx_constant)] fn api_calls() -> VmResult {
let lua = run_state("apicalls.luau", LimitedAllocator);
let thread = lua.main_thread();
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "add")? };
unsafe { thread.push_number(40.0)? };
unsafe { thread.push_number(2.0)? };
unsafe { thread.call(2, 1)? };
assert_eq!(unsafe { thread.type_of(-1) }, LUA_TNUMBER);
assert_eq!(unsafe { thread.to_number(-1) }, Some(42.0));
unsafe { thread.pop(1) };
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "getnresults")? };
unsafe { thread.push_integer(200)? };
unsafe { thread.call(1, LUA_MULTRET)? };
assert_eq!(unsafe { thread.get_top() }, 200);
unsafe { thread.pop(200) };
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "add")? };
unsafe { thread.push_number(40.0)? };
unsafe { thread.push_number(2.0)? };
let status = unsafe { thread.protected_call(2, 1, 0) };
assert_eq!(status, Ok(()));
assert_eq!(unsafe { thread.type_of(-1) }, LUA_TNUMBER);
assert_eq!(unsafe { thread.to_number(-1) }, Some(42.0));
unsafe { thread.pop(1) };
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "getnresults")? };
unsafe { thread.push_integer(200)? };
let status = unsafe { thread.protected_call(1, LUA_MULTRET, 0) };
assert_eq!(status, Ok(()));
assert_eq!(unsafe { thread.get_top() }, 200);
unsafe { thread.pop(200) };
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "pcall")? };
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "getnresults")? };
unsafe { thread.push_integer(200)? };
unsafe { thread.call(2, LUA_MULTRET)? };
assert_eq!(unsafe { thread.get_top() }, 201);
unsafe { thread.pop(200) };
assert_eq!(unsafe { thread.to_boolean(-1) }, 1);
unsafe { thread.pop(1) };
{
let mut should_fail = false;
assert_eq!(
unsafe { thread.protected_native_call(cpcall_test, opaque(&mut should_fail)) },
Ok(())
);
assert_eq!(unsafe { thread.status() }, LUA_OK);
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "cpcallvalue")? };
assert_eq!(
unsafe { thread.check_integer(1) }.expect("integer should check"),
123
);
unsafe { thread.pop(1) };
}
{
let mut should_fail = true;
assert_eq!(
unsafe { thread.protected_native_call(cpcall_test, opaque(&mut should_fail)) },
Err(VmExit::Error(VmError::Runtime))
);
assert_eq!(unsafe { thread.type_of(-1) }, LUA_TSTRING);
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"Failed"
);
unsafe { thread.pop(1) };
assert_eq!(unsafe { thread.status() }, LUA_OK);
}
{
let mut should_fail = false;
assert_eq!(unsafe { thread.get_top() }, 0);
unsafe { thread.lua_check_stack(LUAI_MAX_C_STACK - 1, Some("must succeed"))? };
for _ in 0..LUAI_MAX_C_STACK - 1 {
unsafe { thread.push_number(1.0)? };
}
assert_eq!(
unsafe { thread.protected_native_call(cpcall_test, opaque(&mut should_fail)) },
Err(VmExit::Error(VmError::Runtime))
);
assert_eq!(unsafe { thread.type_of(-1) }, LUA_TSTRING);
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"stack limit"
);
unsafe { thread.pop(1) };
assert_eq!(unsafe { thread.status() }, LUA_OK);
unsafe { thread.pop(LUAI_MAX_C_STACK - 1) };
}
{
let child = unsafe { thread.new_thread() }.expect("child thread should allocate");
unsafe { child.push_native_closure_k(assert_not_yieldable, None, 0, None) }
.expect("callback should push");
unsafe { child.call(0, 0)? };
unsafe { child.get_field(LUA_GLOBALS_INDEX, "getnresults") }.expect("global should read");
unsafe { child.push_integer(1) }.expect("integer should push");
let status = unsafe { child.resume(None, 1) };
assert_eq!(status, Ok(()));
assert_eq!(unsafe { child.get_top() }, 1);
unsafe { child.pop(1) };
unsafe { child.push_native_closure_k(assert_not_yieldable, None, 0, None) }
.expect("callback should push");
unsafe { child.call(0, 0)? };
unsafe { thread.pop(1) };
}
{
let child = unsafe { thread.new_thread() }.expect("child thread should allocate");
unsafe { child.get_field(LUA_GLOBALS_INDEX, "create_with_tm") }
.expect("global should read");
unsafe { child.push_number(42.0) }.expect("number should push");
assert_eq!(unsafe { child.protected_call(1, 1, 0) }, Ok(()));
unsafe { child.get_field(LUA_GLOBALS_INDEX, "create_with_tm") }
.expect("global should read");
unsafe { child.push_number(42.0) }.expect("number should push");
assert_eq!(unsafe { child.protected_call(1, 1, 0) }, Ok(()));
unsafe { child.gc(LUA_GC_COLLECT, 0)? };
unsafe { child.gc(LUA_GC_STEP, 8)? };
assert_eq!(
unsafe { child.equal(-1, -2) }.expect("objects should compare"),
1
);
unsafe { child.pop(2) };
unsafe { thread.pop(1) };
}
{
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "getpi")? };
unsafe { thread.call(0, 1)? };
assert_eq!(unsafe { thread.to_number(-1) }, Some(3.1415926));
unsafe { thread.pop(1) };
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "getpi")? };
unsafe { thread.clone_function(-1)? };
unsafe { thread.create_table(0, 0)? };
unsafe { thread.push_number(42.0)? };
unsafe { thread.set_field(-2, "pi")? };
assert_eq!(unsafe { thread.set_fenv(-2) }, 1);
unsafe { thread.call(0, 1)? };
assert_eq!(unsafe { thread.to_number(-1) }, Some(42.0));
unsafe { thread.pop(1) };
unsafe { thread.call(0, 1)? };
assert_eq!(unsafe { thread.to_number(-1) }, Some(3.1415926));
unsafe { thread.pop(1) };
}
{
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "incuv")? };
unsafe { thread.call(0, 1)? };
assert_eq!(unsafe { thread.to_number(-1) }, Some(1.0));
unsafe { thread.pop(1) };
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "incuv")? };
unsafe { thread.clone_function(-1)? };
unsafe { thread.clone_function(-2)? };
unsafe { thread.call(0, 1)? };
assert_eq!(unsafe { thread.to_number(-1) }, Some(2.0));
unsafe { thread.pop(1) };
unsafe { thread.call(0, 1)? };
assert_eq!(unsafe { thread.to_number(-1) }, Some(3.0));
unsafe { thread.pop(1) };
unsafe { thread.call(0, 1)? };
assert_eq!(unsafe { thread.to_number(-1) }, Some(4.0));
unsafe { thread.pop(1) };
}
{
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "largealloc")? };
let result = unsafe { thread.protected_call(0, 0, 0) };
assert_eq!(result, Err(VmExit::Error(VmError::Memory)));
unsafe { thread.pop(1) };
}
{
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "oops")? };
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "largealloc")? };
let result = unsafe { thread.protected_call(0, 1, -2) };
assert_eq!(result, Err(VmExit::Error(VmError::Memory)));
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"oops"
);
unsafe { thread.pop(2) };
}
{
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "error")? };
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "largealloc")? };
let result = unsafe { thread.protected_call(0, 1, -2) };
assert_eq!(result, Err(VmExit::Error(VmError::ErrorHandler)));
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"error in error handling"
);
unsafe { thread.pop(2) };
}
{
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "largealloc")? };
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "largealloc")? };
let result = unsafe { thread.protected_call(0, 1, -2) };
assert_eq!(result, Err(VmExit::Error(VmError::Memory)));
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"not enough memory"
);
unsafe { thread.pop(2) };
}
{
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "largealloc")? };
unsafe { thread.get_field(LUA_GLOBALS_INDEX, "error")? };
let result = unsafe { thread.protected_call(0, 1, -2) };
assert_eq!(result, Err(VmExit::Error(VmError::ErrorHandler)));
assert_eq!(
unsafe { thread.to_string(-1) }
.expect("string conversion should succeed")
.unwrap()
.as_bytes(),
b"error in error handling"
);
unsafe { thread.pop(2) };
}
assert_eq!(unsafe { thread.get_top() }, 0);
Ok(())
}
#[test]
fn debug_api() {
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.push_number(10.0) }.expect("number should push");
let mut ar = LuaDebug::default();
assert_eq!(
unsafe { thread.get_info(-1, "f", &mut ar) }.expect("debug lookup should run"),
0
);
assert_eq!(
unsafe { thread.get_info(-10, "f", &mut ar) }.expect("debug lookup should run"),
0
);
}
#[test]
fn userdata_api() {
USERDATA_DTOR_HITS.store(0, Ordering::SeqCst);
{
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
assert!(unsafe { thread.get_userdata_dtor(42) }.is_none());
unsafe { thread.set_userdata_dtor(42, userdata_tag_dtor) };
let dtor = unsafe { thread.get_userdata_dtor(42) };
assert!(dtor.is_some());
#[cfg(not(miri))]
assert!(
dtor.is_some_and(|dtor| ptr::fn_addr_eq(
dtor,
userdata_tag_dtor as fn(&Thread, *mut ())
))
);
let mut light_userdata = 0i32;
unsafe { thread.push_light_userdata_tagged(opaque(&mut light_userdata), 0) }
.expect("light userdata should push");
assert_eq!(
unsafe { thread.to_light_userdata(-1) },
opaque(&mut light_userdata)
);
assert_eq!(
unsafe { thread.to_userdata(-1) },
opaque(&mut light_userdata)
);
assert_eq!(
unsafe { thread.to_pointer(-1) },
opaque(&mut light_userdata).cast_const()
);
let userdata1 = unsafe { thread.new_userdata_tagged(4, 0) }
.expect("userdata should allocate")
.cast::<i32>();
unsafe { *userdata1 = 42 };
assert_eq!(unsafe { thread.to_light_userdata(-1) }, ptr::null_mut());
assert_eq!(
unsafe { thread.to_userdata(-1) },
userdata1.cast::<()>().cast()
);
assert_eq!(
unsafe { thread.to_pointer(-1) },
userdata1.cast::<()>().cast_const()
);
let userdata2 = unsafe { thread.new_userdata_tagged(4, 42) }
.expect("userdata should allocate")
.cast::<i32>();
unsafe { *userdata2 = -4 };
assert_eq!(
unsafe { thread.to_userdata_tagged(-1, 42) },
userdata2.cast::<()>().cast()
);
assert_eq!(
unsafe { thread.to_userdata_tagged(-1, 41) },
ptr::null_mut()
);
assert_eq!(unsafe { thread.userdata_tag(-1) }, 42);
unsafe { thread.set_userdata_tag(-1, 43) };
assert_eq!(unsafe { thread.userdata_tag(-1) }, 43);
unsafe { thread.set_userdata_tag(-1, 42) };
let userdata3 = unsafe {
thread
.new_userdata_dtor(4, userdata_inline_i32_dtor)
.expect("userdata should allocate")
.cast::<i32>()
};
let userdata4 = unsafe {
thread
.new_userdata_dtor(1, userdata_inline_i8_dtor)
.expect("userdata should allocate")
.cast::<i8>()
};
unsafe {
*userdata3 = 43;
*userdata4 = 3;
}
assert_eq!(
unsafe { thread.new_metatable("udata1") }.expect("metatable should create"),
1
);
assert_eq!(
unsafe { thread.new_metatable("udata2") }.expect("metatable should create"),
1
);
let userdata5 =
unsafe { thread.new_userdata_tagged(0, 0) }.expect("userdata should allocate");
unsafe { thread.get_field(LUA_REGISTRY_INDEX, "udata1") }.expect("metatable should read");
assert_eq!(
unsafe { thread.set_metatable(-2) }.expect("metatable should set"),
1
);
let userdata6 =
unsafe { thread.new_userdata_tagged(0, 0) }.expect("userdata should allocate");
unsafe { thread.get_field(LUA_REGISTRY_INDEX, "udata2") }.expect("metatable should read");
assert_eq!(
unsafe { thread.set_metatable(-2) }.expect("metatable should set"),
1
);
assert_eq!(
unsafe { thread.check_userdata(-2, "udata1") }.expect("userdata should check"),
userdata5
);
assert_eq!(
unsafe { thread.check_userdata(-1, "udata2") }.expect("userdata should check"),
userdata6
);
assert_eq!(
unsafe { thread.new_metatable("udata3") }.expect("metatable should create"),
1
);
unsafe { thread.push_value(-1) }.expect("metatable should push");
unsafe { thread.set_userdata_metatable(50) };
assert_eq!(
unsafe { thread.new_metatable("udata4") }.expect("metatable should create"),
1
);
unsafe { thread.push_value(-1) }.expect("metatable should push");
unsafe { thread.set_userdata_metatable(51) };
let userdata7 =
unsafe { thread.new_userdata_tagged(16, 50) }.expect("userdata should allocate");
unsafe { thread.get_userdata_metatable(50) }.expect("userdata metatable should push");
assert_eq!(
unsafe { thread.set_metatable(-2) }.expect("metatable should set"),
1
);
let userdata8 = unsafe { thread.new_userdata_tagged_with_metatable(16, 51) }
.expect("userdata should allocate");
assert_eq!(
unsafe { thread.check_userdata(-2, "udata3") }.expect("userdata should check"),
userdata7
);
assert_eq!(
unsafe { thread.check_userdata(-1, "udata4") }.expect("userdata should check"),
userdata8
);
}
assert_eq!(USERDATA_DTOR_HITS.load(Ordering::SeqCst), 42);
}
#[test]
fn userdata_mark_callback() {
flags::LuauGcTraceUdata.set(true);
#[derive(Default)]
struct State {
marked_thread: *mut luau_vm::state::RawLuaState,
marked_data: *mut (),
mark_hits: usize,
}
let mut state = State::default();
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.set_thread_data(opaque(&mut state)) };
let mark = |thread: &Thread, data| {
let state = unsafe { &mut *thread.thread_data().cast::<State>() };
state.marked_thread = thread.as_ptr();
state.marked_data = data;
state.mark_hits += 1;
};
unsafe { thread.set_userdata_mark(42, Some(mark)) };
let userdata = unsafe { thread.new_userdata_tagged(size_of::<i32>(), 42) }
.expect("userdata should allocate");
unsafe { thread.ref_value(-1) }.expect("userdata should be referenced");
unsafe { thread.pop(1) };
unsafe { thread.gc(LUA_GC_COLLECT, 0) }.expect("collection should succeed");
assert_eq!(state.mark_hits, 1);
assert_eq!(state.marked_thread, thread.as_ptr());
assert_eq!(state.marked_data, userdata);
}
#[test]
fn weak_ref_survives_when_marked() {
flags::LuauGcTraceUdata.set(true);
struct State {
reference: i32,
}
let mut state = State { reference: 0 };
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.set_thread_data(opaque(&mut state)) };
unsafe { thread.new_table() }.expect("table should allocate");
state.reference = unsafe { thread.weak_ref_value(-1) }.expect("weak reference should allocate");
let table = unsafe { thread.to_pointer(-1) };
unsafe { thread.pop(1) };
let embedder_gc = |thread: &Thread, mark: Option<luau_vm::state::EmbedderMark>| {
if let Some(mark) = mark {
let state = unsafe { &*thread.thread_data().cast::<State>() };
mark(thread, state.reference);
}
};
unsafe { thread.set_embedder_gc(Some(embedder_gc)) };
unsafe { thread.gc(LUA_GC_COLLECT, 0) }.expect("collection should succeed");
assert_eq!(
unsafe { thread.get_weak_ref(state.reference) }.expect("weak reference should read"),
LUA_TTABLE
);
assert_eq!(unsafe { thread.to_pointer(-1) }, table);
unsafe { thread.pop(1) };
state.reference = unsafe { thread.weak_unref_value(state.reference) };
unsafe { thread.gc(LUA_GC_COLLECT, 0) }.expect("collection should succeed");
assert_eq!(
unsafe { thread.get_weak_ref(state.reference) }.expect("weak reference should read"),
LUA_TNIL
);
}
#[test]
fn weak_ref_collected_when_not_marked() {
flags::LuauGcTraceUdata.set(true);
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.new_table() }.expect("table should allocate");
let reference = unsafe { thread.weak_ref_value(-1) }.expect("weak reference should allocate");
unsafe { thread.pop(1) };
unsafe { thread.set_embedder_gc(Some(|_, _| {})) };
unsafe { thread.gc(LUA_GC_COLLECT, 0) }.expect("collection should succeed");
assert_eq!(
unsafe { thread.get_weak_ref(reference) }.expect("weak reference should read"),
LUA_TNIL
);
}
#[test]
fn weak_ref_full_chain() {
flags::LuauGcTraceUdata.set(true);
#[derive(Default)]
struct State {
native_object_marked: bool,
callback_ref: i32,
marks_requested: usize,
marks_performed: usize,
}
let mut state = State::default();
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
unsafe { thread.set_thread_data(opaque(&mut state)) };
let userdata_mark = |thread: &Thread, _| {
let state = unsafe { &mut *thread.thread_data().cast::<State>() };
state.native_object_marked = true;
};
unsafe { thread.set_userdata_mark(42, Some(userdata_mark)) };
let embedder_gc = |thread: &Thread, mark: Option<luau_vm::state::EmbedderMark>| {
let state = unsafe { &mut *thread.thread_data().cast::<State>() };
if let Some(mark) = mark {
state.marks_requested += 1;
if state.native_object_marked {
state.marks_performed += 1;
mark(thread, state.callback_ref);
}
} else {
state.native_object_marked = false;
state.marks_requested = 0;
state.marks_performed = 0;
}
};
unsafe { thread.set_embedder_gc(Some(embedder_gc)) };
unsafe { thread.new_userdata_tagged(size_of::<i32>(), 42) }.expect("userdata should allocate");
let userdata_ref = unsafe { thread.ref_value(-1) }.expect("userdata should be referenced");
unsafe { thread.pop(1) };
unsafe { thread.new_table() }.expect("table should allocate");
state.callback_ref =
unsafe { thread.weak_ref_value(-1) }.expect("weak reference should allocate");
let table = unsafe { thread.to_pointer(-1) };
unsafe { thread.pop(1) };
unsafe { thread.gc(LUA_GC_COLLECT, 0) }.expect("collection should succeed");
assert_eq!(
unsafe { thread.get_weak_ref(state.callback_ref) }.expect("weak reference should read"),
LUA_TTABLE
);
assert_eq!(unsafe { thread.to_pointer(-1) }, table);
unsafe { thread.pop(1) };
assert_eq!(state.marks_requested, 2);
assert_eq!(state.marks_performed, 2);
unsafe { thread.unref_value(userdata_ref) };
unsafe { thread.gc(LUA_GC_COLLECT, 0) }.expect("collection should succeed");
assert_eq!(
unsafe { thread.get_weak_ref(state.callback_ref) }.expect("weak reference should read"),
LUA_TNIL
);
assert_eq!(state.marks_requested, 1);
assert_eq!(state.marks_performed, 0);
}
#[test]
fn lightuserdata_api() -> VmResult {
let lua = Lua::new().expect("Lua::new should succeed");
let thread = lua.main_thread();
let value = unsafe { NonNull::new_unchecked(0x12345678usize as *mut ()) };
unsafe { thread.push_light_userdata_tagged(value.as_ptr(), 1)? };
assert_eq!(unsafe { thread.light_userdata_tag(-1) }, 1);
assert_eq!(
unsafe { thread.to_light_userdata_tagged(-1, 0) },
ptr::null_mut()
);
assert_eq!(
unsafe { thread.to_light_userdata_tagged(-1, 1) },
value.as_ptr()
);
unsafe { thread.set_light_userdata_name(1, "id")? };
assert!(unsafe { thread.get_light_userdata_name(0) }.is_none());
assert_eq!(
unsafe { thread.get_light_userdata_name(1) }
.unwrap()
.as_bytes(),
b"id"
);
assert_eq!(unsafe { thread.lua_type_name(-1) }.as_bytes(), b"id");
unsafe { thread.pop(1) };
unsafe { thread.push_light_userdata_tagged(value.as_ptr(), 0)? };
unsafe { thread.push_light_userdata_tagged(value.as_ptr(), 1)? };
assert_eq!(unsafe { thread.raw_equal(-1, -2) }, 0);
unsafe { thread.pop(2) };
unsafe { thread.create_table(0, 0)? };
unsafe { thread.push_light_userdata_tagged(value.as_ptr(), 2)? };
unsafe { thread.push_integer(20)? };
unsafe { thread.set_table(-3)? };
unsafe { thread.push_light_userdata_tagged(value.as_ptr(), 3)? };
unsafe { thread.push_integer(30)? };
unsafe { thread.set_table(-3)? };
unsafe { thread.push_light_userdata_tagged(value.as_ptr(), 2)? };
unsafe { thread.get_table(-2)? };
unsafe { thread.push_integer(20)? };
assert_eq!(unsafe { thread.raw_equal(-1, -2) }, 1);
unsafe { thread.pop(2) };
unsafe { thread.push_light_userdata_tagged(value.as_ptr(), 3)? };
unsafe { thread.get_table(-2)? };
unsafe { thread.push_integer(30)? };
assert_eq!(unsafe { thread.raw_equal(-1, -2) }, 1);
unsafe { thread.pop(2) };
unsafe { thread.pop(1) };
unsafe { thread.push_light_userdata_tagged(value.as_ptr(), 0)? };
assert_eq!(unsafe { thread.lua_type_name(-1) }.as_bytes(), b"userdata");
unsafe { thread.create_table(0, 1)? };
unsafe { thread.push_optional_string(Some("luserdata"))? };
unsafe { thread.set_field(-2, "__type")? };
assert_eq!(
unsafe { thread.set_metatable(-2) }.expect("metatable should set"),
1
);
assert_eq!(unsafe { thread.lua_type_name(-1) }.as_bytes(), b"luserdata");
unsafe { thread.pop(1) };
Ok(())
}
#[test]
fn userdata_alignment() -> VmResult {
let lua =
Lua::new_with_allocator(AlignedAllocator).expect("Lua::new_with_allocator should succeed");
let thread = lua.main_thread();
for size in (16..=4096).step_by(4) {
for _ in 0..10 {
let data =
unsafe { thread.new_userdata_tagged(size, 0) }.expect("userdata should allocate");
assert_eq!((data as usize) % 16, 0);
unsafe { thread.pop(1) };
}
for _ in 0..10 {
let data = unsafe { thread.new_userdata_dtor(size, overflow_dtor) }
.expect("userdata should allocate");
assert_eq!((data as usize) % 16, 0);
unsafe { thread.pop(1) };
}
}
Ok(())
}