TL;DR: Macros in Rust are metaprogramming tools that generate code at compile time. There are two kinds: declarative macros (
macro_rules!) which match patterns and expand to code, and procedural macros which are Rust functions that transform token streams. You've already used macros:println!,vec!,#[derive(Debug)], and#[tokio::main]are all macros. Unlike C macros, Rust macros are hygienic, they don't accidentally capture variables from the call site.
What Are Macros in Rust?
Macros are compile-time code generators, they are expanded before the program runs, producing Rust code from patterns or token manipulation.
// vec! is a macro; this:
let v = vec![1, 2, 3];
// expands to roughly this:
let v = {
let mut temp = Vec::new();
temp.push(1);
temp.push(2);
temp.push(3);
temp
};
// println! is a macro; validates format strings at compile time
println!("{} + {} = {}", 1, 2, 1 + 2); // compile error if args mismatchMacros run during compilation and have access to the abstract syntax tree, they can generate arbitrary Rust code, accept variable numbers of arguments, and perform checks that regular functions cannot.
What Is macro_rules!?
macro_rules! defines declarative macros, pattern-matching rules that expand to code. Each arm matches a syntax pattern and produces an expansion.
// Define a macro that creates a HashMap
macro_rules! map {
($($key:expr => $val:expr),* $(,)?) => {
{
let mut m = std::collections::HashMap::new();
$(m.insert($key, $val);)*
m
}
};
}
fn main() {
let scores = map! {
"Alice" => 100,
"Bob" => 85,
"Carol" => 92,
};
println!("{:?}", scores);
}Pattern fragments like $key:expr match Rust expressions. The * and + quantifiers handle repetition. The result is hygienically expanded, variables inside the macro won't conflict with variables at the call site.
What Are Procedural Macros?
Procedural macros are Rust functions that accept a TokenStream and return a TokenStream, they can generate arbitrary code from attributes, derives, or custom syntax.
There are three kinds:
// 1. Derive macros; used with #[derive(...)]
#[derive(Debug, Clone, serde::Serialize)]
struct User {
name: String,
age: u32,
}
// 2. Attribute macros; used as #[attribute]
#[tokio::main] // transforms main() into async-aware entry point
async fn main() { }
#[get("/users")] // Actix-web route registration
async fn list_users() -> impl Responder { }
// 3. Function-like macros; called like functions but with macro power
let query = sqlx::query!("SELECT * FROM users WHERE id = $1", user_id);| Type | Syntax | Common examples |
|---|---|---|
| Derive | #[derive(Trait)] | Debug, Clone, Serialize, thiserror::Error |
| Attribute | #[name(...)] | #[tokio::main], #[test], #[cfg(...)] |
| Function-like | name!(...) | vec!, println!, sqlx::query! |
Why Are Rust Macros Better Than C Macros?
Rust macros are hygienic, they operate on the AST, not raw text. They cannot accidentally capture or shadow variables from the call site.
// C macro; dangerous text substitution
#define DOUBLE(x) x + x
int result = DOUBLE(1 + 2); // expands to 1 + 2 + 1 + 2 = 6, not 2*(1+2)=6
// With multiplication: DOUBLE(1+2)*3 = 1+2+1+2*3 = 10, not 18!// Rust macro; operates on parsed tokens, not text
macro_rules! double {
($x:expr) => { $x + $x }
}
let result = double!(1 + 2); // correctly evaluates 1+2 first: 3+3=6Rust macros also provide compile-time errors with good diagnostics, println! validates its format string at compile time, catching {} mismatches before the program runs.
Frequently Asked Questions
Use a function by default, functions are easier to read, test, and reason about. Write a macro_rules! macro when you need variadic arguments (like vec![1, 2, 3]), or to generate repetitive boilerplate. Write a procedural macro when you need #[derive(YourTrait)] or need to inspect/transform struct fields at compile time.
The ! distinguishes macro calls from function calls, println!() is a macro, println() would be a function. It signals to both the compiler and the reader that code generation is happening.
Yes, they require a separate crate (proc-macro = true in Cargo.toml) and working with TokenStream is verbose. The syn and quote crates make it manageable. Most developers use existing derive macros (serde, thiserror) rather than writing their own.
dbg!(expr) prints the expression's value with file and line number to stderr, then returns the value. It is a debugging macro that is safe to leave in code temporarily: let x = dbg!(1 + 2); prints [src/main.rs:3] 1 + 2 = 3.
Sources
Related Glossary Terms
- Trait: Derive macros implement traits automatically
- Struct: Structs are the primary target of
#[derive(...)] - Generic: Macros and generics both enable code reuse at compile time
- Cargo: Procedural macros require a separate Cargo crate
- macro-rules:
macro_rules!is the declarative macro system most Rust developers learn first - proc-macro: Procedural macros power derives and custom attributes
- Topcoat: New Rust UI frameworks often lean on macros heavily for declarative component ergonomics
Keep Reading
- Rust Serde Guide: derive macros are the most common macro pattern in practice
- Rust Generics and Traits Explained: procedural macros and trait derivation
- Learn Rust in 2026: macros appear early and often
