Skip to content

Macros

Macros are Rust’s compile-time code generation mechanism. Declarative macros are like sophisticated find-and-replace operating on token trees, enabling variadic functions and syntax sugar that the type system cannot express. Procedural macros are Rust programs that transform code, with full access to the abstract syntax tree. They power derive macros that automatically generate trait implementations, eliminating boilerplate while maintaining type safety. Macros trade readability for expressiveness, and should be used when functions and generics are insufficient.

Macros in Rust are a metaprogramming mechanism that operates on the abstract syntax tree (AST) Rather than on values. They expand at compile time, transforming token sequences into new token Sequences that the compiler then processes as ordinary Rust code.

Rust macros solve problems that the type system and generics cannot:

  • Variadic functions: println! accepts any number of arguments of any type. A generic function cannot express “zero or more arguments, each implementing Display” with a single signature.
  • Syntax extension: Macros can introduce new syntactic forms (pattern matching on token trees) that are not representable as function calls. vec![1, 2, 3] is syntactic sugar that would be impossible as a plain function.
  • Code generation: Macros eliminate boilerplate by generating repetitive code at compile time. Derive macros (#[derive(Clone)]) generate impl blocks that would be tedious and error-prone to write by hand.
  • Domain-specific languages: Macros can parse custom syntax within their delimiters, enabling embedded DSLs like sql!(SELECT * FROM users) or route!(GET /users -> list_users).
MechanismOperates onEvaluated atWhen to use
FunctionValuesRuntimeLogic on concrete values
Generic functionTypes (monomorphized)Compile timeLogic parameterized by type
TraitBehavior contractsCompile timeShared behavior across types
Declarative macroToken treesCompile timePattern-matching on syntax, variable-arity
Proc macroAST / TokenStreamCompile timeCode generation requiring full type information

Rust has two fundamentally different macro systems:

  1. Declarative macros (macro_rules!): Pattern-match on token trees and produce new token trees. They are hygienic (variables in the macro cannot capture variables in the call site) but cannot inspect types or perform complex AST manipulation.

  2. Procedural macros: Rust functions that take a TokenStream and return a TokenStream. They have full access to the token stream and, via the syn crate, to the parsed AST. They come in three flavors: derive, attribute-like, and function-like.

A declarative macro is defined with macro_rules! and consists of one or more arms, each with a Pattern and an expansion:

macro_rules! say_hello {
() => {
println!("Hello, world!");
};
}
say_hello!();

Metavariables capture parts of the input and make them available in the expansion. They are prefixed With $ and annotated with a fragment specifier that constrains what tokens they match:

macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("Called function: {}", stringify!($func_name));
}
};
}
create_function!(my_func);
my_func();
SpecifierMatchesExample input
identIdentifierfoo``MyType``_tmp
tyType expressioni32``Vec<String>``&[u8]
exprExpression1 + 2``foo()``x * y
pathPath (module path or type path)std::collections::HashMap
stmtStatement (without trailing semicolon)let x = 5
blockBlock (braced statements){ let x = 1; x + 2 }
patPatternSome(x)``_``1..=100
literalLiteral (string, number, char, bool)"hello"``42``true
metaMeta attribute (inner content of #[...])derive(Debug, Clone)
itemItem (function, struct, impl, etc.)fn foo() {}``struct S;
visVisibility modifierpub``pub(crate)(none)
lifetimeLifetime"a``'static
ttToken tree (single token or matched delimiters)=>``(a, b)``[]

The tt specifier is the most flexible — it matches any single token or any pair of matched Delimiters (parentheses, brackets, or braces). It is the “wildcard” of fragment specifiers.

Declarative macros support repetition with the syntax $( ... ) sep rep where sep is an optional Separator and rep is one of:

  • * — zero or more repetitions
  • + — one or more repetitions
  • ? — zero or one repetition
macro_rules! count_args {
() => { 0usize };
($first:tt $(, $rest:tt)*) => {
1usize + count_args!($($rest),*)
};
}
assert_eq!(count_args!(), 0);
assert_eq!(count_args!(a), 1);
assert_eq!(count_args!(a, b, c), 3);

The separator can be any token. Common separators are , (comma) and ; (semicolon):

macro_rules! vector {
($($elem:expr),* $(,)?) => {
{
let mut v = Vec::new();
$( v.push($elem); )*
v
}
};
}
let v = vector![1, 2, 3];
let v2 = vector![4, 5, 6,];

The trailing $(,)? makes the trailing comma optional, which is idiomatic Rust style.

Repetitions can be nested. The expansion repeats the innermost repetition completely for each Iteration of the outer one:

macro_rules! matrix {
($([$($elem:expr),+]),+ $(,)?) => {
{
vec![
$(
vec![$($elem),+]
),+
]
}
};
}
let m = matrix![
[1, 2, 3],
[4, 5, 6],
];
assert_eq!(m[0], vec![1, 2, 3]);
assert_eq!(m[1], vec![4, 5, 6]);

A macro arm can invoke itself. This is the primary mechanism for processing variable-length input:

macro_rules! find_min {
($x:expr) => { $x };
($x:expr, $($rest:expr),+) => {
{
let rest_min = find_min!($($rest),+);
if $x < rest_min { $x } else { rest_min }
}
};
}
assert_eq!(find_min!(3, 1, 4, 1, 5), 1);