Last updated: April 13, 2025
Table of Contents
1. Introduction: Code that Writes Code
Macros are a fundamental feature of Rust that enable metaprogramming
—writing code that generates
other code at compile time. You've likely already used macros like println!
or vec!
(notice the exclamation mark !
, which typically denotes a macro invocation).
Unlike macros in C/C++, Rust macros are more structured and integrated into the language's syntax tree (AST), making them safer and more powerful. They allow you to reduce boilerplate code, create domain-specific languages (DSLs), and implement features that would otherwise be difficult or impossible with functions alone.
Rust has two main types of macros: declarative
macros and procedural
macros.
This guide assumes familiarity with basic Rust concepts covered in Getting Started with Rust.
2. Macros vs. Functions
Why use macros when functions exist? Macros can do things functions cannot:
- Variadic Arguments: Macros can take a variable number of arguments (e.g.,
println!
can take a format string and any number of arguments to interpolate). - Code Generation: Macros operate on Rust code itself before it's fully compiled, allowing
them to generate trait implementations (like
#[derive(Debug)]
) or entirely new code structures based on patterns. - Control Flow Implementation: They can implement control flow structures or repetitive patterns more concisely than functions.
- Avoiding Runtime Cost: Macro expansion happens at compile time, potentially avoiding function call overhead for simple operations.
However, macros are generally more complex to write and debug than functions, and their code generation can sometimes make compiler errors harder to understand.
3. Declarative Macros with macro_rules!
Declarative macros, often called "macros by example" or defined using the macro_rules!
keyword,
are the simpler form. They work similarly to a match
expression but operate on Rust code syntax.
3.1 Syntax Overview
You define patterns (the "matcher") and corresponding code templates (the "transcriber") that Rust code should be transformed into if it matches the pattern.
macro_rules! macro_name {
( matcher_pattern_1 ) => { transcriber_code_1 };
( matcher_pattern_2 ) => { transcriber_code_2 };
// ... more rules
}
Matchers use special syntax to capture parts of the input code, like:
$ident:ident
: Matches an identifier (variable name, function name).$expr:expr
: Matches an expression.$ty:ty
: Matches a type.$($item:kind),*
: Matches zero or more items separated by commas (repetition).
3.2 Example: A Simple Vector Macro
Let's look at a simplified version of how the standard vec!
macro might be defined:
macro_rules! my_vec {
// Rule 1: Match zero or more expressions separated by commas
( $( $x:expr ),* ) => {
// Transcriber code block
{
let mut temp_vec = Vec::new();
// Repeat push() for each matched expression ($x)
$(
temp_vec.push($x);
)*
temp_vec // Return the vector
}
};
}
fn main() {
let v = my_vec![1, 2, 3, 4]; // Invoking the macro
println!("{:?}", v); // Output: [1, 2, 3, 4]
let empty_v: Vec = my_vec![];
println!("{:?}", empty_v); // Output: []
}
This macro takes a comma-separated list of expressions, creates a new vector, pushes each expression onto the vector, and returns the vector.
3.3 Common Use Cases
- Creating convenient syntax for initializing collections (
vec!
,hashmap!
- often from external crates). - Reducing boilerplate for repetitive code patterns.
- Implementing simple DSLs within Rust.
4. Procedural Macros
4.1 Introduction
Procedural macros (or "proc macros") are more powerful but also significantly more complex to write. Instead of matching patterns, they operate directly on the stream of tokens that make up Rust code during compilation. They are essentially Rust functions that take a stream of tokens as input and produce another stream of tokens as output.
Writing procedural macros requires creating a separate crate with the proc-macro = true
setting
in its Cargo.toml
and using libraries like syn
(for parsing Rust code into an AST)
and quote
(for generating Rust code from an AST).
There are three types of procedural macros:
4.2 Custom #[derive]
Macros
These allow you to automatically implement traits for structs and enums by adding a
#[derive(MyTrait)]
attribute. The most common built-in example is #[derive(Debug)]
,
which generates an implementation of the std::fmt::Debug
trait.
// Using a built-in derive macro
#[derive(Debug, Clone, PartialEq)] // Debug, Clone, PartialEq are derive macros
struct Point {
x: i32,
y: i32,
}
// Libraries like Serde provide custom derive macros:
// #[derive(Serialize, Deserialize)]
// struct User { ... }
4.3 Attribute-like Macros
These custom attributes can be attached to any item (functions, structs, modules, etc.) and can modify the item they're attached to. They look similar to derive macros but are used in different places.
// Example from Actix Web framework
#[actix_web::main] // An attribute-like macro applied to the main function
async fn main() -> std::io::Result<()> {
// ... server setup ...
}
#[get("/users/{id}")] // Another attribute-like macro defining a route
async fn get_user(id: web::Path) -> impl Responder {
// ... handler logic ...
}
4.4 Function-like Macros
These look like regular macro calls (with a !
) but operate using the more flexible token stream
processing of procedural macros. Examples include macros for embedding SQL or parsing complex input formats.
// Hypothetical example (syntax may vary based on actual crate)
// let sql = sql_macro!("SELECT * FROM users WHERE id = {}", user_id);
4.5 Complexity
Writing procedural macros involves understanding Rust syntax trees and working with token streams, making them considerably more complex than declarative macros. They are typically used by library authors to provide powerful abstractions and code generation capabilities.
5. Commonly Encountered Macros
Besides custom macros, you'll frequently use macros from the standard library or popular crates:
println!
/eprintln!
: Print to standard output/error with formatting.format!
: Creates aString
with formatted arguments.vec!
: Creates aVec
with specified elements.assert!
/assert_eq!
/assert_ne!
: Used in tests to assert conditions (panic if false).panic!
: Causes the current thread to panic. See our Error Handling in Rust guide.todo!
/unimplemented!
: Placeholder macros that panic if executed.#[derive(...)]
: Procedural macro attribute for auto-implementing traits.- Framework-specific macros (like
#[tokio::main]
,#[actix_web::get(...)]
).
6. Conclusion
Macros are a powerful metaprogramming feature in Rust, enabling code generation at compile time to reduce boilerplate and create expressive APIs or DSLs.
- Declarative macros (
macro_rules!
) use pattern matching and are suitable for simpler code generation tasks and creating variadic functions likevec!
. - Procedural macros (custom derive, attribute-like, function-like) are more complex but offer greater power by directly manipulating code tokens, enabling features like automatic trait implementations and framework-specific attributes.
While you might not write complex procedural macros often as an application developer, understanding how
macros work and recognizing common patterns like println!
, vec!
, and
#[derive]
is essential for effectively using Rust and its ecosystem.
7. Additional Resources
Related Articles
External Resources
- The Rust Book: Chapter 19.6 - Macros
- The Rust Reference: Macros
- Rust By Example: Macros
syn
Crate (For parsing Rust code in proc macros)quote
Crate (For generating Rust code in proc macros)