The v0.10 version of mlua has goal to improve the user experience while keeping the same performance and safety guarantees. This document highlights the most notable features. For a full list of changes, see the CHANGELOG.
In previous mlua versions, it was required to have a 'lua
lifetime attached to every Lua value. v0.9 introduced (experimental) owned types that are 'static
without a lifetime attached, but they kept strong references to the Lua instance.
In v0.10 all Lua types are 'static
and have only weak reference to the Lua instance. It means they are more flexible and can be used in more places without worrying about memory leaks.
In this version Lua is Send + Sync
when the send
feature flag is enabled (previously was only Send
). It means Lua instance and their values can be safely shared between threads and used in multi threaded async contexts.
let lua = Lua::new();
lua.globals().set("i", 0)?;
let func = lua.load("i = i + ...").into_function()?;
std::thread::scope(|s| {
s.spawn(|| {
for i in 0..5 {
func.call::<()>(i).unwrap();
}
});
s.spawn(|| {
for i in 0..5 {
func.call::<()>(i).unwrap();
}
});
});
assert_eq!(lua.globals().get::<i32>("i")?, 20);
Under the hood, to synchronize access to the Lua state, mlua uses ReentrantMutex
which can be recursively locked by a single thread. Only one thread can execute Lua code at a time, but it's possible to share Lua values between threads.
This has some performance penalties (about 10-20%) compared to the lock free mode. This flag is disabled by default and does not supported in module mode.
The new traits LuaNativeFn
/LuaNativeFnMut
/LuaNativeAsyncFn
have been introduced to provide a way to register Rust functions with variable number of arguments in Lua, without needing to pass all arguments as a tuple.
They are used by Function::wrap
/Function::wrap_mut
/Function::wrap_async
methods:
let add = Function::wrap(|a: i64, b: i64| Ok(a + b));
lua.globals().set("add", add).unwrap();
// Prints 50
lua.load(r#"print(add(5, 45))"#).exec().unwrap();
To wrap functions that return direct value (non-Result
) you can use Function::wrap_raw
method.
For Lua builtin types (like string
, function
, number
, etc.) that have a shared metatable for all instances, it's now possible to set a custom metatable for them.
let mt = lua.create_table()?;
mt.set("__tostring", lua.create_function(|_, b: bool| Ok(if b { "2" } else { "0" }))?)?;
lua.set_type_metatable::<bool>(Some(mt));
lua.load("assert(tostring(true) == '2')").exec().unwrap();
The ObjectLike
trait is a combination of the AnyUserDataExt
and TableExt
traits used in previous versions. It provides a unified interface for working with Lua tables and userdata.
The Either<L, R>
enum is a simple enum that can hold either L
or R
value. It's useful when you need to return or receive one of two types in a function.
This type implements IntoLua
and FromLua
traits and can generate a meaningful error message when conversion fails.
let func = Function::wrap(|x: Either<i32, String>| Ok(format!("received: {x}")));
lua.globals().set("func", func).unwrap();
// Prints: received: 123
lua.load(r#"print(func(123))"#).exec().unwrap();
// Prints: bad argument #1: error converting Lua table to Either<i32, String>
lua.load(r#"print(pcall(func, {}))"#).exec().unwrap();
For advanced users, it's now possible to execute low-level Lua C API code using the Lua::exec_raw
method.
let t = lua.create_sequence_from([1, 2, 3, 4, 5])?;
let sum: i64 = unsafe {
lua.exec_raw(&t, |state| {
// top of the stack: table `t`
let mut sum = 0;
// push nil as the first key
mlua::ffi::lua_pushnil(state);
while mlua::ffi::lua_next(state, -2) != 0 {
sum += mlua::ffi::lua_tointeger(state, -1);
// Remove the value, keep the key for the next iteration
mlua::ffi::lua_pop(state, 1);
}
mlua::ffi::lua_pop(state, 1);
mlua::ffi::lua_pushinteger(state, sum);
// top of the stack: sum
})
}?;
assert_eq!(sum, 15);
The exec_raw
method is longjmp-safe. It's not recommended to move Drop
types into the closure to avoid possible memory leaks.
The new anyhow
feature flag adds IntoLua
and Into<mlua::Error>
implementation for the anyhow::Error
type.
let f = lua.create_function(|_, ()| {
Err(anyhow!("error message"))?;
Ok(())
})?;
The following Scope
methods were changed:
- Removed
Scope::create_any_userdata
Scope::create_nonstatic_userdata
is renamed toScope::create_userdata
Instead, scope has comprehensive support for borrowed userdata: create_any_userdata_ref
, create_any_userdata_ref_mut
, create_userdata_ref
, create_userdata_ref_mut
.
UserDataRef
and UserDataRefMut
are no longer acceptable for scoped userdata access as they require owned underlying data.
In mlua v0.9 this can cause read-after-free bug in some edge cases.
To temporarily borrow underlying data, the AnyUserData::borrow_scoped
and AnyUserData::borrow_mut_scoped
methods were introduced:
let data = "hello".to_string();
lua.scope(|scope| {
let ud = scope.create_any_userdata_ref(&data)?;
// We can only borrow scoped userdata using this method
ud.borrow_scoped::<String, ()>(|s| {
assert_eq!(s, "hello");
})?;
Ok(())
})?;
Those methods work for scoped and regular userdata objects (but still require T: 'static
).
Since mlua::String
holds a weak reference to Lua without any guarantees about the lifetime of the underlying data, getting a &str
or &[u8]
from it is no longer safe.
Lua instance can be destroyed while reference to the data is still alive:
let lua = Lua::new();
let s: mlua::String = lua.create_string("hello, world")?; // only weak reference to Lua!
let s_ref: &str = s.to_str()?; // this is not safe!
drop(lua);
println!("{s_ref}"); // use after free!
To solve this issue, return types of mlua::String::to_str
and mlua::String::as_bytes
methods changed to BorrowedStr
and BorrowedBytes
respectively.
These new types hold a strong reference to the Lua instance and can be safely converted to &str
or &[u8]
:
let lua = Lua::new();
let s: mlua::String = lua.create_string("hello, world")?;
let s_ref: mlua::BorrowedStr = s.to_str()?; // The strong reference to Lua is held here
drop(lua);
println!("{s_ref}"); // ok
The good news is that BorrowedStr
implements Deref<Target = str>
/AsRef<str>
as well as Display
, Debug
, Eq
, PartialEq
and other traits for easy usage.
The same applies to BorrowedBytes
.
Unfortunately, mlua::String::to_string_lossy
cannot return Cow<'a, str>
anymore, because it requires a strong reference to Lua. It now returns Rust String
instead.