Step 1 of 4 · Reading · ~4 min
Learn
Macros and Unsafe
Declarative Macros (macro_rules!)
You will write macro_rules! macros that take lists, recurse on their own input, and generate whole items such as structs and impl blocks. You have called println! and vec! since the first lessons; this lesson writes macros of that kind.
Rules, fragments and repetition
A macro is a list of rules, (matcher) => { transcriber };. The compiler tries the rules top to bottom and expands the first matcher that fits. Inside a matcher, $name:fragment captures a piece of syntax, and the fragment says what kind:
| Fragment | Matches |
|---|---|
expr | an expression, kept as one unit |
ident | an identifier (a name) |
ty | a type |
literal | a literal such as 3 or "a" |
vis | an optional pub or pub(crate) |
lifetime | a lifetime such as 'a |
tt | one token tree (a token or a bracketed group); the catch-all |
$( ... ) sep rep repeats: * is zero or more, + one or more, ? zero or one. The transcriber repeats the same variables the same way. An optional trailing comma is written $(,)?:
use std::collections::BTreeMap;
macro_rules! map {
($($k:expr => $v:expr),* $(,)?) => {{
let mut m = BTreeMap::new();
$( m.insert($k, $v); )*
m
}};
}
fn main() {
let m = map!{"b" => 2, "a" => 1,};
println!("{:?}", m);
}
Output: {"a": 1, "b": 2}. Delete the $(,)? and the same call stops compiling: error: unexpected end of macro invocation ... missing tokens in macro arguments. The doubled {{ }} is deliberate: the inner braces make the expansion a block expression.
Recursion and rule order
A macro may call itself on part of its input. Put the base case first, because order decides which rule wins:
macro_rules! max {
($x:expr) => { $x };
($x:expr, $($rest:expr),+) => {{
let a = $x;
let b = max!($($rest),+);
if a > b { a } else { b }
}};
}
// max!(3, 9, 4) expands to max!(3, max!(9, max!(4))) -> 9
The trap is a catch-all rule above a specific one. With ($($t:tt)*) => { "catch-all" }; first, kind!(1 + 2) prints catch-all; with ($x:expr) => { "one expr" }; first it prints one expr. Peeling a token off the front and recursing on the rest is called tt-munching. Helper rules get a private marker such as (@step ...) so callers cannot hit them by accident.
Hygiene and textual scope
Names created inside a macro do not collide with the caller's names:
macro_rules! twice {
($e:expr) => {{ let a = 2; $e * a }};
}
let a = 10;
println!("{}", twice!(a + 1)); // 22
The a in $e is the caller's a (10), the a in the macro is its own, so the result is (10 + 1) * 2 = 22. An expr capture keeps its grouping; a C preprocessor would give 10 + 1 * 2 = 12. Hygiene also hides a macro's let hidden = 5; from the caller; to hand the caller a name, pass it in as an ident.
A macro must be defined above its first use in the file: calling later!() first gives error: cannot find macro 'later' in this scope. To use one across modules and crates, mark it #[macro_export] and write paths inside it as $crate::path.
Generating items
An ident capture lets a macro declare things, and stringify! turns tokens into a string literal:
macro_rules! unit_struct {
($name:ident) => {
struct $name;
impl $name {
fn label() -> &'static str { stringify!($name) }
}
};
}
unit_struct!(Meter); // Meter::label() returns "Meter"
stringify! is also accepted where a pattern is expected, so a generated match can compare against it.
Common mistakes
- A catch-all
ttrule above a specific one: it matches everything, so the specific rule never runs. - Counting commas or a fixed number of items instead of the items: a trailing comma then changes the count by one.
- Forgetting
$(,)?: an invocation with a trailing comma fails to compile. - Using a macro where a generic function would do. Reach for one only when the arity, an identifier or a whole item must be generated. Procedural macros (
#[derive(...)]) are functions over token streams in a separate crate and are not written here.
Your exercise
Write make_enum! so one invocation generates the enum, its ALL slice, name, from_name and COUNT. The starter's main only uses that generated API. Names match exactly, case included, and the second enum is written with a trailing comma.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…