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:
parent
585c0a25d8
commit
0f4bcca7ce
|
@ -22,6 +22,8 @@ extern "C" {
|
||||||
pub fn meta_newindex_impl(state: *mut lua_State) -> c_int;
|
pub fn meta_newindex_impl(state: *mut lua_State) -> c_int;
|
||||||
pub fn bind_call_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 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 lua_gc_s(L: *mut lua_State) -> c_int;
|
||||||
fn luaL_ref_s(L: *mut lua_State) -> c_int;
|
fn luaL_ref_s(L: *mut lua_State) -> c_int;
|
||||||
|
|
|
@ -440,3 +440,70 @@ int error_traceback_s(lua_State *L) {
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
return error_traceback(L1);
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@ pub use crate::ffi::lua_State;
|
||||||
pub use crate::error::{Error, ExternalError, ExternalResult, Result};
|
pub use crate::error::{Error, ExternalError, ExternalResult, Result};
|
||||||
pub use crate::function::Function;
|
pub use crate::function::Function;
|
||||||
pub use crate::hook::{Debug, DebugNames, DebugSource, DebugStack, HookTriggers};
|
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::multi::Variadic;
|
||||||
pub use crate::scope::Scope;
|
pub use crate::scope::Scope;
|
||||||
pub use crate::stdlib::StdLib;
|
pub use crate::stdlib::StdLib;
|
||||||
|
|
74
src/lua.rs
74
src/lua.rs
|
@ -97,6 +97,48 @@ pub enum GCMode {
|
||||||
Generational,
|
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")]
|
#[cfg(feature = "async")]
|
||||||
pub(crate) static ASYNC_POLL_PENDING: u8 = 0;
|
pub(crate) static ASYNC_POLL_PENDING: u8 = 0;
|
||||||
#[cfg(feature = "async")]
|
#[cfg(feature = "async")]
|
||||||
|
@ -149,7 +191,7 @@ impl Lua {
|
||||||
#[allow(clippy::new_without_default)]
|
#[allow(clippy::new_without_default)]
|
||||||
pub fn new() -> Lua {
|
pub fn new() -> Lua {
|
||||||
mlua_expect!(
|
mlua_expect!(
|
||||||
Self::new_with(StdLib::ALL_SAFE),
|
Self::new_with(StdLib::ALL_SAFE, LuaOptions::default()),
|
||||||
"can't create new safe Lua state"
|
"can't create new safe Lua state"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -159,7 +201,7 @@ impl Lua {
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// The created Lua state would not have safety guarantees and would allow to load C modules.
|
/// The created Lua state would not have safety guarantees and would allow to load C modules.
|
||||||
pub unsafe fn unsafe_new() -> Lua {
|
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.
|
/// 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.
|
/// See [`StdLib`] documentation for a list of unsafe modules that cannot be loaded.
|
||||||
///
|
///
|
||||||
/// [`StdLib`]: struct.StdLib.html
|
/// [`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) {
|
if libs.contains(StdLib::DEBUG) {
|
||||||
return Err(Error::SafetyError(
|
return Err(Error::SafetyError(
|
||||||
"the unsafe `debug` module can't be loaded using safe `new_with`".to_string(),
|
"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) {
|
if libs.contains(StdLib::PACKAGE) {
|
||||||
mlua_expect!(lua.disable_c_modules(), "Error during disabling C modules");
|
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.
|
/// The created Lua state will not have safety guarantees and allow to load C modules.
|
||||||
///
|
///
|
||||||
/// [`StdLib`]: struct.StdLib.html
|
/// [`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))]
|
#[cfg_attr(any(feature = "lua51", feature = "luajit"), allow(dead_code))]
|
||||||
unsafe extern "C" fn allocator(
|
unsafe extern "C" fn allocator(
|
||||||
extra_data: *mut c_void,
|
extra_data: *mut c_void,
|
||||||
|
@ -295,6 +337,28 @@ impl Lua {
|
||||||
);
|
);
|
||||||
mlua_expect!(lua.extra.lock(), "extra is poisoned").libs |= libs;
|
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
|
lua
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ pub use crate::{
|
||||||
AnyUserData as LuaAnyUserData, Chunk as LuaChunk, Error as LuaError,
|
AnyUserData as LuaAnyUserData, Chunk as LuaChunk, Error as LuaError,
|
||||||
ExternalError as LuaExternalError, ExternalResult as LuaExternalResult, FromLua, FromLuaMulti,
|
ExternalError as LuaExternalError, ExternalResult as LuaExternalResult, FromLua, FromLuaMulti,
|
||||||
Function as LuaFunction, GCMode as LuaGCMode, Integer as LuaInteger,
|
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,
|
MultiValue as LuaMultiValue, Nil as LuaNil, Number as LuaNumber, RegistryKey as LuaRegistryKey,
|
||||||
Result as LuaResult, String as LuaString, Table as LuaTable, TableExt as LuaTableExt,
|
Result as LuaResult, String as LuaString, Table as LuaTable, TableExt as LuaTableExt,
|
||||||
TablePairs as LuaTablePairs, TableSequence as LuaTableSequence, Thread as LuaThread,
|
TablePairs as LuaTablePairs, TableSequence as LuaTableSequence, Thread as LuaThread,
|
||||||
|
|
|
@ -5,8 +5,8 @@ use std::sync::Arc;
|
||||||
use std::{error, f32, f64, fmt};
|
use std::{error, f32, f64, fmt};
|
||||||
|
|
||||||
use mlua::{
|
use mlua::{
|
||||||
ChunkMode, Error, ExternalError, Function, Lua, Nil, Result, StdLib, String, Table, UserData,
|
ChunkMode, Error, ExternalError, Function, Lua, LuaOptions, Nil, Result, StdLib, String, Table,
|
||||||
Value, Variadic,
|
UserData, Value, Variadic,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -24,7 +24,7 @@ fn test_safety() -> Result<()> {
|
||||||
assert!(lua.load(r#"require "debug""#).exec().is_ok());
|
assert!(lua.load(r#"require "debug""#).exec().is_ok());
|
||||||
drop(lua);
|
drop(lua);
|
||||||
|
|
||||||
match Lua::new_with(StdLib::DEBUG) {
|
match Lua::new_with(StdLib::DEBUG, LuaOptions::default()) {
|
||||||
Err(Error::SafetyError(_)) => {}
|
Err(Error::SafetyError(_)) => {}
|
||||||
Err(e) => panic!("expected SafetyError, got {:?}", e),
|
Err(e) => panic!("expected SafetyError, got {:?}", e),
|
||||||
Ok(_) => panic!("expected SafetyError, got new Lua state"),
|
Ok(_) => panic!("expected SafetyError, got new Lua state"),
|
||||||
|
@ -64,7 +64,7 @@ fn test_safety() -> Result<()> {
|
||||||
drop(lua);
|
drop(lua);
|
||||||
|
|
||||||
// Test safety rules after dynamically loading `package` library
|
// 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());
|
assert!(lua.globals().get::<_, Option<Value>>("require")?.is_none());
|
||||||
lua.load_from_std_lib(StdLib::PACKAGE)?;
|
lua.load_from_std_lib(StdLib::PACKAGE)?;
|
||||||
match lua.load(r#"package.loadlib()"#).exec() {
|
match lua.load(r#"package.loadlib()"#).exec() {
|
||||||
|
@ -380,10 +380,15 @@ fn test_error() -> Result<()> {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_panic() -> Result<()> {
|
fn test_panic() -> Result<()> {
|
||||||
fn make_lua() -> Result<Lua> {
|
fn make_lua(options: LuaOptions) -> Result<Lua> {
|
||||||
let lua = Lua::new();
|
let lua = Lua::new_with(StdLib::ALL_SAFE, options)?;
|
||||||
let rust_panic_function =
|
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()
|
lua.globals()
|
||||||
.set("rust_panic_function", rust_panic_function)?;
|
.set("rust_panic_function", rust_panic_function)?;
|
||||||
Ok(lua)
|
Ok(lua)
|
||||||
|
@ -391,7 +396,7 @@ fn test_panic() -> Result<()> {
|
||||||
|
|
||||||
// Test triggerting Lua error passing Rust panic (must be resumed)
|
// 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<()> {
|
match catch_unwind(AssertUnwindSafe(|| -> Result<()> {
|
||||||
lua.load(
|
lua.load(
|
||||||
|
@ -417,7 +422,7 @@ fn test_panic() -> Result<()> {
|
||||||
|
|
||||||
// Test returning Rust panic (must be resumed)
|
// Test returning Rust panic (must be resumed)
|
||||||
{
|
{
|
||||||
let lua = make_lua()?;
|
let lua = make_lua(LuaOptions::default())?;
|
||||||
match catch_unwind(AssertUnwindSafe(|| -> Result<()> {
|
match catch_unwind(AssertUnwindSafe(|| -> Result<()> {
|
||||||
let _catched_panic = lua
|
let _catched_panic = lua
|
||||||
.load(
|
.load(
|
||||||
|
@ -447,7 +452,7 @@ fn test_panic() -> Result<()> {
|
||||||
|
|
||||||
// Test representing rust panic as a string
|
// Test representing rust panic as a string
|
||||||
match catch_unwind(|| -> Result<()> {
|
match catch_unwind(|| -> Result<()> {
|
||||||
let lua = make_lua()?;
|
let lua = make_lua(LuaOptions::default())?;
|
||||||
lua.load(
|
lua.load(
|
||||||
r#"
|
r#"
|
||||||
local _, err = pcall(rust_panic_function)
|
local _, err = pcall(rust_panic_function)
|
||||||
|
@ -462,6 +467,48 @@ fn test_panic() -> Result<()> {
|
||||||
Err(_) => panic!("panic was detected"),
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue