Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: proc_macro for LuaUserDataMethods::add_method #288

Open
Caellian opened this issue Nov 30, 2023 · 3 comments
Open

Suggestion: proc_macro for LuaUserDataMethods::add_method #288

Caellian opened this issue Nov 30, 2023 · 3 comments

Comments

@Caellian
Copy link

Caellian commented Nov 30, 2023

It would be really nice if rlua provided a macro that allowed declaring methods via add_method and add_method_mut on UserData in a simpler way.

Something like:

#[rlua::proc_methods]
impl UserData for MyStruct {
  // ... actual UserData methods ...

  #[rlua::method]
  fn methodA<'lua>(&self, abc: LuaContext<'lua>, number: f32, text: String) {
    // same as: methods.add_method("methodA", |abc, _self, (number, text): (f32, String)| { /*block*/ })
  }

  #[rlua::method]
  fn methodB<'lua>(&mut self, cde: LuaContext<'lua>, other_number: f32, some_text: String) {
    // same as: methods.add_method_mut("methodB", |cde, _self, (other_number, some_text): (f32, String)| { /*block*/ })
  }
}

This is quite involved to implement. Note that _self handling requires modification of the inner code block, but syn has a visit-mut feature for precisely this functionality.

Benefits

  • Resulting API would be 10x nicer looking.
  • Types could be checked using generated glue that throws very specific error messages containing exact names of parameters which failed.

Alternative proposal

A normal macro that wraps individual add_method calls would still allow generating more specific error messages while avoiding separation of argument names and types in callback signature.

@jugglerchris
Copy link
Collaborator

That does look like it'd make lots of bindings much nicer!

@Caellian
Copy link
Author

Caellian commented Dec 3, 2023

For reference, I wrote a macro for registering global constructor functions for my bindings which basically does the alternative proposal:

macro_rules! decl_func_constructor {
    ($handle: ident: |$ctx: ident| $imp: block) => {
        paste::paste! {
            fn [<register_ $handle:snake _constructor>]<'lua>(lua: LuaContext<'lua>) -> Result<(), LuaError> {
                let globals = lua.globals();
                let constructor = lua.create_function(|$ctx: LuaContext, ()| {
                    $imp
                })?;
                globals.set(stringify!($handle), constructor)?;
                Ok(())
            }
        }
    };
    
    ($handle: ident: |$ctx: ident, $($name: ident: $value: ident $( < $($gen: tt),* > )?),*| $imp: block) => {
        paste::paste! {
            fn [<register_ $handle:snake _constructor>]<'lua>(lua: LuaContext<'lua>) -> Result<(), LuaError> {
                let globals = lua.globals();
                let constructor = lua.create_function(|$ctx: LuaContext, args: LuaMultiValue| {
                    let mut args = args.into_iter();
                    $(
                        let $name: LuaValue = args.next().ok_or_else(|| LuaError::RuntimeError(
                            format!("missing '{}' argument in {} constructor; expected a value convertible to {}", stringify!($name), stringify!($handle), stringify!($value))
                        ))?;
                        let $name: $value$(<$($gen),*>)? = FromLuaMulti::from_lua_multi(LuaMultiValue::from_vec(vec![$name]), $ctx, &mut 0).map_err(|inner| LuaError::CallbackError {
                            traceback: format!("while converting '{}' argument value", stringify!($name)),
                            cause: std::sync::Arc::new(inner),
                        })?;
                    )*
                    $imp
                })?;
                globals.set(stringify!($handle), constructor)?;
                Ok(())
            }
        }
    };
    ($handle: ident: |$ctx: ident, $multi: ident| $imp: block) => {
        paste::paste! {
            fn [<register_ $handle:snake _constructor>]<'lua>(lua: LuaContext<'lua>) -> Result<(), LuaError> {
                let globals = lua.globals();
                let constructor = lua.create_function(|$ctx: LuaContext, $multi: LuaMultiValue| {
                    $imp
                })?;
                globals.set(stringify!($handle), constructor)?;
                Ok(())
            }
        }
    };
}

would require some minor tweaking to wrap add_method(_mut) but it's a good example.

This would probably have to be tuned to allow FromLuaMulti as in #287 for the last argument. But I'll try to mix this macro into that PR to provide much better error messages.

The issue with that implementation is that it doesn't handle complex types well (e.g. OneOf<String, LuaTable>).

@jugglerchris
Copy link
Collaborator

FWIW in one of my projects, I use a (declarative) macro which looks like:

wrap_lua!(RServer impl () {
    "add_redirect" => server_redirect(mut),                                             
    "add_literal" => server_literal(mut),                                               
    "add_part" => server_part(mut),                                                     
    "shutdown" => server_shutdown(mut)                                                  
});

where the server_redirect etc. are the rlua-style functions implementing the method.

I also support traits:

wrap_trait!(MyTrait {
    "meth" => mytrait_meth(const),
    "othermeth" => mytrait_othermeth(const)
});

wrap_type1(Type1 impl (MyTrait) {
    "type1_meth" => type1_meth(const)
});

From Lua a Type1 userdata then has both its own methods and the trait methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants