Declarative Macros (macrorules!)
Declarative Macros (macro_rules!)
Level 12 — Macros Pattern-matching macros for code generation, syntax extension, and DSL creation evaluated at compile time.
1. Prerequisites
- Functions (
fn) — Understanding function calls and signatures vs macro expansion. match— declarative macros use structural pattern matching very similar tomatcharms.- Expressions vs. Statements — Macro matchers care deeply about syntactic categories like
expr,stmt, andty.
2. Term Category
Rust Macro Primitive (pattern matching macro_rules! definition): macro_rules! is Rust's system for declarative metaprogramming. Unlike functions that evaluate values at runtime, declarative macros operate on syntax (tokens) at compile time using pattern matching to expand into standard Rust code.
3. Explanation
(1) Design Motivation — "Why did we design this?"
In JavaScript and TypeScript, reducing boilerplate or introducing custom domain-specific syntax is often done at runtime (e.g. passing objects, higher-order functions) or via complex build-step transpilers (Babel, SWC, custom AST transformers). However, runtime wrappers add overhead, and external build tools operate outside the language itself.
Rust needed a way to perform compile-time code generation that was:
- Zero-cost at runtime: Code is expanded directly before type checking and compilation.
- Type-safe & Hygienic: Prevents macro variables from silently shadowing local variables in caller scope.
- Variadic & Expressive: Supports taking an arbitrary number of arguments without allocating arrays or slices (e.g.,
vec![1, 2, 3],println!).
Functions in Rust cannot accept a variable number of differently-typed arguments without dynamic dispatch (&dyn Trait) or slices. Declarative macros (macro_rules!) solve this by providing structural AST pattern matching over code tokens.
(2) Reality Metaphor
Imagine a Custom Cookie Cutter Factory.
- A Function is like a standard biscuit box: it accepts a fixed shape of dough (typed parameters) and produces a finished biscuit.
- A Declarative Macro (
macro_rules!) is a machine that inspects the blueprint of the dough you pass in (pattern matching tokens). If you pass in 3 chocolate chips, it instantly stamps out a 3-chip cookie mold. If you pass in 10, it stamps out a 10-chip mold. - The macro doesn't eat or bake the cookies at runtime; it generates the exact physical molds (Rust code) at compile time before the baking (compilation) begins.
(3) Code Examples
Short Snippet (Custom vec!-style Macro)
// Define a macro that creates a HashMap with syntactic sugar
macro_rules! map {
( $( $key:expr => $value:expr ),* $(,)? ) => {{
let mut temp_map = std::collections::HashMap::new();
$(
temp_map.insert($key, $value);
)*
temp_map
}};
}
fn main() {
// Easily construct a HashMap without repetitive .insert() calls
let user_roles = map! {
"Alice" => "Admin",
"Bob" => "User",
};
println!("Alice's role: {:?}", user_roles.get("Alice"));
}
Fuller Example (Variadic Logging & Design Pattern)
/// A declarative macro for structured console logging with custom syntax.
/// Demonstrates multiple matcher arms, designators ($expr, $ident, $ty), and repetition.
macro_rules! log_info {
// Arm 1: Simple message
($msg:expr) => {
println!("[INFO] [{}:{}] {}", file!(), line!(), $msg);
};
// Arm 2: Message with key-value payload context
($msg:expr, $( $key:ident = $val:expr ),+ $(,)?) => {
print!("[INFO] [{}:{}] {} | Context: ", file!(), line!(), $msg);
$(
print!("{}={} ", stringify!($key), $val);
)*
println!();
};
}
fn main() {
let status_code = 200;
let latency_ms = 45;
// Matches Arm 1
log_info!("Server initialized successfully");
// Matches Arm 2 with variadic key-value pairs
log_info!(
"Request processed",
status = status_code,
latency = latency_ms,
path = "/api/v1/health",
);
}
4. Common Mistakes & Pitfalls
Mistake 1: Fragment Specifier Mismatch (e.g., using expr where ident or ty is expected)
The mistake: Using $x:expr when trying to match a type name, variable name, or item definition inside a macro.
Why it's wrong: Rust macro designators (expr, ty, ident, stmt, pat, path) restrict what syntactic elements can match and what tokens can follow them to maintain grammar stability and prevent parsing ambiguity.
Incorrect:
macro_rules! create_struct {
// ❌ Error: $name:expr cannot be used as a struct identifier name
($name:expr) => {
struct $name {
id: u64,
}
};
}
Fix:
macro_rules! create_struct {
// Correct: Use $name:ident for identifiers
($name:ident) => {
struct $name {
id: u64,
}
};
}
create_struct!(User);
Mistake 2: Missing Macro Export Annotations Across Modules
The mistake: Defining a macro inside a child module and expecting it to be visible in other modules using standard use crate::foo::my_macro; without proper macro scope attributes.
Why it's wrong: Declarative macros follow historical textual scoping rules unless annotated with #[macro_export]. Without #[macro_export], macros are only visible after their definition order in the file hierarchy.
Incorrect:
mod logger {
macro_rules! my_log {
($msg:expr) => { println!("{}", $msg); };
}
}
fn main() {
// ❌ Error: cannot find macro `my_log` in this scope
// my_log!("hello");
}
Fix:
// Option A: Export to crate root
#[macro_export]
macro_rules! my_log {
($msg:expr) => { println!("{}", $msg); };
}
fn main() {
my_log!("hello"); // Works!
}
Mistake 3: Trailing Comma Repetition Failure
The mistake: Writing repetition matchers like $( $x:expr ),* without allowing optional trailing commas, causing compile errors when users write trailing commas in multi-line invocations.
Why it's wrong: Rust idiomatic formatting relies on trailing commas. If your macro pattern doesn't explicitly match $(,)?, passing a trailing comma breaks macro pattern matching.
Incorrect:
macro_rules! print_all {
( $( $x:expr ),* ) => { // Doesn't match trailing comma
$( println!("{}", $x); )*
};
}
// print_all!(1, 2, 3,); // ❌ Error: no rules expected the token `,`
Fix:
macro_rules! print_all {
( $( $x:expr ),* $(,)? ) => { // $(,)? matches 0 or 1 trailing comma
$( println!("{}", $x); )*
};
}
fn main() {
print_all!(1, 2, 3,); // Correct!
}
5. Practice Exercises
Exercise 1: Type-Safe Embedded Bitfield Register Macro (no_std)
Scenario: In microcontrollers and hardware driver development (#![no_std]), developers frequently manipulate hardware peripheral registers using raw bit shifts and bitwise masks. Writing manual bitwise code for every register flag leads to copy-paste bugs and typos in bit offset calculations.
Task: Write a declarative macro define_bitflags! that generates a type-safe bitfield struct for a hardware register.
The macro must accept:
- The visibility modifier and name of the generated struct (e.g.,
pub struct UartStatus). - The underlying integer type (e.g.,
u8,u16,u32). - A series of flag definitions formatted as
FLAG_NAME = bit_offset.
The macro should expand to:
- A tuple struct wrapping the inner integer type with
#[derive(Debug, Clone, Copy, PartialEq, Eq)]. pub constassociated constants for each flag name holding its shifted bit mask (1 << bit_offset).- A
new(bits: int_type) -> Selfconstructor and anempty() -> Selfmethod. - Bitwise query and mutation methods:
contains(&self, flag_mask: int_type) -> bool,set(&mut self, flag_mask: int_type),clear(&mut self, flag_mask: int_type), andbits(&self) -> int_type. - Support for optional trailing commas.
Include complete unit tests using #[test] and assertions (assert!, assert_eq!) verifying bit operations.
Answer
Implementation
#![no_std]
macro_rules! define_bitflags {
(
$vis:vis struct $name:ident : $ty:ty {
$( $flag:ident = $bit:expr ),* $(,)?
}
) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
$vis struct $name(pub $ty);
impl $name {
$(
pub const $flag: $ty = 1 << $bit;
)*
#[inline]
pub const fn new(bits: $ty) -> Self {
Self(bits)
}
#[inline]
pub const fn empty() -> Self {
Self(0)
}
#[inline]
pub fn contains(&self, flag_mask: $ty) -> bool {
(self.0 & flag_mask) == flag_mask
}
#[inline]
pub fn set(&mut self, flag_mask: $ty) {
self.0 |= flag_mask;
}
#[inline]
pub fn clear(&mut self, flag_mask: $ty) {
self.0 &= !flag_mask;
}
#[inline]
pub const fn bits(&self) -> $ty {
self.0
}
}
};
}
// Define a status register for a simulated UART hardware peripheral
define_bitflags! {
pub struct UartStatus : u8 {
TX_EMPTY = 0,
RX_READY = 1,
PARITY_ERR = 2,
OVERRUN_ERR = 3,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bitflags_initialization_and_masks() {
assert_eq!(UartStatus::TX_EMPTY, 0b0000_0001);
assert_eq!(UartStatus::RX_READY, 0b0000_0010);
assert_eq!(UartStatus::PARITY_ERR, 0b0000_0100);
assert_eq!(UartStatus::OVERRUN_ERR, 0b0000_1000);
let status = UartStatus::new(0b0000_0011);
assert!(status.contains(UartStatus::TX_EMPTY));
assert!(status.contains(UartStatus::RX_READY));
assert!(!status.contains(UartStatus::PARITY_ERR));
}
#[test]
fn test_bitflags_mutation() {
let mut status = UartStatus::empty();
assert_eq!(status.bits(), 0);
status.set(UartStatus::TX_EMPTY | UartStatus::PARITY_ERR);
assert!(status.contains(UartStatus::TX_EMPTY));
assert!(status.contains(UartStatus::PARITY_ERR));
assert_eq!(status.bits(), 0b0000_0101);
status.clear(UartStatus::TX_EMPTY);
assert!(!status.contains(UartStatus::TX_EMPTY));
assert!(status.contains(UartStatus::PARITY_ERR));
assert_eq!(status.bits(), 0b0000_0100);
}
}
Technical Explanation
- Fragment Specifiers:
$vis:vis: Matches visibility modifiers likepuborpub(crate)(or empty for private).$name:ident: Matches the struct name identifier (UartStatus).$ty:ty: Matches the underlying primitive integer type (u8,u16,u32).$flag:ident = $bit:expr: Matches each constant flag identifier and its bit shift expression.
- Repetition Pattern (
$( ... ),* $(,)?):- Matches zero or more comma-separated flag assignments, with
$(,)?matching zero or one trailing comma.
- Matches zero or more comma-separated flag assignments, with
- Compile-Time Constant Generation:
$( pub const $flag: $ty = 1 << $bit; )*expands into multiplepub constdefinitions insideimpl $name, computing masks at compile time without runtime overhead.
no_stdCompatibility:- The generated code uses only core integer bitwise operations (
&,|,!), avoiding dynamic memory allocation (alloc) or the standard library (std), making it ideal for bare-metal microcontroller drivers.
- The generated code uses only core integer bitwise operations (
Exercise 2: Declarative Telemetry Metrics Registry
Scenario: Production microservices require low-overhead, thread-safe application metrics (counters for HTTP requests, gauges for active queue depths). Manually defining struct fields and atomic boilerplate across multiple metric types introduces duplication and maintenance overhead.
Task: Create a declarative macro metrics_registry! that builds a thread-safe metric container struct wrapping atomic counters and gauges.
Define helper structs Counter and Gauge wrapping std::sync::atomic::AtomicU64.
The macro must accept:
metrics_registry! {
pub struct AppMetrics {
http_requests: Counter,
active_connections: Gauge,
error_count: Counter,
}
}
The macro should expand to:
- A struct where each field is initialized as the specified wrapper type (
CounterorGauge). - A
pub fn new() -> Selfconstructor that initializes all atomic counters to 0. - Allow public field access so caller code can invoke methods like
.inc(),.set(), and.get(). - Support optional trailing commas.
Write comprehensive unit tests with #[test], std::thread, std::sync::Arc, and assertions (assert_eq!) verifying thread-safe metric updates across concurrent threads.
Answer
Implementation
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
/// A thread-safe atomic counter for monotonic increments.
pub struct Counter(AtomicU64);
impl Counter {
pub const fn new() -> Self {
Self(AtomicU64::new(0))
}
pub fn inc(&self) -> u64 {
self.0.fetch_add(1, Ordering::Relaxed) + 1
}
pub fn get(&self) -> u64 {
self.0.load(Ordering::Relaxed)
}
}
/// A thread-safe atomic gauge for dynamic value tracking.
pub struct Gauge(AtomicU64);
impl Gauge {
pub const fn new() -> Self {
Self(AtomicU64::new(0))
}
pub fn set(&self, val: u64) {
self.0.store(val, Ordering::Relaxed);
}
pub fn get(&self) -> u64 {
self.0.load(Ordering::Relaxed)
}
}
macro_rules! metrics_registry {
(
$vis:vis struct $name:ident {
$( $field:ident : $kind:ident ),* $(,)?
}
) => {
$vis struct $name {
$( pub $field: $kind, )*
}
impl $name {
pub fn new() -> Self {
Self {
$( $field: $kind::new(), )*
}
}
}
};
}
// Instantiate an application telemetry registry
metrics_registry! {
pub struct AppMetrics {
http_requests: Counter,
active_connections: Gauge,
error_count: Counter,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_threaded_metrics() {
let metrics = AppMetrics::new();
assert_eq!(metrics.http_requests.get(), 0);
assert_eq!(metrics.active_connections.get(), 0);
metrics.http_requests.inc();
metrics.http_requests.inc();
metrics.active_connections.set(42);
assert_eq!(metrics.http_requests.get(), 2);
assert_eq!(metrics.active_connections.get(), 42);
}
#[test]
fn test_concurrent_metric_updates() {
let metrics = Arc::new(AppMetrics::new());
let mut handles = vec![];
// Spawn 10 threads, each incrementing http_requests 100 times
for _ in 0..10 {
let m = Arc::clone(&metrics);
let handle = thread::spawn(move || {
for _ in 0..100 {
m.http_requests.inc();
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(metrics.http_requests.get(), 1000);
}
}
Technical Explanation
- Ident Matchers (
$kind:ident):- The macro uses
$kind:identto match type names (Counter,Gauge). This allows$kind::new()to construct each field uniformly insideimpl $name.
- The macro uses
- Macro Code Expansion:
$( pub $field: $kind, )*expands the struct definition field by field.$( $field: $kind::new(), )*expands thenew()constructor initializer list.
- Thread Safety & Metaprogramming:
- By generating wrapping fields containing
AtomicU64, the macro guarantees zero-cost, lock-free concurrent updates via atomic operations (Ordering::Relaxed).
- By generating wrapping fields containing
- Composition & Reusability:
- Pairing domain structs (
Counter,Gauge) withmacro_rules!keeps the macro syntax clean and readable while delegating specialized logic to Rust's type system.
- Pairing domain structs (
Exercise 3: Declarative Micro-Router for Command Execution
Scenario: Embedded CLI systems, serial UART controllers, and network message handlers parse incoming byte opcodes (e.g. 0x01 for Ping, 0x02 for Reset) and dispatch execution to specific handler functions. Manually matching opcodes to handlers leads to repetitive match blocks and duplicated error checking.
Task: Write a declarative macro dispatch_table! that constructs an opcode enum and an automated dispatch routing function.
The macro must accept:
dispatch_table! {
pub enum CommandId {
Ping = 0x01 => handle_ping,
SystemReset = 0x02 => handle_reset,
GetStatus = 0x03 => handle_status,
}
}
The macro should expand to:
- A
u8-repr enumCommandIdwith explicit discriminant values (0x01,0x02, etc.) and#[derive(Debug, Clone, Copy, PartialEq, Eq)]. - An
impl CommandIdblock containingpub fn from_u8(val: u8) -> Result<Self, DispatchError>. - A dispatch function
pub fn dispatch(opcode: u8, payload: &[u8]) -> Result<u32, DispatchError>that matches the opcode against the table and invokes the appropriate handler function path$handler(payload). - Custom error type
DispatchErrorhandling unknown opcodes and handler errors.
Write comprehensive unit tests with #[test] and assert_eq! verifying successful command routing, error propagation, and unknown opcode rejection.
Answer
Implementation
#[derive(Debug, PartialEq, Eq)]
pub enum DispatchError {
UnknownOpcode(u8),
HandlerFailed(&'static str),
}
macro_rules! dispatch_table {
(
$vis:vis enum $name:ident {
$( $variant:ident = $opcode:expr => $handler:path ),* $(,)?
}
) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
$vis enum $name {
$( $variant = $opcode, )*
}
impl $name {
pub fn from_u8(val: u8) -> Result<Self, DispatchError> {
match val {
$( $opcode => Ok(Self::$variant), )*
other => Err(DispatchError::UnknownOpcode(other)),
}
}
}
$vis fn dispatch(opcode: u8, payload: &[u8]) -> Result<u32, DispatchError> {
match opcode {
$(
$opcode => $handler(payload).map_err(DispatchError::HandlerFailed),
)*
other => Err(DispatchError::UnknownOpcode(other)),
}
}
};
}
// Handler functions called by the generated dispatcher
fn handle_ping(_payload: &[u8]) -> Result<u32, &'static str> {
Ok(1) // Return status code 1 for Pong
}
fn handle_reset(payload: &[u8]) -> Result<u32, &'static str> {
if payload.is_empty() {
Err("Missing authorization token payload")
} else {
Ok(200) // Reset success code
}
}
fn handle_status(_payload: &[u8]) -> Result<u32, &'static str> {
Ok(100) // System status operational
}
// Declare the command routing table
dispatch_table! {
pub enum CommandId {
Ping = 0x01 => handle_ping,
SystemReset = 0x02 => handle_reset,
GetStatus = 0x03 => handle_status,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_opcode_enum_conversion() {
assert_eq!(CommandId::from_u8(0x01), Ok(CommandId::Ping));
assert_eq!(CommandId::from_u8(0x02), Ok(CommandId::SystemReset));
assert_eq!(CommandId::from_u8(0x03), Ok(CommandId::GetStatus));
assert_eq!(
CommandId::from_u8(0xFF),
Err(DispatchError::UnknownOpcode(0xFF))
);
}
#[test]
fn test_dispatch_routing_and_errors() {
// Test successful Ping dispatch
assert_eq!(dispatch(0x01, &[]), Ok(1));
// Test Reset dispatch with valid payload
assert_eq!(dispatch(0x02, &[0xAA, 0xBB]), Ok(200));
// Test Reset dispatch with missing payload failure
assert_eq!(
dispatch(0x02, &[]),
Err(DispatchError::HandlerFailed("Missing authorization token payload"))
);
// Test unknown opcode rejection
assert_eq!(
dispatch(0x99, &[]),
Err(DispatchError::UnknownOpcode(0x99))
);
}
}
Technical Explanation
- Path Fragment Specifier (
$handler:path):- The macro uses
$handler:pathto match function names or full item paths (e.g.handle_pingorcrate::handlers::handle_ping), enabling flexible handler dispatching.
- The macro uses
- Multi-Item Generation from a Single Invocation:
- A single macro call constructs three distinct compiler items: the
enum CommandId, theimpl CommandIdblock withfrom_u8, and the standalonedispatchrouter function.
- A single macro call constructs three distinct compiler items: the
- Dual Repetition Expansion:
- The pattern
$( $variant:ident = $opcode:expr => $handler:path ),*is expanded twice: once insideenum $nameto define variants, once insidefrom_u8to construct opcode-to-enum arms, and once insidedispatchto map opcodes to$handler(payload).
- The pattern
- Safety & Zero Overhead:
- The generated match statement avoids dynamic memory allocations and trait object vtables (
dyn Fn), allowing the Rust compiler to inline handler calls directly for maximum performance.
- The generated match statement avoids dynamic memory allocations and trait object vtables (
6. Related Terms
- Procedural Macros — Function-like, derive, and attribute macros operating on raw
TokenStreams via Rust code. - Hygiene — The rule system ensuring macros don't accidentally leak or collide with identifiers in caller scopes.
match— Value-level pattern matching, which inspired macro token matching.- Token Stream — The underlying stream of compiler tokens that macros analyze and generate.
- Function-like Macros — Related concept: Function-like Macros.
- Declarative Macros (
macro_rules!) — Related concept: Declarative Macros (macro_rules!). - Macros — Related concept: Macros.
7. Key Takeaways
macro_rules!enables declarative, pattern-matching metaprogramming evaluated entirely at compile time.- Macro arguments use designators (e.g.
$expr:expr,$id:ident,$t:ty) to categorize AST syntax fragments. - Repetition syntax
$( ... ),*allows variadic arguments with optional trailing comma support via$(,)?. - Declarative macros are hygienic: local variables declared inside macro expansions cannot interfere with caller scope variables.
- Use
#[macro_export]to make declarative macros available at the root level of your crate.