Skip to content

Rust Macros Explained — Declarative & Procedural Macro Development

DodaTech Updated 2026-06-23 9 min read

In this tutorial, you'll learn about Rust Macros Explained. We cover key concepts, practical examples, and best practices.

Master Rust macros: declarative macros with macro_rules!, pattern matching and repetition, procedural macros for derive, attribute, and function-like macros, and debugging techniques.

What You'll Learn

In this tutorial, you'll master Rust macros — writing declarative macros with macro_rules! using pattern matching and repetition operators, building procedural macros for derive, attribute, and function-like use cases, debugging macro expansion, and knowing when a macro is the right tool vs a regular function.

Why It Matters

Macros are Rust's metaprogramming facility. They let you write code that writes code — eliminating boilerplate, creating embedded DSLs, and implementing traits automatically. macro_rules! covers 90% of use cases without external dependencies. Procedural macros handle the remaining 10% where you need access to the token stream for derive, attribute, or custom syntax.

Real-World Use

The serde crate uses derive macros for Serialize and Deserialize. Tokio's #[tokio::main] attribute macro sets up the async runtime. The thiserror crate uses a derive macro to generate Display and Error implementations. Durga Antivirus Pro uses a custom #[scan_handler] attribute macro to register scan event callbacks without boilerplate.

flowchart TD
    subgraph "Macro Types"
        A[macro_rules!] --> B[Declarative]
        A --> C[Procedural]
    end
    subgraph "Declarative"
        D[match patterns] --> E[capture fragments]
        E --> F[expand to code]
    end
    subgraph "Procedural"
        G[TokenStream in] --> H[TokenStream out]
        H --> I[Custom derive, attribute, function-like]
    end
â„šī¸ Info

Prerequisites: Macros basics and Traits & Generics. Understanding Rust pattern matching helps.

Declarative Macros: Pattern Matching and Expansion

macro_rules! uses pattern matching on the Rust token tree. Each arm has a pattern and a transcriber:

macro_rules! vec_of {
    ($($x:expr),* $(,)?) => {
        {
            let mut v = Vec::new();
            $(
                v.push($x);
            )*
            v
        }
    };
}

macro_rules! hash_map {
    ($($key:expr => $value:expr),* $(,)?) => {
        {
            let mut m = std::collections::HashMap::new();
            $(
                m.insert($key, $value);
            )*
            m
        }
    };
}

macro_rules! measure {
    ($label:expr, $code:expr) => {
        {
            let start = std::time::Instant::now();
            let result = { $code };
            println!("{}: {:?}", $label, start.elapsed());
            result
        }
    };
}

fn main() {
    let v = vec_of![1, 2, 3, 4, 5];
    println!("vec_of: {:?}", v);
    
    let m = hash_map!["a" => 1, "b" => 2, "c" => 3];
    println!("hash_map: {:?}", m);
    
    let sum = measure!("sum loop", {
        let mut s = 0u64;
        for i in 0..100_000 {
            s += i;
        }
        s
    });
    println!("Sum: {}", sum);
}

Expected output:

vec_of: [1, 2, 3, 4, 5]
hash_map: {"a": 1, "b": 2, "c": 3}
sum loop: ... (duration depends on hardware)
Sum: 4999950000

Repetition Operators and Recursion

Macros support * (zero or more), + (one or more), ? (zero or one), and can be recursive:

macro_rules! json_value {
    (null) => {
        "null"
    };
    (true) => {
        "true"
    };
    (false) => {
        "false"
    };
    ([$($elem:tt), "* $(",)?]) => {
        concat!("[", $(&json_value!($elem)), "* "",", "]",)
    };
    ({$($key:tt : $value:tt),* $(,)?}) => {
        concat!("{", $("", $key, ":", json_value!($value)),* "}", )
    };
    ($e:expr) => {
        stringify!($e)
    };
}

macro_rules! build_struct {
    ($name:ident { $($field:ident: $ty:ty),* $(,)? }) => {
        struct $name {
            $(
                pub $field: $ty,
            )*
        }
        
        impl $name {
            fn new($($field: $ty),*) -> Self {
                Self { $($field),* }
            }
        }
    };
}

build_struct!(User {
    name: String,
    email: String,
    age: u32,
});

fn main() {
    let user = User::new("Alice".into(), "alice@example.com".into(), 30);
    println!("User created: {} ({}), age {}", user.name, user.email, user.age);
    
    // Demonstrate recursive macro
    let nested = json_value!({"name": "test", "values": [1, 2, 3]});
    println!("JSON: {}", nested);
}

Expected output:

User created: Alice (alice@example.com), age 30
JSON: {name:testvalues:[1,2,3]}
âš ī¸ Warning

Macro hygiene: Declarative macros in Rust are hygienic — names introduced in the macro expansion do not conflict with names in the calling scope. Use $crate:: for paths to avoid resolution issues.

Procedural Macros: Derive and Attribute

Procedural macros operate on token streams. A derive macro example using syn and quote:

// This would be in a separate crate (proc_macro crate)
// For demonstration, showing the concept

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(IntoString)]
pub fn into_string_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;
    
    let expanded = quote! {
        impl std::convert::From<#name> for String {
            fn from(val: #name) -> String {
                format!("{:?}", val)
            }
        }
    };
    
    expanded.into()
}

// Since proc macros require a separate crate, here's a
// simulated version with macro_rules!
macro_rules! impl_display {
    ($type:ty, $fmt:expr) => {
        impl std::fmt::Display for $type {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, $fmt, self)
            }
        }
    };
}

#[derive(Debug)]
struct ScanResult {
    path: String,
    threats: u32,
}

impl_display!(ScanResult, "Scanned {}: {} threats found", path = self.path, threats = self.threats);

// Custom implementation matching the intended expansion
impl std::fmt::Display for ScanResult {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Scanned {}: {} threats found", self.path, self.threats)
    }
}

fn main() {
    let result = ScanResult {
        path: "/etc/passwd".into(),
        threats: 0,
    };
    println!("{}", result);
    
    // Demonstrate string conversion via our macro's output
    let s = format!("{:?}", result);
    println!("Debug: {}", s);
}

Expected output:

Scanned /etc/passwd: 0 threats found
Debug: ScanResult { path: "/etc/passwd", threats: 0 }

Debugging Macro Expansion

Techniques for understanding what your macro produces:

macro_rules! debug_expand {
    ($name:ident, $body:expr) => {
        #[allow(unused)]
        mod $name {
            fn check() {
                let _ = $body;
            }
        }
    };
}

macro_rules! trace_expand {
    ($macro:ident!($($args:tt)*)) => {
        {
            let expanded = stringify!($macro!($($args)*));
            eprintln!("Expanded: {}", expanded);
            $macro!($($args)*)
        }
    };
}

// Use cargo expand to see macro output
// Run: cargo expand --test macro_tests

macro_rules! compute {
    ($a:expr, $op:tt, $b:expr) => {
        match $op {
            "+" => $a + $b,
            "-" => $a - $b,
            "*" => $a * $b,
            "/" => $a / $b,
            _ => panic!("Unknown op: {}", $op),
        }
    };
}

fn main() {
    // Use trace_expand to debug
    let result = compute!(10, "+", 20);
    println!("10 + 20 = {}", result);
    
    // For real debugging: cargo expand or rustc -Z unstable-options --pretty expanded
    println!("Macro debugging tips:");
    println!("1. cargo expand");
    println!("2. rustc -Z unstable-options --pretty expanded");
    println!("3. log_syntax!()");
    println!("4. trace_macros!(true)");
}

Expected output:

10 + 20 = 30
Macro debugging tips:
1. cargo expand
2. rustc -Z unstable-options --pretty expanded
3. log_syntax!()
4. trace_macros!(true)

Common Errors

1. Macro repetition mismatches

macro_rules! bad {
    ($x:expr, $y:ident) => { /* ... */ };
}
bad!(1, 2, 3); // ERROR: repetition mismatch

2. Metavariable without repetition

macro_rules! bad {
    ($($x:expr),*) => { $x } // ERROR: $x used outside repetition
}

3. Procedural macro in wrong crate type

// proc-macro crate must have: [lib] proc-macro = true

4. macro_rules! macro name conflicts

macro_rules! vec { ... } // May conflict with std::vec!

5. Missing $crate prefix in macro

macro_rules! my_macro {
    () => { vec![1, 2, 3] }; // Wrong: should use $crate::vec!
}

Practice Questions

1. What is the difference between declarative and procedural macros? Declarative (macro_rules!) match patterns in the token tree and expand to code — no external dependencies needed. Procedural macros receive a TokenStream and return a TokenStream — they require a separate proc-macro crate, syn, and quote.

2. What does the $() repetition operator do?* $() wraps a template that repeats. * means zero or more repetitions, + means one or more, ? means zero or one. Each captured group variable must be used within the matching repetition depth.

3. How do you export macros from a library? Use #[macro_export] before macro_rules! to make the macro available when the crate is imported. Without it, the macro is private to the crate. Procedural macros are always public through their #[proc_macro] annotation.

4. What tools help debug macro expansion? cargo expand shows the expanded code. trace_macros!(true) prints expansion steps during compilation. log_syntax!() prints tokens during expansion. eprintln! inside proc macros for debugging.

5. Challenge: Write a <a href="/design-patterns/builder/">builder</a>! macro that generates a builder pattern for a struct. Given struct Point { x: i32, y: i32 }, calling <a href="/design-patterns/builder/">builder</a>!(Point).x(10).y(20).build() should create Point { x: 10, y: 20 }.

Mini Project: Bitfield Accessor Macro

macro_rules! bitfield {
    ($struct_name:ident { $($field:ident: $start:literal..=$end:literal,)* }) => {
        struct $struct_name(u32);
        
        impl $struct_name {
            fn new(value: u32) -> Self {
                $struct_name(value)
            }
            
            fn raw(&self) -> u32 {
                self.0
            }
            
            $(
                fn $field(&self) -> u32 {
                    let mask = (1u32 << ($end - $start + 1)) - 1;
                    (self.0 >> $start) & mask
                }
                
                fn set_$field(&mut self, value: u32) {
                    let mask = (1u32 << ($end - $start + 1)) - 1;
                    self.0 = (self.0 & !(mask << $start)) | ((value & mask) << $start);
                }
            )*
        }
    };
}

bitfield!(StatusRegister {
    ready: 0..=0,
    error: 1..=1,
    mode: 2..=4,
    count: 5..=15,
})

fn main() {
    let mut sr = StatusRegister::new(0);
    println!("Initial: ready={}, error={}", sr.ready(), sr.error());
    
    sr.set_ready(1);
    sr.set_mode(3);
    sr.set_count(42);
    
    println!("After set: ready={}, error={}, mode={}, count={}",
        sr.ready(), sr.error(), sr.mode(), sr.count());
    println!("Raw value: 0x{:08X}", sr.raw());
}

Expected output:

Initial: ready=0, error=0
After set: ready=1, error=0, mode=3, count=42
Raw value: 0x00002185

FAQ

When should I use a macro instead of a function?

Use a macro when you need to: (1) operate on syntax (not values), (2) generate code with repetition, (3) implement traits automatically, (4) create embedded DSLs. Use functions for everything else — they're type-checked, documented, and easier to debug.

Why do procedural macros need a separate crate?

Procedural macros must be in a proc-macro crate (set proc-macro = true in Cargo.toml) because they run at compile time as compiler plugins. They can only export procedural macros — no other public items — and must use proc_macro::TokenStream from the compiler's API.

Can I use async/await inside a macro?

Macros generate code, so any async code must be inside the generated output, not inside the macro definition. A macro can generate an async function or block, but macro_rules! itself cannot contain .await — it operates on token trees, not runtime constructs.

Macros Basics
Traits & Generics
Testing in Rust
Performance Optimization

What's Next

Test your macros with Testing in Rust, or explore how macros can optimize runtime performance in Performance Optimization.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro