macros/Metaprogramming in Rust?
A macro is code that runs at compile time and generates
more Rust code.
Like instructions which generate new code
Examples:
println!(reduces work of variadic arguments)
vec!, #[tokio::main]
#[derive(Debug)](reduces work of custom derives).
Macros are invoked with a ! (function-like) or with
#[...] / #![...] (attributes).
Rust vs C macros
C preprocessor (#define) |
Rust macros | |
|---|---|---|
| What it does | Text substitution before the C compiler sees the file | Expands to Rust AST (Abstract syntax tree) |
| Type checking | None on macro body | Typechecked |
| Debugging | Hard — error lines point at expansion site oddly | Better errors |
| Hygiene |
Unhygienic the macro introduces an identifier (temp) into the caller's code, and that identifier participates in the caller's normal name lookup.
|
Hygienic
|
Types of macros in Rust
| Type | How you write it | Examples |
|---|---|---|
| Declarative | macro_rules! — pattern match on input tokens |
vec!, println!, your own helpers |
| Procedural — derive | #[derive(TraitName)] on a struct/enum |
Debug, Clone, Serialize
|
| Procedural — attribute | #[some_macro] on item or field |
#[tokio::main], #[test] |
| Procedural — function-like | my_macro!(...) implemented as proc macro |
sqlx::query!, custom DSLs |
1. Declarative macro — macro_rules!
Match input patterns and substitute
|
Convert types before addition
|
Variable number of arguments token type that repeats is enclosed in $()
|
TT(Token Tree) Muncher / Recursive Parsing of Arguments
|
2. Procedural macro
A Rust function marked with
#[proc_macro], #[proc_macro_derive], or
#[proc_macro_attribute]. It takes a
TokenStream, builds new tokens (often with the
syn + quote crates), and returns expanded
code. Lives in a proc-macro = true crate.
We use procedural macros; rarely write them until we need custom derives
or domain-specific attributes.
3. Derive macro (built-in / library)
Attach #[derive(...)] to a type; the compiler calls a proc
macro that implements a trait for you.
Debug, Clone are standard derives from Rust
(and some from crates like serde for
Serialize).
struct is annotated using #[derive(MyMacro)] and function is preceded by
#[proc_macro_derive]
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{:?}", p); // Point { x: 1, y: 2 }
let q = p.clone();
assert_eq!(p, q);
}
4. Custom derive macro (concept)
Same mechanism as #[derive(Debug)], but you define
the trait and the proc macro that implements it — e.g.
#[derive(MyTrait)] generates an
impl MyTrait for YourStruct { ... }.
// ---- in your library (proc-macro crate) ----
// #[proc_macro_derive(MyTrait)]
// pub fn derive_my_trait(input: TokenStream) -> TokenStream {
// // parse struct with `syn`, emit impl with `quote`
// }
// ---- in user code ----
// #[derive(MyTrait)]
// struct Config {
// host: String,
// port: u16,
// }
// // macro generates: impl MyTrait for Config { ... }
Real projects: serde_derive (
#[derive(Serialize, Deserialize)]),
thiserror (#[derive(Error)]). Pattern is
always: parse type definition → emit trait implementation.
Quick map
Need simple repetition / DSL in one crate? →
macro_rules!
Need trait impl from struct fields? →
#[derive(...)] (procedural derive)
Need to wrap functions or modules? → attribute proc macro
(#[tokio::main])
See also: tokio::select! (declarative macro from Tokio).