Rust Macros Explained â Declarative & Procedural Macro Development
In this tutorial, you'll learn about Rust Macros Explained. We cover key concepts, practical examples, and best practices.
Master Rust macros: declarative macros with macro_rules!, pattern matching and repetition, procedural macros for derive, attribute, and function-like macros, and debugging techniques.
What You'll Learn
In this tutorial, you'll master Rust macros â writing declarative macros with macro_rules! using pattern matching and repetition operators, building procedural macros for derive, attribute, and function-like use cases, debugging macro expansion, and knowing when a macro is the right tool vs a regular function.
Why It Matters
Macros are Rust's metaprogramming facility. They let you write code that writes code â eliminating boilerplate, creating embedded DSLs, and implementing traits automatically. macro_rules! covers 90% of use cases without external dependencies. Procedural macros handle the remaining 10% where you need access to the token stream for derive, attribute, or custom syntax.
Real-World Use
The serde crate uses derive macros for Serialize and Deserialize. Tokio's #[tokio::main] attribute macro sets up the async runtime. The thiserror crate uses a derive macro to generate Display and Error implementations. Durga Antivirus Pro uses a custom #[scan_handler] attribute macro to register scan event callbacks without boilerplate.
flowchart TD
subgraph "Macro Types"
A[macro_rules!] --> B[Declarative]
A --> C[Procedural]
end
subgraph "Declarative"
D[match patterns] --> E[capture fragments]
E --> F[expand to code]
end
subgraph "Procedural"
G[TokenStream in] --> H[TokenStream out]
H --> I[Custom derive, attribute, function-like]
end
Prerequisites: Macros basics and Traits & Generics. Understanding Rust pattern matching helps.
Declarative Macros: Pattern Matching and Expansion
macro_rules! uses pattern matching on the Rust token tree. Each arm has a pattern and a transcriber:
macro_rules! vec_of {
($($x:expr),* $(,)?) => {
{
let mut v = Vec::new();
$(
v.push($x);
)*
v
}
};
}
macro_rules! hash_map {
($($key:expr => $value:expr),* $(,)?) => {
{
let mut m = std::collections::HashMap::new();
$(
m.insert($key, $value);
)*
m
}
};
}
macro_rules! measure {
($label:expr, $code:expr) => {
{
let start = std::time::Instant::now();
let result = { $code };
println!("{}: {:?}", $label, start.elapsed());
result
}
};
}
fn main() {
let v = vec_of![1, 2, 3, 4, 5];
println!("vec_of: {:?}", v);
let m = hash_map!["a" => 1, "b" => 2, "c" => 3];
println!("hash_map: {:?}", m);
let sum = measure!("sum loop", {
let mut s = 0u64;
for i in 0..100_000 {
s += i;
}
s
});
println!("Sum: {}", sum);
}
Expected output:
vec_of: [1, 2, 3, 4, 5]
hash_map: {"a": 1, "b": 2, "c": 3}
sum loop: ... (duration depends on hardware)
Sum: 4999950000
Repetition Operators and Recursion
Macros support * (zero or more), + (one or more), ? (zero or one), and can be recursive:
macro_rules! json_value {
(null) => {
"null"
};
(true) => {
"true"
};
(false) => {
"false"
};
([$($elem:tt), "* $(",)?]) => {
concat!("[", $(&json_value!($elem)), "* "",", "]",)
};
({$($key:tt : $value:tt),* $(,)?}) => {
concat!("{", $("", $key, ":", json_value!($value)),* "}", )
};
($e:expr) => {
stringify!($e)
};
}
macro_rules! build_struct {
($name:ident { $($field:ident: $ty:ty),* $(,)? }) => {
struct $name {
$(
pub $field: $ty,
)*
}
impl $name {
fn new($($field: $ty),*) -> Self {
Self { $($field),* }
}
}
};
}
build_struct!(User {
name: String,
email: String,
age: u32,
});
fn main() {
let user = User::new("Alice".into(), "alice@example.com".into(), 30);
println!("User created: {} ({}), age {}", user.name, user.email, user.age);
// Demonstrate recursive macro
let nested = json_value!({"name": "test", "values": [1, 2, 3]});
println!("JSON: {}", nested);
}
Expected output:
User created: Alice (alice@example.com), age 30
JSON: {name:testvalues:[1,2,3]}
Macro hygiene: Declarative macros in Rust are hygienic â names introduced in the macro expansion do not conflict with names in the calling scope. Use $crate:: for paths to avoid resolution issues.
Procedural Macros: Derive and Attribute
Procedural macros operate on token streams. A derive macro example using syn and quote:
// This would be in a separate crate (proc_macro crate)
// For demonstration, showing the concept
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(IntoString)]
pub fn into_string_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let expanded = quote! {
impl std::convert::From<#name> for String {
fn from(val: #name) -> String {
format!("{:?}", val)
}
}
};
expanded.into()
}
// Since proc macros require a separate crate, here's a
// simulated version with macro_rules!
macro_rules! impl_display {
($type:ty, $fmt:expr) => {
impl std::fmt::Display for $type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, $fmt, self)
}
}
};
}
#[derive(Debug)]
struct ScanResult {
path: String,
threats: u32,
}
impl_display!(ScanResult, "Scanned {}: {} threats found", path = self.path, threats = self.threats);
// Custom implementation matching the intended expansion
impl std::fmt::Display for ScanResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Scanned {}: {} threats found", self.path, self.threats)
}
}
fn main() {
let result = ScanResult {
path: "/etc/passwd".into(),
threats: 0,
};
println!("{}", result);
// Demonstrate string conversion via our macro's output
let s = format!("{:?}", result);
println!("Debug: {}", s);
}
Expected output:
Scanned /etc/passwd: 0 threats found
Debug: ScanResult { path: "/etc/passwd", threats: 0 }
Debugging Macro Expansion
Techniques for understanding what your macro produces:
macro_rules! debug_expand {
($name:ident, $body:expr) => {
#[allow(unused)]
mod $name {
fn check() {
let _ = $body;
}
}
};
}
macro_rules! trace_expand {
($macro:ident!($($args:tt)*)) => {
{
let expanded = stringify!($macro!($($args)*));
eprintln!("Expanded: {}", expanded);
$macro!($($args)*)
}
};
}
// Use cargo expand to see macro output
// Run: cargo expand --test macro_tests
macro_rules! compute {
($a:expr, $op:tt, $b:expr) => {
match $op {
"+" => $a + $b,
"-" => $a - $b,
"*" => $a * $b,
"/" => $a / $b,
_ => panic!("Unknown op: {}", $op),
}
};
}
fn main() {
// Use trace_expand to debug
let result = compute!(10, "+", 20);
println!("10 + 20 = {}", result);
// For real debugging: cargo expand or rustc -Z unstable-options --pretty expanded
println!("Macro debugging tips:");
println!("1. cargo expand");
println!("2. rustc -Z unstable-options --pretty expanded");
println!("3. log_syntax!()");
println!("4. trace_macros!(true)");
}
Expected output:
10 + 20 = 30
Macro debugging tips:
1. cargo expand
2. rustc -Z unstable-options --pretty expanded
3. log_syntax!()
4. trace_macros!(true)
Common Errors
1. Macro repetition mismatches
macro_rules! bad {
($x:expr, $y:ident) => { /* ... */ };
}
bad!(1, 2, 3); // ERROR: repetition mismatch
2. Metavariable without repetition
macro_rules! bad {
($($x:expr),*) => { $x } // ERROR: $x used outside repetition
}
3. Procedural macro in wrong crate type
// proc-macro crate must have: [lib] proc-macro = true
4. macro_rules! macro name conflicts
macro_rules! vec { ... } // May conflict with std::vec!
5. Missing $crate prefix in macro
macro_rules! my_macro {
() => { vec![1, 2, 3] }; // Wrong: should use $crate::vec!
}
Practice Questions
1. What is the difference between declarative and procedural macros?
Declarative (macro_rules!) match patterns in the token tree and expand to code â no external dependencies needed. Procedural macros receive a TokenStream and return a TokenStream â they require a separate proc-macro crate, syn, and quote.
2. What does the $() repetition operator do?*
$() wraps a template that repeats. * means zero or more repetitions, + means one or more, ? means zero or one. Each captured group variable must be used within the matching repetition depth.
3. How do you export macros from a library?
Use #[macro_export] before macro_rules! to make the macro available when the crate is imported. Without it, the macro is private to the crate. Procedural macros are always public through their #[proc_macro] annotation.
4. What tools help debug macro expansion?
cargo expand shows the expanded code. trace_macros!(true) prints expansion steps during compilation. log_syntax!() prints tokens during expansion. eprintln! inside proc macros for debugging.
5. Challenge: Write a <a href="/design-patterns/builder/">builder</a>! macro that generates a builder pattern for a struct. Given struct Point { x: i32, y: i32 }, calling <a href="/design-patterns/builder/">builder</a>!(Point).x(10).y(20).build() should create Point { x: 10, y: 20 }.
Mini Project: Bitfield Accessor Macro
macro_rules! bitfield {
($struct_name:ident { $($field:ident: $start:literal..=$end:literal,)* }) => {
struct $struct_name(u32);
impl $struct_name {
fn new(value: u32) -> Self {
$struct_name(value)
}
fn raw(&self) -> u32 {
self.0
}
$(
fn $field(&self) -> u32 {
let mask = (1u32 << ($end - $start + 1)) - 1;
(self.0 >> $start) & mask
}
fn set_$field(&mut self, value: u32) {
let mask = (1u32 << ($end - $start + 1)) - 1;
self.0 = (self.0 & !(mask << $start)) | ((value & mask) << $start);
}
)*
}
};
}
bitfield!(StatusRegister {
ready: 0..=0,
error: 1..=1,
mode: 2..=4,
count: 5..=15,
})
fn main() {
let mut sr = StatusRegister::new(0);
println!("Initial: ready={}, error={}", sr.ready(), sr.error());
sr.set_ready(1);
sr.set_mode(3);
sr.set_count(42);
println!("After set: ready={}, error={}, mode={}, count={}",
sr.ready(), sr.error(), sr.mode(), sr.count());
println!("Raw value: 0x{:08X}", sr.raw());
}
Expected output:
Initial: ready=0, error=0
After set: ready=1, error=0, mode=3, count=42
Raw value: 0x00002185
FAQ
Related Concepts
What's Next
Test your macros with Testing in Rust, or explore how macros can optimize runtime performance in Performance Optimization.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro