use std::mem; use std::ptr; use std::process; use std::sync::Arc; use std::ffi::CStr; use std::any::Any; use std::os::raw::{c_char, c_int, c_void}; use std::panic::{catch_unwind, resume_unwind, UnwindSafe}; use ffi; use error::{Result, Error}; macro_rules! cstr { ($s:expr) => ( concat!($s, "\0") as *const str as *const [c_char] as *const c_char ); } // A panic that clears the given lua stack before panicking macro_rules! lua_panic { ($state:expr) => { { $crate::ffi::lua_settor($state, 0); panic!("rlua internal error"); } }; ($state:expr, $msg:expr) => { { $crate::ffi::lua_settop($state, 0); panic!(concat!("rlua: ", $msg)); } }; ($state:expr, $fmt:expr, $($arg:tt)+) => { { $crate::ffi::lua_settop($state, 0); panic!(concat!("rlua: ", $fmt), $($arg)+); } }; } // An assert that clears the given lua stack before panicking macro_rules! lua_assert { ($state:expr, $cond:expr) => { if !$cond { $crate::ffi::lua_settop($state, 0); panic!("rlua internal error"); } }; ($state:expr, $cond:expr, $msg:expr) => { if !$cond { $crate::ffi::lua_settop($state, 0); panic!(concat!("rlua: ", $msg)); } }; ($state:expr, $cond:expr, $fmt:expr, $($arg:tt)+) => { if !$cond { $crate::ffi::lua_settop($state, 0); panic!(concat!("rlua: ", $fmt), $($arg)+); } }; } // Checks that Lua has enough free stack space for future stack operations. // On failure, this will clear the stack and panic. pub unsafe fn check_stack(state: *mut ffi::lua_State, amount: c_int) { lua_assert!( state, ffi::lua_checkstack(state, amount) != 0, "out of stack space" ); } // Run an operation on a lua_State and check that the stack change is what is // expected. If the stack change does not match, clears the stack and panics. pub unsafe fn stack_guard(state: *mut ffi::lua_State, change: c_int, op: F) -> R where F: FnOnce() -> R, { let expected = ffi::lua_gettop(state) + change; lua_assert!( state, expected >= 0, "internal stack error: too many values would be popped" ); let res = op(); let top = ffi::lua_gettop(state); lua_assert!( state, ffi::lua_gettop(state) == expected, "internal stack error: expected stack to be {}, got {}", expected, top ); res } // Run an operation on a lua_State and automatically clean up the stack before // returning. Takes the lua_State, the expected stack size change, and an // operation to run. If the operation results in success, then the stack is // inspected to make sure the change in stack size matches the expected change // and otherwise this is a logic error and will panic. If the operation results // in an error, the stack is shrunk to the value before the call. If the // operation results in an error and the stack is smaller than the value before // the call, then this is unrecoverable and this will panic. If this function // panics, it will clear the stack before panicking. pub unsafe fn stack_err_guard(state: *mut ffi::lua_State, change: c_int, op: F) -> Result where F: FnOnce() -> Result, { let expected = ffi::lua_gettop(state) + change; lua_assert!( state, expected >= 0, "internal stack error: too many values would be popped" ); let res = op(); let top = ffi::lua_gettop(state); if res.is_ok() { lua_assert!( state, ffi::lua_gettop(state) == expected, "internal stack error: expected stack to be {}, got {}", expected, top ); } else { lua_assert!( state, top >= expected, "internal stack error: {} too many values popped", top - expected ); if top > expected { ffi::lua_settop(state, expected); } } res } // Protected version of lua_gettable, uses 3 stack spaces, does not call checkstack. pub unsafe fn pgettable(state: *mut ffi::lua_State, index: c_int) -> Result { unsafe extern "C" fn gettable(state: *mut ffi::lua_State) -> c_int { ffi::lua_gettable(state, -2); 1 } let table_index = ffi::lua_absindex(state, index); ffi::lua_pushcfunction(state, gettable); ffi::lua_pushvalue(state, table_index); ffi::lua_pushvalue(state, -3); ffi::lua_remove(state, -4); handle_error(state, pcall_with_traceback(state, 2, 1))?; Ok(ffi::lua_type(state, -1)) } // Protected version of lua_settable, uses 4 stack spaces, does not call checkstack. pub unsafe fn psettable(state: *mut ffi::lua_State, index: c_int) -> Result<()> { unsafe extern "C" fn settable(state: *mut ffi::lua_State) -> c_int { ffi::lua_settable(state, -3); 0 } let table_index = ffi::lua_absindex(state, index); ffi::lua_pushcfunction(state, settable); ffi::lua_pushvalue(state, table_index); ffi::lua_pushvalue(state, -4); ffi::lua_pushvalue(state, -4); ffi::lua_remove(state, -5); ffi::lua_remove(state, -5); handle_error(state, pcall_with_traceback(state, 3, 0))?; Ok(()) } // Protected version of luaL_len, uses 2 stack spaces, does not call checkstack. pub unsafe fn plen(state: *mut ffi::lua_State, index: c_int) -> Result { unsafe extern "C" fn len(state: *mut ffi::lua_State) -> c_int { ffi::lua_pushinteger(state, ffi::luaL_len(state, -1)); 1 } let table_index = ffi::lua_absindex(state, index); ffi::lua_pushcfunction(state, len); ffi::lua_pushvalue(state, table_index); handle_error(state, pcall_with_traceback(state, 1, 1))?; let len = ffi::lua_tointeger(state, -1); ffi::lua_pop(state, 1); Ok(len) } // Protected version of lua_geti, uses 3 stack spaces, does not call checkstack. pub unsafe fn pgeti( state: *mut ffi::lua_State, index: c_int, i: ffi::lua_Integer, ) -> Result { unsafe extern "C" fn geti(state: *mut ffi::lua_State) -> c_int { let i = ffi::lua_tointeger(state, -1); ffi::lua_geti(state, -2, i); 1 } let table_index = ffi::lua_absindex(state, index); ffi::lua_pushcfunction(state, geti); ffi::lua_pushvalue(state, table_index); ffi::lua_pushinteger(state, i); handle_error(state, pcall_with_traceback(state, 2, 1))?; Ok(ffi::lua_type(state, -1)) } // Protected version of lua_next, uses 3 stack spaces, does not call checkstack. pub unsafe fn pnext(state: *mut ffi::lua_State, index: c_int) -> Result { unsafe extern "C" fn next(state: *mut ffi::lua_State) -> c_int { if ffi::lua_next(state, -2) == 0 { 0 } else { 2 } } let table_index = ffi::lua_absindex(state, index); ffi::lua_pushcfunction(state, next); ffi::lua_pushvalue(state, table_index); ffi::lua_pushvalue(state, -3); ffi::lua_remove(state, -4); let stack_start = ffi::lua_gettop(state) - 3; handle_error(state, pcall_with_traceback(state, 2, ffi::LUA_MULTRET))?; let nresults = ffi::lua_gettop(state) - stack_start; if nresults == 0 { Ok(0) } else { Ok(1) } } // If the return code indicates an error, pops the error off of the stack and // returns Err. If the error is actually a WrappedPaic, clears the current lua // stack continues the panic. If the error on the top of the stack is actually // a WrappedError, just returns it. Otherwise, interprets the error as the // appropriate lua error. pub unsafe fn handle_error(state: *mut ffi::lua_State, err: c_int) -> Result<()> { if err == ffi::LUA_OK || err == ffi::LUA_YIELD { Ok(()) } else { if let Some(err) = pop_wrapped_error(state) { Err(err) } else if is_wrapped_panic(state, -1) { let panic = &mut *get_userdata::(state, -1); if let Some(p) = panic.0.take() { ffi::lua_settop(state, 0); resume_unwind(p); } else { lua_panic!(state, "internal error: panic was resumed twice") } } else { let err_string = if let Some(s) = ffi::lua_tolstring(state, -1, ptr::null_mut()).as_ref() { CStr::from_ptr(s) .to_str() .unwrap_or_else(|_| "") .to_owned() } else { "".to_owned() }; ffi::lua_pop(state, 1); Err(match err { ffi::LUA_ERRRUN => Error::RuntimeError(err_string), ffi::LUA_ERRSYNTAX => { // This seems terrible, but as far as I can tell, this is exactly what the stock // lua repl does. if err_string.ends_with("") { Error::IncompleteStatement(err_string) } else { Error::SyntaxError(err_string) } } ffi::LUA_ERRERR => Error::ErrorError(err_string), ffi::LUA_ERRMEM => { // TODO: We should provide lua with custom allocators that guarantee aborting on // OOM, instead of relying on the system allocator. Currently, this is a // theoretical soundness bug, because an OOM could trigger a longjmp out of // arbitrary rust code that is unprotectedly calling a lua function marked as // potentially causing memory errors, which is most of them. eprintln!("Lua memory error, aborting!"); process::abort() } ffi::LUA_ERRGCMM => { // This should be impossible, since we wrap setmetatable to protect __gc // metamethods, but if we do end up here then the same logic as setmetatable // applies and we must abort. eprintln!("Lua error during __gc, aborting!"); process::abort() } _ => lua_panic!(state, "internal error: unrecognized lua error code"), }) } } } pub unsafe fn push_string(state: *mut ffi::lua_State, s: &str) { ffi::lua_pushlstring(state, s.as_ptr() as *const c_char, s.len()); } pub unsafe fn push_userdata(state: *mut ffi::lua_State, t: T) -> *mut T { let ud = ffi::lua_newuserdata(state, mem::size_of::>()) as *mut Option; ptr::write(ud, None); *ud = Some(t); (*ud).as_mut().unwrap() } pub unsafe fn get_userdata(state: *mut ffi::lua_State, index: c_int) -> *mut T { let ud = ffi::lua_touserdata(state, index) as *mut Option; lua_assert!(state, !ud.is_null()); (*ud).as_mut().expect("access of expired userdata") } pub unsafe extern "C" fn userdata_destructor(state: *mut ffi::lua_State) -> c_int { match catch_unwind(|| { *(ffi::lua_touserdata(state, 1) as *mut Option) = None; 0 }) { Ok(r) => r, Err(p) => { push_wrapped_panic(state, p); ffi::lua_error(state) } } } // In the context of a lua callback, this will call the given function and if the given function // returns an error, *or if the given function panics*, this will result in a call to lua_error (a // longjmp). The error or panic is wrapped in such a way that when calling handle_error back on // the rust side, it will resume the panic. pub unsafe fn callback_error(state: *mut ffi::lua_State, f: F) -> R where F: FnOnce() -> Result + UnwindSafe, { match catch_unwind(f) { Ok(Ok(r)) => r, Ok(Err(err)) => { push_wrapped_error(state, err); ffi::lua_error(state) } Err(p) => { push_wrapped_panic(state, p); ffi::lua_error(state) } } } // ffi::lua_pcall with a message handler that gives a nice traceback. If the // caught error is actually a Error, will simply pass the error along. Does // not call checkstack, and uses 2 extra stack spaces. pub unsafe fn pcall_with_traceback( state: *mut ffi::lua_State, nargs: c_int, nresults: c_int, ) -> c_int { unsafe extern "C" fn message_handler(state: *mut ffi::lua_State) -> c_int { if let Some(error) = pop_wrapped_error(state) { ffi::luaL_traceback(state, state, ptr::null(), 0); let traceback = CStr::from_ptr(ffi::lua_tolstring(state, -1, ptr::null_mut())) .to_str() .unwrap_or_else(|_| "") .to_owned(); push_wrapped_error(state, Error::CallbackError(traceback, Arc::new(error))); ffi::lua_remove(state, -2); } else if !is_wrapped_panic(state, 1) { let s = ffi::lua_tolstring(state, 1, ptr::null_mut()); if !s.is_null() { ffi::luaL_traceback(state, state, s, 0); } else { ffi::luaL_traceback(state, state, cstr!(""), 0); } ffi::lua_remove(state, -2); } 1 } let msgh_position = ffi::lua_gettop(state) - nargs; ffi::lua_pushcfunction(state, message_handler); ffi::lua_insert(state, msgh_position); let ret = ffi::lua_pcall(state, nargs, nresults, msgh_position); ffi::lua_remove(state, msgh_position); ret } pub unsafe fn resume_with_traceback( state: *mut ffi::lua_State, from: *mut ffi::lua_State, nargs: c_int, ) -> c_int { let res = ffi::lua_resume(state, from, nargs); if res != ffi::LUA_OK && res != ffi::LUA_YIELD { if let Some(error) = pop_wrapped_error(state) { ffi::luaL_traceback(state, state, ptr::null(), 0); let traceback = CStr::from_ptr(ffi::lua_tolstring(state, -1, ptr::null_mut())) .to_str() .unwrap_or_else(|_| "") .to_owned(); push_wrapped_error(state, Error::CallbackError(traceback, Arc::new(error))); ffi::lua_remove(state, -2); } else if !is_wrapped_panic(state, 1) { let s = ffi::lua_tolstring(state, 1, ptr::null_mut()); if !s.is_null() { ffi::luaL_traceback(state, state, s, 0); } else { ffi::luaL_traceback(state, state, cstr!(""), 0); } ffi::lua_remove(state, -2); } } res } // A variant of pcall that does not allow lua to catch panic errors from callback_error pub unsafe extern "C" fn safe_pcall(state: *mut ffi::lua_State) -> c_int { let top = ffi::lua_gettop(state); if top == 0 { push_string(state, "not enough arguments to pcall"); ffi::lua_error(state); } else if ffi::lua_pcall(state, top - 1, ffi::LUA_MULTRET, 0) != ffi::LUA_OK { if is_wrapped_panic(state, -1) { ffi::lua_error(state); } ffi::lua_pushboolean(state, 0); ffi::lua_insert(state, -2); 2 } else { ffi::lua_pushboolean(state, 1); ffi::lua_insert(state, 1); ffi::lua_gettop(state) } } // A variant of xpcall that does not allow lua to catch panic errors from callback_error pub unsafe extern "C" fn safe_xpcall(state: *mut ffi::lua_State) -> c_int { unsafe extern "C" fn xpcall_msgh(state: *mut ffi::lua_State) -> c_int { if is_wrapped_panic(state, -1) { 1 } else { ffi::lua_pushvalue(state, ffi::lua_upvalueindex(1)); ffi::lua_insert(state, 1); ffi::lua_call(state, ffi::lua_gettop(state) - 1, ffi::LUA_MULTRET); ffi::lua_gettop(state) } } let top = ffi::lua_gettop(state); if top < 2 { push_string(state, "not enough arguments to xpcall"); ffi::lua_error(state); } ffi::lua_pushvalue(state, 2); ffi::lua_pushcclosure(state, xpcall_msgh, 1); ffi::lua_copy(state, 1, 2); ffi::lua_replace(state, 1); let res = ffi::lua_pcall(state, ffi::lua_gettop(state) - 2, ffi::LUA_MULTRET, 1); if res != ffi::LUA_OK { if is_wrapped_panic(state, -1) { ffi::lua_error(state); } ffi::lua_pushboolean(state, 0); ffi::lua_insert(state, -2); 2 } else { ffi::lua_pushboolean(state, 1); ffi::lua_insert(state, 1); ffi::lua_gettop(state) } } // Safely call setmetatable, if a __gc function is given, will wrap it in pcall, and panic on error. pub unsafe extern "C" fn safe_setmetatable(state: *mut ffi::lua_State) -> c_int { if ffi::lua_gettop(state) < 2 { push_string(state, "not enough arguments to setmetatable"); ffi::lua_error(state); } // Wrapping the __gc method in setmetatable ONLY works because Lua 5.3 only honors the __gc // method when it exists upon calling setmetatable, and ignores it if it is set later. push_string(state, "__gc"); if ffi::lua_rawget(state, -2) == ffi::LUA_TFUNCTION { unsafe extern "C" fn safe_gc(state: *mut ffi::lua_State) -> c_int { ffi::lua_pushvalue(state, ffi::lua_upvalueindex(1)); ffi::lua_insert(state, 1); if ffi::lua_pcall(state, 1, 0, 0) != ffi::LUA_OK { // If a user supplied __gc metamethod causes an error, we must always abort. We may // be inside a protected context due to being in a callback, but inside an // unprotected ffi call that can cause memory errors, so may be at risk of // longjmping over arbitrary rust. eprintln!("Lua error during __gc, aborting!"); process::abort() } else { ffi::lua_gettop(state) } } ffi::lua_pushcclosure(state, safe_gc, 1); push_string(state, "__gc"); ffi::lua_insert(state, -2); ffi::lua_rawset(state, -3); } else { ffi::lua_pop(state, 1); } ffi::lua_setmetatable(state, -2); 0 } // Does not call checkstack, uses 1 stack space pub unsafe fn main_state(state: *mut ffi::lua_State) -> *mut ffi::lua_State { ffi::lua_rawgeti(state, ffi::LUA_REGISTRYINDEX, ffi::LUA_RIDX_MAINTHREAD); let main_state = ffi::lua_tothread(state, -1); ffi::lua_pop(state, 1); main_state } pub struct WrappedError(pub Error); pub struct WrappedPanic(pub Option>); // Pushes a WrappedError::Error to the top of the stack pub unsafe fn push_wrapped_error(state: *mut ffi::lua_State, err: Error) { unsafe extern "C" fn error_tostring(state: *mut ffi::lua_State) -> c_int { callback_error(state, || if is_wrapped_error(state, -1) { let error = &*get_userdata::(state, -1); push_string(state, &error.0.to_string()); ffi::lua_remove(state, -2); Ok(1) } else { Err(Error::FromLuaConversionError( "internal error: userdata mismatch in Error metamethod" .to_owned(), )) }) } ffi::luaL_checkstack(state, 2, ptr::null()); push_userdata(state, WrappedError(err)); get_error_metatable(state); if ffi::lua_isnil(state, -1) != 0 { ffi::lua_pop(state, 1); ffi::luaL_checkstack(state, 7, ptr::null()); ffi::lua_newtable(state); ffi::lua_pushlightuserdata( state, &ERROR_METATABLE_REGISTRY_KEY as *const u8 as *mut c_void, ); ffi::lua_pushvalue(state, -2); push_string(state, "__gc"); ffi::lua_pushcfunction(state, userdata_destructor::); ffi::lua_settable(state, -3); push_string(state, "__tostring"); ffi::lua_pushcfunction(state, error_tostring); ffi::lua_settable(state, -3); push_string(state, "__metatable"); ffi::lua_pushboolean(state, 0); ffi::lua_settable(state, -3); ffi::lua_settable(state, ffi::LUA_REGISTRYINDEX); } ffi::lua_setmetatable(state, -2); } // Pushes a WrappedError::Panic to the top of the stack pub unsafe fn push_wrapped_panic(state: *mut ffi::lua_State, panic: Box) { ffi::luaL_checkstack(state, 2, ptr::null()); push_userdata(state, WrappedPanic(Some(panic))); get_panic_metatable(state); if ffi::lua_isnil(state, -1) != 0 { ffi::lua_pop(state, 1); ffi::luaL_checkstack(state, 7, ptr::null()); ffi::lua_newtable(state); ffi::lua_pushlightuserdata( state, &PANIC_METATABLE_REGISTRY_KEY as *const u8 as *mut c_void, ); ffi::lua_pushvalue(state, -2); push_string(state, "__gc"); ffi::lua_pushcfunction(state, userdata_destructor::); ffi::lua_settable(state, -3); push_string(state, "__metatable"); ffi::lua_pushboolean(state, 0); ffi::lua_settable(state, -3); ffi::lua_settable(state, ffi::LUA_REGISTRYINDEX); } ffi::lua_setmetatable(state, -2); } // Pops a WrappedError off of the top of the stack, if it is a WrappedError. If // it is not a WrappedError, returns None and does not pop anything. pub unsafe fn pop_wrapped_error(state: *mut ffi::lua_State) -> Option { if ffi::lua_gettop(state) == 0 || !is_wrapped_error(state, -1) { None } else { let err = &*get_userdata::(state, -1); let err = err.0.clone(); ffi::lua_pop(state, 1); Some(err) } } // Checks if the value at the given index is a WrappedError pub unsafe fn is_wrapped_error(state: *mut ffi::lua_State, index: c_int) -> bool { assert_ne!( ffi::lua_checkstack(state, 2), 0, "somehow not enough stack space to check if a value is a WrappedError" ); let index = ffi::lua_absindex(state, index); let userdata = ffi::lua_touserdata(state, index); if userdata.is_null() { return false; } if ffi::lua_getmetatable(state, index) == 0 { return false; } get_error_metatable(state); let res = ffi::lua_rawequal(state, -1, -2) != 0; ffi::lua_pop(state, 2); res } // Checks if the value at the given index is a WrappedPanic pub unsafe fn is_wrapped_panic(state: *mut ffi::lua_State, index: c_int) -> bool { assert_ne!( ffi::lua_checkstack(state, 2), 0, "somehow not enough stack space to check if a value is a wrapped panic" ); let index = ffi::lua_absindex(state, index); let userdata = ffi::lua_touserdata(state, index); if userdata.is_null() { return false; } if ffi::lua_getmetatable(state, index) == 0 { return false; } get_panic_metatable(state); let res = ffi::lua_rawequal(state, -1, -2) != 0; ffi::lua_pop(state, 2); return res; } pub unsafe fn get_error_metatable(state: *mut ffi::lua_State) -> c_int { ffi::lua_pushlightuserdata( state, &ERROR_METATABLE_REGISTRY_KEY as *const u8 as *mut c_void, ); ffi::lua_gettable(state, ffi::LUA_REGISTRYINDEX) } pub unsafe fn get_panic_metatable(state: *mut ffi::lua_State) -> c_int { ffi::lua_pushlightuserdata( state, &PANIC_METATABLE_REGISTRY_KEY as *const u8 as *mut c_void, ); ffi::lua_gettable(state, ffi::LUA_REGISTRYINDEX) } static ERROR_METATABLE_REGISTRY_KEY: u8 = 0; static PANIC_METATABLE_REGISTRY_KEY: u8 = 0;