Skip to main content
Rust advanced Lesson 27 of 30

Macros in Rust

Write declarative macros with macro_rules!, understand procedural macros, and use derive macros.

Declarative Macros with macro_rules!

Declarative macros match patterns on the input token stream and expand to Rust code:

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

fn main() {
    say_hello!();           // Hello!
    say_hello!("Alice");    // Hello, Alice!
    say_hello!("Bob");      // Hello, Bob!
}

Metavariable Types

DesignatorMatches
exprAn expression
identAn identifier or keyword
tyA type
patA pattern
stmtA statement
blockA block { ... }
itemA top-level item (fn, struct, etc.)
literalA literal value
ttA single token tree
pathA path like std::io::Write

Variadic Macros with Repetition

$(...)* matches zero or more; $(...)+ matches one or more:

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

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

fn main() {
    println!("{}", max!(3, 1, 4, 1, 5, 9, 2, 6)); // 9

    let map = hashmap! {
        "one"   => 1,
        "two"   => 2,
        "three" => 3,
    };
    println!("{}", map["two"]); // 2
}

A Practical assert_approx_eq! Macro

macro_rules! assert_approx_eq {
    ($a:expr, $b:expr) => {
        assert_approx_eq!($a, $b, 1e-9)
    };
    ($a:expr, $b:expr, $eps:expr) => {
        let (a, b, eps) = ($a, $b, $eps);
        assert!(
            (a - b).abs() < eps,
            "assertion failed: |{} - {}| = {} >= {}",
            a, b, (a - b).abs(), eps
        );
    };
}

fn main() {
    assert_approx_eq!(0.1 + 0.2, 0.3);
    assert_approx_eq!(1.0, 1.0001, 0.001);
    println!("all assertions passed");
}

Generating Boilerplate with Macros

macro_rules! impl_from_str_for_enum {
    ($enum:ident, $($variant:ident => $s:literal),+) => {
        impl std::str::FromStr for $enum {
            type Err = String;

            fn from_str(s: &str) -> Result<Self, String> {
                match s {
                    $($s => Ok($enum::$variant),)+
                    _ => Err(format!("unknown {}: {}", stringify!($enum), s)),
                }
            }
        }

        impl std::fmt::Display for $enum {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                match self {
                    $($enum::$variant => write!(f, $s),)+
                }
            }
        }
    };
}

#[derive(Debug, PartialEq)]
enum Color { Red, Green, Blue }

impl_from_str_for_enum!(Color,
    Red   => "red",
    Green => "green",
    Blue  => "blue"
);

fn main() {
    let c: Color = "green".parse().unwrap();
    println!("{:?}", c);        // Green
    println!("{}", Color::Blue); // blue
}

Procedural Macros

Procedural macros are Rust functions that transform token streams. They live in their own crate with proc-macro = true.

Cargo.toml for the macro crate:

[lib]
proc-macro = true

[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"

Custom Derive Macro

// In the proc-macro crate: my_macros/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

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

    let expanded = quote! {
        impl #name {
            pub fn describe() -> &'static str {
                concat!("I am a ", stringify!(#name))
            }
        }
    };

    TokenStream::from(expanded)
}

Usage in the main crate:

use my_macros::Describe;

#[derive(Describe)]
struct Robot;

#[derive(Describe)]
struct Human;

fn main() {
    println!("{}", Robot::describe());  // I am a Robot
    println!("{}", Human::describe());  // I am a Human
}

Attribute Macros

// proc-macro crate
#[proc_macro_attribute]
pub fn log_call(attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as syn::ItemFn);
    let name = &input.sig.ident;
    let name_str = name.to_string();

    let expanded = quote! {
        #input  // keep the original function

        // This is simplified — a real impl wraps the body
    };

    TokenStream::from(expanded)
}

derive Macros from the Ecosystem

serde — Serialization

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    name: String,
    age: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    email: Option<String>,
    #[serde(rename = "createdAt")]
    created_at: String,
}

fn main() {
    let user = User {
        name: "Alice".into(),
        age: 30,
        email: Some("alice@example.com".into()),
        created_at: "2024-01-01".into(),
    };

    let json = serde_json::to_string_pretty(&user).unwrap();
    println!("{}", json);

    let back: User = serde_json::from_str(&json).unwrap();
    println!("{:?}", back);
}

thiserror — Error Types

use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("not found: {0}")]
    NotFound(String),

    #[error("invalid input: {0}")]
    InvalidInput(String),

    #[error(transparent)]
    Io(#[from] std::io::Error),
}

clap — CLI Parsing

[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;

#[derive(Parser, Debug)]
#[command(name = "my-tool", about = "A CLI tool")]
struct Args {
    #[arg(short, long)]
    name: String,

    #[arg(short, long, default_value = "1")]
    count: u32,

    #[arg(long)]
    verbose: bool,
}

fn main() {
    let args = Args::parse();
    for _ in 0..args.count {
        println!("Hello, {}!", args.name);
    }
}

Built-in Macros Reference

fn main() {
    // Formatting
    let s = format!("{:>10}", "right"); // right-aligned
    let msg = format!("{:0>5}", 42);    // "00042"

    // Panicking
    panic!("something went wrong: {}", "details");
    todo!("implement this later");
    unimplemented!("not yet");
    unreachable!("this path should never be reached");

    // Compile-time
    let file = file!();     // current file path
    let line = line!();     // current line number
    let col = column!();    // current column number
    let pkg = env!("CARGO_PKG_NAME");

    // Debug helpers
    dbg!(1 + 2);            // prints "[src/main.rs:10] 1 + 2 = 3" to stderr

    // Include files
    // let data = include_str!("data.txt");
    // let bytes = include_bytes!("image.png");
}

Frequently Asked Questions

What is the difference between declarative and procedural macros?
Declarative macros (macro_rules!) use pattern matching on token trees and are simpler to write. Procedural macros operate on the AST as Rust code and are more powerful but require a separate crate.
When should I write a macro instead of a function?
Use a macro when you need variadic arguments, when you need to generate code at compile time, or when you need to work with syntax (like identifiers or types) that functions cannot accept.
What crates help write procedural macros?
syn parses Rust token streams into an AST. quote turns Rust code back into token streams. proc-macro2 provides stable versions of the proc_macro types.