Skip to content

Latest commit

 

History

History
361 lines (250 loc) · 12.9 KB

v0.9.md

File metadata and controls

361 lines (250 loc) · 12.9 KB

mlua v0.9 release notes

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.

New features

1. New Any UserData API

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.

2. Scope support for the new any userdata types

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}!");

3. Owned types (unstable)

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/ OwnedThreadthat 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.

New ffi module

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

Luau JIT support

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).

Improvements

1. Better error reporting

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()?;

2. Error context

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()?;

4. New methods Function::wrap/AnyUserData::wrap

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.

UserDataRef and UserDataRefMut type wrappers

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.

New AnyUserDataExt trait

Similar to the TableExt trait, the AnyUserDataExt provides a set of extra methods for the AnyUserData type.

  1. AnyUserDataExt::get()/set() to get/set a value by key from the userdata, assuming it has __index metamethod.

  2. AnyUserDataExt::call() to call the userdata as a function assuming it has __call metamethod.

  3. AnyUserData::call_method(name, ...) to call the userdata method, assuming it has __index metamethod and the associated function.

Pretty formatting Lua values

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).

Environment for Lua functions

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");

Performance optimizations

The new mlua version has a number of performance improvements. Please check the benchmarks results to see how mlua compares to rlua and rhai.

Changes in module mode

New attributes

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.

Improved Windows target

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.

Breaking changes

  1. ToLua/ToLuaMulti traits have been renamed to IntoLua/IntoLuaMulti respectively (with the methods called into_lua/into_lua_multi).

The main reason for this change is following the Rust self convention.

  1. Removed FromLua implementation for T: 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.