Have you ever made a macro that defines a macro for defining a macro?
⚓ Rust 📅 2025-12-14 👤 surdeus 👁️ 7I'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)
10, actually, I want to recreate
strumusing ONLY declarative macros ↩︎"higher-order macros" are also an interesting concept, which take a
$macro:identas an argument and call it ↩︎
4 posts - 2 participants
🏷️ Rust_feed