mlua-rs / mlua

High level Lua 5.4/5.3/5.2/5.1 (including LuaJIT) and Roblox Luau bindings to Rust with async/await support
Other
1.75k stars 139 forks source link

Making LuaJIT metatables mind their manners #469

Closed alerque closed 1 month ago

alerque commented 1 month ago

I'm trying to get a workable type system in Rust that can be used from Lua ergonomically as well. I'm having a bit of trouble with meta tables. Actually Lua 5.4 works fine, but in LuaJIT things are more cantankerous. Far example you can't set comparison functions in meta tables and use them to compare objects unless the metatable ID is actually the same across both tables. An identical meta table won't work, it has to be the same table.

Here is an MWE with some Lua chunks that work (because the mt is reused) and a very similar Rust version with a struct and impl IntoLua that does not work.

#!/usr/bin/env rust-script
//! ```cargo
//! [dependencies]
//! mlua = { version = "0.10.0-rc.1", features = [ "luajit", "vendored" ] }
//! ```

#[derive(Debug)]
struct Thing {
    value: u8,
}

impl mlua::IntoLua for Thing {
    #[inline]
    fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> {
        let table = lua.create_table()?;
        let metatable = lua.create_table()?;
        let less_than = lua.create_function(|_, args: (mlua::Table, mlua::Table)| {
            let v1: u8 = args.0.get("value")?;
            let v2: u8 = args.1.get("value")?;
            Ok(v1 < v2)
        })?;
        metatable.set("__lt", less_than)?;
        let to_string = lua.create_function(|_, a: mlua::Table| {
            let v: u8 = a.get("value")?;
            Ok(format!("{}", v))
        })?;
        metatable.set("__tostring", to_string)?;
        table.set_metatable(Some(metatable));
        table.set("value", self.value).unwrap();
        Ok(mlua::Value::Table(table))
    }
}

fn main() {
    let test = mlua::Lua::new();
    let x = Thing { value: 1 };
    let y = Thing { value: 2 };
    let z = Thing { value: 2 };
    test.globals().set("x", x).unwrap();
    test.globals().set("y", y).unwrap();
    test.globals().set("z", z).unwrap();
    test.load(
        r#"
        local mt = {
            __lt = function (a, b) return a.value < b.value end,
            __tostring = function (a) return a.value end,
        }
        local lua_thing = function (value)
            return setmetatable({ value = value }, mt)
        end
        local a, b, c = lua_thing(1), lua_thing(2), lua_thing(2)
        print(a < b, a, a.value, b, b.value, b < c, c)
        print(x < y, x, x.value, y, y.value, y < z, z)
        "#,
    )
    .exec()
    .unwrap();
}

How should I be setting up IntoLua that creates usable types on the Lua side?

khvzak commented 1 month ago

To reuse a single metatable inside IntoLua definition you can: 1) Store (once) metatable in registry using Lua::set_named_registry_value and then retrieve when it's needed (recommended)

2) Store metatable in Lua::set_app_data

alerque commented 1 month ago

Thanks! This is an annoying thing to have to work around in LuaJIT, but that isn't mlua's fault. I've had to accomodate this is in plain LuaJIT too.

For anybody else running into this, here in the original MWE modified as a POC for stashing a meta table in the registry as suggested:

#!/usr/bin/env rust-script
//! ```cargo
//! [dependencies]
//! mlua = { version = "0.10.0-rc.1", features = [ "luajit", "vendored" ] }
//! ```

#[derive(Debug)]
struct Thing {
    value: u8,
}

fn get_metatable_from_registy(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
    let key = "thing";
    let metatable: mlua::Table = match lua.named_registry_value(key)? {
        mlua::Value::Table(metatable) => metatable,
        mlua::Value::Nil => {
            let metatable = lua.create_table()?;
            let less_than = lua.create_function(|_, args: (mlua::Table, mlua::Table)| {
                let v1: u8 = args.0.get("value")?;
                let v2: u8 = args.1.get("value")?;
                Ok(v1 < v2)
            })?;
            metatable.set("__lt", less_than)?;
            let to_string = lua.create_function(|_, a: mlua::Table| {
                let v: u8 = a.get("value")?;
                Ok(format!("{}", v))
            })?;
            metatable.set("__tostring", to_string)?;
            lua.set_named_registry_value(key, &metatable)?;
            metatable
        }
        _ => panic!("Unexpected type returned from from registry lookup"),
    };
    Ok(metatable)
}

impl mlua::IntoLua for Thing {
    #[inline]
    fn into_lua(self, lua: &mlua::Lua) -> mlua::Result<mlua::Value> {
        let table = lua.create_table()?;
        let metatable: mlua::Table = get_metatable_from_registy(&lua)?;
        table.set_metatable(Some(metatable));
        table.set("value", self.value).unwrap();
        Ok(mlua::Value::Table(table))
    }
}

fn main() {
    let test = mlua::Lua::new();
    let x = Thing { value: 1 };
    let y = Thing { value: 2 };
    let z = Thing { value: 2 };
    test.globals().set("x", x).unwrap();
    test.globals().set("y", y).unwrap();
    test.globals().set("z", z).unwrap();
    test.load(
        r#"
        local mt = {
            __lt = function (a, b) return a.value < b.value end,
            __tostring = function (a) return a.value end,
        }
        local lua_thing = function (value)
            return setmetatable({ value = value }, mt)
        end
        local a, b, c = lua_thing(1), lua_thing(2), lua_thing(2)
        print(a < b, a, a.value, b, b.value, b < c, c)
        print(x < y, x, x.value, y, y.value, y < z, z)
        "#,
    )
    .exec()
    .unwrap();
}