The v0.9 version of mlua is a major release that includes a number of API changes and improvements. This release is a stepping stone towards the v1.0. This document highlights the most important changes. For a full list of changes, see the CHANGELOG.
This is a long awaited feature that allows to register in Lua foreign types that cannot implement UserData
trait because of the Rust orphan rules.
Now you can register any type that implements Any
trait as a userdata type.
Consider the following example:
lua.register_userdata_type::<std::string::String>(|reg| {
reg.add_method("len", |_, this, ()| Ok(this.len()));
reg.add_method_mut("push", |_, this, s: String| {
this.push_str(&s);
Ok(())
});
reg.add_meta_method(MetaMethod::ToString, |lua, this, ()| lua.create_string(this));
})?;
let s = lua.create_any_userdata("hello".to_string())?;
lua.load(chunk! {
print("s:len() is " .. $s:len())
$s:push(" world")
// Prints: hello, world
print($s)
})
.exec()?;
In this example we registered std::string::String
as a userdata type with a set of methods and then created an instance of this type in Lua.
It's not required to register a type before using the Lua::create_any_userdata()
method, instead an empty metatable will be created for you.
You can also register the same type multiple times with different methods. Any previously created instances will share the old metatable, while new instances will have the new one.
The new set of API is called any_userdata
because it allows to register types that implements Any
trait.
When you need to create non-static userdata instances in Lua, the usual way is use Lua::scope()
helper to make them scoped. When out of scope, any scoped objects will be automatically
dropped. The only downside of this approach is that every new instance will have a new metatable. This is not very fast if you need to create a lot of instances.
With the new Any UserData API, you can place non-static references &T
where T: 'static
into a scope and they will share a single static metatable.
lua.register_userdata_type::<std::string::String>(|reg| {
reg.add_method_mut("replace", |_, this, (pat, to): (String, String)| {
*this = this.replace(&pat, &to);
Ok(())
});
reg.add_meta_method(MetaMethod::ToString, |lua, this, ()| lua.create_string(this));
})?;
let mut s = "hello, world".to_string();
lua.scope(|scope| {
// This userdata instance holds only a mutable reference to our string
let ud = scope.create_any_userdata_ref_mut(&mut s)?;
lua.load(chunk! {
$ud:replace("world", "user")
})
.exec()
})?;
// Prints: hello, user!
println!("{s}!");
One of the common questions was how to embed a Lua type into Rust struct to use it later. It was non-trivial to do because of the 'lua
lifetime attached to every Lua value.
In v0.9 mlua introduces "owned" types OwnedTable
/OwnedFunction
/OwnedString
/OwnedAnyUserData
/ OwnedThread
that are 'static
(no lifetime attached).
let lua = Lua::new();
struct MyStruct {
table: OwnedTable,
func: OwnedFunction,
}
let my_struct = MyStruct {
table: lua.globals().into_owned(),
func: lua
.create_function(|_, t: Table| Ok(format!("{t:#?}")))?
.into_owned(),
};
// It's safe to drop Lua!
drop(lua);
let result = my_struct.func.call::<_, String>(my_struct.table)?;
println!("{result}");
Prior to v0.9, it was possible to do by creating a reference to the Lua value in registry using Lua::create_registry_value()
and retrieving value later using Lua::registry_value()
method.
All owned handles hold a strong reference to the current Lua instance.
Be warned, if you place them into a Lua type (eg. UserData
or a Rust callback), it is very easy
to accidentally cause reference cycles that would prevent destroying Lua instance.
Please note this functionality is available under the unstable
feature flag and not available when the send
feature is enabled.
In v0.9 release the internal ffi
module has been moved into the new mlua-sys
crate and became available for public use.
This crate provides unified Lua FFI API (targeting Lua 5.4) using a (limited) compatibility layer for older versions.
mlua re-exports the ffi
module aliasing the mlua-sys
crate and provides (unsafe) functionality to work with raw Lua state:
unsafe {
unsafe extern "C-unwind" fn lua_add(state: *mut mlua::lua_State) -> i32 {
let a = mlua::ffi::luaL_checkinteger(state, 1);
let b = mlua::ffi::luaL_checkinteger(state, 2);
mlua::ffi::lua_pushinteger(state, a + b);
1
}
let add = lua.create_c_function(lua_add)?;
assert_eq!(add.call::<_, i32>((2, 3))?, 5);
}
mlua brings support for the new Luau JIT backend under the luau-jit
feature flag.
It will automatically trigger JIT compilation for new Lua chunks. To disable it, just call lua.enable_jit(false)
before loading Lua code
(but any previously compiled chunks will remain JIT-compiled).
When calling a Rust function from Lua and passing wrong arguments, previous mlua versions reported a error message without any context or reference to the particular argument.
In v0.9 it reports a error message with the argument index and expected type:
let func = lua.create_function(|_, _a: i32| Ok(()))?;
lua.load(chunk! {
local ok, err = pcall($func, "not a number")
// Prints: bad argument #1: error converting Lua string to i32 (expected number or string coercible to number)
print(err)
})
.exec()?;
Similar changes have been made for userdata functions and methods:
lua.register_userdata_type::<&'static str>(|reg| {
reg.add_method("len", |_, this, ()| Ok(this.len()));
})?;
let s = lua.create_any_userdata("hello")?;
lua.load(chunk! {
local ok, err = pcall($s.len, 123)
// Prints: bad argument `self` to `&str.len`: error converting Lua integer to userdata
print(err)
})
.exec()?;
Similar to the anyhow
Error type, now it's possible to attach context to Lua errors:
let read = lua.create_function(|lua, path: String| {
let bytes = std::fs::read(&path)
.into_lua_err()
.context(format!("Failed to open `{path}`"))?;
Ok(lua.create_string(bytes))
})?;
lua.load(chunk! {
local ok, err = pcall($read, "/nonexistent")
/// Prints:
/// Failed to open /nonexistent
/// No such file or directory (os error 2)
/// stack traceback:
/// ...
print(err)
})
.exec()?;
Sometimes it's useful to have IntoLua
trait implementation for a Rust function or type T: Any
without needing to call Lua::create_function()
/Lua::create_any_userdata()
methods.
Since v0.9 you can call the new methods Function::wrap()
/AnyUserData::wrap()
that allows to do this. They return an abstract type that impl IntoLua
:
lua.globals().set("print_rust", Function::wrap(|_, s: String| Ok(println!("{}", s))))?;
lua.globals().set("rust_ud", AnyUserData::wrap("hello"))?;
In addition there are also Function::wrap_mut()
/Function::wrap_async()
methods that allow to wrap mutable and async functions respectively.
For a T: 'UserData + 'static
the IntoLua
trait is still always implemented.
The new wrappers UserDataRef
and UserDataRefMut
are receivers for userdata type T
and borrow underlying instance for the lifetime of the wrapper.
lua.globals()
.set("ud", AnyUserData::wrap("hello".to_string()))?;
let mut ud_mut: UserDataRefMut<String> = lua.globals().get("ud")?;
ud_mut.push_str(", Rust");
drop(ud_mut);
let ud_ref: UserDataRef<String> = lua.globals().get("ud")?;
// Prints: hello, Rust
println!("{}", *ud_ref);
In the previous mlua versions the same functionality can be achieved by receiving AnyUserData
and calling AnyUserData::borrow()
/AnyUserData::borrow_mut()
methods.
The new wrappers are identical to Rust Ref
/RefMut
types.
Similar to the TableExt
trait, the AnyUserDataExt
provides a set of extra methods for the AnyUserData
type.
-
AnyUserDataExt::get()/set()
to get/set a value by key from the userdata, assuming it has__index
metamethod. -
AnyUserDataExt::call()
to call the userdata as a function assuming it has__call
metamethod. -
AnyUserData::call_method(name, ...)
to call the userdata method, assuming it has__index
metamethod and the associated function.
mlua::Value
implements a new format :#?
that allows to (recursively) pretty print Lua values:
println!("{:#?}", lua.globals());
Prints:
{
["_G"] = table: 0x7fa2d0706260,
["_VERSION"] = "Lua 5.4",
["assert"] = function: 0x10451d11d,
["collectgarbage"] = function: 0x10451d198,
["coroutine"] = {
["close"] = function: 0x10451e28f,
...
},
["dofile"] = function: 0x10451d37c,
...
}
In addition a new method Value::to_string()
has been added to convert Value
to a string (using __tostring
metamethod if available).
Any Lua functions have an associated environment table that is used to resolve global variables. By default it sets to a Lua globals table.
In the new release it's possible to get or update a function environment using Function::environment()
or Function::set_environment()
methods respectively.
let f = lua.load("return a").into_function()?;
assert_eq!(f.environment(), Some(lua.globals()));
lua.globals().set("a", 1)?;
assert_eq!(f.call::<_, i32>(())?, 1);
f.set_environment(lua.create_table_from([("a", "hello")])?)?;
assert_eq!(f.call::<_, mlua::String>(())?, "hello");
The new mlua version has a number of performance improvements. Please check the benchmarks results to see how mlua compares to rlua and rhai.
The lua_module
macro now support the following attributes:
name=...
- sets name of the module (defaults to the name of the function).
Eg.:
#[mlua::lua_module(name = "alt_module")]
fn my_module(lua: &Lua) -> LuaResult<LuaTable> {
lua.create_table()
}
Under the hood a new function luaopen_alt_module
will be created for the Lua module loader.
skip_memory_check
- skip memory allocation checks for some operations.
In module mode, mlua runs in unknown environment and cannot say are there any memory limits or not. As result, some operations that require memory allocation runs in protected mode. Setting this attribute will improve performance of such operations with risk of having uncaught exceptions and memory leaks.
In previous mlua versions, building a Lua module for Windows requires having Lua development libraries installed on the system.
In contrast, on Linux and macOS, modules can be built without any external dependencies using the -undefined=dynamic_lookup
linker flag.
With Rust 1.71+ it's now possible to lift this restriction for Windows as well. You can build modules normally and they will be linked with
lua54.dll
/lua53.dll
/lua52.dll
/lua51.dll
depending on the enabled Lua version.
You still need to have the dll although, linked to application where the module will be loaded.
ToLua
/ToLuaMulti
traits have been renamed toIntoLua
/IntoLuaMulti
respectively (with the methods calledinto_lua
/into_lua_multi
).
The main reason for this change is following the Rust self convention.
- Removed
FromLua
implementation forT: UserData + Clone
.
During the usage of mlua, it was found that this implementation is not very useful and prevents custom FromLua
implementations for T: UserData
.
It should be a developer decision to opt-in FromLua
for their T
if needed rather than having enabled it unconditionally.
To opt-in FromLua
for T: Clone
you can use a simple #[derive(FromLua)]
macro (requires feature = "macros"
):
#[derive(Clone, Copy, mlua::FromLua)]
struct MyUserData(i32);
T
is not required to implement UserData
because of the new relaxed restrictions on userdata types.