TechLead
Lesson 25 of 28
5 min read
Rust

Macros in Rust

Learn Rust macros: declarative macros with macro_rules!, procedural derive macros, attribute macros, and practical macro patterns.

Macros in Rust

Macros in Rust are a form of metaprogramming — code that writes code. They run at compile time and can generate repetitive code, create DSLs, and implement derive functionality. Rust has two main kinds: declarative macros (macro_rules!) and procedural macros.

Declarative Macros (macro_rules!)

// Simple macro
macro_rules! say_hello {
    () => {
        println!("Hello from macro!");
    };
    ($name:expr) => {
        println!("Hello, {}!", $name);
    };
}

// vec!-like macro
macro_rules! my_vec {
    () => { Vec::new() };
    ( $( $x:expr ),+ $(,)? ) => {
        {
            let mut v = Vec::new();
            $( v.push($x); )+
            v
        }
    };
}

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

fn main() {
    say_hello!();
    say_hello!("Rust");

    let v = my_vec![1, 2, 3, 4, 5];
    println!("{:?}", v);

    let scores = hashmap! {
        "Alice" => 100,
        "Bob" => 85,
        "Charlie" => 92,
    };
    println!("{:?}", scores);
}

Fragment Types

Macro Fragment Specifiers

  • $x:expr — Expressions: 42, a + b, func()
  • $x:ident — Identifiers: foo, MyStruct
  • $x:ty — Types: i32, Vec<String>
  • $x:pat — Patterns: Some(x), _
  • $x:stmt — Statements: let x = 5;
  • $x:tt — Token tree (most flexible): any single token or group
  • $x:block — Block expression: { ... }
  • $x:literal — Literal values: 42, "hello"

Advanced Declarative Macros

// Generate struct with getters
macro_rules! make_struct {
    ($name:ident { $( $field:ident : $ty:ty ),* $(,)? }) => {
        #[derive(Debug)]
        struct $name {
            $( $field: $ty, )*
        }

        impl $name {
            $(
                fn $field(&self) -> &$ty {
                    &self.$field
                }
            )*
        }
    };
}

make_struct!(Person {
    name: String,
    age: u32,
    email: String,
});

// Implement a trait for multiple types
macro_rules! impl_display {
    ( $( $t:ty ),* ) => {
        $(
            impl std::fmt::Display for $t {
                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                    write!(f, "{:?}", self)
                }
            }
        )*
    };
}

// Recursive macro for compile-time computation
macro_rules! count {
    () => (0usize);
    ($x:tt $($xs:tt)*) => (1usize + count!($($xs)*));
}

fn main() {
    let p = Person {
        name: "Alice".into(),
        age: 30,
        email: "alice@test.com".into(),
    };
    println!("{}: age {}", p.name(), p.age());

    let n = count!(a b c d e);
    println!("Count: {n}"); // 5
}

Procedural Macros

// Procedural macros live in their own crate
// Cargo.toml:
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = "2"
// quote = "1"
// proc-macro2 = "1"

// --- Derive macro example ---
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;

    let expanded = quote! {
        impl HelloMacro for #name {
            fn hello() {
                println!("Hello from {}!", stringify!(#name));
            }
        }
    };

    TokenStream::from(expanded)
}

// Usage:
// #[derive(HelloMacro)]
// struct Pancakes;
//
// fn main() {
//     Pancakes::hello(); // "Hello from Pancakes!"
// }

// --- Attribute macro example ---
// #[proc_macro_attribute]
// pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
//     // Parse the HTTP method and path from attr
//     // Wrap the function in routing logic
//     item
// }
//
// #[route(GET, "/api/users")]
// async fn list_users() -> Json> { ... }

Popular Macro Crates

// serde — Serialize/Deserialize derives
#[derive(serde::Serialize, serde::Deserialize)]
struct Config { name: String }

// thiserror — Error type derives
#[derive(thiserror::Error, Debug)]
enum AppError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

// strum — Enum utilities
// #[derive(strum::EnumString, strum::Display)]
// enum Color { Red, Blue, Green }

// tokio — Async runtime macro
// #[tokio::main]
// async fn main() { }

// Debugging macros:
// cargo expand              # Show expanded macro output
// cargo expand main         # Expand specific module

// trace_macros!(true);      # Print macro expansion steps
// log_syntax!("hello");     # Print tokens at compile time

Key Takeaways

  • macro_rules! creates pattern-matching macros for code generation
  • ✅ Fragment specifiers (expr, ident, ty, tt) match different kinds of syntax
  • ✅ Procedural macros (derive, attribute, function-like) use syn and quote crates
  • ✅ Use cargo expand to debug and inspect generated macro output
  • ✅ Prefer functions and generics over macros when possible — macros are a last resort

Continue Learning