Macros in Rust
Write declarative macros with macro_rules!, understand procedural macros, and use derive macros.
Declarative Macros with macro_rules!
Declarative macros match patterns on the input token stream and expand to Rust code:
macro_rules! say_hello {
() => {
println!("Hello!");
};
($name:expr) => {
println!("Hello, {}!", $name);
};
}
fn main() {
say_hello!(); // Hello!
say_hello!("Alice"); // Hello, Alice!
say_hello!("Bob"); // Hello, Bob!
}
Metavariable Types
| Designator | Matches |
|---|---|
expr | An expression |
ident | An identifier or keyword |
ty | A type |
pat | A pattern |
stmt | A statement |
block | A block { ... } |
item | A top-level item (fn, struct, etc.) |
literal | A literal value |
tt | A single token tree |
path | A path like std::io::Write |
Variadic Macros with Repetition
$(...)* matches zero or more; $(...)+ matches one or more:
macro_rules! max {
($x:expr) => { $x };
($x:expr, $($rest:expr),+) => {
{
let rest_max = max!($($rest),+);
if $x > rest_max { $x } else { rest_max }
}
};
}
macro_rules! hashmap {
($($key:expr => $val:expr),* $(,)?) => {
{
let mut m = std::collections::HashMap::new();
$(m.insert($key, $val);)*
m
}
};
}
fn main() {
println!("{}", max!(3, 1, 4, 1, 5, 9, 2, 6)); // 9
let map = hashmap! {
"one" => 1,
"two" => 2,
"three" => 3,
};
println!("{}", map["two"]); // 2
}
A Practical assert_approx_eq! Macro
macro_rules! assert_approx_eq {
($a:expr, $b:expr) => {
assert_approx_eq!($a, $b, 1e-9)
};
($a:expr, $b:expr, $eps:expr) => {
let (a, b, eps) = ($a, $b, $eps);
assert!(
(a - b).abs() < eps,
"assertion failed: |{} - {}| = {} >= {}",
a, b, (a - b).abs(), eps
);
};
}
fn main() {
assert_approx_eq!(0.1 + 0.2, 0.3);
assert_approx_eq!(1.0, 1.0001, 0.001);
println!("all assertions passed");
}
Generating Boilerplate with Macros
macro_rules! impl_from_str_for_enum {
($enum:ident, $($variant:ident => $s:literal),+) => {
impl std::str::FromStr for $enum {
type Err = String;
fn from_str(s: &str) -> Result<Self, String> {
match s {
$($s => Ok($enum::$variant),)+
_ => Err(format!("unknown {}: {}", stringify!($enum), s)),
}
}
}
impl std::fmt::Display for $enum {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
$($enum::$variant => write!(f, $s),)+
}
}
}
};
}
#[derive(Debug, PartialEq)]
enum Color { Red, Green, Blue }
impl_from_str_for_enum!(Color,
Red => "red",
Green => "green",
Blue => "blue"
);
fn main() {
let c: Color = "green".parse().unwrap();
println!("{:?}", c); // Green
println!("{}", Color::Blue); // blue
}
Procedural Macros
Procedural macros are Rust functions that transform token streams. They live in their own crate with proc-macro = true.
Cargo.toml for the macro crate:
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
Custom Derive Macro
// In the proc-macro crate: my_macros/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Describe)]
pub fn describe_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let expanded = quote! {
impl #name {
pub fn describe() -> &'static str {
concat!("I am a ", stringify!(#name))
}
}
};
TokenStream::from(expanded)
}
Usage in the main crate:
use my_macros::Describe;
#[derive(Describe)]
struct Robot;
#[derive(Describe)]
struct Human;
fn main() {
println!("{}", Robot::describe()); // I am a Robot
println!("{}", Human::describe()); // I am a Human
}
Attribute Macros
// proc-macro crate
#[proc_macro_attribute]
pub fn log_call(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as syn::ItemFn);
let name = &input.sig.ident;
let name_str = name.to_string();
let expanded = quote! {
#input // keep the original function
// This is simplified — a real impl wraps the body
};
TokenStream::from(expanded)
}
derive Macros from the Ecosystem
serde — Serialization
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
name: String,
age: u32,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
#[serde(rename = "createdAt")]
created_at: String,
}
fn main() {
let user = User {
name: "Alice".into(),
age: 30,
email: Some("[email protected]".into()),
created_at: "2024-01-01".into(),
};
let json = serde_json::to_string_pretty(&user).unwrap();
println!("{}", json);
let back: User = serde_json::from_str(&json).unwrap();
println!("{:?}", back);
}
thiserror — Error Types
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("not found: {0}")]
NotFound(String),
#[error("invalid input: {0}")]
InvalidInput(String),
#[error(transparent)]
Io(#[from] std::io::Error),
}
clap — CLI Parsing
[dependencies]
clap = { version = "4", features = ["derive"] }
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "my-tool", about = "A CLI tool")]
struct Args {
#[arg(short, long)]
name: String,
#[arg(short, long, default_value = "1")]
count: u32,
#[arg(long)]
verbose: bool,
}
fn main() {
let args = Args::parse();
for _ in 0..args.count {
println!("Hello, {}!", args.name);
}
}
Built-in Macros Reference
fn main() {
// Formatting
let s = format!("{:>10}", "right"); // right-aligned
let msg = format!("{:0>5}", 42); // "00042"
// Panicking
panic!("something went wrong: {}", "details");
todo!("implement this later");
unimplemented!("not yet");
unreachable!("this path should never be reached");
// Compile-time
let file = file!(); // current file path
let line = line!(); // current line number
let col = column!(); // current column number
let pkg = env!("CARGO_PKG_NAME");
// Debug helpers
dbg!(1 + 2); // prints "[src/main.rs:10] 1 + 2 = 3" to stderr
// Include files
// let data = include_str!("data.txt");
// let bytes = include_bytes!("image.png");
} Frequently Asked Questions
What is the difference between declarative and procedural macros?
Declarative macros (macro_rules!) use pattern matching on token trees and are simpler to write. Procedural macros operate on the AST as Rust code and are more powerful but require a separate crate.
When should I write a macro instead of a function?
Use a macro when you need variadic arguments, when you need to generate code at compile time, or when you need to work with syntax (like identifiers or types) that functions cannot accept.
What crates help write procedural macros?
syn parses Rust token streams into an AST. quote turns Rust code back into token streams. proc-macro2 provides stable versions of the proc_macro types.