From 2848d1deeefb9de37a05277f40485087e2caa567 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Sun, 28 Jan 2024 13:32:08 -0500 Subject: [PATCH] Implement an error handler that collects the stacktrace --- src/main.rs | 44 ++++++++++++++--- src/state.rs | 137 ++++++++++++++++++++++++++++++++++++++++++--------- src/value.rs | 22 +++++++++ 3 files changed, 173 insertions(+), 30 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3ce86c8..f5cade0 100755 --- a/src/main.rs +++ b/src/main.rs @@ -47,7 +47,7 @@ fn main() -> Result<()> { //let globals = lua.globals()?; globals.set("X", tbl)?; - lua.execute(r#" + let res = lua.execute(r#" require "util" --[[function dump_table(tbl) @@ -99,10 +99,33 @@ fn main() -> Result<()> { local v2 = Vec2.new(500, 500) print("v2 is (" .. v2.x .. ", " .. v2.y .. ")") - local v_add = v1:add(v2) + local v_add = v1 + v2 v_add.x = 90 print("v_add is (" .. v_add.x .. ", " .. v_add.y .. ")") - "#).unwrap(); + + function f1(v1, v2) + --print("f1 tb: " .. debug.traceback(1)) + local v_add = v1:add(v2) + end + + function f2(v1, v2) + --print("f2 tb: " .. debug.traceback(1)) + f1(v1, v2) + end + + function f3(v1, v2) + --print("f3 tb: " .. debug.traceback(1)) + f2(v1, v2) + end + + f3(v1, v2) + "#); + + // if unwrapped, the new lines in the message would be escaped making + // the traceback in the error difficult to read. + if let Err(e) = res { + panic!("{}", e); + } unsafe { assert_eq!(lua::lua_gettop(lua.state_ptr()), 0); // ensure that nothing is left on the stack @@ -155,19 +178,24 @@ impl<'a> PushToLuaStack<'a> for LuaRef { #[derive(Debug, thiserror::Error)] pub enum Error { + #[error("Syntax error: {0}")] + Syntax(String), /// An error returned from lua #[error("Lua runtime error: {0}")] Runtime(String), + #[error("Error when running a __gc metamethod")] + GcFailure(String), /// Ran into a not enough memory error when trying to grow the lua stack. - #[error("Ran out of memory when attempting to use `lua_checkstack`")] - Oom, + #[error("Failed to allocate memory")] + MemoryAlloc, #[error("Ran into a nill value on the stack")] Nil, #[error("Unexpected type, expected {0} but got {1}")] UnexpectedType(String, String), - #[error("Bad argument provided to `{}` at position {arg_index}{}, cause: {error}", - .func.clone().unwrap_or("Unknown".to_string()), - .arg_name.clone().map(|a| format!(" (name: {})", a)).unwrap_or("".to_string()))] + #[error("bad argument #{arg_index}{} to `{}` ({error})", + .arg_name.clone().map(|a| format!(" (name: {})", a)).unwrap_or("".to_string()), + .func.clone().unwrap_or("Unknown".to_string()) + )] BadArgument { func: Option, arg_index: i32, diff --git a/src/state.rs b/src/state.rs index 9d529fe..475d787 100755 --- a/src/state.rs +++ b/src/state.rs @@ -1,5 +1,5 @@ use core::ffi; -use std::{alloc::{self, Layout}, any::TypeId, collections::HashMap, ffi::{CString, CStr}, mem, ptr::{self, NonNull}, str::Utf8Error}; +use std::{alloc::{self, Layout}, any::TypeId, collections::{HashMap, VecDeque}, ffi::{CStr, CString}, mem, ptr::{self, NonNull}, str::Utf8Error}; use mlua_sys as lua; @@ -150,22 +150,64 @@ impl State { } } + unsafe fn get_error_str(&self) -> &str { + let error_c = CStr::from_ptr(lua::lua_tostring(self.state_ptr(), -1)); + let error_str = error_c.to_str() + .unwrap_or_else(|_| { + let b = error_c.to_bytes(); + std::str::from_utf8_unchecked(b) + }); + lua::lua_pop(self.state_ptr(), 1); + error_str + } + + /// A function that handles errors from calling scripts. It modifies the message to include + /// the stack traceback. + fn state_error_handler(&self, msg: String) -> Result { + self.traceback(Some(&msg)) + } + pub fn execute(&self, text: &str) -> Result<()> { let text = format!("{}\0", text); unsafe { + self.ensure_stack(3)?; + let _g = StackGuard::new(self); + let lua = self.lua.as_ptr(); - let text_c = text.as_ptr() as *const ffi::c_char; - if lua::luaL_dostring(lua, text_c) != 0 { - let error_c = CStr::from_ptr(lua::lua_tostring(lua, -1)); - let error_str = error_c.to_str() - .unwrap_or_else(|_| { - let b = error_c.to_bytes(); - std::str::from_utf8_unchecked(b) - }); - //.expect("Error bytes are invalid!"); - //std::str::from - return Err(Error::runtime(error_str)); + let text_c = text.as_ptr().cast(); + + // Subtract one from the length to exclude the null terminator + match lua::luaL_loadbuffer(self.state_ptr(), text_c, text.len() - 1, "test.lua\0".as_ptr().cast()) { + lua::LUA_ERRSYNTAX => { + return Err(Error::Syntax(self.get_error_str().to_string())); + }, + lua::LUA_ERRMEM => { + return Err(Error::MemoryAlloc); + } + _ => {}, + } + + let handler = self.create_function(|lua, msg: String| { + lua.state_error_handler(msg) + })?; + handler.push_to_lua_stack(self)?; + lua::lua_insert(lua, -2); + + match lua::lua_pcall(lua, 0, lua::LUA_MULTRET, -2) { + lua::LUA_ERRRUN => { + let er = self.get_error_str(); + return Err(Error::runtime(er)); + }, + lua::LUA_ERRMEM => { + return Err(Error::MemoryAlloc); + }, + lua::LUA_ERRERR => { + let er = self.get_error_str(); + // if this panics, its a bug + panic!("Failure when trying to execute error handler function! ({})", er); + }, + _ => {} } } @@ -176,7 +218,7 @@ impl State { let lua = self.lua.as_ptr(); unsafe { - let base = "_G".as_ptr() as *const i8; + let base = "_G\0".as_ptr().cast(); lua::luaL_requiref(lua, base, lua::luaopen_base, 1); assert_eq!(lua::lua_tonumber(lua, -1), 0.0); // ensure that loading base worked lua::lua_pop(lua, 1); @@ -210,7 +252,7 @@ impl State { /// The result will be Ok if the stack has space for `n` values. pub unsafe fn ensure_stack(&self, n: i32) -> Result<()> { if lua::lua_checkstack(self.state_ptr(), n) == 0 { - Err(Error::Oom) + Err(Error::MemoryAlloc) } else { Ok(()) } @@ -328,14 +370,8 @@ impl State { let ptr = lua::lua_newuserdata(s, mem::size_of::()).cast::(); ptr::write(ptr, data); - let name_cstr = format!("{}\0", name); - let name_cstr = name_cstr.as_str(); - let name_cstr = name_cstr.as_ptr() as *const i8; - - // attempt to get the metatable - - let udmts = &mut self.get_extra_space().userdata_metatables;//.get(&TypeId::of::()) - + // get the current metatable, or create a new one and push it to the stack. + let udmts = &mut self.get_extra_space().userdata_metatables; if !udmts.contains_key(&TypeId::of::()) { // create the userdata's metatable and store it in extra space let mt = self.create_userdata_metatable::()?; @@ -428,4 +464,61 @@ impl State { Ok(mt) } + + pub(crate) unsafe fn debug_info(&self) -> Box { + let s = self.state_ptr(); + + // lua_Debug doesn't implement default + let ar = alloc::alloc(Layout::new::()) + .cast::(); + lua::lua_getstack(s, 1, ar); + let what = "nSl".as_ptr() as *const i8; + lua::lua_getinfo(s, what, ar); + + Box::from_raw(ar) + } + + pub fn current_line(&self) -> usize { + unsafe { + let ar = self.debug_info(); + ar.currentline as usize + } + } + + pub fn traceback(&self, msg: Option<&str>) -> Result { + unsafe { + let _g = StackGuard::new(self); + let s = self.state_ptr(); + + let globals = self.globals()?; + let debug = globals.get::<_, Table>("debug")?; + let tb_fn = debug.get::<_, Function>("traceback")?; + tb_fn.push_to_lua_stack(self)?; + + if let Some(msg) = msg { + lua::lua_pushstring(s, msg.as_ptr().cast()); + } else { + lua::lua_pushnil(s); + } + + // skip the call to debug.traceback, and the + // error handler (which hopefully called this) + lua::lua_pushnumber(s, 2.0); + + match lua::lua_pcall(s, 2, 1, 0) { + lua::LUA_ERRRUN => { + let er = self.get_error_str(); + return Err(Error::runtime(er)); + }, + lua::LUA_ERRMEM => { + return Err(Error::MemoryAlloc) + }, + _ => {} + } + + let v = Value::from_lua_stack(self)?; + let s = v.into_string()?; + Ok(s) + } + } } \ No newline at end of file diff --git a/src/value.rs b/src/value.rs index 133ad1f..98914f9 100755 --- a/src/value.rs +++ b/src/value.rs @@ -63,6 +63,15 @@ impl<'a> Value<'a> { } } } + + pub fn into_string(self) -> crate::Result { + match self { + Value::String(s) => Ok(s), + _ => { + Err(crate::Error::UnexpectedType("String".to_string(), self.type_name().to_string())) + } + } + } } impl<'a> PushToLuaStack<'a> for Value<'a> { @@ -241,6 +250,19 @@ impl<'a> FromLuaVec<'a> for ValueVec<'a> { } } +impl<'a, T: FromLua<'a>> FromLuaVec<'a> for T { + fn from_lua_value_vec(state: &'a State, mut values: ValueVec<'a>) -> crate::Result { + if let Some(val) = values.pop_front() { + T::from_lua(state, val) + } else { + Err(crate::Error::IncorrectArgCount { + arg_expected: 1, + arg_count: values.len() as i32, + }) + } + } +} + macro_rules! impl_from_lua_vec_tuple { ( $count: expr, $first: tt, $( $name: tt ),+ ) => ( #[allow(non_snake_case)]