Skip to content

UserData derive macro #689

@WASDetchan

Description

@WASDetchan

UserData derive macro feature proposal

First, this is not a request for a feature. I am willing to implement the macro. This proposal is a brief overview of my idea. I will write the detailed proposal and start implementing the macro only after the maintainer confirms this idea makes sense and may be accepted into mlua.

Overview

I suggest implementing:

  • Derive macro for the UserData trait, where user may specify fields and mutable fields with attributes
  • Attribute macro placed on impl block where user may specify with attributes:
    • get/set field methods
    • normal methods
    • metamethods
  • Methods may return mlua::Result or return a value if marked with infallible
  • Methods may optionally accept &Lua and other arguments

Example

#[derive(Clone, UserData)]
struct MyStruct {
    #[field]
    a: u64,
    #[field_mut]
    b: u64,
}

#[mlua::userdata_impl]
impl MyStruct {
    #[field_get(name = "s", infallible)]
    fn sum(&self) -> u64 {
        self.a + self.b
    }
    #[method(infallible)]
    fn double_a(&mut self) {
        self.a *= 2;
    }

    #[method]
    fn parse_and_set_b(&mut self, _: &mlua::Lua, b: String) -> mlua::Result<()> {
        self.b = b.parse().map_err(|e| mlua::Error::ExternalError(Arc::new(e)))?;
        Ok(())
    }

    #[meta_add(infallible)]
    fn add(&self, _: &mlua::Lua, other: Self) -> Self {
        Self {
            a: self.a + other.a,
            b: self.b + other.b,
        }
    }
}

#[test]
fn test_userdata_derive() {
    let lua = Lua::new();
    lua.globals().set("A", MyStruct { a: 1, b: 0 }).unwrap();
    lua.globals().set("B", MyStruct { a: 2, b: 4 }).unwrap();
    let chunk = lua.load(
        r#"
    A:double_a()
    B:parse_and_set_b "15"
    A.b = A.a
    C = A + B
    return C.s
    "#,
    );
    assert_eq!(chunk.eval::<f64>().unwrap(), 21.0);
}

The expected macro expansion is something like this (of course with proper hygiene and static assertions, this was written manually):

#[derive(Clone, FromLua)]
struct MyStruct {
    a: u64,
    b: u64,
}

impl MyStruct {
    fn sum(&self) -> u64 {
        self.a + self.b
    }

    fn double_a(&mut self) {
        self.a *= 2;
    }

    fn parse_and_set_b(&mut self, _: &mlua::Lua, b: String) -> mlua::Result<()> {
        self.b = b.parse().map_err(|e| mlua::Error::ExternalError(Arc::new(e)))?;
        Ok(())
    }

    fn add(&self, _: &mlua::Lua, other: Self) -> Self {
        Self {
            a: self.a + other.a,
            b: self.b + other.b,
        }
    }
}

impl UserData for MyStruct
where
    MyStruct: UserDataImpl,
{
    fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
        <MyStruct as UserDataImpl>::add_fields(fields);
        fields.add_field_method_get("a", |_, this| Ok(this.a));
        fields.add_field_method_get("b", |_, this| Ok(this.b));
        fields.add_field_method_set("b", |_, this, b: u64| {
            this.b = b;
            Ok(())
        });

    }
    fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
        <MyStruct as UserDataImpl>::add_methods(methods);
    }
}

impl UserDataImpl for MyStruct {
    fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
        fields.add_field_method_get("s", |_, this| Ok(this.sum()));
    }
    fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
        methods.add_method_mut("double_a", |_, this, _: ()| {
            this.double_a();
            Ok(())
        });
        methods.add_method_mut("parse_and_set_b", |lua, this, (b,): (String,)| {
            this.parse_and_set_b(lua, b)
        });

        methods.add_meta_method(MetaMethod::Add, |lua, this, (other,): (Self,)| {
            Ok(this.add(lua, other))
        });
    }
}

where

pub trait UserDataImpl: Sized {
    fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F);
    fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M);
}

The macro expansion was written manually. I apologize if there are mismatches between the expanded and the original code.

Edit: Added missing field getters/setters, fixed a typo

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions