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

⚓ Rust    📅 2025-12-14    👤 surdeus    👁️ 7      

surdeus

Warning

This post was published 33 days ago. The information described in this article may have changed.

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 ↩︎

4 posts - 2 participants

Read full topic

🏷️ Rust_feed