Core Concept
Macros are code that writes code at compile time. They use pattern matching on Rust syntax to generate repetitive code.
Basic Structure
macro_rules! macro_name {
(pattern) => {
// code to generate
};
}
Fragment Specifiers
$name:ident- matches an identifier (function name, variable name)$name:expr- matches any expression (literals, variables, function calls, blocks)$name:tt- matches any token tree (most flexible)
Key Insight: Everything is an Expression
A single number like 5 is an expression because it evaluates to itself. This is why $x:expr can match literals, variables, or complex expressions.
The Process
- Write boilerplate 2-3 times - solve the problem first
- Notice the pattern - what varies vs what stays the same
- Extract to macro - capture the varying parts as parameters
- Use multiple match arms - handle different patterns
Example: Static File Handler
Started with repetitive functions:
pub async fn get_css_file() -> impl IntoResponse {
let css_file = Asset::get("styles.css").unwrap();
let contents = std::str::from_utf8(css_file.data.as_ref())
.unwrap()
.to_string();
Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/css; charset=utf-8")
.body(contents)
.unwrap()
}
Extracted to macro with multiple patterns:
macro_rules! static_file_handler {
// Text files
($fn_name:ident, $filename:expr, $content_type:expr, text) => {
pub async fn $fn_name() -> impl IntoResponse {
let asset = Asset::get($filename).unwrap();
let contents = std::str::from_utf8(asset.data.as_ref())
.unwrap()
.to_string();
Response::builder()
.status(StatusCode::OK)
.header("content-type", $content_type)
.body(contents)
.unwrap()
}
};
// Binary files
($fn_name:ident, $filename:expr, $content_type:expr, binary) => {
pub async fn $fn_name() -> impl IntoResponse {
let asset = Asset::get($filename).unwrap();
let contents = asset.data.as_ref().to_vec();
Response::builder()
.status(StatusCode::OK)
.header("content-type", $content_type)
.body(Body::from(contents))
.unwrap()
}
};
}
Usage:
static_file_handler!(get_css_file, "styles.css", "text/css; charset=utf-8", text);
static_file_handler!(get_scripts_file, "scripts.js", "text/javascript", text);
static_file_handler!(get_image_file, "favicon.png", "image/png", binary);
When to Use Macros
- You’ve copy-pasted similar code 2-3 times
- The pattern is clear: some parts vary, most stays the same
- The varying parts can be captured as parameters
- Don’t try to design macros upfront - write the boilerplate first
Types of Macros
Declarative macros (macro_rules!):
- Pattern matching on tokens
- 80% of macro use cases
- What we learned here
Procedural macros (for later):
- Derive macros:
#[derive(Debug)] - Function-like macros:
sqlx::query!() - Attribute macros:
#[tokio::main] - More complex, require separate crate
- Learn these after you’re comfortable with declarative macros
Build Up, Then Sand Down
Same principle as learning other Rust concepts:
- Write working code (even if repetitive)
- Notice patterns
- Refactor with macros
- Don’t try to be perfect upfront
Comments