Have you ever made a macro that defines a macro for defining a macro?

I'm making a derive macro[1] using macro_rules! (#![feature(macro_derive)], tracking issue) and as part of this process I need to extract fields from macro derive "helper" attributes using only declarative macros

So, given a bunch of these attributes:

#[serde(deny_unknown_fields)]
#[zenum(rename_all = "kebab-case")]
#[repr(u8, packed)]

I want to get the value of rename_all somehow.

I accomplished it via fairly basic TT munching:

macro_rules! extract_field {
    // Entry point. $field is the field we're looking for in the list of #[$attr]ibutes
    ($field:ident: $(#[$($attr:tt)*])*) => {
        $(
            // Try to extract from each individual `#[attr]`
            if let Some(val) = $crate::extract_field!(@ $field: $($attr)*) {
                Some(val)
            } else
        )* {
            None
        }
    };
    // We only want to parse this part
    //
    // #[zenum(rename_all = "kebab-case")]
    //         ^^^^^^^^^^^^^^^^^^^^^^^^^
    (@ $field:ident: zenum($($attr:tt)*)) => { $crate::extract_field!(! $field: $($attr)*) };
    // Any other field is totally ignored, e.g. `#[serde(rename_all = "kebab-case")]`
    (@ $field:ident: $($ignore:tt)*) => { None };
    //
    // SUPPORTED FIELDS, AND what we are looking for
    //
    // odd = this value is at the start or in the middle of the input
    // even = this value is at the end of the input
    //
    (! rename_all: rename_all = $value:expr, $($attr:tt)+) => { Some($value) };
    (! rename_all: rename_all = $value:expr $(,)?) => { Some($value) };
    (! disabled: disabled, $($attr:tt)+) => { Some(()) };
    (! disabled: disabled $(,)?) => { Some(()) };
    (! rename: rename = $value:expr, $($attr:tt)+) => { Some($value) };
    (! rename: rename = $value:expr $(,)?) => { Some($value) };
    (! aliases: aliases = $value:expr, $($attr:tt)+) => { Some($value) };
    (! aliases: aliases = $value:expr $(,)?) => { Some($value) };
    //
    // SUPPORTED FIELDS, not what we are looking for
    //
    // odd = this value is at the start or in the middle of the input
    // even = this value is at the end of the input
    //
    (! $field:ident: rename_all = $value:expr, $($attr:tt)+) => { $crate::extract_field!(! $field: $($attr)+) };
    (! $field:ident: rename_all = $value:expr $(,)?) => { None };
    (! $field:ident: disabled, $($attr:tt)+) => { $crate::extract_field!(! $field: $($attr)*) };
    (! $field:ident: disabled $(,)?) => { None };
    (! $field:ident: rename = $value:expr, $($attr:tt)+) => { $crate::extract_field!(! $field: $($attr)+) };
    (! $field:ident: rename = $value:expr $(,)?) => { None };
    (! $field:ident: aliases = $value:expr, $($attr:tt)+) => { $crate::extract_field!(! $field: $($attr)+) };
    (! $field:ident: aliases = $value:expr $(,)?) => { None };
    //
    // CATCHALL: if we go here, it's an error. Unrecognized token
    //
    (! $field:ident: $ignore:ident $($attr:tt)*) => {
        compile_error!(concat!(
            "unexpected token: `",
            stringify!($ignore),
            "`, allowed `#[zenum(/* ... */)]` arguments are:\n",
            "• `rename_all = $_:expr`\n",
            "• `disabled`\n",
            "• `rename = $_:expr`\n"
            "• `aliases = $_:expr`\n"
        ))
    };

The macro takes input like this:

rename:
#[foo]
#[zenum(rename_all = "kebab-case", rename = "hello world")]
#[bar]

It looks for the rename field because that's the first identifier, and it expands to Some if it finds, or None if it doesn't:

Some("hello world")

This is amazing, the fact that this works: I even get stellar compile errors that lists all possible attributes if I got them wrong.

But the problem is this: Any time I want to support more helper attributes, I must change 4+ places. Worse, I'm planning on having a dedicated macro for each of these places:

  • enum variants
  • enum variant fields
  • enums themselves

For this, I'd ideally need 3 macros: extract_field_variant!, extract_field_enum! and extract_field_enum!

This is a great use-case for a macro. But this macro will expand to macro_rules!, making it a sort of "meta-macro"[2]

Here it goes:

#[macro_export]
macro_rules! define_extract_macro {
    (
        // Escaped `$`, same as `$$` which is currently nightly
        $_:tt
        // the `macro_rules!` to generate
        macro_rules! $macro_name:ident;

        match ? in #[$helper_attr_name:ident(?)] {
            $(
                { $value_ident:ident $($value:tt)* } => { $($captured:tt)* }
            )*
        }
    ) => {
        #[doc(hidden)]
        #[macro_export]
        macro_rules! $macro_name {
            // Entry point. \$_ _ field is the field we're looking for in the list of #[$_ _ attr]ibutes
            ($_ field:ident: $_ (#[$_ ($_ attr:tt)*])*) => {
                $_ (
                    // Try to extract from each individual `#[attr]`
                    if let $_ crate::private::Some(val) = $_ crate::$macro_name!(@ $_ field: $_ ($_ attr)*) {
                        $_ crate::private::Some(val)
                    } else
                )* {
                    $_ crate::private::None
                }
            };
            // We only want to parse this part
            //
            // #[zenum(rename_all = "kebab-case")]
            //         ^^^^^^^^^^^^^^^^^^^^^^^^^
            (@ $_ field:ident: $helper_attr_name($_ ($_ attr:tt)*)) => { $_ crate::$macro_name!(! $_ field: $_ ($_ attr)*) };
            // Any other field is totally ignored, e.g. `#[serde(rename_all = "kebab-case")]`
            (@ $_ field:ident: $_ ($_ ignore:tt)*) => { $_ crate::private::None };
            //
            // SUPPORTED FIELDS, AND what we are looking for
            //
            $(
                // this value is at the start or in the middle of the input
                (! $value_ident: $value_ident $($value)*, $_ ($_ attr:tt)+) => { $_ crate::private::Some($($captured)*) };
                // this value is at the end of the input
                (! $value_ident: $value_ident $($value)* $_ (,)?) => { $_ crate::private::Some($($captured)*) };
            )*
            //
            // SUPPORTED FIELDS, not what we are looking for
            //
            $(
                // this value is at the start or in the middle of the input
                (! $_ field:ident: $value_ident $($value)*, $_ ($_ attr:tt)+) => { $_ crate::$macro_name!(! $_ field: $_ ($_ attr)+) };
                // this value is at the end of the input
                (! $_ field:ident: $value_ident $($value)* $_ (,)?) => { $_ crate::private::None };
            )*
            //
            // CATCHALL: if we go here, it's an error. Unrecognized token
            //
            (! $_ field:ident: $_ ignore:ident $_ ($_ attr:tt)*) => {
                compile_error!(concat!(
                    "unexpected token: `",
                    stringify!($_ ignore),
                    "`, allowed `#[", stringify!($macro_name), "(/* ... */)]` arguments are:\n",
                    $(
                        "• ", "`",  stringify!($value_ident $($value)*), "`\n",
                    )*
                ))
            };
        }
    };

Note that $_ is strange, but that's because using just $ will refer to meta-variables from the outer macro. We need to "escape" it, but that's a nightly feature: $$.

In order to actually use dollar token here, we just pass it in from the outer macro and refer to it as $_. Cursed, but it works!

This is how I'll re-define the same macro, with an invocation like this:

define_extract_macro! {$
    macro_rules! extract_field;

    match ? in #[zenum(?)] {
        { rename_all = $value:expr } => { $value }
        { disabled } => { () }
        { rename = $value:expr } => { $value }
        { aliases = $value:expr } => { $value }
    }
}

I can define the same macro as I've always defined.

This is not the first time I wrote a macro_rules! macro that creates a macro_rules! macro, so this made me wonder: Has anyone ever written a macro_rules! macro that creates macro_rules! macro for creating macro_rules!, and why did you even need that?

I imagine the deeper the "stack" of macros is, the less likely that someone actually needed to do this (and didn't do it just for fun)


  1. 10, actually, I want to recreate strum using ONLY declarative macros ↩︎

  2. "higher-order macros" are also an interesting concept, which take a $macro:ident as an argument and call it ↩︎

1 Like

Interesting example. If you go too deep with the expansions, you'll hit the default recursion_limit of 128 :'D

Oh yeah, considering this macro will recurse potentially for every token, if there are 128 tokens of attributes then we will hit problems

do you have any ideas of how this could be implemented without recursion? Not sure if this will actually become a problem, but it might

Alternatively, I could just remove the limit, e.g. #![recursion_limit = 9999999999]. is there any reason against doing that?

Why not just use syn?

Because I believe compile speeds can be massively improved by implementing the macros as macro_rules!. 0 dependencies, and most importantly, each derive macro calls to a macro_rules! instead of a proc macro

I'm pretty sure proc macros are orders of magnitude more expensive to call, but I havent benchmarked it yet

Depends on whether you want to count your compile time in minutes or hours…

1 Like

Your example shows #[serde(...)] attributes, so syn is likely already in your dependency tree and it is getting compiled anyway. The compile time win from avoiding proc-macros might be smaller than you expect, but it's an interesting example anyway.