Skip to content

Macros | Rust - Wyatt's Notes

import Citations from ‘@components/Citations.astro’

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);

Macros are Rust’s compile-time code generators. Think of them as copy-paste with intelligence: you write a pattern once, and the compiler expands it into the full code wherever you use it. Declarative macros (macro_rules!) are like templates: they match token patterns and produce new tokens. Procedural macros are like mini-programs that run during compilation and can inspect types and generate arbitrary code. Macros solve the “variadic arguments” problem (println! takes any number of arguments) and eliminate boilerplate (derive macros generate trait implementations automatically).

import { Citation } from “@components/Citations.astro”

<Citations sources={[ {title=“The Rust Programming Language”, author=“Klabnik and Nichols”, year=“2024”, type=“book”, url=“https://doc.rust-lang.org/book/”}, {title=“Rust for Rustaceans”, author=“Gjengset”, year=“2021”, type=“book”}, ]} />