RustDeclarative Macros

Declarative Macros in Rust

Declarative macros — written with macro_rules! — are the most common kind of macro you will encounter and write in Rust. They work by matching a pattern against the syntax of the code that invokes them, and replacing the invocation with an expanded body. Think of them as a very powerful find-and-replace that operates on code structure rather than raw text.

You have already used declarative macros constantly: vec!, println!, and assert_eq! are all implemented with macro_rules!.

Basic Syntax

A macro_rules! definition looks like a match expression, but the arms match syntax patterns rather than runtime values.

RUST
macro_rules! say_hello {
    () => {
        println!("Hello from a macro!");
    };
}

fn main() {
    say_hello!();
}
Hello from a macro!

The structure is:

  • macro_rules! name — declares a macro named name
  • ( pattern ) => { expansion } — one or more match arms
  • Patterns use fragment specifiers to capture pieces of the caller's code
Fragment Specifiers

Fragment specifiers tell the macro what kind of syntax to accept and bind to a metavariable. The metavariable is prefixed with $ and used in the expansion body.

Specifier

Matches

Example input

$x:expr

Any expression

1 + 2, foo(), "hello"

$x:ident

An identifier (variable or function name)

my_var, println

$x:ty

A type

i32, Vec<String>, Option<u8>

$x:literal

A literal value

42, "hello", true

$x:stmt

A single statement

let x = 5;

$x:pat

A pattern (as used in match)

Some(x), (a, b), _

$x:block

A block expression { ... }

{ let x = 1; x + 1 }

$x:tt

A single token tree — the catch-all

Any single token or bracketed group

$x:meta

A meta item (attribute content)

derive(Debug), cfg(test)

$x:lifetime

A lifetime

'a, 'static

RUST
macro_rules! print_type_and_value {
    ($name:ident, $value:expr) => {
        println!("{} = {:?}", stringify!($name), $value);
    };
}

fn main() {
    let temperature = 36.6_f64;
    let active = true;

    print_type_and_value!(temperature, temperature);
    print_type_and_value!(active, active);
    print_type_and_value!(computed, 2 + 2 * 10);
}
temperature = 36.6
active = true
computed = 22
Repetition Syntax

The most powerful feature of macro_rules! is repetition: accepting zero or more (or one or more) repeated pieces of syntax and generating repeated output.

The syntax is $( ... ),* for zero-or-more and $( ... ),+ for one-or-more, where the comma is the separator (which can be any token, or omitted entirely).

RUST
macro_rules! print_all {
    // $( $x:expr ),* matches zero or more comma-separated expressions
    ( $( $x:expr ),* ) => {
        $(
            println!("{:?}", $x);
        )*
    };
}

fn main() {
    print_all!(1, "hello", true, 3.14);
}
1
"hello"
true
3.14
Tip
The repetition in the pattern \`$( $x:expr ),*\` and in the body \`$( ... )*\` must correspond. Every metavariable captured inside a repetition must be used inside a repetition in the body, and vice versa.
Building vec! From Scratch

The standard library's vec! macro is one of the best examples of declarative macros. Here is a simplified version that shows exactly how it works:

RUST
macro_rules! my_vec {
    // Match zero or more comma-separated expressions
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            // Repeat the push for every matched expression
            $( temp_vec.push($x); )*
            temp_vec
        }
    };

    // Also accept a trailing comma: my_vec![1, 2, 3,]
    ( $( $x:expr ),+ , ) => {
        my_vec![ $( $x ),* ]
    };
}

fn main() {
    let v1 = my_vec![1, 2, 3];
    let v2 = my_vec!["a", "b", "c",]; // trailing comma works too
    let v3: Vec<i32> = my_vec![];     // empty vec

    println!("{:?}", v1);
    println!("{:?}", v2);
    println!("{:?}", v3);
}
[1, 2, 3]
["a", "b", "c"]
[]
Note
The outer braces in the expansion body { ... } create a block expression that evaluates to the Vec. This is a common pattern when a macro needs to create local variables without leaking them into the caller's scope.
Multiple Match Arms

A macro can have multiple arms, tried in order from top to bottom — just like match. This lets a single macro behave differently depending on how it is called.

RUST
macro_rules! greet {
    // No arguments — use a default greeting
    () => {
        println!("Hello, stranger!");
    };

    // One identifier — greet by name
    ($name:ident) => {
        println!("Hello, {}!", stringify!($name));
    };

    // Name + title (two arguments separated by a comma)
    ($title:literal, $name:ident) => {
        println!("Hello, {} {}!", $title, stringify!($name));
    };
}

fn main() {
    greet!();
    greet!(Alice);
    greet!("Dr.", Smith);
}
Hello, stranger!
Hello, Alice!
Hello, Dr. Smith!
A Practical Logging Macro

Here is a realistic example: a macro that prefixes log messages with a severity level and the current file/line information, without needing to pass those manually.

RUST
macro_rules! log {
    (INFO, $( $arg:tt )* ) => {
        println!("[INFO  {}:{}] {}", file!(), line!(), format!( $( $arg )* ));
    };
    (WARN, $( $arg:tt )* ) => {
        println!("[WARN  {}:{}] {}", file!(), line!(), format!( $( $arg )* ));
    };
    (ERROR, $( $arg:tt )* ) => {
        eprintln!("[ERROR {}:{}] {}", file!(), line!(), format!( $( $arg )* ));
    };
}

fn connect(host: &str, port: u16) -> bool {
    log!(INFO, "Connecting to {}:{}", host, port);

    if port == 0 {
        log!(ERROR, "Invalid port: {}", port);
        return false;
    }

    log!(WARN, "Connection to {} is insecure", host);
    true
}

fn main() {
    connect("example.com", 80);
    connect("example.com", 0);
}
[INFO  src/main.rs:12] Connecting to example.com:80
[WARN  src/main.rs:18] Connection to example.com is insecure
[INFO  src/main.rs:12] Connecting to example.com:0
[ERROR src/main.rs:15] Invalid port: 0
Tip
Using $( $arg:tt )* (a repetition of token trees) is the standard way to forward format-string arguments to another macro like format!or println!. It accepts any sequence of tokens.
Exporting Macros

By default, a macro_rules! macro is only visible in the module where it is defined. To make it available to other modules and crates, use the #[macro_export] attribute.

RUST
// In src/lib.rs (or any module)

#[macro_export]
macro_rules! hashmap {
    ( $( $key:expr => $val:expr ),* ) => {{
        let mut map = std::collections::HashMap::new();
        $( map.insert($key, $val); )*
        map
    }};
}

// Users of your crate can now write:
// use my_crate::hashmap;
// let m = hashmap!{ "a" => 1, "b" => 2 };

With #[macro_export], the macro is placed at the crate root and becomes available with use my_crate::macro_name; — or even #[macro_use] on the extern crate declaration in older code.

Note
#[macro_export] moves the macro to the crate root regardless of which module it is defined in. If you want to keep it organised, define it in a submodule but document that it is accessible from the crate root.
A HashMap Creation Macro in Action

RUST
use std::collections::HashMap;

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

fn main() {
    let scores: HashMap<&str, u32> = hashmap! {
        "Alice" => 95,
        "Bob"   => 87,
        "Carol" => 92
    };

    for (name, score) in &scores {
        println!("{}: {}", name, score);
    }
}
Declarative Macros vs Functions

Knowing when to use a declarative macro versus a plain function is a judgement call. Here is a decision guide.

Situation

Prefer

Fixed, known number of arguments

Function

Variable number of arguments of mixed types

Declarative macro

All arguments have the same type

Function with a slice or iterator parameter

Need to generate repetitive struct/impl blocks

Declarative macro (or proc macro)

Embedding compile-time file/env data

Built-in macro (include_str!, env!)

Complex AST transformation or custom derive

Procedural macro

Simple, debuggable, reusable logic

Function — always the default choice

Limitations of Declarative Macros
  • No type information — patterns match syntax tokens, not types. You cannot write a macro that behaves differently based on whether an argument is i32 or String.

  • Harder to debug — errors in expanded macro code produce messages that point into the generated code, not the macro call, which can be disorienting.

  • No IDE support for internals — autocompletion and jump-to-definition do not work inside macro_rules! pattern arms in most editors.

  • Hygiene edge cases — while Rust macros are hygienic by default, some advanced patterns (like intentionally introducing names into the caller scope) require workarounds.

  • Pattern ambiguity limits — certain fragment specifiers cannot be followed by certain tokens due to the macro parser needing to look ahead unambiguously.

Success
Declarative macros are one of the most powerful tools in your Rust toolbox. They let you eliminate repetition, build expressive DSLs, and generate code that would be tedious or impossible to write by hand — all at compile time with no runtime overhead whatsoever.