Skip to content

Rust Macros — Declarative & Procedural

DodaTech Updated 2026-06-21 6 min read

Rust macros enable compile-time code generation, from simple pattern-based expansions with macro_rules! to powerful procedural macros that implement custom derives, attributes, and function-like transformations.

What You'll Learn

In this tutorial, you'll learn how Rust macros work: declarative macros with macro_rules!, pattern matching on token trees, procedural macros for custom #[derive], and how macros reduce boilerplate while maintaining type safety.

Why It Matters

Systems code often involves repetitive patterns: implementing traits, defining serialization, writing boilerplate error handling. Macros automate this generation at compile time, producing code that is both more concise and less error-prone than hand-written equivalents.

Real-World Use

The serde crate uses procedural macros for serialization. The thiserror crate generates error implementations. Web frameworks like Axum use macros for route definitions. Durga Antivirus Pro uses macros to generate scan dispatchers for hundreds of threat signatures.

flowchart TD
    MAC[Macro Invocation] --> EXP[Expansion Phase]
    EXP --> TT[Token Tree Matching]
    TT -->|Declarative| REP[Replacement Pattern]
    TT -->|Procedural| PROC[Custom Rust Code]
    REP --> AST[Generated AST]
    PROC --> AST
    AST --> COMP[Compilation]
â„šī¸ Info

Prerequisites: Traits & Generics and Error Handling. Understanding Rust syntax is required.

Declarative Macros with macro_rules!

Declarative macros match patterns and generate replacement code.

macro_rules! create_enum {
    ($name:ident { $($variant:ident),+ $(,)? }) => {
        #[derive(Debug, Clone, PartialEq)]
        enum $name {
            $($variant),+
        }
    };
}

create_enum!(StatusCode { Ok, NotFound, Error, Timeout });

fn main() {
    let status = StatusCode::Ok;
    let codes = vec![StatusCode::Ok, StatusCode::NotFound, StatusCode::Error];
    println!("{:?}", status);
    println!("{:?}", codes);
    assert_eq!(StatusCode::Ok, StatusCode::Ok);
}

Expected output:

Ok
[Ok, NotFound, Error]

Repetition in Macros

Macros can repeat patterns for variadic arguments.

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

macro_rules! vec_of_strings {
    ($($s:expr),*) => {{
        let mut v = Vec::new();
        $(v.push(String::from($s));)*
        v
    }};
}

fn main() {
    let map = hashmap! {
        "name" => "Rust",
        "year" => "2015",
        "type" => "systems",
    };
    println!("Map len: {}", map.len());

    let strings = vec_of_strings!("hello", "world", "macro");
    println!("Strings: {:?}", strings);
}

Expected output:

Map len: 3
Strings: ["hello", "world", "macro"]

Procedural Macros Overview

Procedural macros operate on the token stream and produce new token streams. They require a separate crate.

// This is a simplified example of using a derive macro
// In practice, use serde::Serialize, thiserror::Error, etc.

use std::fmt;

// Custom derive macro (defined in a separate crate)
// #[derive(Debug)] is a built-in derive macro
#[derive(Debug)]
struct ScanResult {
    path: String,
    threats_found: u32,
    duration_ms: u64,
}

// Attribute macro example (simulated)
fn main() {
    let result = ScanResult {
        path: String::from("/tmp/test"),
        threats_found: 0,
        duration_ms: 150,
    };

    // #[derive(Debug)] generated this implementation
    println!("Scan result: {:?}", result);
    println!("Path: {}", result.path);
}

Expected output:

Scan result: ScanResult { path: "/tmp/test", threats_found: 0, duration_ms: 150 }
Path: /tmp/test

Building a Declarative Test Macro

Macros enable concise test definitions.

macro_rules! test_module {
    ($name:ident, $($test_name:ident: $test_body:expr),* $(,)?) => {
        mod $name {
            use super::*;
            $(pub fn $test_name() { $test_body })*
        }
    };
}

test_module!(math_tests,
    test_add: { assert_eq!(2 + 2, 4); },
    test_multiply: { assert_eq!(3 * 3, 9); },
    test_divide: { assert_eq!(10 / 2, 5); },
);

fn main() {
    println!("Running math tests...");
    math_tests::test_add();
    math_tests::test_multiply();
    math_tests::test_divide();
    println!("All tests passed!");
}

Expected output:

Running math tests...
All tests passed!

Hygiene and Macro Best Practices

Rust macros are hygienic — they cannot accidentally capture variables from the calling scope.

macro_rules! hygienic_add {
    ($a:expr, $b:expr) => {
        {
            let result = $a + $b; // 'result' does not conflict with outer scope
            result
        }
    };
}

fn main() {
    let result = 5; // This is a separate variable
    let sum = hygienic_add!(10, 20);
    println!("Outer result: {}, Sum: {}", result, sum);
}

Expected output:

Outer result: 5, Sum: 30

Common Mistakes

1. Missing Dollar Signs in Repetition

Forgetting the $ prefix in repetition patterns causes confusing errors. Always use $($pattern),* syntax.

2. Macro Ambiguity

macro_rules! patterns can be ambiguous. Use distinct delimiters or add unique syntax to avoid matching the wrong pattern.

3. Overusing Macros When Functions Suffice

If a macro does not need to generate new syntax or work with multiple types, prefer a generic function. Macros are harder to debug and maintain.

4. Forgetting to Export Macros

Macros defined in one module must be marked #[macro_export] to be usable from other modules or crates.

5. Not Handling Trailing Commas

User expectations: trailing commas should work. Use $($pattern),* $(,)? to accept optional trailing commas.

Practice Questions

1. What is the difference between declarative and procedural macros? Declarative macros (macro_rules!) match patterns and generate code. Procedural macros are Rust functions that operate on token streams, used for #[derive], attribute macros, and function-like macros.

2. What does macro hygiene mean? Hygiene means a macro cannot accidentally capture or interfere with variables from the calling scope. Generated identifiers are unique and do not collide with user code.

3. When would you use a macro instead of a generic function? When you need to: generate new items (structs, functions), work with syntax not representable as types, or implement custom derives.

4. What is the #[macro_export] attribute for? It makes a macro available to other modules and crates. Without it, the macro is only usable within the defining module.

5. Challenge: Write a macro_rules! macro called bitflags! that generates a struct with constants for each flag, supporting OR, AND, and NOT operations.

Mini Project: Builder Macro

macro_rules! define_config {
    ($struct_name:ident, host: $host:ty, port: $port:ty, timeout: $timeout:ty) => {
        struct $struct_name {
            host: $host,
            port: $port,
            timeout: $timeout,
        }

        struct ConfigBuilder {
            host: Option<$host>,
            port: Option<$port>,
            timeout: Option<$timeout>,
        }

        impl ConfigBuilder {
            fn new() -> Self {
                ConfigBuilder { host: None, port: None, timeout: None }
            }
            fn host(mut self, v: $host) -> Self { self.host = Some(v); self }
            fn port(mut self, v: $port) -> Self { self.port = Some(v); self }
            fn timeout(mut self, v: $timeout) -> Self { self.timeout = Some(v); self }
            fn build(self) -> Result<$struct_name, &'static str> {
                Ok($struct_name {
                    host: self.host.ok_or("host required")?,
                    port: self.port.ok_or("port required")?,
                    timeout: self.timeout.ok_or("timeout required")?,
                })
            }
        }
    };
}

define_config!(Config, host: String, port: u16, timeout: u64);

builder!(Config {
    host: String,
    port: u16,
    timeout: u64,
});

fn main() {
    let config = ConfigBuilder::new()
        .host("localhost".into())
        .port(8080)
        .timeout(30)
        .build()
        .unwrap();
    println!("Config: {}:{}, timeout={}", config.host, config.port, config.timeout);
}

FAQ

What is the difference between macro_rules! and procedural macros?

macro_rules! is pattern-based and simpler. Procedural macros are Rust code that transforms token streams, offering more power for complex transformations like custom derives.

Can macros generate new identifiers?

Declarative macros use the calling context's scope. To generate unique identifiers, use procedural macros with helper attributes or paste crate for identifier concatenation.

Are macros evaluated at compile time?

Yes. Macros are expanded during compilation, before type checking. The generated code must be valid Rust, but the macro expansion itself happens at compile time.

Unsafe Rust
Rust FFI
Testing & Documentation

What's Next

Learn Rust FFI for calling C code, and Testing & Documentation for testing macro-generated code.

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

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro