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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions