Rust macros are a powerful tool for metaprogramming, allowing you to write code that writes code. They come in two main flavors: declarative and procedural.

Declarative macros, defined with macro_rules!, are essentially glorified match statements. They’re great for simple pattern matching and code generation based on token structures. Procedural macros, on the other hand, are more powerful and flexible, operating on the Abstract Syntax Tree (AST) of your code. They are defined using functions annotated with #[proc_macro], #[proc_macro_derive], or #[proc_macro_attribute].

Let’s see a declarative macro in action. Imagine you want a simple macro to print a message with a timestamp.

macro_rules! log {

    ($msg:expr) => {{

        let now = chrono::Local::now();
        println!("[{}] {}", now.format("%Y-%m-%d %H:%M:%S"), $msg);
    }};
}

fn main() {
    log!("Application started.");
}

This log! macro takes an expression $msg, captures the current time using the chrono crate, formats it, and then prints the formatted time followed by the message. When log!("Application started.") is called, the compiler replaces it with the expanded code.

Now, let’s look at a procedural macro. Procedural macros are defined in separate crates and are more complex to write but offer immense power. The #[derive] attribute is the most common form, used to automatically implement traits.

Consider deriving Debug for a custom struct. Instead of manually writing impl Debug for MyStruct { ... }, you can use #[derive(Debug)]. The compiler, at compile time, invokes a procedural macro associated with Debug which generates the impl Debug block for you.

Let’s create a simple #[derive(MyDisplay)] macro that automatically implements the Display trait for a struct.

First, we need a procedural macro crate. Create a new library crate: cargo new my_macros --lib. Edit Cargo.toml in my_macros:

[package]
name = "my_macros"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
syn = "2.0" # For parsing Rust code
quote = "1.0" # For generating Rust code

Now, src/lib.rs in my_macros:

extern crate proc_macro;

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

#[proc_macro_derive(MyDisplay)]
pub fn my_display_derive(input: TokenStream) -> TokenStream {
    // Parse the input tokens into a DeriveInput struct
    let input = parse_macro_input!(input as DeriveInput);

    // Get the name of the struct
    let name = input.ident;

    // Generate the implementation of the Display trait
    let expanded = quote! {
        impl std::fmt::Display for #name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                // For simplicity, we'll just print the struct name
                // A real implementation would inspect fields
                write!(f, "{}", stringify!(#name))
            }
        }
    };

    // Convert the generated tokens back into a TokenStream
    TokenStream::from(expanded)
}

Now, in your main application crate, add my_macros as a dependency:

[dependencies]
my_macros = { path = "../my_macros" } # Adjust path as needed

And use the macro:

use my_macros::MyDisplay;

#[derive(MyDisplay)]
struct MyStruct;

fn main() {
    let my_instance = MyStruct;
    println!("My struct: {}", my_instance);
}

When compiled, the #[derive(MyDisplay)] attribute on MyStruct will trigger the my_display_derive function in my_macros. This function parses the MyStruct definition, generates an impl std::fmt::Display for MyStruct block, and returns it to the compiler. The output will be: My struct: MyStruct.

The most surprising thing about procedural macros is that they are executed during compilation. They are not runtime code; they are a compiler plugin that inspects and generates Rust code based on specific annotations or macro invocations. This means they can perform complex analysis and transformations that are impossible with simple text-based macros like macro_rules!.

The syn and quote crates are indispensable for writing procedural macros. syn allows you to parse Rust code into a structured representation (the AST), and quote lets you generate new Rust code from this structured representation. You essentially parse the input code, manipulate the AST, and then generate new code from the modified AST.

A key aspect of procedural macros is their scope. #[proc_macro_derive] is for implementing traits, #[proc_macro_attribute] is for functions that can be used as attributes on items (like #[my_attribute] fn foo() {}), and #[proc_macro] is for general-purpose, function-like macros that don’t fit the other two. Each has a distinct purpose and signature.

A common pitfall is misunderstanding the TokenStream itself. It’s not a direct representation of the source code text but a sequence of tokens. syn helps bridge this gap by parsing TokenStream into a more usable AST. When generating code with quote, you’re not writing raw strings; you’re constructing a sequence of tokens that the compiler can understand.

The next step in understanding Rust’s metaprogramming is exploring how to build more complex procedural macros, such as attribute-like macros or general-purpose function-like macros, and how to handle error reporting within them effectively.

Want structured learning?

Take the full Rust course →