Rust Macros â Declarative & Procedural
Rust macros enable compile-time code generation, from simple pattern-based expansions with macro_rules! to powerful procedural macros that implement custom derives, attributes, and function-like transformations.
What You'll Learn
In this tutorial, you'll learn how Rust macros work: declarative macros with macro_rules!, pattern matching on token trees, procedural macros for custom #[derive], and how macros reduce boilerplate while maintaining type safety.
Why It Matters
Systems code often involves repetitive patterns: implementing traits, defining serialization, writing boilerplate error handling. Macros automate this generation at compile time, producing code that is both more concise and less error-prone than hand-written equivalents.
Real-World Use
The serde crate uses procedural macros for serialization. The thiserror crate generates error implementations. Web frameworks like Axum use macros for route definitions. Durga Antivirus Pro uses macros to generate scan dispatchers for hundreds of threat signatures.
flowchart TD
MAC[Macro Invocation] --> EXP[Expansion Phase]
EXP --> TT[Token Tree Matching]
TT -->|Declarative| REP[Replacement Pattern]
TT -->|Procedural| PROC[Custom Rust Code]
REP --> AST[Generated AST]
PROC --> AST
AST --> COMP[Compilation]
Prerequisites: Traits & Generics and Error Handling. Understanding Rust syntax is required.
Declarative Macros with macro_rules!
Declarative macros match patterns and generate replacement code.
macro_rules! create_enum {
($name:ident { $($variant:ident),+ $(,)? }) => {
#[derive(Debug, Clone, PartialEq)]
enum $name {
$($variant),+
}
};
}
create_enum!(StatusCode { Ok, NotFound, Error, Timeout });
fn main() {
let status = StatusCode::Ok;
let codes = vec![StatusCode::Ok, StatusCode::NotFound, StatusCode::Error];
println!("{:?}", status);
println!("{:?}", codes);
assert_eq!(StatusCode::Ok, StatusCode::Ok);
}
Expected output:
Ok
[Ok, NotFound, Error]
Repetition in Macros
Macros can repeat patterns for variadic arguments.
macro_rules! hashmap {
($($key:expr => $val:expr),* $(,)?) => {{
let mut map = std::collections::HashMap::new();
$(map.insert($key, $val);)*
map
}};
}
macro_rules! vec_of_strings {
($($s:expr),*) => {{
let mut v = Vec::new();
$(v.push(String::from($s));)*
v
}};
}
fn main() {
let map = hashmap! {
"name" => "Rust",
"year" => "2015",
"type" => "systems",
};
println!("Map len: {}", map.len());
let strings = vec_of_strings!("hello", "world", "macro");
println!("Strings: {:?}", strings);
}
Expected output:
Map len: 3
Strings: ["hello", "world", "macro"]
Procedural Macros Overview
Procedural macros operate on the token stream and produce new token streams. They require a separate crate.
// This is a simplified example of using a derive macro
// In practice, use serde::Serialize, thiserror::Error, etc.
use std::fmt;
// Custom derive macro (defined in a separate crate)
// #[derive(Debug)] is a built-in derive macro
#[derive(Debug)]
struct ScanResult {
path: String,
threats_found: u32,
duration_ms: u64,
}
// Attribute macro example (simulated)
fn main() {
let result = ScanResult {
path: String::from("/tmp/test"),
threats_found: 0,
duration_ms: 150,
};
// #[derive(Debug)] generated this implementation
println!("Scan result: {:?}", result);
println!("Path: {}", result.path);
}
Expected output:
Scan result: ScanResult { path: "/tmp/test", threats_found: 0, duration_ms: 150 }
Path: /tmp/test
Building a Declarative Test Macro
Macros enable concise test definitions.
macro_rules! test_module {
($name:ident, $($test_name:ident: $test_body:expr),* $(,)?) => {
mod $name {
use super::*;
$(pub fn $test_name() { $test_body })*
}
};
}
test_module!(math_tests,
test_add: { assert_eq!(2 + 2, 4); },
test_multiply: { assert_eq!(3 * 3, 9); },
test_divide: { assert_eq!(10 / 2, 5); },
);
fn main() {
println!("Running math tests...");
math_tests::test_add();
math_tests::test_multiply();
math_tests::test_divide();
println!("All tests passed!");
}
Expected output:
Running math tests...
All tests passed!
Hygiene and Macro Best Practices
Rust macros are hygienic â they cannot accidentally capture variables from the calling scope.
macro_rules! hygienic_add {
($a:expr, $b:expr) => {
{
let result = $a + $b; // 'result' does not conflict with outer scope
result
}
};
}
fn main() {
let result = 5; // This is a separate variable
let sum = hygienic_add!(10, 20);
println!("Outer result: {}, Sum: {}", result, sum);
}
Expected output:
Outer result: 5, Sum: 30
Common Mistakes
1. Missing Dollar Signs in Repetition
Forgetting the $ prefix in repetition patterns causes confusing errors. Always use $($pattern),* syntax.
2. Macro Ambiguity
macro_rules! patterns can be ambiguous. Use distinct delimiters or add unique syntax to avoid matching the wrong pattern.
3. Overusing Macros When Functions Suffice
If a macro does not need to generate new syntax or work with multiple types, prefer a generic function. Macros are harder to debug and maintain.
4. Forgetting to Export Macros
Macros defined in one module must be marked #[macro_export] to be usable from other modules or crates.
5. Not Handling Trailing Commas
User expectations: trailing commas should work. Use $($pattern),* $(,)? to accept optional trailing commas.
Practice Questions
1. What is the difference between declarative and procedural macros?
Declarative macros (macro_rules!) match patterns and generate code. Procedural macros are Rust functions that operate on token streams, used for #[derive], attribute macros, and function-like macros.
2. What does macro hygiene mean? Hygiene means a macro cannot accidentally capture or interfere with variables from the calling scope. Generated identifiers are unique and do not collide with user code.
3. When would you use a macro instead of a generic function? When you need to: generate new items (structs, functions), work with syntax not representable as types, or implement custom derives.
4. What is the #[macro_export] attribute for? It makes a macro available to other modules and crates. Without it, the macro is only usable within the defining module.
5. Challenge: Write a macro_rules! macro called bitflags! that generates a struct with constants for each flag, supporting OR, AND, and NOT operations.
Mini Project: Builder Macro
macro_rules! define_config {
($struct_name:ident, host: $host:ty, port: $port:ty, timeout: $timeout:ty) => {
struct $struct_name {
host: $host,
port: $port,
timeout: $timeout,
}
struct ConfigBuilder {
host: Option<$host>,
port: Option<$port>,
timeout: Option<$timeout>,
}
impl ConfigBuilder {
fn new() -> Self {
ConfigBuilder { host: None, port: None, timeout: None }
}
fn host(mut self, v: $host) -> Self { self.host = Some(v); self }
fn port(mut self, v: $port) -> Self { self.port = Some(v); self }
fn timeout(mut self, v: $timeout) -> Self { self.timeout = Some(v); self }
fn build(self) -> Result<$struct_name, &'static str> {
Ok($struct_name {
host: self.host.ok_or("host required")?,
port: self.port.ok_or("port required")?,
timeout: self.timeout.ok_or("timeout required")?,
})
}
}
};
}
define_config!(Config, host: String, port: u16, timeout: u64);
builder!(Config {
host: String,
port: u16,
timeout: u64,
});
fn main() {
let config = ConfigBuilder::new()
.host("localhost".into())
.port(8080)
.timeout(30)
.build()
.unwrap();
println!("Config: {}:{}, timeout={}", config.host, config.port, config.timeout);
}
FAQ
Related Concepts
What's Next
Learn Rust FFI for calling C code, and Testing & Documentation for testing macro-generated code.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro