Rust Macros
Declarative Macros (macro_rules!)
macro_rules! vec_of_strings {
($($x:expr),* $(,)?) => {
vec![$($x.to_string()),*]
};
}
let names = vec_of_strings!["alice", "bob", "charlie"];
// Multiple arms
macro_rules! assert_approx_eq {
($a:expr, $b:expr) => { assert_approx_eq!($a, $b, 1e-6) };
($a:expr, $b:expr, $tol:expr) => {
let diff = ($a - $b).abs();
assert!(diff < $tol, "assertion failed: |{} - {}| = {} >= {}", $a, $b, diff, $tol);
};
}
// impl From for multiple types
macro_rules! impl_from {
($from:ty => $to:ty, $variant:ident) => {
impl From<$from> for $to {
fn from(e: $from) -> Self { <$to>::$variant(e) }
}
};
}
impl_from!(std::io::Error => AppError, Io);
impl_from!(serde_json::Error => AppError, Json);
Procedural Macros: Custom Derive
# my-derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
// my-derive/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 name_str = name.to_string();
TokenStream::from(quote! {
impl Describe for #name {
fn describe(&self) -> &'static str { #name_str }
}
})
}
// Usage
use my_derive::Describe;
trait Describe { fn describe(&self) -> &'static str; }
#[derive(Describe)]
struct User { name: String }
let u = User { name: "alice".into() };
println!("{}", u.describe()); // "User"
Attribute Macros
#[proc_macro_attribute]
pub fn retry(attr: TokenStream, item: TokenStream) -> TokenStream {
let attempts: usize = parse_retry_attr(attr);
let item_fn = parse_macro_input!(item as syn::ItemFn);
let fn_sig = &item_fn.sig;
let fn_body = &item_fn.block;
TokenStream::from(quote! {
#fn_sig {
let mut remaining = #attempts;
loop {
let result = (|| #fn_body)();
match result {
Ok(v) => return Ok(v),
Err(e) if remaining > 1 => { remaining -= 1; }
Err(e) => return Err(e),
}
}
}
})
}
Standard Compile-Time Macros
// Include files at compile time
let config = include_str!("../config/default.toml");
let icon = include_bytes!("../assets/icon.png");
// Environment variables at compile time
let version = env!("CARGO_PKG_VERSION");
let opt_debug = option_env!("DEBUG_MODE");
// Conditional compilation
#[cfg(target_os = "macos")]
fn platform_init() { /* macOS specific */ }
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
fn optimized_path() { /* 64-bit only */ }
// Derive multiple standard traits
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
struct UserId(u64);
Common Anti-Patterns
- Macros instead of functions — prefer functions; only use macros when repetition is inexpressible otherwise
- Unhygienic macros — use
$crate:: to refer to your own crate items
- Proc macros without good error messages — use
syn::Error::new_spanned to point to the exact token
- Recursive macros without a base case — they will expand infinitely and hit the recursion limit
- Forgetting
#[macro_export] — without it, the macro is private to the module