Chapter 18
Macros - Reducing Boilerplate
You've been using macros since line one: println!, vec!, format!. Now let's write our own to eliminate repetitive patterns in our server code.
Declarative Macros with macro_rules!
A declarative macro (also called "macros by example") works by pattern matching on the code you pass in. You define patterns, and the compiler replaces matching invocations with the template code you provide. It's code that writes code - at compile time.
// The simplest possible macro
macro_rules! say_hello {
() => {
println!("Hello from a macro!");
};
}
say_hello!(); // expands to: println!("Hello from a macro!");Let's break down the syntax:
macro_rules! name- declares the macro with a name.() => { ... }- a pattern-match arm. The left side matches what you write insidesay_hello!(...). The right side is the code it expands to.- Macros are invoked with
!- this distinguishes them from function calls.
Capturing Arguments
Macros capture input with $name:kind where kind is a fragment specifier - the type of syntax to match:
macro_rules! log_request {
($method:expr, $path:expr) => {
println!("[REQUEST] {} {}", $method, $path);
};
}
log_request!("GET", "/api/todos");
// expands to: println!("[REQUEST] {} {}", "GET", "/api/todos");| Specifier | Matches | Example |
|---|---|---|
| expr | Any expression | 42, x + 1, foo() |
| ident | An identifier | my_var, Request |
| ty | A type | u32, String, Vec<u8> |
| pat | A pattern | Some(x), (a, b) |
| tt | A single token tree | anything - the most flexible |
| literal | A literal value | "hello", 42, true |
| block | A block of code | { x + 1 } |
Multiple Arms
Like match, macros can have multiple arms. The first matching pattern wins:
macro_rules! response {
// response!(200)
($status:expr) => {
Response::new($status, status_reason($status), "")
};
// response!(200, "OK body")
($status:expr, $body:expr) => {
Response::new($status, status_reason($status), $body)
};
// response!(200, "OK", "custom body")
($status:expr, $reason:expr, $body:expr) => {
Response::new($status, $reason, $body)
};
}
fn status_reason(code: u16) -> &'static str {
match code {
200 => "OK", 201 => "Created", 204 => "No Content",
400 => "Bad Request", 404 => "Not Found",
500 => "Internal Server Error", _ => "Unknown",
}
}
let r1 = response!(404); // no body
let r2 = response!(200, "<h1>Hello</h1>"); // with body
let r3 = response!(200, "OK", "custom"); // full controlRepetition
Macros handle variable numbers of arguments with repetition syntax:$( ... ),* matches zero or more comma-separated items.
/// Create a HashMap from key-value pairs.
macro_rules! map {
( $( $key:expr => $value:expr ),* $(,)? ) => {{
let mut m = std::collections::HashMap::new();
$( m.insert($key, $value); )*
m
}};
}
let headers = map! {
"Content-Type" => "text/html",
"Server" => "rust-webserver",
"X-Powered-By" => "macros",
};
// Expands to:
// let mut m = HashMap::new();
// m.insert("Content-Type", "text/html");
// m.insert("Server", "rust-webserver");
// m.insert("X-Powered-By", "macros");
// mThe $(...),* in the pattern matches each pair. The $(...)* in the expansion repeats for each match. The $(,)? at the end allows an optional trailing comma.
vec![] is implemented in the standard library. vec![1, 2, 3] expands to a series of push calls on a pre-allocated Vec.Writing a route! Macro for Cleaner Routing
Our router setup has repetitive boilerplate. Let's replace it with a macro:
// Before: verbose and repetitive
fn build_router() -> Router {
let mut router = Router::new();
router.add(Method::Get, "/", home);
router.add(Method::Get, "/about", about);
router.add(Method::Get, "/api/todos", list_todos);
router.add(Method::Post, "/api/todos", create_todo);
router.add(Method::Get, "/stats", stats_page);
router
}// The macro
macro_rules! routes {
( $( $method:ident $path:literal => $handler:expr ),* $(,)? ) => {{
let mut router = Router::new();
$(
router.add(Method::$method, $path, $handler);
)*
router
}};
}
// After: concise and scannable
fn build_router() -> Router {
routes! {
Get "/" => home,
Get "/about" => about,
Get "/api/todos" => list_todos,
Post "/api/todos" => create_todo,
Get "/stats" => stats_page,
}
}The improvement: every route is one line with a consistent format - method, path, handler. No noise from router.add, Method::, or semicolons. The macro handles it all.
Let's go further with a macro that also defines inline handlers:
macro_rules! routes {
( $( $method:ident $path:literal => $body:block ),* $(,)? ) => {{
let mut router = Router::new();
$(
router.add(Method::$method, $path, |_req: &Request| -> Result<Response, ServerError> {
$body
});
)*
router
}};
}
fn build_router() -> Router {
routes! {
Get "/" => {
Ok(Response::html("<h1>Welcome!</h1>"))
},
Get "/about" => {
Ok(Response::html("<h1>About</h1>"))
},
Get "/health" => {
Ok(Response::json_ok(&serde_json::json!({"status": "ok"})))
},
}
}cargo expand (install with cargo install cargo-expand) to see what your macro expands to. It's invaluable for debugging: cargo expand --bin webserver shows all macros fully expanded.When to Use Macros vs Generics vs Traits
Macros, generics, and traits all reduce code duplication, but in different ways. Choosing the right tool matters for readability and maintainability:
Use Generics when...
You want the same logic for different types. The compiler generates specialized code for each concrete type. Type-safe, inspectable, and the IDE understands it.
// Good use of generics
fn send<T: ToHttp>(response: &T, stream: &mut TcpStream) {
stream.write_all(&response.to_http()).unwrap();
}Use Traits when...
You want different types to share a common interface. Types provide their own implementation. Best for polymorphism and abstraction.
// Good use of traits
trait Handler {
fn handle(&self, req: &Request) -> Result<Response, ServerError>;
}Use Macros when...
You need to generate code, not abstract over types. Macros can do things generics and traits can't:
- Variable numbers of arguments (
vec![1, 2, 3]) - Generating new identifiers or types
- DSLs with custom syntax (
routes!) - Conditional compilation
- Repeating a pattern with different names
| Feature | Generics | Traits | Macros |
|---|---|---|---|
| Type checking | Compile time | Compile time | After expansion |
| IDE support | Full | Full | Limited |
| Variadic args | No | No | Yes |
| Generate code | No | No | Yes |
| Custom syntax | No | No | Yes |
| Error messages | Clear | Clear | Confusing |
A Brief Look at Procedural Macros and Derive Macros
macro_rules! macros work by pattern matching on tokens. Procedural macros are more powerful - they're Rust functions that receive a token stream and return a modified token stream. They can inspect and transform code arbitrarily.
You've been using procedural macros throughout this guide:
// Derive macros - generate trait implementations
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Todo {
id: u64,
title: String,
completed: bool,
}
// Attribute macros - transform the annotated item
#[tokio::main]
async fn main() { /* ... */ }
// #[tokio::main] transforms this into:
// fn main() {
// tokio::runtime::Runtime::new().unwrap()
// .block_on(async { /* ... */ });
// }There are three kinds of procedural macros:
Derive macros - #[derive(MyTrait)]
Generate trait implementations from struct/enum definitions. Used by serde, Debug, Clone.
Attribute macros - #[my_attribute]
Transform the item they're attached to. Used by #[tokio::main], #[test], #[route("GET", "/")] in web frameworks.
Function-like macros - my_macro!(...)
Look like macro_rules! macros but are implemented as Rust functions. Used by serde_json::json!, sqlx::query!.
Writing procedural macros requires a separate crate (they must be compiled before the code that uses them). Here's what a derive macro looks like at a high level:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
/// Derive macro that implements Display for a struct
/// by printing its name and fields.
#[proc_macro_derive(AutoDisplay)]
pub fn auto_display(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let expanded = quote! {
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", stringify!(#name))
}
}
};
expanded.into()
}
// Usage:
// #[derive(AutoDisplay)]
// struct MyStruct { ... }
// println!("{}", my_struct); // prints "MyStruct"The key crates for procedural macros:
syn- parses Rust source code into an AST (abstract syntax tree) you can inspect.quote- generates Rust source code from templates withquote!.proc_macro- the compiler interface (provided by Rust itself).
#[derive(Serialize)] and move on. Understanding that they exist and how they work is enough - you'll know when you need one.Useful Macros You Should Know
The standard library and common crates provide macros worth memorizing:
// todo!() - marks unfinished code, compiles but panics at runtime
fn handle_put(req: &Request) -> Response {
todo!("implement PUT handler")
}
// unimplemented!() - like todo but for things you may never implement
fn handle_trace(req: &Request) -> Response {
unimplemented!("TRACE is not supported")
}
// unreachable!() - asserts this code path is impossible
fn status_reason(code: u16) -> &'static str {
match code {
200 => "OK",
404 => "Not Found",
_ => unreachable!("unexpected status code: {}", code),
}
}
// dbg!() - prints expression + value to stderr, returns the value
let port = dbg!(8080 + 1); // [src/main.rs:42] 8080 + 1 = 8081
let len = dbg!(body.len()); // prints and returns the length
// include_str!() / include_bytes!() - embed files at compile time
const INDEX_HTML: &str = include_str!("../public/index.html");
const FAVICON: &[u8] = include_bytes!("../public/favicon.ico");
// cfg!() - compile-time feature/platform check
if cfg!(debug_assertions) {
println!("Debug mode - extra logging enabled");
}
// concat!() - concatenate string literals at compile time
const VERSION: &str = concat!("webserver/", env!("CARGO_PKG_VERSION"));
// "webserver/0.1.0"include_str! and include_bytes! are powerful for small servers - you can embed your HTML templates, favicon, and CSS directly into the binary. No file I/O at runtime, no missing files in deployment. The tradeoff is binary size and needing to recompile when assets change.Exercise
- Write a
headers!macro that creates aVec<(String, String)>from pairs:headers!("Content-Type" => "text/html", "Server" => "rust"). - Extend the
routes!macro to support an optional third field for middleware:Get "/admin" => admin_page [auth_middleware]. - Write a
json_response!macro that takes a status code and a JSON-like body:json_response!(200, {"status": "ok"}). Hint: useserde_json::json!internally. - Use
include_str!to embed yourpublic/index.htmlinto the binary. Serve it from the embedded string instead of reading from disk. Compare: what are the tradeoffs? - Install
cargo-expandand runcargo expandon your project. Find the expanded#[derive(Debug)]for one of your structs. How much code does the derive macro generate?
What's Next
Macros are a sharp tool - powerful for eliminating boilerplate, but worth reaching for only when functions, generics, and traits aren't enough. You now know declarative macros with macro_rules!, the three kinds of procedural macros, and when to use each approach.
In the next chapter, we test our server - unit tests for individual functions, integration tests that hit the HTTP endpoint, and property-based testing to find edge cases we didn't think of.