Compare commits

...

13 Commits

Author SHA1 Message Date
Alex Orlenko d607039a31 v0.8.0-beta.3 2022-04-03 23:38:41 +01:00
Alex Orlenko 0ea65a2985 Fix doc test 2022-04-03 22:13:54 +01:00
Alex Orlenko 8c333354d3 Update Luau to 0.521 2022-03-31 23:28:37 +01:00
Alex Orlenko f63f147265 Add set_mutable_globals to Luau compiler 2022-03-31 20:35:52 +01:00
Alex Orlenko 595dc3e95f Move some Luau functionality to a new module
Immplement native "vector" function to construct vectors
2022-03-31 19:31:37 +01:00
Alex Orlenko ac28c8d8d2 Add vector_lib/vector_ctor options to Luau Compiler (hidden) 2022-03-31 19:05:19 +01:00
Alex Orlenko d5315da8d1 Fix tests 2022-03-31 12:23:21 +01:00
Alex Orlenko a7cc7f328a Increase minimum lua-src version to 544 2022-03-31 00:09:52 +01:00
Alex Orlenko 516f01ed44 Increase internal caches size 2022-03-31 00:07:05 +01:00
Alex Orlenko 4492a20bbc Make LuaHook as Fn instead of FnMut to remove Mutex and improve performance 2022-03-30 23:55:34 +01:00
Alex Orlenko 595bc3a2b3 Support Luau interrupts (closes #138) 2022-03-30 22:01:06 +01:00
Alex Orlenko 87c10ca93d Sandboxing support 2022-03-28 23:42:35 +01:00
Alex Orlenko f75b7b7879 Remove Lua::into_static/from_static 2022-03-26 00:30:57 +00:00
15 changed files with 744 additions and 191 deletions
+8
View File
@@ -1,3 +1,11 @@
## v0.8.0-beta.3
- Luau vector constructor
- Luau sandboxing support
- Luau interrupts (yieldable)
- More Luau compiler options (mutable globals)
- Other performance improvements
## v0.8.0-beta.2
- Luau vector datatype support
+4 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "mlua"
version = "0.8.0-beta.2" # remember to update html_root_url and mlua_derive
version = "0.8.0-beta.3" # remember to update html_root_url and mlua_derive
authors = ["Aleksandr Orlenko <zxteam@pm.me>", "kyren <catherine@chucklefish.org>"]
edition = "2018"
repository = "https://github.com/khvzak/mlua"
@@ -57,9 +57,9 @@ erased-serde = { version = "0.3", optional = true }
[build-dependencies]
cc = { version = "1.0" }
pkg-config = { version = "0.3.17" }
lua-src = { version = ">= 540.0.0, < 550.0.0", optional = true }
lua-src = { version = ">= 544.0.0, < 550.0.0", optional = true }
luajit-src = { version = ">= 210.3.1, < 220.0.0", optional = true }
luau0-src = { version = "0.2.1", optional = true }
luau0-src = { version = "0.2.2", optional = true }
[dev-dependencies]
rustyline = "9.0"
@@ -70,6 +70,7 @@ hyper = { version = "0.14", features = ["client", "server"] }
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
futures-timer = "3.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
maplit = "1.0"
tempfile = "3"
+2 -2
View File
@@ -104,7 +104,7 @@ Add to `Cargo.toml` :
``` toml
[dependencies]
mlua = { version = "0.8.0-beta.2", features = ["lua54", "vendored"] }
mlua = { version = "0.8.0-beta.3", features = ["lua54", "vendored"] }
```
`main.rs`
@@ -139,7 +139,7 @@ Add to `Cargo.toml` :
crate-type = ["cdylib"]
[dependencies]
mlua = { version = "0.8.0-beta.2", features = ["lua54", "vendored", "module"] }
mlua = { version = "0.8.0-beta.3", features = ["lua54", "vendored", "module"] }
```
`lib.rs` :
+110 -23
View File
@@ -66,11 +66,14 @@ pub enum ChunkMode {
/// Luau compiler
#[cfg(any(feature = "luau", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
pub struct Compiler {
optimization_level: u8,
debug_level: u8,
coverage_level: u8,
vector_lib: Option<String>,
vector_ctor: Option<String>,
mutable_globals: Vec<String>,
}
#[cfg(any(feature = "luau", doc))]
@@ -81,6 +84,9 @@ impl Default for Compiler {
optimization_level: 1,
debug_level: 1,
coverage_level: 0,
vector_lib: None,
vector_ctor: None,
mutable_globals: Vec::new(),
}
}
}
@@ -95,10 +101,10 @@ impl Compiler {
/// Sets Luau compiler optimization level.
///
/// Possible values:
/// 0 - no optimization
/// 1 - baseline optimization level that doesn't prevent debuggability (default)
/// 2 - includes optimizations that harm debuggability such as inlining
pub fn set_optimization_level(mut self, level: u8) -> Self {
/// * 0 - no optimization
/// * 1 - baseline optimization level that doesn't prevent debuggability (default)
/// * 2 - includes optimizations that harm debuggability such as inlining
pub fn set_optimization_level(&mut self, level: u8) -> &mut Self {
self.optimization_level = level;
self
}
@@ -106,10 +112,10 @@ impl Compiler {
/// Sets Luau compiler debug level.
///
/// Possible values:
/// 0 - no debugging support
/// 1 - line info & function names only; sufficient for backtraces (default)
/// 2 - full debug info with local & upvalue names; necessary for debugger
pub fn set_debug_level(mut self, level: u8) -> Self {
/// * 0 - no debugging support
/// * 1 - line info & function names only; sufficient for backtraces (default)
/// * 2 - full debug info with local & upvalue names; necessary for debugger
pub fn set_debug_level(&mut self, level: u8) -> &mut Self {
self.debug_level = level;
self
}
@@ -117,27 +123,70 @@ impl Compiler {
/// Sets Luau compiler code coverage level.
///
/// Possible values:
/// 0 - no code coverage support (default)
/// 1 - statement coverage
/// 2 - statement and expression coverage (verbose)
pub fn set_coverage_level(mut self, level: u8) -> Self {
/// * 0 - no code coverage support (default)
/// * 1 - statement coverage
/// * 2 - statement and expression coverage (verbose)
pub fn set_coverage_level(&mut self, level: u8) -> &mut Self {
self.coverage_level = level;
self
}
#[doc(hidden)]
pub fn set_vector_lib(&mut self, lib: Option<String>) -> &mut Self {
self.vector_lib = lib;
self
}
#[doc(hidden)]
pub fn set_vector_ctor(&mut self, ctor: Option<String>) -> &mut Self {
self.vector_ctor = ctor;
self
}
/// Sets a list of globals that are mutable.
///
/// It disables the import optimization for fields accessed through these.
pub fn set_mutable_globals(&mut self, globals: Vec<String>) -> &mut Self {
self.mutable_globals = globals;
self
}
/// Compiles the `source` into bytecode.
pub fn compile(&self, source: impl AsRef<[u8]>) -> Vec<u8> {
use std::os::raw::c_int;
use std::ptr;
let vector_lib = self.vector_lib.clone();
let vector_lib = vector_lib.and_then(|lib| CString::new(lib).ok());
let vector_lib = vector_lib.as_ref();
let vector_ctor = self.vector_ctor.clone();
let vector_ctor = vector_ctor.and_then(|ctor| CString::new(ctor).ok());
let vector_ctor = vector_ctor.as_ref();
let mutable_globals = self
.mutable_globals
.iter()
.map(|name| CString::new(name.clone()).ok())
.collect::<Option<Vec<_>>>()
.unwrap_or_default();
let mut mutable_globals = mutable_globals
.iter()
.map(|s| s.as_ptr())
.collect::<Vec<_>>();
let mut mutable_globals_ptr = ptr::null_mut();
if mutable_globals.len() > 0 {
mutable_globals.push(ptr::null());
mutable_globals_ptr = mutable_globals.as_mut_ptr();
}
unsafe {
let options = ffi::lua_CompileOptions {
optimizationLevel: self.optimization_level as c_int,
debugLevel: self.debug_level as c_int,
coverageLevel: self.coverage_level as c_int,
vectorLib: ptr::null(),
vectorCtor: ptr::null(),
mutableGlobals: ptr::null_mut(),
vectorLib: vector_lib.map_or(ptr::null(), |s| s.as_ptr()),
vectorCtor: vector_ctor.map_or(ptr::null(), |s| s.as_ptr()),
mutableGlobals: mutable_globals_ptr,
};
ffi::luau_compile(source.as_ref(), options)
}
@@ -146,7 +195,7 @@ impl Compiler {
impl<'lua, 'a> Chunk<'lua, 'a> {
/// Sets the name of this chunk, which results in more informative error traces.
pub fn set_name<S: AsRef<[u8]> + ?Sized>(mut self, name: &S) -> Result<Chunk<'lua, 'a>> {
pub fn set_name<S: AsRef<[u8]> + ?Sized>(mut self, name: &S) -> Result<Self> {
let name =
CString::new(name.as_ref().to_vec()).map_err(|e| Error::ToLuaConversionError {
from: "&str",
@@ -168,7 +217,7 @@ impl<'lua, 'a> Chunk<'lua, 'a> {
/// All global variables (including the standard library!) are looked up in `_ENV`, so it may be
/// necessary to populate the environment in order for scripts using custom environments to be
/// useful.
pub fn set_environment<V: ToLua<'lua>>(mut self, env: V) -> Result<Chunk<'lua, 'a>> {
pub fn set_environment<V: ToLua<'lua>>(mut self, env: V) -> Result<Self> {
// Prefer to propagate errors here and wrap to `Ok`
self.env = Ok(Some(env.to_lua(self.lua)?));
Ok(self)
@@ -178,7 +227,7 @@ impl<'lua, 'a> Chunk<'lua, 'a> {
///
/// Be aware, Lua does not check the consistency of the code inside binary chunks.
/// Running maliciously crafted bytecode can crash the interpreter.
pub fn set_mode(mut self, mode: ChunkMode) -> Chunk<'lua, 'a> {
pub fn set_mode(mut self, mode: ChunkMode) -> Self {
self.mode = Some(mode);
self
}
@@ -187,7 +236,7 @@ impl<'lua, 'a> Chunk<'lua, 'a> {
///
/// See [`Compiler::set_optimization_level`] for details.
///
/// Requires `feature = "luau`
/// Requires `feature = "luau"`
#[cfg(any(feature = "luau", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
pub fn set_optimization_level(mut self, level: u8) -> Self {
@@ -215,7 +264,7 @@ impl<'lua, 'a> Chunk<'lua, 'a> {
///
/// See [`Compiler::set_coverage_level`] for details.
///
/// Requires `feature = "luau`
/// Requires `feature = "luau"`
#[cfg(any(feature = "luau", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
pub fn set_coverage_level(mut self, level: u8) -> Self {
@@ -225,6 +274,40 @@ impl<'lua, 'a> Chunk<'lua, 'a> {
self
}
#[cfg(any(feature = "luau", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
#[doc(hidden)]
pub fn set_vector_lib(mut self, lib: Option<String>) -> Self {
self.compiler
.get_or_insert_with(Default::default)
.set_vector_lib(lib);
self
}
#[cfg(any(feature = "luau", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
#[doc(hidden)]
pub fn set_vector_ctor(mut self, ctor: Option<String>) -> Self {
self.compiler
.get_or_insert_with(Default::default)
.set_vector_ctor(ctor);
self
}
/// Sets a list of globals that are mutable for Luau compiler.
///
/// See [`Compiler::set_mutable_globals`] for details.
///
/// Requires `feature = "luau"`
#[cfg(any(feature = "luau", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
pub fn set_mutable_globals(mut self, globals: Vec<String>) -> Self {
self.compiler
.get_or_insert_with(Default::default)
.set_mutable_globals(globals);
self
}
/// Compiles the chunk and changes mode to binary.
///
/// It does nothing if the chunk is already binary.
@@ -234,7 +317,7 @@ impl<'lua, 'a> Chunk<'lua, 'a> {
if self.detect_mode() == ChunkMode::Text {
let data = self
.compiler
.unwrap_or_default()
.get_or_insert_with(Default::default)
.compile(self.source.as_ref());
self.mode = Some(ChunkMode::Binary);
self.source = Cow::Owned(data);
@@ -373,7 +456,11 @@ impl<'lua, 'a> Chunk<'lua, 'a> {
let source = self.expression_source();
// We don't need to compile source if no compiler options set
#[cfg(feature = "luau")]
let source = self.compiler.map(|c| c.compile(&source)).unwrap_or(source);
let source = self
.compiler
.as_ref()
.map(|c| c.compile(&source))
.unwrap_or(source);
self.lua
.load_chunk(&source, self.name.as_ref(), self.env()?, None)
+28
View File
@@ -72,6 +72,11 @@ extern "C" {
// TODO: luaL_findtable
pub fn luaL_typename(L: *mut lua_State, idx: c_int) -> *const c_char;
// sandbox libraries and globals
#[link_name = "luaL_sandbox"]
pub fn luaL_sandbox_(L: *mut lua_State);
pub fn luaL_sandboxthread(L: *mut lua_State);
}
//
@@ -123,6 +128,29 @@ pub unsafe fn luaL_unref(L: *mut lua_State, t: c_int, r#ref: c_int) {
lua::lua_unref(L, r#ref)
}
pub unsafe fn luaL_sandbox(L: *mut lua_State, enabled: c_int) {
use super::lua::*;
// set all libraries to read-only
lua_pushnil(L);
while lua_next(L, LUA_GLOBALSINDEX) != 0 {
if lua_istable(L, -1) != 0 {
lua_setreadonly(L, -1, enabled);
}
lua_pop(L, 1);
}
// set all builtin metatables to read-only
lua_pushliteral(L, "");
lua_getmetatable(L, -1);
lua_setreadonly(L, -1, enabled);
lua_pop(L, 2);
// set globals to readonly and activate safeenv since the env is immutable
lua_setreadonly(L, LUA_GLOBALSINDEX, enabled);
lua_setsafeenv(L, LUA_GLOBALSINDEX, enabled);
}
//
// TODO: Generic Buffer Manipulation
//
-4
View File
@@ -26,8 +26,4 @@ extern "C" {
// open all builtin libraries
pub fn luaL_openlibs(L: *mut lua_State);
// sandbox libraries and globals
pub fn luaL_sandbox(L: *mut lua_State);
pub fn luaL_sandboxthread(L: *mut lua_State);
}
+4 -2
View File
@@ -72,7 +72,7 @@
//! [`serde::Deserialize`]: https://docs.serde.rs/serde/de/trait.Deserialize.html
// mlua types in rustdoc of other crates get linked to here.
#![doc(html_root_url = "https://docs.rs/mlua/0.8.0-beta.2")]
#![doc(html_root_url = "https://docs.rs/mlua/0.8.0-beta.3")]
// Deny warnings inside doc tests / examples. When this isn't present, rustdoc doesn't show *any*
// warnings at all.
#![doc(test(attr(deny(warnings))))]
@@ -88,6 +88,8 @@ mod ffi;
mod function;
mod hook;
mod lua;
#[cfg(feature = "luau")]
mod luau;
mod multi;
mod scope;
mod stdlib;
@@ -126,7 +128,7 @@ pub use crate::hook::HookTriggers;
#[cfg(any(feature = "luau", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
pub use crate::chunk::Compiler;
pub use crate::{chunk::Compiler, types::VmState};
#[cfg(feature = "async")]
pub use crate::thread::AsyncThread;
+228 -139
View File
@@ -47,6 +47,9 @@ use {
#[cfg(not(feature = "luau"))]
use crate::{hook::HookTriggers, types::HookCallback};
#[cfg(feature = "luau")]
use crate::types::{InterruptCallback, VmState};
#[cfg(feature = "async")]
use {
crate::types::{AsyncCallback, AsyncCallbackUpvalue, AsyncPollUpvalue},
@@ -108,6 +111,11 @@ struct ExtraData {
hook_callback: Option<HookCallback>,
#[cfg(feature = "lua54")]
warn_callback: Option<WarnCallback>,
#[cfg(feature = "luau")]
interrupt_callback: Option<InterruptCallback>,
#[cfg(feature = "luau")]
sandboxed: bool,
}
#[cfg_attr(any(feature = "lua51", feature = "luajit"), allow(dead_code))]
@@ -204,8 +212,8 @@ impl LuaOptions {
pub(crate) static ASYNC_POLL_PENDING: u8 = 0;
pub(crate) static EXTRA_REGISTRY_KEY: u8 = 0;
const WRAPPED_FAILURES_CACHE_SIZE: usize = 16;
const MULTIVALUE_CACHE_SIZE: usize = 16;
const WRAPPED_FAILURES_CACHE_SIZE: usize = 32;
const MULTIVALUE_CACHE_SIZE: usize = 32;
/// Requires `feature = "send"`
#[cfg(feature = "send")]
@@ -232,6 +240,13 @@ impl Drop for Lua {
ffi::lua_replace(extra.ref_thread, extra.ref_waker_idx);
extra.ref_free.push(extra.ref_waker_idx);
}
#[cfg(feature = "luau")]
{
let callbacks = ffi::lua_callbacks(self.state);
let extra_ptr = (*callbacks).userdata as *mut Arc<UnsafeCell<ExtraData>>;
drop(Box::from_raw(extra_ptr));
(*callbacks).userdata = ptr::null_mut();
}
mlua_debug_assert!(
ffi::lua_gettop(extra.ref_thread) == extra.ref_stack_top
&& extra.ref_stack_top as usize == extra.ref_free.len(),
@@ -548,6 +563,10 @@ impl Lua {
hook_callback: None,
#[cfg(feature = "lua54")]
warn_callback: None,
#[cfg(feature = "luau")]
interrupt_callback: None,
#[cfg(feature = "luau")]
sandboxed: false,
}));
mlua_expect!(
@@ -576,6 +595,14 @@ impl Lua {
);
assert_stack(main_state, ffi::LUA_MINSTACK);
// Set Luau callbacks userdata to extra data
// We can use global callbacks userdata since we don't allow C modules in Luau
#[cfg(feature = "luau")]
{
let extra_raw = Box::into_raw(Box::new(Arc::clone(&extra)));
(*ffi::lua_callbacks(main_state)).userdata = extra_raw as *mut c_void;
}
Lua {
state,
main_state: maybe_main_state,
@@ -696,32 +723,6 @@ impl Lua {
Ok(())
}
/// Consumes and leaks `Lua` object, returning a static reference `&'static Lua`.
///
/// This function is useful when the `Lua` object is supposed to live for the remainder
/// of the program's life.
/// In particular in asynchronous context this will allow to spawn Lua tasks to execute
/// in background.
///
/// Dropping the returned reference will cause a memory leak. If this is not acceptable,
/// the reference should first be wrapped with the [`Lua::from_static`] function producing a `Lua`.
/// This `Lua` object can then be dropped which will properly release the allocated memory.
///
/// [`Lua::from_static`]: #method.from_static
#[doc(hidden)]
pub fn into_static(self) -> &'static Self {
Box::leak(Box::new(self))
}
/// Constructs a `Lua` from a static reference to it.
///
/// # Safety
/// This function is unsafe because improper use may lead to memory problems or undefined behavior.
#[doc(hidden)]
pub unsafe fn from_static(lua: &'static Lua) -> Self {
*Box::from_raw(lua as *const Lua as *mut Lua)
}
// Executes module entrypoint function, which returns only one Value.
// The returned value then pushed onto the stack.
#[doc(hidden)]
@@ -776,6 +777,60 @@ impl Lua {
self.entrypoint(move |lua, _: ()| func(lua))
}
/// Enables (or disables) sandbox mode on this Lua instance.
///
/// This method, in particular:
/// - Set all libraries to read-only
/// - Set all builtin metatables to read-only
/// - Set globals to read-only (and activates safeenv)
/// - Setup local environment table that performs writes locally and proxies reads
/// to the global environment.
///
/// # Examples
///
/// ```
/// # use mlua::{Lua, Result};
/// # fn main() -> Result<()> {
/// let lua = Lua::new();
///
/// lua.sandbox(true)?;
/// lua.load("var = 123").exec()?;
/// assert_eq!(lua.globals().get::<_, u32>("var")?, 123);
///
/// // Restore the global environment (clear changes made in sandbox)
/// lua.sandbox(false)?;
/// assert_eq!(lua.globals().get::<_, Option<u32>>("var")?, None);
/// # Ok(())
/// # }
/// ```
///
/// Requires `feature = "luau"`
#[cfg(feature = "luau")]
pub fn sandbox(&self, enabled: bool) -> Result<()> {
unsafe {
let extra = &mut *self.extra.get();
if extra.sandboxed != enabled {
let state = self.main_state.ok_or(Error::MainThreadNotAvailable)?;
check_stack(state, 3)?;
protect_lua!(state, 0, 0, |state| {
if enabled {
ffi::luaL_sandbox(state, 1);
ffi::luaL_sandboxthread(state);
} else {
// Restore original `LUA_GLOBALSINDEX`
self.ref_thread_exec(|ref_thread| {
ffi::lua_xpush(ref_thread, state, ffi::LUA_GLOBALSINDEX);
ffi::lua_replace(state, ffi::LUA_GLOBALSINDEX);
});
ffi::luaL_sandbox(state, 0);
}
})?;
extra.sandboxed = enabled;
}
Ok(())
}
}
/// Sets a 'hook' function that will periodically be called as Lua code executes.
///
/// When exactly the hook function is called depends on the contents of the `triggers`
@@ -813,7 +868,7 @@ impl Lua {
#[cfg_attr(docsrs, doc(cfg(not(feature = "luau"))))]
pub fn set_hook<F>(&self, triggers: HookTriggers, callback: F) -> Result<()>
where
F: 'static + MaybeSend + FnMut(&Lua, Debug) -> Result<()>,
F: 'static + MaybeSend + Fn(&Lua, Debug) -> Result<()>,
{
unsafe extern "C" fn hook_proc(state: *mut ffi::lua_State, ar: *mut ffi::lua_Debug) {
let lua = match Lua::make_from_ptr(state) {
@@ -825,29 +880,24 @@ impl Lua {
let debug = Debug::new(&lua, ar);
let hook_cb = (*lua.extra.get()).hook_callback.clone();
let hook_cb = mlua_expect!(hook_cb, "no hook callback set in hook_proc");
#[allow(clippy::match_wild_err_arm)]
match hook_cb.try_lock() {
Ok(mut cb) => cb(&lua, debug),
Err(_) => {
mlua_panic!("Lua should not allow hooks to be called within another hook")
}
}?;
Ok(())
if Arc::strong_count(&hook_cb) > 2 {
return Ok(()); // Don't allow recursion
}
hook_cb(&lua, debug)
})
}
let state = self.main_state.ok_or(Error::MainThreadNotAvailable)?;
unsafe {
(*self.extra.get()).hook_callback = Some(Arc::new(Mutex::new(callback)));
(*self.extra.get()).hook_callback = Some(Arc::new(callback));
ffi::lua_sethook(state, Some(hook_proc), triggers.mask(), triggers.count());
}
Ok(())
}
/// Remove any hook previously set by `set_hook`. This function has no effect if a hook was not
/// previously set.
/// Removes any hook previously set by `set_hook`.
///
/// This function has no effect if a hook was not previously set.
#[cfg(not(feature = "luau"))]
#[cfg_attr(docsrs, doc(cfg(not(feature = "luau"))))]
pub fn remove_hook(&self) {
@@ -862,6 +912,102 @@ impl Lua {
}
}
/// Sets an 'interrupt' function that will periodically be called by Luau VM.
///
/// Any Luau code is guaranteed to call this handler "eventually"
/// (in practice this can happen at any function call or at any loop iteration).
///
/// The provided interrupt function can error, and this error will be propagated through
/// the Luau code that was executing at the time the interrupt was triggered.
/// Also this can be used to implement continuous execution limits by instructing Luau VM to yield
/// by returning [`VmState::Yield`].
///
/// This is similar to [`Lua::set_hook`] but in more simplified form.
///
/// # Example
///
/// Periodically yield Luau VM to suspend execution.
///
/// ```
/// # use std::sync::{Arc, atomic::{AtomicU64, Ordering}};
/// # use mlua::{Lua, Result, ThreadStatus, VmState};
/// # fn main() -> Result<()> {
/// let lua = Lua::new();
/// let count = Arc::new(AtomicU64::new(0));
/// lua.set_interrupt(move |_lua| {
/// if count.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
/// return Ok(VmState::Yield);
/// }
/// Ok(VmState::Continue)
/// });
///
/// let co = lua.create_thread(
/// lua.load(r#"
/// local b = 0
/// for _, x in ipairs({1, 2, 3}) do b += x end
/// "#)
/// .into_function()?,
/// )?;
/// while co.status() == ThreadStatus::Resumable {
/// co.resume(())?;
/// }
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "luau")]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
pub fn set_interrupt<F>(&self, callback: F)
where
F: 'static + MaybeSend + Fn(&Lua) -> Result<VmState>,
{
unsafe extern "C" fn interrupt_proc(state: *mut ffi::lua_State, gc: c_int) {
if gc != -1 {
// We don't support GC interrupts since they cannot survive Lua exceptions
return;
}
// TODO: think about not using drop types here
let lua = match Lua::make_from_ptr(state) {
Some(lua) => lua,
None => return,
};
let extra = lua.extra.get();
let result = callback_error_ext(state, extra, move |_| {
let interrupt_cb = (*extra).interrupt_callback.clone();
let interrupt_cb =
mlua_expect!(interrupt_cb, "no interrupt callback set in interrupt_proc");
if Arc::strong_count(&interrupt_cb) > 2 {
return Ok(VmState::Continue); // Don't allow recursion
}
interrupt_cb(&lua)
});
match result {
VmState::Continue => {}
VmState::Yield => {
ffi::lua_yield(state, 0);
}
}
}
let state = mlua_expect!(self.main_state, "Luau should always has main state");
unsafe {
(*self.extra.get()).interrupt_callback = Some(Arc::new(callback));
(*ffi::lua_callbacks(state)).interrupt = Some(interrupt_proc);
}
}
/// Removes any 'interrupt' previously set by `set_interrupt`.
///
/// This function has no effect if an 'interrupt' was not previously set.
#[cfg(feature = "luau")]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
pub fn remove_interrupt(&self) {
let state = mlua_expect!(self.main_state, "Luau should always has main state");
unsafe {
(*self.extra.get()).interrupt_callback = None;
(*ffi::lua_callbacks(state)).interrupt = None;
}
}
/// Sets the warning function to be used by Lua to emit warnings.
///
/// Requires `feature = "lua54"`
@@ -1450,7 +1596,11 @@ impl Lua {
&'lua self,
func: Function<'lua>,
) -> Result<Thread<'lua>> {
#[cfg(any(feature = "lua54", all(feature = "luajit", feature = "vendored")))]
#[cfg(any(
feature = "lua54",
all(feature = "luajit", feature = "vendored"),
feature = "luau",
))]
unsafe {
let _sg = StackGuard::new(self.state);
check_stack(self.state, 1)?;
@@ -1460,6 +1610,14 @@ impl Lua {
let thread_state = ffi::lua_tothread(extra.ref_thread, index);
self.push_ref(&func.0);
ffi::lua_xmove(self.state, thread_state, 1);
#[cfg(feature = "luau")]
{
// Inherit `LUA_GLOBALSINDEX` from the caller
ffi::lua_xpush(self.state, thread_state, ffi::LUA_GLOBALSINDEX);
ffi::lua_replace(thread_state, ffi::LUA_GLOBALSINDEX);
}
return Ok(Thread(LuaRef { lua: self, index }));
}
};
@@ -1468,7 +1626,11 @@ impl Lua {
/// Resets thread (coroutine) and returns to the cache for later use.
#[cfg(feature = "async")]
#[cfg(any(feature = "lua54", all(feature = "luajit", feature = "vendored")))]
#[cfg(any(
feature = "lua54",
all(feature = "luajit", feature = "vendored"),
feature = "luau",
))]
pub(crate) unsafe fn recycle_thread(&self, thread: &mut Thread) {
let extra = &mut *self.extra.get();
let thread_state = ffi::lua_tothread(extra.ref_thread, thread.0.index);
@@ -2619,105 +2781,11 @@ impl Lua {
Ok(())
}
#[cfg(feature = "luau")]
unsafe fn prepare_luau_state(&self) -> Result<()> {
use std::ffi::CStr;
// Since Luau has some missing standard function, we re-implement them here
unsafe extern "C" fn lua_collectgarbage(state: *mut ffi::lua_State) -> c_int {
let option = ffi::luaL_optstring(state, 1, cstr!("collect"));
let option = CStr::from_ptr(option);
match option.to_str() {
Ok("collect") => {
ffi::lua_gc(state, ffi::LUA_GCCOLLECT, 0);
0
}
Ok("count") => {
let n = ffi::lua_gc(state, ffi::LUA_GCCOUNT, 0);
ffi::lua_pushnumber(state, n as ffi::lua_Number);
1
}
// TODO: More variants
_ => ffi::luaL_error(
state,
cstr!("collectgarbage must be called with 'count' or 'collect'"),
),
}
}
fn lua_require(lua: &Lua, name: Option<std::string::String>) -> Result<Value> {
let name = name.ok_or_else(|| Error::RuntimeError("name is nil".into()))?;
// Find module in the cache
let loaded = unsafe {
let _sg = StackGuard::new(lua.state);
check_stack(lua.state, 2)?;
protect_lua!(lua.state, 0, 1, fn(state) {
ffi::luaL_getsubtable(state, ffi::LUA_REGISTRYINDEX, cstr!("_LOADED"));
})?;
Table(lua.pop_ref())
};
if let Some(v) = loaded.raw_get(name.clone())? {
return Ok(v);
}
// Load file from filesystem
let mut search_path = std::env::var("LUAU_PATH").unwrap_or_default();
if search_path.is_empty() {
search_path = "?.luau;?.lua".into();
}
let mut source = None;
for path in search_path.split(';') {
if let Ok(buf) = std::fs::read(path.replacen('?', &name, 1)) {
source = Some(buf);
break;
}
}
let source =
source.ok_or_else(|| Error::RuntimeError(format!("cannot find '{}'", name)))?;
let value = lua
.load(&source)
.set_name(&format!("={}", name))?
.set_mode(ChunkMode::Text)
.call::<_, Value>(())?;
// Save in the cache
loaded.raw_set(
name,
match value.clone() {
Value::Nil => Value::Boolean(true),
v => v,
},
)?;
Ok(value)
}
let globals = self.globals();
globals.raw_set(
"collectgarbage",
self.create_c_function(lua_collectgarbage)?,
)?;
globals.raw_set("require", self.create_function(lua_require)?)?;
Ok(())
}
pub(crate) unsafe fn make_from_ptr(state: *mut ffi::lua_State) -> Option<Self> {
let _sg = StackGuard::new(state);
assert_stack(state, 1);
let extra_key = &EXTRA_REGISTRY_KEY as *const u8 as *const c_void;
if ffi::lua_rawgetp(state, ffi::LUA_REGISTRYINDEX, extra_key) != ffi::LUA_TUSERDATA {
return None;
}
let extra_ptr = ffi::lua_touserdata(state, -1) as *mut Arc<UnsafeCell<ExtraData>>;
let extra = Arc::clone(&*extra_ptr);
ffi::lua_pop(state, 1);
let extra = extra_data(state)?;
let safe = (*extra.get()).safe;
Some(Lua {
state,
@@ -2749,6 +2817,27 @@ impl Lua {
}
}
#[cfg(feature = "luau")]
unsafe fn extra_data(state: *mut ffi::lua_State) -> Option<Arc<UnsafeCell<ExtraData>>> {
let extra_ptr = (*ffi::lua_callbacks(state)).userdata as *mut Arc<UnsafeCell<ExtraData>>;
if extra_ptr.is_null() {
return None;
}
Some(Arc::clone(&*extra_ptr))
}
#[cfg(not(feature = "luau"))]
unsafe fn extra_data(state: *mut ffi::lua_State) -> Option<Arc<UnsafeCell<ExtraData>>> {
let extra_key = &EXTRA_REGISTRY_KEY as *const u8 as *const c_void;
if ffi::lua_rawgetp(state, ffi::LUA_REGISTRYINDEX, extra_key) != ffi::LUA_TUSERDATA {
return None;
}
let extra_ptr = ffi::lua_touserdata(state, -1) as *mut Arc<UnsafeCell<ExtraData>>;
let extra = Arc::clone(&*extra_ptr);
ffi::lua_pop(state, 1);
Some(extra)
}
// Creates required entries in the metatable cache (see `util::METATABLE_CACHE`)
pub(crate) fn init_metatable_cache(cache: &mut FxHashMap<TypeId, u8>) {
cache.insert(TypeId::of::<Arc<UnsafeCell<ExtraData>>>(), 0);
+106
View File
@@ -0,0 +1,106 @@
use std::ffi::CStr;
use std::os::raw::{c_float, c_int};
use crate::chunk::ChunkMode;
use crate::error::{Error, Result};
use crate::ffi;
use crate::lua::Lua;
use crate::table::Table;
use crate::util::{check_stack, StackGuard};
use crate::value::Value;
// Since Luau has some missing standard function, we re-implement them here
impl Lua {
pub(crate) unsafe fn prepare_luau_state(&self) -> Result<()> {
let globals = self.globals();
globals.raw_set(
"collectgarbage",
self.create_c_function(lua_collectgarbage)?,
)?;
globals.raw_set("require", self.create_function(lua_require)?)?;
globals.raw_set("vector", self.create_c_function(lua_vector)?)?;
Ok(())
}
}
unsafe extern "C" fn lua_collectgarbage(state: *mut ffi::lua_State) -> c_int {
let option = ffi::luaL_optstring(state, 1, cstr!("collect"));
let option = CStr::from_ptr(option);
match option.to_str() {
Ok("collect") => {
ffi::lua_gc(state, ffi::LUA_GCCOLLECT, 0);
0
}
Ok("count") => {
let n = ffi::lua_gc(state, ffi::LUA_GCCOUNT, 0);
ffi::lua_pushnumber(state, n as ffi::lua_Number);
1
}
// TODO: More variants
_ => ffi::luaL_error(
state,
cstr!("collectgarbage must be called with 'count' or 'collect'"),
),
}
}
fn lua_require(lua: &Lua, name: Option<std::string::String>) -> Result<Value> {
let name = name.ok_or_else(|| Error::RuntimeError("name is nil".into()))?;
// Find module in the cache
let loaded = unsafe {
let _sg = StackGuard::new(lua.state);
check_stack(lua.state, 2)?;
protect_lua!(lua.state, 0, 1, fn(state) {
ffi::luaL_getsubtable(state, ffi::LUA_REGISTRYINDEX, cstr!("_LOADED"));
})?;
Table(lua.pop_ref())
};
if let Some(v) = loaded.raw_get(name.clone())? {
return Ok(v);
}
// Load file from filesystem
let mut search_path = std::env::var("LUAU_PATH").unwrap_or_default();
if search_path.is_empty() {
search_path = "?.luau;?.lua".into();
}
let mut source = None;
for path in search_path.split(';') {
if let Ok(buf) = std::fs::read(path.replacen('?', &name, 1)) {
source = Some(buf);
break;
}
}
let source = source.ok_or_else(|| Error::RuntimeError(format!("cannot find '{}'", name)))?;
let value = lua
.load(&source)
.set_name(&format!("={}", name))?
.set_mode(ChunkMode::Text)
.call::<_, Value>(())?;
// Save in the cache
loaded.raw_set(
name,
match value.clone() {
Value::Nil => Value::Boolean(true),
v => v,
},
)?;
Ok(value)
}
// Luau vector datatype constructor
unsafe extern "C" fn lua_vector(state: *mut ffi::lua_State) -> c_int {
let x = ffi::luaL_checknumber(state, 1) as c_float;
let y = ffi::luaL_checknumber(state, 2) as c_float;
let z = ffi::luaL_checknumber(state, 3) as c_float;
ffi::lua_pushvector(state, x, y, z);
1
}
+8
View File
@@ -15,6 +15,14 @@ pub use crate::{
Value as LuaValue,
};
#[cfg(not(feature = "luau"))]
#[doc(no_inline)]
pub use crate::HookTriggers as LuaHookTriggers;
#[cfg(feature = "luau")]
#[doc(no_inline)]
pub use crate::VmState as LuaVmState;
#[cfg(feature = "async")]
#[doc(no_inline)]
pub use crate::AsyncThread as LuaAsyncThread;
+7 -1
View File
@@ -356,7 +356,13 @@ impl<'lua> Table<'lua> {
pub fn set_readonly(&self, enabled: bool) {
let lua = self.0.lua;
unsafe {
lua.ref_thread_exec(|refthr| ffi::lua_setreadonly(refthr, self.0.index, enabled as _));
lua.ref_thread_exec(|refthr| {
ffi::lua_setreadonly(refthr, self.0.index, enabled as _);
if !enabled {
// Reset "safeenv" flag
ffi::lua_setsafeenv(refthr, self.0.index, 0);
}
});
}
}
+63 -1
View File
@@ -214,6 +214,13 @@ impl<'lua> Thread<'lua> {
lua.push_ref(&func.0);
ffi::lua_xmove(lua.state, thread_state, 1);
#[cfg(feature = "luau")]
{
// Inherit `LUA_GLOBALSINDEX` from the caller
ffi::lua_xpush(lua.state, thread_state, ffi::LUA_GLOBALSINDEX);
ffi::lua_replace(thread_state, ffi::LUA_GLOBALSINDEX);
}
Ok(())
}
}
@@ -278,6 +285,57 @@ impl<'lua> Thread<'lua> {
recycle: false,
}
}
/// Enables sandbox mode on this thread.
///
/// Under the hood replaces the global environment table with a new table,
/// that performs writes locally and proxies reads to caller's global environment.
///
/// This mode ideally should be used together with the global sandbox mode [`Lua::sandbox()`].
///
/// Please note that Luau links environment table with chunk when loading it into Lua state.
/// Therefore you need to load chunks into a thread to link with the thread environment.
///
/// # Examples
///
/// ```
/// # #[cfg(feature = "luau")]
/// # fn main() -> mlua::Result<()> {
/// use mlua::Lua;
/// let lua = Lua::new();
/// let thread = lua.create_thread(lua.create_function(|lua2, ()| {
/// lua2.load("var = 123").exec()?;
/// assert_eq!(lua2.globals().get::<_, u32>("var")?, 123);
/// Ok(())
/// })?)?;
/// thread.sandbox()?;
/// thread.resume(())?;
///
/// // The global environment should be unchanged
/// assert_eq!(lua.globals().get::<_, Option<u32>>("var")?, None);
/// # Ok(())
/// # }
///
/// # #[cfg(not(feature = "luau"))]
/// fn main() {}
/// ```
///
/// Requires `feature = "luau"`
#[cfg(any(feature = "luau", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
#[doc(hidden)]
pub fn sandbox(&self) -> Result<()> {
let lua = self.0.lua;
unsafe {
let thread = lua.ref_thread_exec(|t| ffi::lua_tothread(t, self.0.index));
check_stack(thread, 1)?;
check_stack(lua.state, 3)?;
// Inherit `LUA_GLOBALSINDEX` from the caller
ffi::lua_xpush(lua.state, thread, ffi::LUA_GLOBALSINDEX);
ffi::lua_replace(thread, ffi::LUA_GLOBALSINDEX);
protect_lua!(lua.state, 0, 0, |_| ffi::luaL_sandboxthread(thread))
}
}
}
impl<'lua> PartialEq for Thread<'lua> {
@@ -295,7 +353,11 @@ impl<'lua, R> AsyncThread<'lua, R> {
}
#[cfg(feature = "async")]
#[cfg(any(feature = "lua54", all(feature = "luajit", feature = "vendored")))]
#[cfg(any(
feature = "lua54",
all(feature = "luajit", feature = "vendored"),
feature = "luau",
))]
impl<'lua, R> Drop for AsyncThread<'lua, R> {
fn drop(&mut self) {
if self.recycle {
+17 -2
View File
@@ -49,11 +49,26 @@ pub(crate) struct AsyncPollUpvalue<'lua> {
pub(crate) lua: Lua,
pub(crate) fut: LocalBoxFuture<'lua, Result<MultiValue<'lua>>>,
}
/// Type to set next Luau VM action after executing interrupt function.
#[cfg(any(feature = "luau", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
pub enum VmState {
Continue,
Yield,
}
#[cfg(all(feature = "send", not(feature = "luau")))]
pub(crate) type HookCallback = Arc<Mutex<dyn FnMut(&Lua, Debug) -> Result<()> + Send>>;
pub(crate) type HookCallback = Arc<dyn Fn(&Lua, Debug) -> Result<()> + Send>;
#[cfg(all(not(feature = "send"), not(feature = "luau")))]
pub(crate) type HookCallback = Arc<Mutex<dyn FnMut(&Lua, Debug) -> Result<()>>>;
pub(crate) type HookCallback = Arc<dyn Fn(&Lua, Debug) -> Result<()>>;
#[cfg(all(feature = "luau", feature = "send"))]
pub(crate) type InterruptCallback = Arc<dyn Fn(&Lua) -> Result<VmState> + Send>;
#[cfg(all(feature = "luau", not(feature = "send")))]
pub(crate) type InterruptCallback = Arc<dyn Fn(&Lua) -> Result<VmState>>;
#[cfg(all(feature = "send", feature = "lua54"))]
pub(crate) type WarnCallback = Box<dyn Fn(&Lua, &CStr, bool) -> Result<()> + Send>;
+4 -4
View File
@@ -3,6 +3,7 @@
use std::cell::RefCell;
use std::ops::Deref;
use std::str;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, Mutex};
use mlua::{DebugEvent, Error, HookTriggers, Lua, Result, Value};
@@ -128,18 +129,17 @@ fn test_error_within_hook() -> Result<()> {
#[test]
fn test_limit_execution_instructions() -> Result<()> {
let lua = Lua::new();
let mut max_instructions = 10000;
#[cfg(feature = "luajit")]
// For LuaJIT disable JIT, as compiled code does not trigger hooks
#[cfg(feature = "luajit")]
lua.load("jit.off()").exec()?;
let max_instructions = AtomicI64::new(10000);
lua.set_hook(
HookTriggers::every_nth_instruction(30),
move |_lua, debug| {
assert_eq!(debug.event(), DebugEvent::Count);
max_instructions -= 30;
if max_instructions < 0 {
if max_instructions.fetch_sub(30, Ordering::Relaxed) <= 30 {
Err(Error::RuntimeError("time's up".to_string()))
} else {
Ok(())
+155 -10
View File
@@ -2,8 +2,10 @@
use std::env;
use std::fs;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use mlua::{Error, Lua, Result, Value};
use mlua::{Error, Lua, Result, Table, ThreadStatus, Value, VmState};
#[test]
fn test_require() -> Result<()> {
@@ -34,17 +36,32 @@ fn test_require() -> Result<()> {
fn test_vectors() -> Result<()> {
let lua = Lua::new();
let globals = lua.globals();
globals.set(
"vector",
lua.create_function(|_, (x, y, z)| Ok(Value::Vector(x, y, z)))?,
)?;
let v: [f32; 3] = lua
.load("return vector(1, 2, 3) + vector(3, 2, 1)")
.eval()?;
let v: [f32; 3] = lua.load("vector(1, 2, 3) + vector(3, 2, 1)").eval()?;
assert_eq!(v, [4.0, 4.0, 4.0]);
// Test vector methods
lua.load(
r#"
local v = vector(1, 2, 3)
assert(v.x == 1)
assert(v.y == 2)
assert(v.z == 3)
"#,
)
.exec()?;
// Test vector methods (fastcall)
lua.load(
r#"
local v = vector(1, 2, 3)
assert(v.x == 1)
assert(v.y == 2)
assert(v.z == 3)
"#,
)
.set_vector_ctor(Some("vector".to_string()))
.exec()?;
Ok(())
}
@@ -67,3 +84,131 @@ fn test_readonly_table() -> Result<()> {
Ok(())
}
#[test]
fn test_sandbox() -> Result<()> {
let lua = Lua::new();
lua.sandbox(true)?;
lua.load("global = 123").exec()?;
let n: i32 = lua.load("return global").eval()?;
assert_eq!(n, 123);
assert_eq!(lua.globals().get::<_, Option<i32>>("global")?, Some(123));
// Threads should inherit "main" globals
let f = lua.create_function(|lua, ()| lua.globals().get::<_, i32>("global"))?;
let co = lua.create_thread(f.clone())?;
assert_eq!(co.resume::<_, Option<i32>>(())?, Some(123));
// Sandboxed threads should also inherit "main" globals
let co = lua.create_thread(f)?;
co.sandbox()?;
assert_eq!(co.resume::<_, Option<i32>>(())?, Some(123));
lua.sandbox(false)?;
// Previously set variable `global` should be cleared now
assert_eq!(lua.globals().get::<_, Option<i32>>("global")?, None);
// Readonly flags should be cleared as well
let table = lua.globals().get::<_, Table>("table")?;
table.set("test", "test")?;
Ok(())
}
#[test]
fn test_sandbox_threads() -> Result<()> {
let lua = Lua::new();
let f = lua.create_function(|lua, v: Value| lua.globals().set("global", v))?;
let co = lua.create_thread(f.clone())?;
co.resume(321)?;
// The main state should see the `global` variable (as the thread is not sandboxed)
assert_eq!(lua.globals().get::<_, Option<i32>>("global")?, Some(321));
let co = lua.create_thread(f.clone())?;
co.sandbox()?;
co.resume(123)?;
// The main state should see the previous `global` value (as the thread is sandboxed)
assert_eq!(lua.globals().get::<_, Option<i32>>("global")?, Some(321));
// Try to reset the (sandboxed) thread
co.reset(f)?;
co.resume(111)?;
assert_eq!(lua.globals().get::<_, Option<i32>>("global")?, Some(111));
Ok(())
}
#[test]
fn test_interrupts() -> Result<()> {
let lua = Lua::new();
let interrupts_count = Arc::new(AtomicU64::new(0));
let interrupts_count2 = interrupts_count.clone();
lua.set_interrupt(move |_lua| {
interrupts_count2.fetch_add(1, Ordering::Relaxed);
Ok(VmState::Continue)
});
let f = lua
.load(
r#"
local x = 2 + 3
local y = x * 63
local z = string.len(x..", "..y)
"#,
)
.into_function()?;
f.call(())?;
assert!(interrupts_count.load(Ordering::Relaxed) > 0);
//
// Test yields from interrupt
//
let yield_count = Arc::new(AtomicU64::new(0));
let yield_count2 = yield_count.clone();
lua.set_interrupt(move |_lua| {
if yield_count2.fetch_add(1, Ordering::Relaxed) == 1 {
return Ok(VmState::Yield);
}
Ok(VmState::Continue)
});
let co = lua.create_thread(
lua.load(
r#"
local a = {1, 2, 3}
local b = 0
for _, x in ipairs(a) do b += x end
return b
"#,
)
.into_function()?,
)?;
co.resume(())?;
assert_eq!(co.status(), ThreadStatus::Resumable);
let result: i32 = co.resume(())?;
assert_eq!(result, 6);
assert_eq!(yield_count.load(Ordering::Relaxed), 7);
assert_eq!(co.status(), ThreadStatus::Unresumable);
//
// Test errors in interrupts
//
lua.set_interrupt(|_| Err(Error::RuntimeError("error from interrupt".into())));
match f.call::<_, ()>(()) {
Err(Error::CallbackError { cause, .. }) => match *cause {
Error::RuntimeError(ref m) if m == "error from interrupt" => {}
ref e => panic!("expected RuntimeError with a specific message, got {:?}", e),
},
r => panic!("expected CallbackError, got {:?}", r),
}
lua.remove_interrupt();
Ok(())
}