Search code examples
rustrust-proc-macros

How to properly create a call to a function in proc macro with a name depending on the argument in Rust?


I've recently tried to learn more about macros. I've decided to create a macro that will introduce a convenience features in the lib I'm usually working with. My idea depends on two macros, one that is dynamically creating the implementation for the struct (via derive macro and that works as expected). The second one, that is causing problems, is a #[proc_macro_attribute]. It's supposed to insert a call to one of the functions implemented in the derive macro. However, the function name is "dynamically" created from the argument provided to the macro.

Unfortunately, the error I'm getting during compilation is:

error[E0424]: expected value, found module `self`
src/lib.rs:29:5
>    |
29 |     #[with_rbac(owner)]
>    |     ^^^^^^^^^^^^^^^^^^^ `self` value is a keyword only available in methods with a `self` parameter
30 |     pub fn change_value(&mut self, new_value: u64) {
this function can't have a `self` parameter
>    |
note: this error originates in the attribute macro `with_rbac` (in Nightly builds, run with -Z macro-backtrace for more info)

The macro function:

use proc_macro::TokenStream;
use quote::quote;
use quote::ToTokens;

#[proc_macro_attribute]
pub fn with_rbac(args: TokenStream, input: TokenStream) -> TokenStream {
    let mut item = syn::parse(input).unwrap();
    let fn_item = match &mut item {
        syn::Item::Fn(fn_item) => fn_item,
        _ => panic!("Expected function"),
    };

    let args_ident: syn::Ident = syn::parse(args).expect("Parsing args works");
    let func_name = format!("rbac_only_{}", &args_ident);
    let func_ident = proc_macro2::Ident::new(func_name.as_str(), proc_macro2::Span::call_site());
    let check_to_add = quote! {
        self.#func_ident();
    };

    fn_item.block.stmts.insert(
        0,
        syn::parse(check_to_add.into()).expect("Could parse added check"),
    );

    item.into_token_stream().into()
}

However, if I run cargo expand I can see that the macro generates the code as expected.

I suspected that compiler might be parsing the code before the macro is completely expanded making it see the "self" keyword where it did not expect one. I've tried creating a function-like helper proc macro and make the "with_rbac" macro insert only a call to that helper macro, which in turn would generate an appropriate line of code, but it results in the same error.

And the expected behaviour would be to convert this:

impl SomeStruct {
    // other functions here

    #[with_rbac(owner)]
    pub fn change_value(&mut self, new_value: u64) {
        // some code here
    }
}

to this:

impl SomeStruct {
    // other functions here unchanged

    pub fn change_value(&mut self, new_value: u64) {
        self.rbac_only_owner();
        // some code here
    }
}

I've only started learning about how to write macros so please excuse unnecessary lines of code in the macro definition itself (although, of course, I welcome pointing them out).

I do understand that rust macros are not simple text substitution and are more powerful. But for this particular case, I exactly that text substitution, which works fine according to cargo expand but not according to the compiler.


Solution

  • I've deeply analyzed the dependencies I've used. Apparently I got the error because my code was then processed by another macro (from external lib) which put my code in the function that did not have a self parameter. So yes - the code in the question is actually correct (or at least it does what it is supposed to).