Add `LuaOptions` to customize Lua/Rust behaviour.

The only option is `catch_rust_panics` to optionally disable catching Rust panics via pcall/xpcall.
This commit is contained in:
Alex Orlenko 2021-05-03 21:16:42 +01:00
parent 585c0a25d8
commit 0f4bcca7ce
6 changed files with 197 additions and 17 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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<Lua> {
pub fn new_with(libs: StdLib, options: LuaOptions) -> Result<Lua> {
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
}

View File

@ -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,

View File

@ -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<Value>>("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<Lua> {
let lua = Lua::new();
fn make_lua(options: LuaOptions) -> Result<Lua> {
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<StdString>| -> 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::<StdString>().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::<StdString>().unwrap() == "rust panic from lua"),
}
Ok(())
}