Guide contents

Lesson 79 of 94

Declarative Macros

You've used macros since the first lesson: println!, vec!, format!, assert_eq!, #[derive(Debug)]. A macro is code that writes code at compile time. The ! (or the #[...] attribute) tells you a macro is involved.

Rust's derive macros (#[derive(Debug, Clone, Serialize)]) generate code from attributes at compile time, and macros in general often do at compile time what other approaches do at runtime.

Rust has two kinds of macros:

Kind Defined with Used as Typical use
declarative macro_rules! in normal code name!(...) small syntax helpers, repetitive code, DSLs
procedural a separate proc-macro crate, Rust functions over token streams #[derive(Name)], #[name], name!(...) derives (serde, thiserror), frameworks (tokio, axum)

This lesson covers macro_rules!, which you can write in any file. The next lesson, Procedural Macros, covers derive, attribute, and function-like macros.

When not to write a macro: if a function or a generic does the job, use it. Macros are harder to read, debug, and document. Reach for one when you need something functions can't do: variable numbers of arguments, generating items (functions, structs, impls, tests), or custom syntax.

Part 1: macro_rules!

1. The simplest macro

A macro_rules! definition has one or more rules, each (pattern) => { expansion }. This one has an empty pattern.

Rust
macro_rules! banner {
    () => {
        println!("==========");
        println!("  REPORT  ");
        println!("==========");
    };
}

fn main() {
    banner!();
    println!("all systems nominal");
}
Output
==========
  REPORT
==========
all systems nominal

At compile time, banner!() is replaced by the three println! lines. The macro must be defined above its first use in the file.

2. Taking an expression: $x:expr

Patterns capture input into metavariables like $x, each with a fragment specifier saying what kind of syntax it matches. expr matches any expression.

Rust
macro_rules! square {
    ($x:expr) => {
        $x * $x
    };
}

fn main() {
    println!("{}", square!(7));
    println!("{}", square!(2 + 3)); // 25: `$x` is substituted as a whole expression
    let side = 1.5;
    println!("{}", square!(side));
}
Output
49
25
2.25

Unlike a C preprocessor macro, square!(2 + 3) gives 25, not 2 + 3 * 2 + 3 = 11: an expr fragment behaves like a parenthesized unit.

3. Evaluate arguments once

The expansion pastes the expression wherever $x appears, so square!(next_id()) would call next_id() twice. Bind it to a variable first.

Rust
use std::cell::Cell;

thread_local! {
    static CALLS: Cell<u32> = const { Cell::new(0) };
}

fn next_value() -> i32 {
    CALLS.with(|c| c.set(c.get() + 1));
    4
}

macro_rules! square_twice_evaluated {
    ($x:expr) => {
        $x * $x
    };
}

macro_rules! square {
    ($x:expr) => {{
        let v = $x; // evaluate once
        v * v
    }};
}

fn main() {
    println!("{}", square_twice_evaluated!(next_value()));
    println!("calls so far: {}", CALLS.with(|c| c.get()));
    println!("{}", square!(next_value()));
    println!("calls so far: {}", CALLS.with(|c| c.get()));
}
Output
16
calls so far: 2
16
calls so far: 3

The double braces {{ ... }} make the expansion a block expression, so it can contain statements and still produce a value.

4. Fragment specifiers

Specifier Matches Example input
expr an expression a + 1, f(x), vec![1]
ident an identifier user_id, Point
ty a type u32, Vec<String>, &'a str
pat a pattern Some(x), 1..=5, _
literal a literal 42, "text", 'c', true
block a { ... } block { let x = 1; x }
path a path std::fmt::Display, Self::new
lifetime a lifetime 'a
vis a visibility (possibly empty) pub, pub(crate)
item an item fn f() {}, struct S;
tt a single token tree +, foo, ( ... ), [ ... ]

5. ident: generating a function

With an ident fragment, a macro can define new items with names you choose. Here each call creates a validation function.

Rust
macro_rules! range_validator {
    ($name:ident, $min:expr, $max:expr) => {
        fn $name(value: i64) -> Result<i64, String> {
            if ($min..=$max).contains(&value) {
                Ok(value)
            } else {
                Err(format!("{} must be between {} and {}, got {}", stringify!($name), $min, $max, value))
            }
        }
    };
}

range_validator!(check_age, 0, 150);
range_validator!(check_port, 1, 65535);
range_validator!(check_percent, 0, 100);

fn main() {
    println!("{:?}", check_age(34));
    println!("{:?}", check_port(80800));
    println!("{:?}", check_percent(-5));
}
Output
Ok(34)
Err("check_port must be between 1 and 65535, got 80800")
Err("check_percent must be between 0 and 100, got -5")

stringify!($name) turns the tokens into a string literal at compile time: "check_port".

6. Several rules: overloading by shape

A macro can have several rules; the first one whose pattern matches is used. That gives you overloading by syntax.

Rust
macro_rules! greet {
    () => {
        String::from("Hello!")
    };
    ($name:expr) => {
        format!("Hello, {}!", $name)
    };
    ($name:expr, formal) => {
        format!("Good day, {}.", $name)
    };
}

fn main() {
    println!("{}", greet!());
    println!("{}", greet!("Ana"));
    println!("{}", greet!("Dr. Lee", formal));
}
Output
Hello!
Hello, Ana!
Good day, Dr. Lee.

The third rule matches the literal word formal: patterns can contain fixed tokens, not just metavariables.

7. Repetition: $( ... ),*

$( $x:expr ),* matches zero or more comma-separated expressions. In the expansion, $( ... )* repeats once per match. This is how vec! accepts any number of elements.

Rust
macro_rules! sum {
    ( $( $x:expr ),* ) => {
        0 $( + $x )*
    };
}

macro_rules! strings {
    ( $( $s:expr ),* ) => {
        vec![ $( String::from($s) ),* ]
    };
}

fn main() {
    println!("{}", sum!());
    println!("{}", sum!(1, 2, 3, 4));
    println!("{:?}", strings!("a", "b", "c"));
}
Output
0
10
["a", "b", "c"]

The separators and repetition operators: * (zero or more), + (one or more), ? (zero or one). The separator (, or ; or others) goes before the operator.

8. Allowing a trailing comma

Users will write trailing commas in multi-line calls. $(,)? at the end accepts an optional one.

Rust
macro_rules! csv_row {
    ( $( $field:expr ),+ $(,)? ) => {
        [ $( $field.to_string() ),+ ].join(",")
    };
}

fn main() {
    println!("{}", csv_row!(1, "ana", 34.5));
    println!(
        "{}",
        csv_row!(
            2,
            "ben",
            true, // trailing comma is fine
        )
    );
}
Output
1,ana,34.5
2,ben,true

9. A hashmap! literal

Repetition can capture several fragments per item. $( $k:expr => $v:expr ),* matches key => value pairs, giving you a map literal.

Rust
use std::collections::HashMap;

macro_rules! hashmap {
    ( $( $k:expr => $v:expr ),* $(,)? ) => {{
        let mut map = HashMap::new();
        $( map.insert($k, $v); )*
        map
    }};
}

fn main() {
    let status_text = hashmap! {
        200 => "OK",
        404 => "Not Found",
        500 => "Internal Server Error",
    };
    let mut codes: Vec<_> = status_text.iter().collect();
    codes.sort();
    println!("{codes:?}");
}
Output
[(200, "OK"), (404, "Not Found"), (500, "Internal Server Error")]

10. Recursion: processing one argument at a time

A macro can call itself. A common pattern peels off the first argument and recurses on the rest. Here max! works for any number of arguments.

Rust
macro_rules! max {
    ($x:expr) => {
        $x
    };
    ($x:expr, $( $rest:expr ),+) => {{
        let a = $x;
        let b = max!($( $rest ),+);
        if a > b { a } else { b }
    }};
}

fn main() {
    println!("{}", max!(3));
    println!("{}", max!(3, 9, 2));
    println!("{}", max!(1.5, -2.0, 0.25, 1.75));
}
Output
3
9
1.75

The compiler has a recursion limit (128 by default), so this style is for modest inputs.

11. Counting arguments at compile time

Counting is a classic recursion trick: replace each argument with 1 +. The result is a constant expression, usable for array sizes.

Rust
macro_rules! count {
    () => { 0usize };
    ($head:tt $( $tail:tt )*) => { 1usize + count!($( $tail )*) };
}

macro_rules! const_list {
    ($name:ident: $( $item:literal ),* ) => {
        const $name: [&str; count!($( $item )*)] = [ $( $item ),* ];
    };
}

const_list!(WEEKDAYS: "Mon", "Tue", "Wed", "Thu", "Fri");

fn main() {
    println!("{}", count!(a b c d));
    println!("{} weekdays: {:?}", WEEKDAYS.len(), WEEKDAYS);
}
Output
4
5 weekdays: ["Mon", "Tue", "Wed", "Thu", "Fri"]

12. Hygiene: macro variables don't leak

Variables created inside a macro live in their own scope, so they can't accidentally clash with, or capture, variables at the call site. This is called hygiene.

Rust
macro_rules! double_it {
    ($x:expr) => {{
        let value = $x * 2; // this `value` is private to the macro
        value
    }};
}

fn main() {
    let value = 10;
    let doubled = double_it!(value + 1); // `value` here is the caller's
    println!("value = {value}, doubled = {doubled}");
}
Output
value = 10, doubled = 22

Identifiers passed in by the caller (via $name:ident) do refer to the caller's scope, which is how example 5 could define a function the caller then calls.

13. Generating a newtype with impls

Macros shine at removing boilerplate that generics can't. Here one line per type generates a newtype struct, a constructor, and Display.

Rust
use std::fmt;

macro_rules! id_type {
    ($name:ident, $prefix:literal) => {
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
        pub struct $name(u64);

        impl $name {
            pub fn new(raw: u64) -> Self {
                $name(raw)
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "{}-{:05}", $prefix, self.0)
            }
        }
    };
}

id_type!(UserId, "USR");
id_type!(OrderId, "ORD");

fn ship(order: OrderId, to: UserId) -> String {
    format!("shipping {order} to {to}")
}

fn main() {
    let user = UserId::new(42);
    let order = OrderId::new(7);
    println!("{}", ship(order, user));
    // ship(user, order); // error: mismatched types, the macro made them distinct
}
Output
shipping ORD-00007 to USR-00042

14. Generating an enum with string conversions

An enum whose variants map to and from strings is tedious to keep in sync by hand. The macro lists each variant once.

Rust
macro_rules! string_enum {
    ($name:ident { $( $variant:ident => $text:literal ),+ $(,)? }) => {
        #[derive(Debug, Clone, Copy, PartialEq)]
        enum $name {
            $( $variant ),+
        }

        impl $name {
            const ALL: &'static [$name] = &[ $( $name::$variant ),+ ];

            fn as_str(self) -> &'static str {
                match self {
                    $( $name::$variant => $text ),+
                }
            }

            fn parse(s: &str) -> Option<$name> {
                match s {
                    $( $text => Some($name::$variant), )+
                    _ => None,
                }
            }
        }
    };
}

string_enum!(Status {
    Active => "active",
    Suspended => "suspended",
    Deleted => "deleted",
});

fn main() {
    println!("{:?}", Status::ALL);
    println!("{}", Status::Suspended.as_str());
    println!("{:?} {:?}", Status::parse("deleted"), Status::parse("archived"));
}
Output
[Active, Suspended, Deleted]
suspended
Some(Deleted) None

15. $vis:vis and attributes pass-through

To let callers choose visibility and add their own attributes (#[derive], doc comments), capture them: $vis:vis and $( #[$meta:meta] )*.

Rust
mod model {
    macro_rules! plain_struct {
        ( $( #[$meta:meta] )* $vis:vis struct $name:ident { $( $field:ident : $ty:ty ),* $(,)? } ) => {
            $( #[$meta] )*
            $vis struct $name {
                $( pub $field: $ty ),*
            }

            impl $name {
                pub fn field_names() -> &'static [&'static str] {
                    &[ $( stringify!($field) ),* ]
                }
            }
        };
    }

    plain_struct! {
        /// A product in the catalog.
        #[derive(Debug, Clone)]
        pub struct Product {
            sku: String,
            price_cents: u32,
            in_stock: bool,
        }
    }
}

fn main() {
    let p = model::Product { sku: String::from("BK-1"), price_cents: 1500, in_stock: true };
    println!("{p:?}");
    println!("{:?}", model::Product::field_names());
}
Output
Product { sku: "BK-1", price_cents: 1500, in_stock: true }
["sku", "price_cents", "in_stock"]

16. Generating tests from a table

The Testing lesson mentioned this: a macro that turns a table of cases into one named #[test] per case, so each shows up separately in the test report.

Rustcargo test
pub fn is_valid_isbn10(s: &str) -> bool {
    let digits: Vec<u32> = s.chars().filter(|c| *c != '-').map(|c| if c == 'X' { 10 } else { c.to_digit(10).unwrap_or(99) }).collect();
    digits.len() == 10 && digits.iter().all(|&d| d <= 10) && digits.iter().enumerate().map(|(i, d)| (10 - i as u32) * d).sum::<u32>() % 11 == 0
}

#[cfg(test)]
mod tests {
    use super::*;

    macro_rules! isbn_tests {
        ( $( $name:ident: $input:literal => $expected:literal, )* ) => {
            $(
                #[test]
                fn $name() {
                    assert_eq!(is_valid_isbn10($input), $expected, "input {:?}", $input);
                }
            )*
        };
    }

    isbn_tests! {
        valid_plain: "0306406152" => true,
        valid_with_dashes: "0-306-40615-2" => true,
        valid_with_x: "0-8044-2957-X" => true,
        wrong_checksum: "0306406153" => false,
        too_short: "030640615" => false,
        letters: "03064A6152" => false,
    }
}
Output
running 6 tests
test tests::letters ... ok
test tests::too_short ... ok
test tests::valid_plain ... ok
test tests::valid_with_dashes ... ok
test tests::valid_with_x ... ok
test tests::wrong_checksum ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

17. compile_error! for helpful messages

A catch-all rule that expands to compile_error!("...") turns invalid input into a clear error instead of a confusing "no rules expected this token".

Rust✗ does not compile
macro_rules! http_status {
    (ok) => { 200 };
    (not_found) => { 404 };
    ($other:ident) => {
        compile_error!(concat!("unknown status `", stringify!($other), "`; expected `ok` or `not_found`"))
    };
}

fn main() {
    let a = http_status!(ok);
    let b = http_status!(teapot);
    println!("{a} {b}");
}
Output
error: unknown status `teapot`; expected `ok` or `not_found`
  --> src/main.rs:5:9
   |
 5 |         compile_error!(concat!("unknown status `", stringify!($other), "`; expected `ok` or `not_found`"))
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
11 |     let b = http_status!(teapot);
   |             -------------------- in this macro invocation
   |

18. A small DSL with token trees

tt (token tree) fragments accept almost anything, which lets macros define mini-languages. This one builds routing rules from a readable syntax.

Rust
#[derive(Debug)]
struct Route {
    method: &'static str,
    path: &'static str,
    handler: fn(&str) -> String,
}

macro_rules! routes {
    ( $( $method:ident $path:literal => $handler:expr ; )* ) => {
        vec![ $( Route { method: stringify!($method), path: $path, handler: $handler } ),* ]
    };
}

fn list_users(_: &str) -> String {
    String::from("[ana, ben]")
}

fn main() {
    let table = routes! {
        GET "/users" => list_users;
        POST "/users" => |body| format!("created {body}");
        DELETE "/users" => |_| String::from("forbidden");
    };

    for (method, path, body) in [("GET", "/users", ""), ("POST", "/users", "cy"), ("PUT", "/users", "")] {
        match table.iter().find(|r| r.method == method && r.path == path) {
            Some(r) => println!("{method} {path} -> {}", (r.handler)(body)),
            None => println!("{method} {path} -> 405 Method Not Allowed"),
        }
    }
}
Output
GET /users -> [ana, ben]
POST /users -> created cy
PUT /users -> 405 Method Not Allowed

19. Exporting macros: #[macro_export] and $crate

Inside a crate, a macro_rules! macro is visible after its definition. To make it usable from other crates, add #[macro_export]; it then lives at the crate root (mycrate::my_macro!). Paths inside the expansion should start with $crate::, which always refers to the macro's own crate, whatever the caller has imported.

Rust
mod metrics {
    pub fn record(name: &str, value: f64) -> String {
        format!("metric {name}={value}")
    }

    #[macro_export]
    macro_rules! metric {
        ($name:literal, $value:expr) => {
            // `$crate` makes this path work from any module or crate.
            $crate::metrics::record($name, $value as f64)
        };
    }
}

mod handlers {
    pub fn checkout() -> String {
        crate::metric!("checkout.latency_ms", 42) // exported macros live at the crate root
    }
}

fn main() {
    println!("{}", handlers::checkout());
    println!("{}", metric!("queue.depth", 7));
}
Output
metric checkout.latency_ms=42
metric queue.depth=7

20. Seeing the expansion

When a macro misbehaves, look at what it expands to. cargo expand (a Cargo plugin) prints your code with every macro expanded, including derives.

Terminal
cargo install cargo-expand
cargo expand            # the whole crate
cargo expand handlers   # one module

Inside a macro, stringify! is useful for printing what was matched while you debug:

Rust
macro_rules! show {
    ($e:expr) => {
        println!("{} = {:?}", stringify!($e), $e)
    };
}

fn main() {
    let prices = [120, 80, 45];
    show!(prices.iter().sum::<i32>());
    show!(prices.iter().max());
    show!(prices.len() > 2);
}
Output
prices.iter().sum::<i32>() = 245
prices.iter().max() = Some(120)
prices.len() > 2 = true

That's essentially the standard dbg! macro, which prints to stderr with the file and line.

Part 2: Built-in macros worth knowing

21. Assertions and "not yet" markers

Macro Does
assert!(cond, "msg {}", x) panics if false (always)
debug_assert!(...) same, but only in debug builds
assert_eq! / assert_ne! compares and prints both sides
matches!(value, pattern) pattern test that returns bool
todo!() panics with "not yet implemented"; type-checks as any type
unimplemented!() panics with "not implemented"
unreachable!() panics with "entered unreachable code"
Rust
#[derive(Debug)]
enum Shape {
    Circle(f64),
    Square(f64),
    Polygon(Vec<(f64, f64)>),
}

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Square(side) => side * side,
        Shape::Polygon(_) => todo!("polygon area"), // compiles now, panics if reached
    }
}

fn main() {
    let shapes = [Shape::Circle(1.0), Shape::Square(2.0)];
    let round = shapes.iter().filter(|s| matches!(s, Shape::Circle(r) if *r > 0.5)).count();
    println!("round shapes: {round}");
    println!("{:.2}", area(&shapes[0]));
    debug_assert!(area(&shapes[1]) == 4.0, "square area is wrong");
    let _ = Shape::Polygon(vec![]);
}
Output
round shapes: 1
3.14

22. Compile-time information: file!, line!, concat!, env!, include_str!

These expand to constants known during compilation.

Rust
macro_rules! here {
    () => {
        concat!(file!(), ":", line!())
    };
}

const VERSION: &str = concat!("v", 2, ".", 4); // concat! joins literals at compile time
const README: &str = include_str!("main.rs"); // embeds a file's text in the binary

fn main() {
    println!("logged at {}", here!());
    println!("version {VERSION}");
    println!("module {}", module_path!());
    println!("this source file has {} lines", README.lines().count());
    println!("built for {}", if cfg!(target_pointer_width = "64") { "64-bit" } else { "32-bit" });
}
Output
logged at src/main.rs:11
version v2.4
module main
this source file has 16 lines
built for 64-bit

env!("NAME") reads an environment variable at compile time (Cargo sets CARGO_PKG_VERSION and friends), and include_bytes! embeds binary files. Rust bakes them into the executable.

23. format_args! and writing your own println!-style macro

All formatting macros are built on format_args!, which checks the format string at compile time. Forwarding $($arg:tt)* to it gives your own macros the full {} syntax, with compile-time checking.

Rust
use std::fmt::Write;

macro_rules! log_info {
    ($($arg:tt)*) => {
        println!("[INFO {}] {}", module_path!(), format_args!($($arg)*))
    };
}

macro_rules! appendln {
    ($buf:expr, $($arg:tt)*) => {
        writeln!($buf, $($arg)*).expect("writing to a String cannot fail")
    };
}

fn main() {
    let user = "ana";
    log_info!("user {user} logged in from {}", "10.0.0.7");
    log_info!("{} items in cart, total {:.2}", 3, 59.9);

    let mut report = String::new();
    appendln!(report, "{:<6}|{:>5}", "name", "qty");
    appendln!(report, "{:<6}|{:>5}", "pen", 12);
    print!("{report}");
}
Output
[INFO main] user ana logged in from 10.0.0.7
[INFO main] 3 items in cart, total 59.90
name  |  qty
pen   |   12

A typo like log_info!("{} {}", 1) is a compile error, not a runtime error.

Part 3: Patterns from real codebases

24. Internal rules: private helpers with @

A macro often needs a helper, such as the counting trick from example 11. Instead of defining a second macro, real code puts the helper inside the same macro as a rule that starts with a marker like @count. Callers never type @count, so these internal rules act like private methods. Put them first, so the public rule doesn't try to match them.

Rust
macro_rules! status_enum {
    // Internal rules.
    (@count) => { 0 };
    (@count $head:ident $($tail:ident)*) => { 1 + status_enum!(@count $($tail)*) };

    // The public rule.
    ($name:ident { $($variant:ident),* $(,)? }) => {
        #[derive(Debug, Clone, Copy, PartialEq)]
        enum $name {
            $($variant),*
        }

        impl $name {
            const COUNT: usize = status_enum!(@count $($variant)*);
            const ALL: [$name; status_enum!(@count $($variant)*)] = [$($name::$variant),*];
        }
    };
}

status_enum!(OrderStatus { Pending, Paid, Shipped, Delivered });

fn main() {
    println!("{} statuses", OrderStatus::COUNT);
    for status in OrderStatus::ALL {
        println!("{status:?}");
    }
    println!("{}", OrderStatus::ALL.contains(&OrderStatus::Paid));
}
Output
4 statuses
Pending
Paid
Shipped
Delivered
true

25. TT munchers: consuming input one piece at a time

When the input has several shapes mixed together, a single repetition can't match it. A TT muncher matches the first piece, handles it, and calls itself on the remaining tokens ($($rest:tt)*). An accumulator in brackets carries the output built so far. Here a query string accepts both key = value pairs and bare flags:

Rust
macro_rules! query {
    // Nothing left: join the accumulated parts.
    (@acc [$($parts:expr),*]) => {
        vec![$($parts),*].join("&")
    };
    // `key = value, ...`
    (@acc [$($parts:expr),*] $key:ident = $value:expr $(, $($rest:tt)*)?) => {
        query!(@acc [$($parts,)* format!("{}={}", stringify!($key), $value)] $($($rest)*)?)
    };
    // `flag, ...`
    (@acc [$($parts:expr),*] $flag:ident $(, $($rest:tt)*)?) => {
        query!(@acc [$($parts,)* stringify!($flag).to_string()] $($($rest)*)?)
    };
    // Entry point: start with an empty accumulator.
    ($($input:tt)+) => {
        query!(@acc [] $($input)+)
    };
}

fn main() {
    let page = 2;
    println!("{}", query!(q = "rust", page = page, verbose, sort = "date"));
    println!("{}", query!(dry_run));
    println!("{}", query!(limit = 10 * page, archived,));
}
Output
q=rust&page=2&verbose&sort=date
dry_run
limit=20&archived

Every step re-parses the remaining input, so munchers get slow on very long inputs. They are common in crates that accept rich syntax, such as serde_json::json!.

26. Implementing a trait for many types

Crates often need the same impl for a list of types. The standard library does this for its number types with private macros, which you'll see throughout its source.

Rust
trait Describe {
    fn describe(&self) -> String;
}

macro_rules! impl_describe {
    ($($t:ty => $kind:literal),* $(,)?) => {
        $(
            impl Describe for $t {
                fn describe(&self) -> String {
                    format!("{} {} (size {})", $kind, self, std::mem::size_of::<$t>())
                }
            }
        )*
    };
}

impl_describe! {
    u8 => "unsigned",
    i32 => "signed",
    u64 => "unsigned",
    f64 => "float",
}

fn main() {
    let values: [&dyn Describe; 4] = [&255u8, &-40i32, &18_000_000_000u64, &2.5f64];
    for value in values {
        println!("{}", value.describe());
    }
}
Output
unsigned 255 (size 1)
signed -40 (size 4)
unsigned 18000000000 (size 8)
float 2.5 (size 8)

27. Where a macro is visible

macro_rules! macros don't follow the normal module rules. By default they are textually scoped: usable after the definition, in the same module and in child modules written after it. There are two ways to use a macro elsewhere in the crate:

  • #[macro_use] on a mod keeps the module's macros visible after the mod item.
  • pub(crate) use name; after the definition turns the macro into a normal item that you import by path.
Rust
macro_rules! greet {
    ($name:expr) => {
        format!("hello, {}", $name)
    };
}

mod shop {
    // A child module written after the definition can use it.
    pub fn welcome() -> String {
        greet!("shop")
    }
}

#[macro_use]
mod text {
    macro_rules! shout {
        ($s:expr) => {
            $s.to_uppercase()
        };
    }
}

mod math {
    macro_rules! double {
        ($x:expr) => {
            $x * 2
        };
    }
    pub(crate) use double;
}

fn main() {
    println!("{}", shop::welcome());
    println!("{}", shout!("sale"));         // visible thanks to #[macro_use]
    println!("{}", math::double!(21));      // imported by path
}
Output
hello, shop
SALE
42

Newer code prefers pub(crate) use, because a reader can follow the path to the definition. #[macro_export] (example 19) is for macros that other crates use.

28. Realistic: an ensure! macro for validation

anyhow provides ensure!(condition, "message"), which returns an error when the condition is false. Writing our own shows how macros can use return to exit the calling function, something a helper function can't do.

Rust
macro_rules! ensure {
    ($cond:expr, $($fmt:tt)+) => {
        if !$cond {
            return Err(format!($($fmt)+));
        }
    };
}

#[derive(Debug)]
struct Transfer {
    from: String,
    to: String,
    cents: i64,
}

fn validate(t: &Transfer) -> Result<(), String> {
    ensure!(t.cents > 0, "amount must be positive, got {}", t.cents);
    ensure!(t.from != t.to, "cannot transfer from {} to itself", t.from);
    ensure!(t.cents <= 1_000_000, "amount {} exceeds the daily limit", t.cents);
    Ok(())
}

fn main() {
    let cases = [
        Transfer { from: "ana".into(), to: "ben".into(), cents: 5_000 },
        Transfer { from: "ana".into(), to: "ana".into(), cents: 100 },
        Transfer { from: "ana".into(), to: "ben".into(), cents: -3 },
        Transfer { from: "ana".into(), to: "ben".into(), cents: 5_000_000 },
    ];
    for t in &cases {
        println!("{:?}", validate(t));
    }
}
Output
Ok(())
Err("cannot transfer from ana to itself")
Err("amount must be positive, got -3")
Err("amount 5000000 exceeds the daily limit")

Summary

  • Macros generate code at compile time. Prefer functions and generics; use macros for variable arguments, generating items, or custom syntax.
  • macro_rules! matches input against patterns with fragment specifiers (expr, ident, ty, tt, ...), supports multiple rules, repetition ($( ... ),*), and recursion.
  • Bind expr arguments to a local to evaluate them once. Macro-internal variables are hygienic.
  • Macros can generate functions, structs, enums, impls, and tests; $vis:vis and #[$meta:meta] pass visibility and attributes through; compile_error! gives clear errors.
  • #[macro_export] publishes a macro at the crate root; use $crate:: for paths in the expansion. cargo expand shows what macros produce.
  • Built-ins such as matches!, todo!, concat!, include_str!, and format_args! cover common compile-time needs.
  • Internal @rules keep helpers private to one macro, a TT muncher consumes its input a piece at a time, and one macro can implement a trait for a whole list of types.
  • A macro is visible textually after its definition; #[macro_use] extends that past a module, and pub(crate) use name; lets you import it by path.

Next: Procedural Macros: derive, attribute, and function-like macros written with syn and quote.