Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to define a macro with an indefinite amount of arguments inside another macro?

Tags:

rust

This works, but test_macro accepts only one argument:

macro_rules! test_define (
    ($name:ident) => (
        macro_rules! $name (
            ( $x:expr ) => (
                // something
            )
        );
    )
);

test_define!(test_macro);

If I try and do this:

macro_rules! test_define2 (
    ($name:ident) => (
        macro_rules! $name (
            ( $($x:expr),* ) => (
                // something
            )
        );
    )
);

test_define2!(test_macro2);

Compilation fails with:

error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth
 --> src/main.rs:4:16
  |
4 |             ( $($x:expr),* ) => (
  |                ^^^^^^^^^
like image 1000
Rijenkii Avatar asked Jan 29 '23 18:01

Rijenkii


2 Answers

It is a known bug that nested macros don't allow repetitions in binding patterns (issue #35853).

Unfortunately, there is no workaround. The only solution is to change your API to not rely on repetition inside nested macros.

like image 175
kennytm Avatar answered Feb 03 '23 12:02

kennytm


While it is not possible to do this directly, as kennytm described in this answer, you could do this using procedural macros(in case you really need to, otherwise I would not recommend it). While it's probably a lot simpler when using nightly, it is also possible in stable.

Limitations

Please tell me in case I missed anything or in case there might be problems when using custom derive like this in the future.

  • You need to create a struct, which causes one of the following problems
    1. there is either a maximum of 1 macro call per module
    2. the user is required to add another string of characters when calling the outer macro
    3. you create a private module which contains the required struct and could be accessed

So let's just copy the example from here, and just look at this part of the code:

fn impl_hello_world(ast: &syn::DeriveInput) -> quote::Tokens {
    let name = &ast.ident;
    quote! {
        impl HelloWorld for #name {
            fn hello_world() {
                println!("Hello, World! My name is {}", stringify!(#name));
            }
        }
    }
}

inside of the quote! macro, you are not limited to the implementation of the struct. you could change this to

quote! {
    macro_rules! #name {
         ($($expr:expr),*) => {
         // something
         }
    }
}

now you have a macro with the same name as a struct which takes an infinite amount of arguments.

To do this in another macro, the outer macro just has to look like this:

macro_rules! test_define {
    ($name:ident) => {
        #[allow(non_camel_case_types)] // because macro names are lower case
        #[allow(dead_code)] // because this struct should never be used
        #[derive(HelloWorld)]
        struct $name { }
    }
};

And now you can call test_define and then the inner macro:

test_define!(foo);

fn main() {
    foo!()
}

However, there is still one problem: people could accidentally access your struct. So there are ways to circumvent this(each solution is a directly linked the problem with the same number):

  1. name the struct in a way which prevents accidental access by pure chance:

    macro_rules! test_define {
        ($name:ident) => {
            #[allow(dead_code)]
            #[derive(HelloWorld)]
            struct Dgfggsdfgujksdsdsdfsdfsdg { 
                $name: u8,
            }
        }
    };
    

    You have to change your proc macro to use the struct field instead of name inside of quote! and in case the test_define! is called more than once in the same crate you have 2 structs with identical names which causes a compile time error.

  2. to prevent two identical struct names you could also change test_define! to take an additional argument:

    macro_rules! test_define {
        ($name:ident, $rep_guard:ident) => {
            #[allow(non_camel_case_types)]
            #[allow(dead_code]
            #[derive(HelloWorld)]
            struct $rep_guard { 
                $name: u8,
            }
        }
    };
    

    You are using a struct field instead of the name with this method. To use you now have to write test_define!(foo,fvgfdgdfgdfgd), which is really awkward, so I would not recommend this.

  3. This is probably the best option, now you can keep your strange struct name from solution 1 and just put the whole thing in a module. Meaning that no one can accidentally access the created struct and you can have an infinite amount of calls to test_define!.

    macro_rules! test_define {
        ($name:ident) => {
            #[macro_use] // to use the macro in the current module
            mod $name {
                #[allow(dead_code)]
                #[derive(HelloWorld)]
                struct Dgfggsdfgujksdsdsdfsdfsdg { 
                    $name: u8,
                }
            }
        }
    };
    

The compiler should simply remove all of those structs, as they are dead_code (at least when building with the --release flag). You could adapt the quote! by adding #[macro_export] if you need it.

Another advantage is that proc macros use your source code in the same way as a String or as Tokens which can be cast to a String, this means that you could create multiple macros, for example:

test_derive!(foo) => foo!(), foo_with_var!(75)

In case there is anything you don't understand, just ask.

like image 36
lncr Avatar answered Feb 03 '23 14:02

lncr