From 1b8c461d7ec1462ee95a4b992c0693c6a78b65b7 Mon Sep 17 00:00:00 2001 From: luaneko Date: Sat, 21 Jun 2025 05:09:43 +1000 Subject: [PATCH] Add more documentation to luajit --- crates/luajit/src/lib.rs | 1013 ++++++++++++++++++++++++++++---------- src/main.rs | 10 +- 2 files changed, 756 insertions(+), 267 deletions(-) diff --git a/crates/luajit/src/lib.rs b/crates/luajit/src/lib.rs index e019ebb..4a6964c 100644 --- a/crates/luajit/src/lib.rs +++ b/crates/luajit/src/lib.rs @@ -1,12 +1,14 @@ -#![allow(non_camel_case_types, non_snake_case)] +#![allow(non_snake_case)] use bitflags::bitflags; use bstr::{BStr, BString, ByteSlice}; use luaffi::future::lua_pollable; use luajit_sys::*; use std::{ alloc::{Layout, alloc, dealloc, realloc}, - ffi::{CString, NulError}, + borrow::Cow, + ffi::{CStr, CString, NulError}, fmt, + marker::PhantomData, ops::{Deref, DerefMut}, os::raw::{c_char, c_int, c_void}, process, @@ -17,7 +19,7 @@ use std::{ }; use thiserror::Error; -/// LuaJIT error. +/// Lua error. #[derive(Debug, Error)] #[non_exhaustive] pub enum Error { @@ -47,7 +49,7 @@ pub enum Error { /// Lua error message. msg: BString, /// Lua stack trace. - trace: BString, + trace: Option, }, /// Type mismatch type error. #[error("{0} expected, got {1}")] @@ -62,6 +64,18 @@ pub enum Error { InvalidUtf8(#[from] Utf8Error), } +impl Error { + /// Lua stack trace, if it was collected. + /// + /// Currently this is only available for [`Error::Resume`]. + pub fn trace(&self) -> Option<&BStr> { + match self { + Self::Resume { trace, .. } => trace.as_ref().map(|s| s.as_ref()), + _ => None, + } + } +} + /// Lua type. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum Type { @@ -195,12 +209,41 @@ impl fmt::Display for ResumeStatus { } bitflags! { + /// Flags for [`Stack::load`]. + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct LoadMode: usize { + /// No flags. + const NONE = 0b_00_00; + /// Auto-detect chunk type (flag `bt`). const AUTO = 0b_00_11; + /// Text chunk only (flag `t`). const TEXT = 0b_00_01; + /// Binary chunk only (flag `b`). const BINARY = 0b_00_10; - const GC64 = 0b_01_10; + /// Force 32-bit binary format (flag `W`, implies `b`). const GC32 = 0b_10_10; + /// Force 64-bit binary format (flag `X`, implies `b`). + const GC64 = 0b_01_10; + } +} + +impl LoadMode { + fn to_mode_str(&self) -> Cow<'static, CStr> { + Cow::Borrowed(match *self { + Self::NONE => c"", + Self::AUTO => c"bt", + Self::TEXT => c"t", + Self::BINARY => c"b", + _ => { + let mut s = String::new(); + self.contains(Self::TEXT).then(|| s.push_str("t")); + self.contains(Self::BINARY).then(|| s.push_str("b")); + self.contains(Self::GC32).then(|| s.push_str("W")); + self.contains(Self::GC64).then(|| s.push_str("X")); + return Cow::Owned(CString::new(s).unwrap()); + } + }) } } @@ -210,62 +253,89 @@ impl Default for LoadMode { } } -impl LoadMode { - fn mode_str(&self) -> CString { - let mut s = String::new(); - - if self.contains(Self::TEXT) { - s.push_str("t"); - } - - if self.contains(Self::BINARY) { - s.push_str("b"); - if self.contains(Self::GC64) { - s.push_str("X"); - } else if self.contains(Self::GC32) { - s.push_str("W"); - } - } - - CString::new(s).unwrap() +bitflags! { + /// Flags for [`Stack::dump`]. + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct DumpMode: usize { + /// No flags. + const NONE = 0b_00_0_0; + /// Strip debug information (flag `s`). + const STRIP = 0b_00_0_1; + /// Deterministic mode (flag `d`). + const DETERMINISTIC = 0b_00_1_0; + /// Force 32-bit binary format (flag `W`). + const GC32 = 0b_10_0_0; + /// Force 64-bit binary format (flag `X`). + const GC64 = 0b_01_0_0; } } -bitflags! { - pub struct DumpMode: usize { - const DEFAULT = 0b_00_1_0; - const STRIP = 0b_00_0_1; - const DETERMINISTIC = 0b_00_1_0; - const GC64 = 0b_01_0_0; - const GC32 = 0b_10_0_0; +impl DumpMode { + fn to_mode_str(&self) -> Cow<'static, CStr> { + Cow::Borrowed(match *self { + Self::NONE => c"", + Self::STRIP => c"s", + Self::DETERMINISTIC => c"d", + _ => { + let mut s = String::new(); + self.contains(Self::STRIP).then(|| s.push_str("s")); + self.contains(Self::DETERMINISTIC).then(|| s.push_str("d")); + self.contains(Self::GC32).then(|| s.push_str("W")); + self.contains(Self::GC64).then(|| s.push_str("X")); + return Cow::Owned(CString::new(s).unwrap()); + } + }) } } impl Default for DumpMode { fn default() -> Self { - Self::DEFAULT + Self::DETERMINISTIC } } -impl DumpMode { - fn mode_str(&self) -> CString { - let mut s = String::new(); +/// Lua chunk data. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Chunk { + name: BString, + content: BString, +} - if self.contains(Self::STRIP) { - s.push_str("s"); +impl Chunk { + /// Creates a named [`Chunk`] with the given content. + pub fn named(name: impl Into, content: impl Into) -> Self { + Self { + name: name.into(), + content: content.into(), } + } - if self.contains(Self::DETERMINISTIC) { - s.push_str("d"); + /// Creates an unnamed [`Chunk`] with the given content. + pub fn unnamed(content: impl Into) -> Self { + Self { + name: "?".into(), + content: content.into(), } + } - if self.contains(Self::GC64) { - s.push_str("X"); - } else if self.contains(Self::GC32) { - s.push_str("W"); - } + /// Name of this chunk. + pub fn name(&self) -> &BStr { + self.name.as_ref() + } +} - CString::new(s).unwrap() +impl Deref for Chunk { + type Target = BString; + + fn deref(&self) -> &Self::Target { + &self.content + } +} + +impl DerefMut for Chunk { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.content } } @@ -277,6 +347,7 @@ struct GlobalState { impl GlobalState { pub fn new() -> Result { unsafe { + // SAFETY: lua_newstate may return a null pointer if allocation fails let ptr = NonNull::new(lua_newstate(Some(Self::alloc_cb), ptr::null_mut())) .ok_or(Error::OutOfMemory)?; @@ -285,6 +356,20 @@ impl GlobalState { } } + unsafe extern "C" fn panic_cb(L: *mut lua_State) -> c_int { + // SAFETY: we cannot recover from uncaught lua exceptions; abort instead + assert!(!L.is_null()); + + let stack = unsafe { Stack::new_unchecked(L) }; + let msg = stack + .slot(-1) + .string() + .unwrap_or(b"unknown lua panic".into()); + + eprintln!("lua panicked: {msg}"); + process::abort() + } + unsafe extern "C" fn alloc_cb( _ud: *mut c_void, ptr: *mut c_void, @@ -308,33 +393,31 @@ impl GlobalState { const ALIGNOF_MAX_ALIGN_T: usize = 16; let old_layout = Layout::from_size_align(osize, ALIGNOF_MAX_ALIGN_T) - .expect("alloc error: requested osize is too large"); + .expect("lua alloc error: requested osize is too large"); let new_layout = Layout::from_size_align(nsize, ALIGNOF_MAX_ALIGN_T) - .expect("alloc error: requested nsize is too large"); + .expect("lua alloc error: requested nsize is too large"); - if nsize != 0 { + // SAFETY: from lua documentation: + // When nsize is zero, the allocator must return NULL; if osize is not zero, it should + // free the block pointed to by ptr. When nsize is not zero, the allocator returns NULL if + // and only if it cannot fill the request. When nsize is not zero and osize is zero, the + // allocator should behave like malloc. When nsize and osize are not zero, the allocator + // behaves like realloc. Lua assumes that the allocator never fails when osize >= nsize. + if nsize == 0 { if osize != 0 { - unsafe { realloc(ptr.cast(), old_layout, nsize).cast() } - } else { - unsafe { alloc(new_layout).cast() } - } - } else { - if osize != 0 { - unsafe { dealloc(ptr.cast(), old_layout) } + unsafe { dealloc(ptr as *mut u8, old_layout) } } ptr::null_mut() + } else { + if osize == 0 { + unsafe { alloc(new_layout) as *mut c_void } + } else { + unsafe { realloc(ptr as *mut u8, old_layout, nsize) as *mut c_void } + } } } - unsafe extern "C" fn panic_cb(L: *mut lua_State) -> c_int { - // we cannot recover from uncaught lua exceptions; abort instead - let stack = unsafe { Stack::new_unchecked(L) }; - let msg = stack.string(-1).unwrap_or(b"unknown lua panic".into()); - eprintln!("lua panicked: {msg}"); - process::abort() - } - - pub fn as_ptr(&self) -> *mut lua_State { + fn as_ptr(&self) -> *mut lua_State { self.ptr.as_ptr() } } @@ -345,39 +428,69 @@ impl Drop for GlobalState { } } +/// Lua value handle into the registry. +/// +/// The value is managed by [`luaL_ref`] and [`luaL_unref`] with [`LUA_REGISTRYINDEX`] as the target +/// table. +#[derive(Debug)] +pub struct Ref { + state: Rc, + key: c_int, +} + +impl Drop for Ref { + fn drop(&mut self) { + // SAFETY: luaL_unref is guaranteed to not fail + unsafe { luaL_unref(self.state.as_ptr(), LUA_REGISTRYINDEX, self.key) } + } +} + +/// Lua state handle. +/// +/// A state instance can be manipulated using the [`Stack`] object that it mutably dereferences to. #[derive(Debug)] pub struct State { + thread_ref: Ref, stack: Stack, - ref_key: c_int, - global: Rc, } impl State { + /// Creates a new [`State`]. + /// + /// All built-in libraries are opened by default. + /// + /// This may return an error if allocation or library initialisation fails. pub fn new() -> Result { - let global = Rc::new(GlobalState::new()?); + let state = Rc::new(GlobalState::new()?); let mut state = Self { - stack: unsafe { Stack::new_unchecked(global.as_ptr()) }, - ref_key: LUA_NOREF, - global, + stack: unsafe { Stack::new_unchecked(state.as_ptr()) }, + thread_ref: Ref { + state, + key: LUA_NOREF, + }, }; - state.push_function(Some(Self::open_cb)); + state.push_function_raw(Some(Self::open_cb), 0); state.call(0, 0)?; Ok(state) } unsafe extern "C" fn open_cb(L: *mut lua_State) -> c_int { + // SAFETY: we wrap luaL_openlibs so we can potentially catch any library open errors unsafe { luaL_openlibs(L) } 0 } + /// Creates a new empty thread (coroutine) associated with this state. pub fn new_thread(&self) -> Self { - // SAFETY: lua_newthread never returns null, but may panic on oom self.ensure(1); Self { + // SAFETY: lua_newthread never returns null, but may panic on oom stack: unsafe { Stack::new_unchecked(lua_newthread(self.as_ptr())) }, - ref_key: unsafe { luaL_ref(self.as_ptr(), LUA_REGISTRYINDEX) }, - global: Rc::clone(&self.global), + thread_ref: Ref { + state: Rc::clone(&self.thread_ref.state), + key: unsafe { luaL_ref(self.as_ptr(), LUA_REGISTRYINDEX) }, + }, } } } @@ -396,80 +509,178 @@ impl DerefMut for State { } } -impl Drop for State { - fn drop(&mut self) { - unsafe { luaL_unref(self.global.as_ptr(), LUA_REGISTRYINDEX, self.ref_key) } - } -} - -pub struct Stack { - ptr: NonNull, -} +/// Lua stack handle. +/// +/// This simply wraps a pointer to the actual [`lua_State`] object and provides safe methods to +/// manipulate the state. Lua states can only be manipulated if the Rust caller holds a mutable +/// reference to its [`Stack`]. +/// +/// A mutable [`State`] can be dereferenced to obtain a mutable reference to its inner [`Stack`] +/// safely. +/// +/// # Indexing +/// +/// All indices are 1-based as in Lua convention. Positive values are absolute indices from the +/// bottom of the stack, where index `1` is the first value, index `2` is the second value, and so +/// on. Negative values are relative indices from the top of the stack, where index `-1` is the last +/// value, index `-2` is the second last value, and so on. Index `0` is either invalid or indicates +/// an empty stack. +/// +/// # Mutability +/// +/// All methods which mutate the *visible contents* of the stack require a mutable reference. For +/// example, pushing values to or popping values from the stack require `&mut Stack`, while parsing +/// a value on the stack into a Rust value requires `&Stack`. +/// +/// Although a [`lua_State`] as a C object is internally mutable, this limitation exists to allow +/// for zero-copy marshalling of values like Lua `string`s into Rust `&str` without any allocations. +/// It is guaranteed that the lifetime of an `&str` will not outlive the lifetime of the original +/// `string` while the stack is immutable and the original string cannot be removed from the stack, +/// preventing it from being garbage-collected by Lua. +#[repr(transparent)] +pub struct Stack(NonNull); impl Stack { - unsafe fn new_unchecked(ptr: *mut lua_State) -> Self { - let ptr = unsafe { NonNull::new_unchecked(ptr) }; - Self { ptr } + /// Creates a new [`Stack`] from a raw pointer. + /// + /// # Safety + /// + /// The pointer must not be null. See also mutability guarantees on [`Stack`](Stack#mutability). + pub unsafe fn new_unchecked(ptr: *mut lua_State) -> Self { + assert!(!ptr.is_null(), "attempt to create Stack with null pointer"); + Self(unsafe { NonNull::new_unchecked(ptr) }) } + /// Pointer to the [`lua_State`]. pub fn as_ptr(&self) -> *mut lua_State { - self.ptr.as_ptr() + self.0.as_ptr() } - pub fn top(&self) -> c_int { + /// Size of the stack. + /// + /// This is the number of values on the stack and points to the value at the top of the stack + /// when used as an index. + /// + /// Equivalent to [`lua_gettop`]. + pub fn size(&self) -> c_int { unsafe { lua_gettop(self.as_ptr()) } } - pub fn set_top(&mut self, idx: c_int) { - assert!(0 <= idx, "cannot resize to {idx}"); - // lua_settop throws on oom when growing, so we call ensure first - let top = self.top(); - (top < idx).then(|| self.ensure(idx - top)); - unsafe { lua_settop(self.as_ptr(), idx) } + /// Resizes the stack to fit exactly `n` values, reallocating the stack and popping any + /// extraneous values or pushing nils to fill the space as necessary. + /// + /// Equivalent to [`lua_settop`]. + /// + /// # Panic + /// + /// Panics if `n` is negative. + pub fn resize(&mut self, n: c_int) { + // SAFETY: lua_settop can throw on oom (doesn't cpgrowstack) when growing, so we call ensure + // first + assert!(0 <= n, "cannot resize to size {n}"); + let size = self.size(); + (n > size).then(|| self.ensure(n - size)); + unsafe { lua_settop(self.as_ptr(), n) } } + /// Reallocates the stack to fit `n` more values, if necessary. + /// + /// This does not resize the stack and thus does not require a mutable reference as it does not + /// mutate the *visible contents* of the stack. + /// + /// Equivalent to [`lua_checkstack`]. + /// + /// # Panic + /// + /// Panics if `n` is negative or reallocation fails. pub fn ensure(&self, n: c_int) { // lua_checkstack throws on oom in puc lua 5.1, but it is fine in luajit + assert!(n >= 0, "ensure called with a negative value"); unsafe { assert!(lua_checkstack(self.as_ptr(), n) != 0, "stack out of memory") } } + /// Pops `n` values at the top of the stack. + /// + /// Equivalent to [`lua_pop`]. + /// + /// # Panic + /// + /// Panics if there are less than `n` values on the stack. pub fn pop(&mut self, n: c_int) { - assert!(0 <= n && n <= self.top(), "cannot pop {n}: {self:?}"); + assert!(0 <= n && n <= self.size(), "cannot pop {n}: {self:?}"); unsafe { lua_pop(self.as_ptr(), n) } } + /// Pops the value at the top of the stack and replaces the value at index `idx` with it. + /// + /// If the index `idx` points to the top of the stack, this still pops the value and is + /// functionally equivalent to [`Stack::pop`] in that case. + /// + /// Equivalent to [`lua_replace`]. + /// + /// # Panic + /// + /// Panics if the stack is empty or the index `idx` is invalid. pub fn pop_replace(&mut self, idx: c_int) { - assert!(1 <= self.top(), "cannot pop 1: {self:?}"); - unsafe { lua_replace(self.as_ptr(), self.index(idx).index()) } + assert!(self.size() >= 1, "cannot pop 1: {self:?}"); + unsafe { lua_replace(self.as_ptr(), self.slot(idx).index()) } } + /// Status of the current thread. + /// + /// Equivalent to [`lua_status`]. pub fn status(&self) -> Status { Status::from_code(unsafe { lua_status(self.as_ptr()) }).unwrap() } + /// Iterator over all values on the stack. pub fn iter<'s>(&'s self) -> StackIter<'s> { StackIter::new(self) } + /// Returns a guard that resets the stack to the current size when dropped. + /// + /// This is useful for implementing idempotent stack operations which must keep the stack size + /// consistent. pub fn guard<'s>(&'s mut self) -> StackGuard<'s> { - StackGuard::new(self) + StackGuard::new(self, false) } - pub fn index<'s>(&'s self, idx: c_int) -> Value<'s> { - self.try_index(idx) + /// Similar to [`guard`](Self::guard) but takes an immutable reference to [`Stack`]. + /// + /// # Safety + /// + /// The caller must ensure that the stack from this point onwards until the returned + /// [`StackGuard`] is dropped never mutates any values already on the stack *before* the guard + /// was created. The caller may push new values onto the guarded stack and mutate or pop values + /// that they themselves have pushed, but mutating or popping any values that were already on + /// the stack before the guard was created is **undefined behaviour**. + pub unsafe fn guard_unchecked<'s>(&'s self) -> StackGuard<'s> { + StackGuard::new(self, true) + } + + /// Handle for the value at index `idx`. + /// + /// # Panic + /// + /// Panics if the index `idx` is invalid. + pub fn slot<'s>(&'s self, idx: c_int) -> Slot<'s> { + self.try_slot(idx) .unwrap_or_else(|| panic!("invalid index {idx}: {self:?}")) } - pub fn try_index<'s>(&'s self, idx: c_int) -> Option> { + /// Handle for the value at index `idx`, or [`None`] if there is no value at that index. + pub fn try_slot<'s>(&'s self, idx: c_int) -> Option> { self.absindex(idx) - .map(|idx| unsafe { Value::new_unchecked(self, idx) }) + .map(|idx| unsafe { Slot::new_unchecked(self, idx) }) } fn absindex(&self, idx: c_int) -> Option { if LUA_REGISTRYINDEX < idx && idx <= 0 { // SAFETY: must check any relative index that gets passed to index2adr in lj_api.c - // luajit doesn't check out-of-bounds access for relative indices with assertions disabled - let top = self.top(); + // because luajit doesn't check for out-of-bounds access for relative indices with + // assertions disabled + let top = self.size(); let idx = top + idx + 1; (0 < idx && idx <= top).then_some(idx) } else { @@ -477,106 +688,118 @@ impl Stack { } } - pub fn type_of(&self, idx: c_int) -> Type { - self.index(idx).type_of() - } - - pub fn parse<'s, T: Parse<'s>>(&'s self, idx: c_int) -> Result { - self.index(idx).parse() - } - - pub fn boolean(&self, idx: c_int) -> bool { - self.index(idx).boolean() - } - - pub fn lightuserdata(&self, idx: c_int) -> *mut T { - self.index(idx).lightuserdata() - } - - pub fn number(&self, idx: c_int) -> Option { - self.index(idx).number() - } - - pub fn integer(&self, idx: c_int) -> Option { - self.index(idx).integer() - } - - pub fn string(&self, idx: c_int) -> Option<&BStr> { - self.index(idx).string() - } - - pub fn function(&self, idx: c_int) -> lua_CFunction { - self.index(idx).function() - } - - pub fn cdata(&self, idx: c_int) -> *const T { - self.index(idx).cdata() - } - - pub fn pointer(&self, idx: c_int) -> *const c_void { - self.index(idx).pointer() - } - + /// Pushes the given value at the top of the stack. + /// + /// Equivalent to the `lua_push*` family of functions depending on the type of `T`. pub fn push(&mut self, value: T) { value.push(self) } - pub fn push_table(&mut self) { - self.ensure(1); - unsafe { lua_newtable(self.as_ptr()) } - } - - pub fn push_function(&mut self, f: lua_CFunction) { + /// Pushes the given C function at the top of the stack. + /// + /// Equivalent to [`lua_pushcclosure`]. + /// + /// # Panic + /// + /// Panics if the given function pointer is null. + pub fn push_function_raw(&mut self, f: lua_CFunction, upvals: c_int) { assert!(f.is_some(), "function must not be null"); self.ensure(1); - unsafe { lua_pushcfunction(self.as_ptr(), f) } - } - - pub fn push_thread(&mut self) -> bool { - self.ensure(1); - unsafe { lua_pushthread(self.as_ptr()) != 0 } - } - - pub fn push_index(&mut self, idx: c_int) { - self.ensure(1); - unsafe { lua_pushvalue(self.as_ptr(), self.index(idx).index()) } + unsafe { lua_pushcclosure(self.as_ptr(), f, upvals) } } + /// Gets a field of the table at index `idx` using the value at the top of the stack as the key, + /// and replaces the key with the retrieved value. + /// + /// This function does not invoke the `__index` metamethod. + /// + /// Equivalent to [`lua_rawget`]. + /// + /// # Panic + /// + /// Panics if the value at index `idx` is not a table. pub fn get(&mut self, idx: c_int) { - assert!(1 <= self.top(), "expected 1 value: {self:?}"); - unsafe { lua_rawget(self.as_ptr(), self.index(idx).index()) } + assert!(self.size() >= 1, "expected 1 value: {self:?}"); + let table = self.slot(idx); + assert!( + table.type_of() == Type::Table, + "expected table at index {idx}: {self:?}" + ); + unsafe { lua_rawget(self.as_ptr(), table.index()) } } + /// Gets a field of the table at index `idx` using `n` as the key, and pushes the retrieved + /// value at the top of the stack. + /// + /// This function does not invoke the `__index` metamethod. + /// + /// Equivalent to [`lua_rawgeti`]. + /// + /// # Panic + /// + /// Panics if the value at index `idx` is not a table. pub fn geti(&mut self, idx: c_int, n: c_int) { + let table = self.slot(idx); + assert!( + table.type_of() == Type::Table, + "expected table at index {idx}: {self:?}" + ); self.ensure(1); - unsafe { lua_rawgeti(self.as_ptr(), self.index(idx).index(), n) } + unsafe { lua_rawgeti(self.as_ptr(), table.index(), n) } } + /// Sets a field of the table at index `idx` using two values at the top of the stack as the key + /// and value. + /// + /// Both the key and value are popped from the stack. + /// + /// This function does not invoke the `__newindex` metamethod. + /// + /// Equivalent to [`lua_rawset`]. + /// + /// # Panic + /// + /// Panics if the value at index `idx` is not a table. pub fn set(&mut self, idx: c_int) { - assert!(2 <= self.top(), "expected 2 values: {self:?}"); - unsafe { lua_rawset(self.as_ptr(), self.index(idx).index()) } + assert!(self.size() >= 2, "expected 2 values: {self:?}"); + let table = self.slot(idx); + assert!( + table.type_of() == Type::Table, + "expected table at index {idx}: {self:?}" + ); + unsafe { lua_rawset(self.as_ptr(), table.index()) } } + /// Sets a field of the table at index `idx` using `n` is the key and the value at the top of + /// the stack as the value. + /// + /// The value is popped from the stack. + /// + /// This function does not invoke the `__newindex` metamethod. + /// + /// Equivalent to [`lua_rawseti`]. + /// + /// # Panic + /// + /// Panics if the value at index `idx` is not a table. pub fn seti(&mut self, idx: c_int, n: c_int) { - assert!(1 <= self.top(), "expected 1 value: {self:?}"); - unsafe { lua_rawseti(self.as_ptr(), self.index(idx).index(), n) } + assert!(self.size() >= 1, "expected 1 value: {self:?}"); + let table = self.slot(idx); + assert!( + table.type_of() == Type::Table, + "expected table at index {idx}: {self:?}" + ); + unsafe { lua_rawseti(self.as_ptr(), table.index(), n) } } - pub fn load( - &mut self, - name: Option>, - chunk: impl AsRef<[u8]>, - mode: LoadMode, - ) -> Result<(), Error> { - let mode = mode.mode_str(); - let name = name - .map(|s| CString::new(s.as_ref())) - .transpose() - .map_err(Error::BadChunkName)? - .unwrap_or(c"?".into()); - + /// Pushes the given chunk as a function at the top of the stack. + /// + /// Equivalent to [`lua_loadx`]. + pub fn load(&mut self, chunk: &Chunk, mode: LoadMode) -> Result<(), Error> { type State<'s> = Option<&'s [u8]>; - let mut state: State = Some(chunk.as_ref()); + let mut state: State = Some(chunk.content.as_ref()); + let name = CString::new(chunk.name.to_vec()).map_err(Error::BadChunkName)?; + let mode = mode.to_mode_str(); unsafe extern "C" fn reader_cb( _L: *mut lua_State, @@ -612,7 +835,12 @@ impl Stack { } LUA_ERRSYNTAX => { let chunk = name.into_bytes().into(); - let msg = self.string(-1).unwrap_or(b"unknown error".into()).into(); + let msg = self + .slot(-1) + .string() + .unwrap_or(b"unknown error".into()) + .into(); + self.pop(1); Err(Error::Syntax { chunk, msg }) } @@ -620,52 +848,84 @@ impl Stack { } } - pub fn dump(&mut self, idx: c_int, mode: DumpMode) -> Result { - let value = self.index(idx); - let idx = match value.type_of() { - Type::Function => value.index(), - _ => panic!("expected function at index {}: {self:?}", value.index()), - }; + /// Dumps the function at index `idx` into bytecode. + /// + /// Equivalent to `string.dump(f, mode)`. + /// + /// # Panic + /// + /// Panics if the value at index `idx` is not a function. + pub fn dump(&self, idx: c_int, mode: DumpMode) -> Result { + let func = self.slot(idx); + assert!( + func.type_of() == Type::Function, + "expected function at index {idx}: {self:?}" + ); - let mut s = self.guard(); - s.push("string"); - s.get(LUA_GLOBALSINDEX); - s.push("dump"); - s.get(-2); // local dump = string.dump - s.push_index(idx); - s.push(mode.mode_str().as_bytes()); - s.call(2, 1)?; - s.index(-1).parse() // return dump(idx, mode) + unsafe { + let idx = func.index(); + let mut s = self.guard_unchecked(); + s.push("string"); + s.get(LUA_GLOBALSINDEX); + s.push("dump"); + s.get(-2); // local dump = string.dump + s.push(Index(idx)); + s.push(mode.to_mode_str().to_bytes()); + s.call(2, 1)?; + s.slot(-1).parse() // return dump(idx, mode) + } } + /// Calls a function on the stack synchronously with `narg` values at the top of the stack as + /// arguments. + /// + /// There must be `narg + 1` values at the top of the stack, including the function to call at + /// the index `top - narg` (i.e. the function is pushed first and then `narg` values as + /// arguments). All arguments and the function are popped from the stack and then any return + /// values are pushed. If `nret` is not [`LUA_MULTRET`], then the number of return values pushed + /// will be exactly `nret`, filling with nils if necessary. Finally, the number of return values + /// pushed to the stack is returned. + /// + /// The current thread status must not be suspended or dead. + /// + /// Equivalent to [`lua_pcall`]. + /// + /// # Panic + /// + /// Panics if there are not enough values on the stack, the function to call is not on the + /// stack, or thread status is invalid. pub fn call(&mut self, narg: c_int, nret: c_int) -> Result { assert!(0 <= narg && (0 <= nret || nret == LUA_MULTRET)); - let top = self.top(); + let top = self.size(); let need = narg + 1; // need the function on the stack - assert!(need <= top, "expected {need} values: {self:?}"); + let base = top - need; + + assert!(base >= 0, "expected {need} values: {self:?}"); + assert!( + self.slot(base + 1).type_of() == Type::Function, + "expected function at index {}: {self:?}", + base + 1 + ); assert!( self.status() == Status::Normal, - "thread {:p} called in wrong state", - self.as_ptr() + "thread {self:p} called in wrong state" ); - let base = top - need; + // TODO: use error handler to collect backtrace match unsafe { lua_pcall(self.as_ptr(), narg, nret, 0) } { - LUA_OK => { - let n = self.top() - base; - assert!( - n == nret || nret == LUA_MULTRET, - "expected {nret} values, got {n}: {self:?}" - ); - Ok(n) - } + LUA_OK => Ok(self.size() - base), LUA_ERRMEM => { self.pop(1); Err(Error::OutOfMemory) } LUA_ERRRUN | LUA_ERRERR => { - let msg = self.string(-1).unwrap_or(b"unknown error".into()).into(); + let msg = self + .slot(-1) + .string() + .unwrap_or(b"unknown error".into()) + .into(); + self.pop(1); Err(Error::Call { msg }) } @@ -673,13 +933,61 @@ impl Stack { } } - pub async fn call_async(&mut self, mut narg: c_int) -> Result<(), Error> { + /// Calls a function on the stack asynchronously with `narg` values at the top of the stack as + /// arguments. + /// + /// There must be `narg + 1` values at the top of the stack, including the function to call at + /// the index `top - narg` (i.e. the function is pushed first and then `narg` values as + /// arguments). All arguments and the function are popped from the stack and then any return + /// values are pushed. If `nret` is not [`LUA_MULTRET`], then the number of return values pushed + /// will be exactly `nret`, filling with nils if necessary. Finally, the number of return values + /// pushed to the stack is returned. + /// + /// If the thread yields a Rust [`Future`] value, then it will be polled to completion before + /// the thread is resumed with the output of the [`Future`] as the argument. If the thread + /// yields any other value, the thread is resumed immediately with no arguments. + /// + /// The current thread status must not be suspended or dead. + /// + /// Equivalent to multiple calls to [`lua_resume`] until the thread completes with a normal + /// result. + /// + /// # Panic + /// + /// Panics if there are not enough values on the stack, the function to call is not on the + /// stack, or thread status is invalid. + pub async fn call_async(&mut self, mut narg: c_int, nret: c_int) -> Result { + assert!(0 <= narg && (0 <= nret || nret == LUA_MULTRET)); + + let top = self.size(); + let need = narg + 1; // need the function on the stack + let base = top - need; + + assert!(base >= 0, "expected {need} values: {self:?}"); + assert!( + self.slot(base + 1).type_of() == Type::Function, + "expected function at index {}: {self:?}", + base + 1 + ); + assert!( + self.status() == Status::Normal, + "thread {self:p} called in wrong state" + ); + loop { match self.resume(narg)? { - ResumeStatus::Ok => return Ok(()), + ResumeStatus::Ok => { + let n = self.size() - base; + break Ok(if nret == LUA_MULTRET { + n + } else { + self.resize(nret); + nret + }); + } ResumeStatus::Suspended => { - self.set_top(1); - let value = self.index(1); + self.resize(1); + let value = self.slot(1); let ptr = value.cdata::().cast_mut(); if ptr.is_null() { return Err(Error::InvalidType( @@ -688,20 +996,45 @@ impl Stack { )); } else { unsafe { (&mut *ptr).await } - narg = 1; + narg = 0; } } } } } + /// Resumes the current thread with `narg` values on the stack as arguments. + /// + /// If the current thread status is normal, then there must be `narg + 1` values at the top of + /// the stack, including the function to call at the index `top - narg` (i.e. the function is + /// pushed first and then `narg` values as arguments). If the current thread status is + /// suspended, then there must be `narg` values at the top of the stack. All arguments and the + /// function are popped from the stack and then any yielded values are pushed. Finally, the new + /// status of the thread is returned. + /// + /// The current thread status must not be dead. + /// + /// Equivalent to [`lua_resume`]. + /// + /// # Panic + /// + /// Panics if there are not enough values on the stack, the function to call is not on the + /// stack, or thread status is invalid. pub fn resume(&mut self, narg: c_int) -> Result { assert!(0 <= narg); - let need = match self.status() { + let status = self.status(); + let need = match status { + Status::Normal => narg + 1, // need the function on the stack Status::Suspended => narg, - _ => narg + 1, // need the function on the stack + Status::Dead => panic!("thread {self:p} resumed in wrong state"), }; - assert!(need <= self.top(), "expected {need} values: {self:?}"); + let base = self.size() - need; + assert!(base >= 0, "expected {need} values: {self:?}"); + assert!( + status == Status::Suspended || self.slot(base + 1).type_of() == Type::Function, + "expected function at index {}: {self:?}", + base + 1 + ); match unsafe { lua_resume(self.as_ptr(), narg) } { LUA_OK => Ok(ResumeStatus::Ok), @@ -711,22 +1044,46 @@ impl Stack { Err(Error::OutOfMemory) } LUA_ERRRUN | LUA_ERRERR => { - unsafe { luaL_traceback(self.as_ptr(), self.as_ptr(), ptr::null(), 0) } - - let msg = self.string(-2).unwrap_or(b"unknown error".into()).into(); - let trace = self - .string(-1) - .map(|s| s.strip_prefix(b"stack traceback:\n").unwrap_or(s).as_bstr()) - .filter(|s| !s.is_empty()) - .unwrap_or(b"".into()) + let msg = self + .slot(-1) + .string() + .unwrap_or(b"unknown error".into()) .into(); - self.pop(2); + self.pop(1); + let trace = self.backtrace(0); Err(Error::Resume { msg, trace }) } _ => unreachable!(), } } + + /// Captures a stack backtrace of the current thread. + /// + /// This does not return any useful data unless the thread terminated with an error without + /// unwinding, which only happens when resuming a thread, or it is called inside the error + /// handler of a protected call. Both of these cases are already handled by this library + /// which automatically provides the backtrace with [`Error::trace`] if it is available. + /// + /// Equivalent to [`luaL_traceback`]. + pub fn backtrace(&self, level: c_int) -> Option { + assert!(level >= 0, "backtrace level must be nonnegative"); + self.ensure(LUA_MINSTACK); + unsafe { + luaL_traceback(self.as_ptr(), self.as_ptr(), ptr::null(), 0); + + // SAFETY: must clone the trace string here before popping it off the stack + let trace = self + .slot(-1) + .string() + .map(|s| s.strip_prefix(b"stack traceback:\n").unwrap_or(s).as_bstr()) + .filter(|s| !s.is_empty()) + .map(|s| s.into()); + + lua_pop(self.as_ptr(), 1); + trace + } + } } impl fmt::Debug for Stack { @@ -756,22 +1113,34 @@ impl fmt::Debug for Stack { } f.debug_struct("Stack") - .field("ptr", &self.ptr) + .field("ptr", &self.0) .field("values", &Values(self)) .finish() } } +/// Guarded Lua stack handle. +/// +/// This value resets the stack to the original size when dropped. This is useful for implementing +/// idempotent stack operations which must keep the stack size consistent. +/// +/// Can be obtained by [`Stack::guard`]. #[derive(Debug)] pub struct StackGuard<'s> { - stack: &'s mut Stack, - idx: c_int, + parent: PhantomData<&'s mut Stack>, + stack: Stack, + size: c_int, + check_overpop: bool, } impl<'s> StackGuard<'s> { - pub fn new(stack: &'s mut Stack) -> Self { - let idx = stack.top(); - Self { stack, idx } + fn new(stack: &'s Stack, check_overpop: bool) -> Self { + Self { + parent: PhantomData, + stack: unsafe { Stack::new_unchecked(stack.as_ptr()) }, // SAFETY: stack.as_ptr() is never null + size: stack.size(), + check_overpop, + } } } @@ -779,22 +1148,35 @@ impl<'s> Deref for StackGuard<'s> { type Target = Stack; fn deref(&self) -> &Self::Target { - self.stack + &self.stack } } impl<'s> DerefMut for StackGuard<'s> { fn deref_mut(&mut self) -> &mut Self::Target { - self.stack + &mut self.stack } } impl<'s> Drop for StackGuard<'s> { fn drop(&mut self) { - self.stack.set_top(self.idx); + #[cfg(debug_assertions)] + if self.check_overpop { + let new_size = self.stack.size(); + assert!( + self.size <= new_size, + "StackGuard detected over-popping by {} values (this is UB!)", + self.size - new_size + ); + } + + self.stack.resize(self.size); } } +/// Iterator over the values in a [`Stack`]. +/// +/// Can be obtained by [`Stack::iter`]. #[derive(Debug)] pub struct StackIter<'s> { stack: &'s Stack, @@ -803,75 +1185,122 @@ pub struct StackIter<'s> { } impl<'s> StackIter<'s> { - pub fn new(stack: &'s Stack) -> Self { - let top = stack.top(); + fn new(stack: &'s Stack) -> Self { + let top = stack.size(); Self { stack, idx: 0, top } } } impl<'s> Iterator for StackIter<'s> { - type Item = Value<'s>; + type Item = Slot<'s>; fn next(&mut self) -> Option { (self.idx < self.top).then(|| { self.idx += 1; - unsafe { Value::new_unchecked(self.stack, self.idx) } + unsafe { Slot::new_unchecked(self.stack, self.idx) } }) } } -pub struct Value<'s> { +/// Lua value handle into the stack. +pub struct Slot<'s> { stack: &'s Stack, idx: c_int, } -impl<'s> Value<'s> { - unsafe fn new_unchecked(stack: &'s Stack, idx: c_int) -> Self { +impl<'s> Slot<'s> { + /// Creates a new [`Slot`] for given index into the stack. + /// + /// # Safety + /// + /// Index `idx` must be a valid absolute index. + pub unsafe fn new_unchecked(stack: &'s Stack, idx: c_int) -> Self { Self { stack, idx } } + /// Index of this slot within the stack. + /// + /// This value is always a valid absolute positive index or a negative pseudo-index. It is never + /// a negative relative index. pub fn index(&self) -> c_int { self.idx } + /// Type of the value in this slot. pub fn type_of(&self) -> Type { Type::from_code(unsafe { lua_type(self.stack.as_ptr(), self.idx) }).unwrap_or(Type::Nil) } + /// Parses the value in this slot as a `T`. pub fn parse>(&self) -> Result { T::parse(self) } + /// Parses the value in this slot as a [`bool`]. + /// + /// If the value is not a `boolean`, then this returns false. + /// + /// Equivalent to [`lua_toboolean`]. pub fn boolean(&self) -> bool { self.parse().unwrap_or(false) } + /// Parses the value in this slot as a `lightuserdata` pointer. + /// + /// If the value is not a `lightuserdata`, then this returns a null pointer. + /// + /// Equivalent to [`lua_touserdata`]. pub fn lightuserdata(&self) -> *mut T { - self.parse().unwrap_or(ptr::null_mut()) + (self.type_of() == Type::Lightuserdata) + .then(|| self.parse().ok()) + .flatten() + .unwrap_or(ptr::null_mut()) } + /// Parses the value in this slot as a [`lua_Number`]. + /// + /// Equivalent to [`lua_tonumberx`]. pub fn number(&self) -> Option { self.parse().ok() } + /// Parses the value in this slot as a [`lua_Integer`]. + /// + /// Equivalent to [`lua_tointegerx`]. pub fn integer(&self) -> Option { self.parse().ok() } + /// Parses the value in this slot as a binary string. + /// + /// Equivalent to [`lua_tolstring`]. pub fn string(&self) -> Option<&'s BStr> { self.parse().ok() } - pub fn function(&self) -> lua_CFunction { + /// Parses the value in this slot as a UTF-8 string. + /// + /// Equivalent to [`lua_tolstring`]. + pub fn string_utf8(&self) -> Option<&'s str> { + self.parse().ok() + } + + /// Parses the value in this slot as a [`lua_CFunction`]. + pub fn function_raw(&self) -> lua_CFunction { unsafe { lua_tocfunction(self.stack.as_ptr(), self.idx) } } + /// Parses the value in this slot as a `cdata` pointer. + /// + /// If the value is a `cdata`, then the returned pointer is the address of the base of the cdata + /// payload. Otherwise this returns a null pointer. pub fn cdata(&self) -> *const T { (self.type_of() == Type::Cdata) .then(|| self.pointer().cast()) .unwrap_or(ptr::null_mut()) } + /// Parses the value in this slot as a generic pointer. pub fn pointer(&self) -> *const c_void { unsafe { lua_topointer(self.stack.as_ptr(), self.idx).cast() } } @@ -984,13 +1413,67 @@ impl_push_str!(BString); impl_push_str!(&str); impl_push_str!(String); +#[derive(Debug, Default, Clone, Copy, Hash)] +pub struct Index(pub c_int); + +impl Push for Index { + fn push(&self, stack: &mut Stack) { + stack.ensure(1); + unsafe { lua_pushvalue(stack.as_ptr(), stack.slot(self.0).index()) } + } +} + +#[derive(Debug, Default, Clone, Copy, Hash)] +pub struct NewTable { + pub narr: c_int, + pub nrec: c_int, +} + +impl NewTable { + pub fn new() -> Self { + Self::new_sized(0, 0) + } + + pub fn new_array(size: c_int) -> Self { + Self::new_sized(size, 0) + } + + pub fn new_record(size: c_int) -> Self { + Self::new_sized(0, size) + } + + pub fn new_sized(narr: c_int, nrec: c_int) -> Self { + Self { narr, nrec } + } +} + +impl Push for NewTable { + fn push(&self, stack: &mut Stack) { + let Self { narr, nrec } = *self; + assert!(0 <= narr, "size of table array part must be nonnegative"); + assert!(0 <= nrec, "size of table hash part must be nonnegative"); + stack.ensure(1); + unsafe { lua_createtable(stack.as_ptr(), narr, nrec) } + } +} + +#[derive(Debug, Default, Clone, Copy, Hash)] +pub struct CurrentThread; + +impl Push for CurrentThread { + fn push(&self, stack: &mut Stack) { + stack.ensure(1); + unsafe { lua_pushthread(stack.as_ptr()) }; + } +} + pub trait Parse<'s>: Sized { - fn parse(value: &Value<'s>) -> Result; + fn parse(slot: &Slot<'s>) -> Result; } impl Parse<'_> for () { - fn parse(value: &Value) -> Result { - match value.type_of() { + fn parse(slot: &Slot) -> Result { + match slot.type_of() { Type::Nil => Ok(()), ty => Err(Error::InvalidType("nil", ty.name())), } @@ -998,20 +1481,20 @@ impl Parse<'_> for () { } impl Parse<'_> for bool { - fn parse(value: &Value) -> Result { - Ok(unsafe { lua_toboolean(value.stack.as_ptr(), value.idx) != 0 }) + fn parse(slot: &Slot) -> Result { + Ok(unsafe { lua_toboolean(slot.stack.as_ptr(), slot.index()) != 0 }) } } macro_rules! impl_parse_ptr { ($type:ty) => { impl Parse<'_> for $type { - fn parse(value: &Value) -> Result { - let ptr = unsafe { lua_touserdata(value.stack.as_ptr(), value.idx) }; + fn parse(slot: &Slot) -> Result { + let ptr = unsafe { lua_touserdata(slot.stack.as_ptr(), slot.idx) }; if !ptr.is_null() { Ok(ptr as $type) } else { - Err(Error::InvalidType("lightuserdata", value.type_of().name())) + Err(Error::InvalidType("userdata", slot.type_of().name())) } } } @@ -1024,13 +1507,13 @@ impl_parse_ptr!(*const T); macro_rules! impl_parse_num { ($type:ty) => { impl Parse<'_> for $type { - fn parse(value: &Value) -> Result { + fn parse(slot: &Slot) -> Result { let mut isnum = 0; - let n = unsafe { lua_tonumberx(value.stack.as_ptr(), value.idx, &raw mut isnum) }; + let n = unsafe { lua_tonumberx(slot.stack.as_ptr(), slot.idx, &raw mut isnum) }; if isnum != 0 { Ok(n as $type) } else { - Err(Error::InvalidType("number", value.type_of().name())) + Err(Error::InvalidType("number", slot.type_of().name())) } } } @@ -1043,13 +1526,13 @@ impl_parse_num!(f64); macro_rules! impl_parse_int { ($type:ty) => { impl Parse<'_> for $type { - fn parse(value: &Value) -> Result { + fn parse(slot: &Slot) -> Result { let mut isnum = 0; - let n = unsafe { lua_tointegerx(value.stack.as_ptr(), value.idx, &raw mut isnum) }; + let n = unsafe { lua_tointegerx(slot.stack.as_ptr(), slot.idx, &raw mut isnum) }; if isnum != 0 { Ok(n as $type) } else { - Err(Error::InvalidType("number", value.type_of().name())) + Err(Error::InvalidType("number", slot.type_of().name())) } } } @@ -1070,13 +1553,13 @@ impl_parse_int!(isize); macro_rules! impl_parse_str { ($type:ty) => { impl<'s> Parse<'s> for $type { - fn parse(value: &Value<'s>) -> Result { + fn parse(slot: &Slot<'s>) -> Result { let mut len = 0; - let ptr = unsafe { lua_tolstring(value.stack.as_ptr(), value.idx, &mut len) }; + let ptr = unsafe { lua_tolstring(slot.stack.as_ptr(), slot.idx, &mut len) }; if !ptr.is_null() { Ok(unsafe { slice::from_raw_parts(ptr.cast(), len).into() }) } else { - Err(Error::InvalidType("string", value.type_of().name())) + Err(Error::InvalidType("string", slot.type_of().name())) } } } @@ -1086,8 +1569,8 @@ macro_rules! impl_parse_str { macro_rules! impl_parse_str_utf8 { ($type:ty) => { impl<'s> Parse<'s> for $type { - fn parse(value: &Value<'s>) -> Result { - Ok(std::str::from_utf8(Parse::parse(value)?)?.into()) + fn parse(slot: &Slot<'s>) -> Result { + Ok(std::str::from_utf8(Parse::parse(slot)?)?.into()) } } }; diff --git a/src/main.rs b/src/main.rs index fe5570b..c523af8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -141,7 +141,10 @@ fn init_vm(_args: &Args) -> luajit::State { println!("{registry}"); state - .load(Some("@[luby]"), registry.done(), luajit::LoadMode::TEXT) + .load( + &luajit::Chunk::named("@[luby]", registry.done()), + luajit::LoadMode::TEXT, + ) .and_then(|()| state.call(0, 0)) .unwrap_or_else(|err| panic!("failed to load modules: {err}")); @@ -156,7 +159,10 @@ async fn run(args: Args) { Err(err) => return eprintln!("{}", format!("{path}: {err}").red()), }; - if let Err(err) = state.load(Some(format!("@{path}")), chunk, Default::default()) { + if let Err(err) = state.load( + &luajit::Chunk::named(format!("@{path}"), chunk), + Default::default(), + ) { return eprintln!("{}", err.red()); }