Skip to content

Latest commit

 

History

History
195 lines (137 loc) · 7.36 KB

v0.10.md

File metadata and controls

195 lines (137 loc) · 7.36 KB

mlua v0.10 release notes

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.

New features

'static Lua types

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.

Truly send feature

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.

Register Rust functions with variable number of arguments

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.

Setting metatable for Lua builtin types

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

Improvements

New ObjectLike trait

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.

Either<L, R> enum

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

Lua::exec_raw helper to execute low-level Lua C API code

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.

anyhow feature flag

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

Breaking changes

Scope changes

The following Scope methods were changed:

  • Removed Scope::create_any_userdata
  • Scope::create_nonstatic_userdata is renamed to Scope::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).

String changes

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.