diff --git a/src/ffi/safe.rs b/src/ffi/safe.rs index ee70316..77931dd 100644 --- a/src/ffi/safe.rs +++ b/src/ffi/safe.rs @@ -22,6 +22,8 @@ extern "C" { pub fn meta_newindex_impl(state: *mut lua_State) -> c_int; pub fn bind_call_impl(state: *mut lua_State) -> c_int; pub fn error_traceback(state: *mut lua_State) -> c_int; + pub fn lua_nopanic_pcall(state: *mut lua_State) -> c_int; + pub fn lua_nopanic_xpcall(state: *mut lua_State) -> c_int; fn lua_gc_s(L: *mut lua_State) -> c_int; fn luaL_ref_s(L: *mut lua_State) -> c_int; diff --git a/src/ffi/shim/shim.c b/src/ffi/shim/shim.c index f88fe2d..6309970 100644 --- a/src/ffi/shim/shim.c +++ b/src/ffi/shim/shim.c @@ -440,3 +440,70 @@ int error_traceback_s(lua_State *L) { lua_pop(L, 1); return error_traceback(L1); } + +// A `pcall` implementation that does not allow Lua to catch Rust panics. +// Instead, panics automatically resumed. +int lua_nopanic_pcall(lua_State *state) { + luaL_checkstack(state, 2, NULL); + + int top = lua_gettop(state); + if (top == 0) { + lua_pushstring(state, "not enough arguments to pcall"); + lua_error(state); + } + + if (lua_pcall(state, top - 1, LUA_MULTRET, 0) == LUA_OK) { + lua_pushboolean(state, 1); + lua_insert(state, 1); + return lua_gettop(state); + } + + if (is_wrapped_struct(state, -1, MLUA_WRAPPED_PANIC_KEY)) { + lua_error(state); + } + lua_pushboolean(state, 0); + lua_insert(state, -2); + return 2; +} + +// A `xpcall` implementation that does not allow Lua to catch Rust panics. +// Instead, panics automatically resumed. + +static int xpcall_msgh(lua_State *state) { + luaL_checkstack(state, 2, NULL); + if (is_wrapped_struct(state, -1, MLUA_WRAPPED_PANIC_KEY)) { + return 1; + } + lua_pushvalue(state, lua_upvalueindex(1)); + lua_insert(state, 1); + lua_call(state, lua_gettop(state) - 1, LUA_MULTRET); + return lua_gettop(state); +} + +int lua_nopanic_xpcall(lua_State *state) { + luaL_checkstack(state, 2, NULL); + + int top = lua_gettop(state); + if (top < 2) { + lua_pushstring(state, "not enough arguments to xpcall"); + lua_error(state); + } + + lua_pushvalue(state, 2); + lua_pushcclosure(state, xpcall_msgh, 1); + lua_copy(state, 1, 2); + lua_replace(state, 1); + + if (lua_pcall(state, lua_gettop(state) - 2, LUA_MULTRET, 1) == LUA_OK) { + lua_pushboolean(state, 1); + lua_insert(state, 2); + return lua_gettop(state) - 1; + } + + if (is_wrapped_struct(state, -1, MLUA_WRAPPED_PANIC_KEY)) { + lua_error(state); + } + lua_pushboolean(state, 0); + lua_insert(state, -2); + return 2; +} diff --git a/src/lib.rs b/src/lib.rs index bdffff2..4735aca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,7 +103,7 @@ pub use crate::ffi::lua_State; pub use crate::error::{Error, ExternalError, ExternalResult, Result}; pub use crate::function::Function; pub use crate::hook::{Debug, DebugNames, DebugSource, DebugStack, HookTriggers}; -pub use crate::lua::{Chunk, ChunkMode, GCMode, Lua}; +pub use crate::lua::{Chunk, ChunkMode, GCMode, Lua, LuaOptions}; pub use crate::multi::Variadic; pub use crate::scope::Scope; pub use crate::stdlib::StdLib; diff --git a/src/lua.rs b/src/lua.rs index e441f24..b4a734a 100644 --- a/src/lua.rs +++ b/src/lua.rs @@ -97,6 +97,48 @@ pub enum GCMode { Generational, } +/// Controls Lua interpreter behaviour such as Rust panics handling. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct LuaOptions { + /// Catch Rust panics when using [`pcall`]/[`xpcall`]. + /// + /// If disabled, wraps these functions and automatically resumes panic if found. + /// Also in Lua 5.1 adds ability to provide arguments to [`xpcall`] similar to Lua >= 5.2. + /// + /// If enabled, keeps [`pcall`]/[`xpcall`] unmodified. + /// Panics are still automatically resumed if returned back to the Rust side. + /// + /// Default: **true** + /// + /// [`pcall`]: https://www.lua.org/manual/5.3/manual.html#pdf-pcall + /// [`xpcall`]: https://www.lua.org/manual/5.3/manual.html#pdf-xpcall + pub catch_rust_panics: bool, +} + +impl Default for LuaOptions { + fn default() -> Self { + LuaOptions { + catch_rust_panics: true, + } + } +} + +impl LuaOptions { + /// Retruns a new instance of `LuaOptions` with default parameters. + pub fn new() -> Self { + Self::default() + } + + /// Sets [`catch_rust_panics`] option. + /// + /// [`catch_rust_panics`]: #structfield.catch_rust_panics + pub fn catch_rust_panics(mut self, enabled: bool) -> Self { + self.catch_rust_panics = enabled; + self + } +} + #[cfg(feature = "async")] pub(crate) static ASYNC_POLL_PENDING: u8 = 0; #[cfg(feature = "async")] @@ -149,7 +191,7 @@ impl Lua { #[allow(clippy::new_without_default)] pub fn new() -> Lua { mlua_expect!( - Self::new_with(StdLib::ALL_SAFE), + Self::new_with(StdLib::ALL_SAFE, LuaOptions::default()), "can't create new safe Lua state" ) } @@ -159,7 +201,7 @@ impl Lua { /// # Safety /// The created Lua state would not have safety guarantees and would allow to load C modules. pub unsafe fn unsafe_new() -> Lua { - Self::unsafe_new_with(StdLib::ALL) + Self::unsafe_new_with(StdLib::ALL, LuaOptions::default()) } /// Creates a new Lua state and loads the specified safe subset of the standard libraries. @@ -173,7 +215,7 @@ impl Lua { /// See [`StdLib`] documentation for a list of unsafe modules that cannot be loaded. /// /// [`StdLib`]: struct.StdLib.html - pub fn new_with(libs: StdLib) -> Result { + pub fn new_with(libs: StdLib, options: LuaOptions) -> Result { if libs.contains(StdLib::DEBUG) { return Err(Error::SafetyError( "the unsafe `debug` module can't be loaded using safe `new_with`".to_string(), @@ -188,7 +230,7 @@ impl Lua { } } - let mut lua = unsafe { Self::unsafe_new_with(libs) }; + let mut lua = unsafe { Self::unsafe_new_with(libs, options) }; if libs.contains(StdLib::PACKAGE) { mlua_expect!(lua.disable_c_modules(), "Error during disabling C modules"); @@ -207,7 +249,7 @@ impl Lua { /// The created Lua state will not have safety guarantees and allow to load C modules. /// /// [`StdLib`]: struct.StdLib.html - pub unsafe fn unsafe_new_with(libs: StdLib) -> Lua { + pub unsafe fn unsafe_new_with(libs: StdLib, options: LuaOptions) -> Lua { #[cfg_attr(any(feature = "lua51", feature = "luajit"), allow(dead_code))] unsafe extern "C" fn allocator( extra_data: *mut c_void, @@ -295,6 +337,28 @@ impl Lua { ); mlua_expect!(lua.extra.lock(), "extra is poisoned").libs |= libs; + if !options.catch_rust_panics { + mlua_expect!( + (|| -> Result<()> { + let _sg = StackGuard::new(lua.state); + + #[cfg(any(feature = "lua54", feature = "lua53", feature = "lua52"))] + ffi::lua_rawgeti(lua.state, ffi::LUA_REGISTRYINDEX, ffi::LUA_RIDX_GLOBALS); + #[cfg(any(feature = "lua51", feature = "luajit"))] + ffi::lua_pushvalue(lua.state, ffi::LUA_GLOBALSINDEX); + + ffi::lua_pushcfunction(lua.state, ffi::safe::lua_nopanic_pcall); + ffi::safe::lua_rawsetfield(lua.state, -2, "pcall")?; + + ffi::lua_pushcfunction(lua.state, ffi::safe::lua_nopanic_xpcall); + ffi::safe::lua_rawsetfield(lua.state, -2, "xpcall")?; + + Ok(()) + })(), + "Error during applying option `catch_rust_panics`" + ) + } + lua } diff --git a/src/prelude.rs b/src/prelude.rs index b043ac6..5ec7f3e 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,7 +4,7 @@ pub use crate::{ AnyUserData as LuaAnyUserData, Chunk as LuaChunk, Error as LuaError, ExternalError as LuaExternalError, ExternalResult as LuaExternalResult, FromLua, FromLuaMulti, Function as LuaFunction, GCMode as LuaGCMode, Integer as LuaInteger, - LightUserData as LuaLightUserData, Lua, MetaMethod as LuaMetaMethod, + LightUserData as LuaLightUserData, Lua, LuaOptions, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, Number as LuaNumber, RegistryKey as LuaRegistryKey, Result as LuaResult, String as LuaString, Table as LuaTable, TableExt as LuaTableExt, TablePairs as LuaTablePairs, TableSequence as LuaTableSequence, Thread as LuaThread, diff --git a/tests/tests.rs b/tests/tests.rs index 634bb2b..6925a08 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use std::{error, f32, f64, fmt}; use mlua::{ - ChunkMode, Error, ExternalError, Function, Lua, Nil, Result, StdLib, String, Table, UserData, - Value, Variadic, + ChunkMode, Error, ExternalError, Function, Lua, LuaOptions, Nil, Result, StdLib, String, Table, + UserData, Value, Variadic, }; #[test] @@ -24,7 +24,7 @@ fn test_safety() -> Result<()> { assert!(lua.load(r#"require "debug""#).exec().is_ok()); drop(lua); - match Lua::new_with(StdLib::DEBUG) { + match Lua::new_with(StdLib::DEBUG, LuaOptions::default()) { Err(Error::SafetyError(_)) => {} Err(e) => panic!("expected SafetyError, got {:?}", e), Ok(_) => panic!("expected SafetyError, got new Lua state"), @@ -64,7 +64,7 @@ fn test_safety() -> Result<()> { drop(lua); // Test safety rules after dynamically loading `package` library - let lua = Lua::new_with(StdLib::NONE)?; + let lua = Lua::new_with(StdLib::NONE, LuaOptions::default())?; assert!(lua.globals().get::<_, Option>("require")?.is_none()); lua.load_from_std_lib(StdLib::PACKAGE)?; match lua.load(r#"package.loadlib()"#).exec() { @@ -380,10 +380,15 @@ fn test_error() -> Result<()> { #[test] fn test_panic() -> Result<()> { - fn make_lua() -> Result { - let lua = Lua::new(); + fn make_lua(options: LuaOptions) -> Result { + let lua = Lua::new_with(StdLib::ALL_SAFE, options)?; let rust_panic_function = - lua.create_function(|_, ()| -> Result<()> { panic!("rust panic") })?; + lua.create_function(|_, msg: Option| -> Result<()> { + if let Some(msg) = msg { + panic!("{}", msg) + } + panic!("rust panic") + })?; lua.globals() .set("rust_panic_function", rust_panic_function)?; Ok(lua) @@ -391,7 +396,7 @@ fn test_panic() -> Result<()> { // Test triggerting Lua error passing Rust panic (must be resumed) { - let lua = make_lua()?; + let lua = make_lua(LuaOptions::default())?; match catch_unwind(AssertUnwindSafe(|| -> Result<()> { lua.load( @@ -417,7 +422,7 @@ fn test_panic() -> Result<()> { // Test returning Rust panic (must be resumed) { - let lua = make_lua()?; + let lua = make_lua(LuaOptions::default())?; match catch_unwind(AssertUnwindSafe(|| -> Result<()> { let _catched_panic = lua .load( @@ -447,7 +452,7 @@ fn test_panic() -> Result<()> { // Test representing rust panic as a string match catch_unwind(|| -> Result<()> { - let lua = make_lua()?; + let lua = make_lua(LuaOptions::default())?; lua.load( r#" local _, err = pcall(rust_panic_function) @@ -462,6 +467,48 @@ fn test_panic() -> Result<()> { Err(_) => panic!("panic was detected"), } + // Test disabling `catch_rust_panics` option / pcall correctness + match catch_unwind(|| -> Result<()> { + let lua = make_lua(LuaOptions::new().catch_rust_panics(false))?; + lua.load( + r#" + local ok, err = pcall(function(msg) error(msg) end, "hello") + assert(not ok and err:find("hello") ~= nil) + + ok, err = pcall(rust_panic_function, "rust panic from lua") + -- Nothing to return, panic should be automatically resumed + "#, + ) + .exec() + }) { + Ok(r) => panic!("no panic was detected: {:?}", r), + Err(p) => assert!(*p.downcast::().unwrap() == "rust panic from lua"), + } + + // Test enabling `catch_rust_panics` option / xpcall correctness + match catch_unwind(|| -> Result<()> { + let lua = make_lua(LuaOptions::new().catch_rust_panics(false))?; + lua.load( + r#" + local msgh_ok = false + local msgh = function(err) + msgh_ok = err ~= nil and err:find("hello") ~= nil + return err + end + local ok, err = xpcall(function(msg) error(msg) end, msgh, "hello") + assert(not ok and err:find("hello") ~= nil) + assert(msgh_ok) + + ok, err = xpcall(rust_panic_function, msgh, "rust panic from lua") + -- Nothing to return, panic should be automatically resumed + "#, + ) + .exec() + }) { + Ok(r) => panic!("no panic was detected: {:?}", r), + Err(p) => assert!(*p.downcast::().unwrap() == "rust panic from lua"), + } + Ok(()) }