Start now →

Rust Macros: A Way for Writing Code That Writes Code

By Yashwant Sanjay Saste · Published April 12, 2026 · 31 min read · Source: Blockchain Tag
Ethereum
Rust Macros: A Way for Writing Code That Writes Code

Rust Macros: A Way for Writing Code That Writes Code

Yashwant Sanjay SasteYashwant Sanjay Saste24 min read·Just now

--

From your first println! to building your own DSL — everything you need to understand, use, and master Rust macros.

Press enter or click to view image in full size

The Exclamation Mark That Started It All

The first time most developers encounter Rust, they type something like this:

fn main() {
println!("Hello, Rust!");
}

It works. It prints. You feel good. But then, maybe a few hours later, you pause and think:Wait. Why does println have an exclamation mark? Is that a typo?

That little ! is one of the most important symbols in Rust. It's not an accident, it's not excitement, and it's definitely not a typo. It's Rust's way of telling you:

“This is not a regular function. This is a macro.”

And once you understand what that means — once you really get why that ! exists — a whole new dimension of Rust programming opens up.

1. What Are Macros in Rust?

A macro in Rust is a way to write code that generates other code.

That might sound abstract, so let’s use an analogy.

Imagine you’re a baker who makes sandwiches. A regular function is like a trained baker — you give them bread and fillings, and they make you a sandwich. The baker follows a fixed process. You can’t change how they work, just what you give them.

A macro is like a recipe template generator. Instead of making a sandwich, it writes a new recipe for you. You describe the pattern, and the macro creates the actual instructions that will then be followed. Before any baking (running) happens, the recipe (code) is already written.

In Rust terms:

Your source code

[Macro Expansion] ← happens here, before compilation

Expanded source code

[Compilation]

Binary / Executable

This is the key insight. When you write println!("Hello"), the compiler doesn't just call a function named println. It first expands that macro into a block of real Rust code — a whole set of formatting instructions — and then compiles that expanded code.

You never see the expanded code unless you go looking for it. It happens silently, before your eyes.

That’s why macros feel a little magical at first. The code you write is not quite the code that gets compiled. Macros are the bridge between the two.

2. Why Rust Needs Macros

If Rust is so powerful, why aren’t regular functions enough?

Great question. Let’s look at some concrete limitations.

Problem 1: Functions Can’t Accept a Variable Number of Arguments

In many languages, you can write something like:

print("a", "b", "c", "d")  # Python handles this fine

In Rust, a function must have a fixed, typed signature. You cannot define a regular function that accepts one, two, three, or ten arguments of potentially different types. Rust’s strict type system simply doesn’t allow it.

But this works perfectly fine:

println!("{} {} {} {}", "a", "b", "c", "d");

Why? Because println! is a macro. It doesn't have a fixed signature — it's a pattern matcher that generates the right code depending on whatever you pass it.

Problem 2: Functions Can’t Inspect Their Own Syntax

Macros receive tokens — the raw text of your code — before type checking. This lets them manipulate syntax itself.

Consider vec!:

let v = vec![1, 2, 3, 4, 5];

This creates a Vec<i32> initialized with five elements. To replicate this as a function, you'd need something clunky:

let v = Vec::from([1, 2, 3, 4, 5]);  // A bit verbose and less flexible

The vec! macro is cleaner, more flexible, and can handle different types automatically. You couldn't write it as a regular function without losing clarity.

Problem 3: Functions Can’t Reduce Boilerplate at a Structural Level

Imagine you’re implementing a trait for twenty different types. Every implementation looks nearly identical — just the type name changes. With a macro, you can write the pattern once and stamp it out for all twenty types automatically.

Problem 4: Functions Can’t Create Domain-Specific Languages

Some of the most powerful uses of macros are in creating mini-languages embedded inside Rust:

route!("/home" => home_handler);
route!("/login" => login_handler, method = POST);

This kind of expressive, readable syntax isn’t possible with regular functions. Macros let you define your own syntax rules.

3. Macro vs Function: A Clear Comparison

Before diving into how to write macros, let’s make sure the macro vs. function distinction is crystal clear.

Side-by-Side Example

Here’s a simple add function:

fn add(a: i32, b: i32) -> i32 {
a + b
}
let result = add(3, 4); // works fine

And here’s a macro that does something similar:

macro_rules! add {
($a:expr, $b:expr) => {
$a + $b
};
}
let result = add!(3, 4); // also works

They look similar, but there are deep differences:

Press enter or click to view image in full size

When to Use Which

Use a function when:

Use a macro when:

4. Declarative Macros: macro_rules!

There are two broad types of macros in Rust:

  1. Declarative macros — defined with macro_rules!
  2. Procedural macros — more advanced, defined as separate crates

We’ll spend most of our time on declarative macros because they’re where every Rust developer starts. They’re powerful, they’re approachable, and they cover 80% of macro use cases.

Your First Macro

macro_rules! say_hello {
() => {
println!("Hello from a macro!");
};
}
fn main() {
say_hello!(); // prints: Hello from a macro!
}

Let’s dissect every symbol:

macro_rules! say_hello {
│ │
│ └── The name of the macro (used as say_hello!)
└── The keyword that defines a declarative macro
() => {
│ │
│ └── The expansion block: what code gets generated
└── The pattern to match against (empty here - no arguments)
println!("Hello from a macro!");
};

└── Semicolon ends this match arm (like a match arm in Rust's match expression)
}

The structure of macro_rules! is essentially a series of pattern => expansion arms, very similar to Rust's match expression.

When you write say_hello!(), Rust looks at the argument list — in this case, nothing — and finds the arm whose pattern matches. It then replaces your macro call with the content of the expansion block.

After expansion, the compiler sees:

fn main() {
println!("Hello from a macro!"); // ← this is what the compiler actually compiles
}

You wrote say_hello!(). The compiler compiled println!(...). The macro was the transformation in between.

Multiple Arms

Just like match, macro_rules! can have multiple arms:

macro_rules! greet {
() => {
println!("Hello, stranger!");
};
($name:ident) => {
println!("Hello, {}!", stringify!($name));
};
}
fn main() {
greet!(); // Hello, stranger!
greet!(Alice); // Hello, Alice!
}

Rust checks each arm from top to bottom and uses the first one that matches.

5. Fragment Specifiers: The Grammar of Macros

This is one of the most important sections in this article. Read it carefully.

When you write a macro that accepts input, you need to tell Rust what kind of thing you’re accepting. These are called fragment specifiers, and they’re written after the : in a pattern like $x:expr.

Think of fragment specifiers as types for macro patterns. Just as i32 tells Rust "this function parameter is an integer," expr tells Rust "this macro parameter is an expression."

Let’s go through all the major ones.

expr — An Expression

macro_rules! double {
($x:expr) => {
$x * 2
};
}
let result = double!(5 + 3); // expands to: (5 + 3) * 2 = 16

Matches any Rust expression: a literal, a function call, a math operation, a closure, a block — basically anything that produces a value.

Common confusion: expr captures the whole expression as a unit. So double!(5 + 3) expands to (5 + 3) * 2, not 5 + 3 * 2. The expression is preserved intact.

ident — An Identifier

macro_rules! create_variable {
($name:ident, $value:expr) => {
let $name = $value;
};
}
create_variable!(my_var, 42);
// expands to: let my_var = 42;
println!("{}", my_var); // prints: 42

Matches any valid Rust identifier: variable names, function names, type names, etc. It does not match keywords like fn or let.

Common confusion: Beginners sometimes try to use $name:expr when they want a variable name. Use ident for names, expr for values.

ty — A Type

macro_rules! create_vec_of_type {
($t:ty) => {
Vec::<$t>::new()
};
}
let v: Vec<i32> = create_vec_of_type!(i32);

Matches a Rust type — i32, String, Option<bool>, Vec<u8>, etc.

Use case: Generating code for specific types, like implementing traits.

stmt — A Statement

macro_rules! log_and_run {
($s:stmt) => {
println!("Running statement...");
$s
};
}
log_and_run!(let x = 5);

Matches a complete statement: a let binding, an expression statement, etc.

Use case: Wrapping statements with before/after logic (like timing or logging).

block — A Block of Code

macro_rules! time_it {
($b:block) => {
let start = std::time::Instant::now();
$b
println!("Elapsed: {:?}", start.elapsed());
};
}
time_it!({
let mut sum = 0;
for i in 0..1000 { sum += i; }
println!("Sum: {}", sum);
});

Matches a { ... } block. Very useful for wrapping arbitrary code.

tt — A Token Tree

macro_rules! echo {
($($t:tt)*) => {
println!("{}", stringify!($($t)*))
};
}
echo!(hello world this is anything);

The most flexible specifier. tt matches a single token or a grouped token tree (anything inside (), [], or {}). Using $($t:tt)* captures everything.

Use case: When you don’t know what the user will pass, or you want to forward tokens to another macro.

pat — A Pattern

macro_rules! match_value {
($val:expr, $p:pat => $body:expr) => {
match $val {
$p => $body,
_ => println!("no match"),
}
};
}
match_value!(Some(42), Some(x) => println!("Got {}", x));

Matches a Rust pattern (like the left side of a match arm).

item — A Rust Item

macro_rules! export_item {
($item:item) => {
pub $item
};
}
export_item!(fn greet() { println!("hi"); });
// expands to: pub fn greet() { println!("hi"); }

Matches a top-level Rust item: a function definition, struct, enum, trait, impl block, etc.

path — A Path

macro_rules! use_type {
($p:path) => {
let _: $p;
};
}
use_type!(std::collections::HashMap::<String, i32>);

Matches a Rust path like std::io::Error, MyModule::MyType, etc.

meta — A Meta Item (Used in Attributes)

macro_rules! apply_attr {
(#[$m:meta] $item:item) => {
#[$m]
$item
};
}

Matches the content inside an attribute like #[derive(Debug)] — the derive(Debug) part is a meta.

lifetime — A Lifetime

macro_rules! with_lifetime {
($lt:lifetime, $t:ty) => {
&$lt $t
};
}

Matches a Rust lifetime like 'a, 'static, '_.

vis — A Visibility Modifier

macro_rules! make_field {
($v:vis $name:ident: $t:ty) => {
$v $name: $t
};
}

Matches visibility keywords: pub, pub(crate), pub(super), or nothing (private).

literal — A Literal Value

macro_rules! announce {
($lit:literal) => {
println!("The value is: {}", $lit);
};
}
announce!(42);
announce!("hello");
announce!(3.14);

Matches literal values only: numbers, strings, booleans, characters. Unlike expr, it won't match expressions like 1 + 2.

6. Macros with Parameters

Now that you understand fragment specifiers, let’s look at how to capture and use parameters in macros.

Basic Parameter Capture

macro_rules! square {
($x:expr) => {
$x * $x
};
}
let result = square!(5); // 25
let result2 = square!(2 + 3); // (2 + 3) * (2 + 3) = 25

The $x is a metavariable (sometimes called a macro variable). The $ prefix distinguishes it from regular Rust variables. You can reuse $x as many times as you want in the expansion.

Multiple Parameters

macro_rules! max {
($a:expr, $b:expr) => {
if $a > $b { $a } else { $b }
};
}
let m = max!(10, 20); // 20
let m2 = max!(3 * 4, 2 + 9); // 12 vs 11, so 12

Parameters with Different Types

macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("Function {:?} called", stringify!($func_name));
}
};
}
create_function!(foo);
create_function!(bar);
fn main() {
foo(); // Function "foo" called
bar(); // Function "bar" called
}

Here, $func_name:ident captures an identifier, which is then used as a function name in the expansion. This is genuinely code generating code — we defined two functions by calling the macro twice.

7. Multiple Pattern Arms in Detail

Macros support multiple arms, which lets them behave differently based on what they receive — similar to function overloading in other languages.

macro_rules! describe {
() => {
println!("Nothing was passed.");
};
($x:expr) => {
println!("Got one thing: {:?}", $x);
};
($x:expr, $y:expr) => {
println!("Got two things: {:?} and {:?}", $x, $y);
};
($x:expr, $y:expr, $z:expr) => {
println!("Got three things: {:?}, {:?}, and {:?}", $x, $y, $z);
};
}
describe!();           // Nothing was passed.
describe!(42); // Got one thing: 42
describe!(1, 2); // Got two things: 1 and 2
describe!(1, 2, 3); // Got three things: 1, 2, and 3

Rust checks arms in order, from top to bottom, and uses the first match. This means you should generally put more specific patterns before more general ones.

Comparing to match

This multi-arm structure mirrors Rust’s match expression:

// Rust match
match value {
0 => println!("zero"),
1 => println!("one"),
_ => println!("other"),
}
// Macro with multiple arms
macro_rules! my_macro {
(zero) => { println!("zero") };
(one) => { println!("one") };
($x:expr) => { println!("other: {}", $x) };
}

Both are pattern-based. The key difference is that match runs at runtime and matches against values; macro_rules! arms are checked at compile time and match against token patterns.

8. Repetition and Variadic Patterns

This is where macros get truly powerful — and where beginners sometimes get lost. Take your time with this section.

The magic of println! accepting any number of arguments, or vec! accepting any number of elements, comes from repetition patterns.

The Three Repetition Operators

Operator Meaning * Zero or more + One or more ? Zero or one (optional)

These look just like regex quantifiers, and they work similarly.

Zero or More: *

macro_rules! sum {
($($x:expr),*) => {
{
let mut total = 0;
$(total += $x;)*
total
}
};
}
let s0 = sum!(); // 0
let s1 = sum!(5); // 5
let s2 = sum!(1, 2, 3); // 6
let s3 = sum!(10, 20, 30, 40); // 100

Let’s break down ($($x:expr),*):

(          ← outer delimiter for this arm's pattern
$( ← start of repetition group
$x:expr ← capture one expression as $x
), ← the separator between repetitions (a comma)
* ← repeat zero or more times
)

And in the expansion, $(total += $x;)* means "repeat the statement total += $x; once for each captured $x."

One or More: +

macro_rules! first {
($head:expr $(, $tail:expr)+) => {
$head
};
}
// Must have at least 2 elements
let f = first!(1, 2, 3); // 1
// first!(); ← compile error! + requires at least one occurrence

Use + when you need at least one element. Using * would allow empty input.

Optional: ?

macro_rules! greet {
($name:expr $(, $greeting:expr)?) => {
match (Some($name), ${count($greeting)}) {
_ => {
println!("Hello, {}!", $name);
}
}
};
}

The ? operator is a bit more subtle. It's often used for optional trailing syntax or optional configuration parameters.

A cleaner example:

macro_rules! config {
($key:ident $(= $val:expr)?) => {
// $val is either present or not
};
}
config!(debug); // no value
config!(timeout = 30); // with value

The Separator

In $($x:expr),*, the comma between ) and * is the separator. It appears between repetitions but not after the last one.

So sum!(1, 2, 3) matches correctly: 1, then , 2, then , 3. The comma is the separator, not part of the expression itself.

What about trailing commas? sum!(1, 2, 3,) — that last comma would fail! The separator appears between elements, not after the last one.

To handle trailing commas, you can use a common trick:

macro_rules! sum {
($($x:expr),* $(,)?) => {
{
let mut total = 0;
$(total += $x;)*
total
}
};
}
let s = sum!(1, 2, 3,); // Now this works!

The $(,)? at the end matches an optional trailing comma.

9. Separator Handling and Edge Cases

Edge cases are where most beginner macro writers run into trouble. Let’s go through them systematically.

The Trailing Comma Problem

macro_rules! sum_basic {
($($x:expr),*) => { /* ... */ };
}
sum_basic!(1, 2, 3); // ✅ works
sum_basic!(1, 2, 3,); // ❌ compile error: unexpected token ','

The trailing comma is one of the most common issues. Fix it with $(,)?:

macro_rules! sum_fixed {
($($x:expr),* $(,)?) => { /* ... */ };
}
sum_fixed!(1, 2, 3); // ✅ works
sum_fixed!(1, 2, 3,); // ✅ also works now

This pattern is so common that it’s essentially considered best practice for any variadic macro.

The Empty Invocation

sum!()     // uses * → expands to total = 0; returns 0 ✅
sum!() // uses + → compile error! + needs at least one ❌

Choose * if empty is valid, + if you need at least one.

Nested Repetition

You can have repetition inside repetition, which is powerful but tricky:

macro_rules! matrix {
($([$($x:expr),*]),*) => {
vec![$(vec![$($x),*]),*]
};
}
let m = matrix!([1, 2, 3], [4, 5, 6], [7, 8, 9]);
// creates: vec![vec![1,2,3], vec![4,5,6], vec![7,8,9]]

The outer $( ... ),* iterates over rows. The inner $( $x:expr ),* iterates over elements within each row.

Important rule: In nested repetition, you can only use a variable in the repetition depth where it was captured. You can’t mix inner and outer variables carelessly.

Ambiguity in Patterns

Rust’s macro matcher can sometimes become ambiguous. For example, if you have an arm that matches $x:expr and another that matches $x:expr, $y:expr, the matcher needs to know when to stop. Rust handles this with a concept called FOLLOW sets — certain tokens can follow certain fragment types.

For instance, after $x:expr, only certain tokens are allowed (like ,, ;, =>, |, etc.). If you try:

macro_rules! bad {
($x:expr $y:expr) => { /* ... */ }; // ❌ ambiguous!
}

The compiler will complain because there’s no separator between $x and $y, making it impossible to know where one ends and the other begins.

10. Nested Macros: Macros Calling Macros

Macros can call other macros — both standard ones like println! and custom ones you define.

macro_rules! log {
($msg:expr) => {
println!("[LOG] {}", $msg);
};
}
macro_rules! debug_value {
($name:ident, $val:expr) => {
log!(format!("{} = {:?}", stringify!($name), $val));
};
}
fn main() {
let x = 42;
debug_value!(x, x); // [LOG] x = 42
}

Here, debug_value! expands and the expansion itself contains a call to log!, which then gets expanded further. The expansion happens in rounds: Rust keeps expanding until no more macros remain.

Expansion Order

Macros are expanded from the outside in. If a! expands to b!(), then b! is expanded next. This continues until all macros are resolved.

The stringify! Macro

Notice stringify!($name) in the example. This is a built-in macro that converts its argument to a string literal at compile time. So stringify!(x) becomes "x". It's incredibly useful inside other macros.

Readability Warning

Deeply nested macros quickly become hard to read and debug. If you find yourself with three or four levels of nesting, consider whether some of the inner logic should be a function instead.

11. Scope and Visibility

Unlike functions and types, macros have their own scoping rules that confuse many beginners.

Local Scope

By default, a macro is available from the point of definition to the end of the current scope:

fn main() {
macro_rules! local_macro {
() => { println!("local!") };
}
local_macro!(); // ✅ works here
{
local_macro!(); // ✅ works in inner scope too
}
}
// local_macro!(); // ❌ can't use here - out of scope

Module Scope

Macros defined at the module level are not automatically available in other modules, even child modules:

mod my_module {
macro_rules! inner_macro {
() => { println!("inner") };
}
pub fn use_it() {
inner_macro!(); // ✅ works here
}
}
fn main() {
// inner_macro!(); // ❌ not accessible here
my_module::use_it(); // ✅ works
}

#[macro_export]

To make a macro available throughout your crate (and to users of your crate), mark it with #[macro_export]:

#[macro_export]
macro_rules! my_public_macro {
() => { println!("exported!") };
}

When #[macro_export] is used, the macro is placed at the crate root, not in the module where it was defined. This is an important detail — even if you define it inside my_module, it's exported as if it were at the crate root.

Using Macros from Other Crates

If you depend on a crate that exports macros, you can use them with:

use some_crate::some_macro;
// or to get all macros:
use some_crate::*;

Since Rust 2018 edition, you no longer need #[macro_use] on external crates for most purposes (though it still exists for legacy compatibility).

12. Macro Hygiene: The Invisible Superpower

Hygiene is one of Rust’s most underrated macro features. It’s what prevents your macros from accidentally breaking the code around them.

The Problem It Solves

Imagine a macro that creates a temporary variable named result. Now imagine someone uses that macro inside a function that already has a variable called result. In many languages, the macro would accidentally shadow or corrupt the outer result.

Let’s see what could go wrong without hygiene:

// Hypothetically, without hygiene:
let result = 100;
some_macro!(); // internally creates `let result = 42;`
println!("{}", result); // Would this print 100 or 42?

This is the classic variable capture problem. It causes subtle, hard-to-debug bugs.

How Rust Handles It

Rust’s macro system is hygienic by default. Variables introduced inside a macro expansion exist in their own distinct scope — they cannot accidentally collide with variables in the calling code.

macro_rules! add_one {
($x:expr) => {{
let result = $x + 1; // This `result` is private to the macro
result
}};
}
let result = 100;             // This `result` belongs to the outer code
let y = add_one!(5); // The macro's `result` is different
println!("{}", result); // Still prints 100 ← hygiene!

The two result variables live in different "universes" as far as the compiler is concerned. They have the same text name, but they're fundamentally separate bindings.

When You Want to Escape Hygiene

Sometimes you want a macro to define a variable that the caller can use. This requires using $name:ident patterns where the caller provides the name:

macro_rules! assign_double {
($name:ident, $val:expr) => {
let $name = $val * 2;
};
}
assign_double!(my_result, 21);
println!("{}", my_result); // 42

By using $name:ident and having the caller pass my_result, you're letting the calling code name the variable. This is intentional and hygienic — the caller chose the name.

13. Built-in Standard Macros

Before writing your own, it’s worth understanding the macros you use every day. They’re all built with macro_rules! (or procedural macros) and follow the same rules.

println! and print!

println!("Hello");
println!("Hello, {}!", "world");
println!("{:?}", vec![1, 2, 3]);
println!("{name} is {age}", name = "Alice", age = 30);

println! is a variadic macro. It takes a format string plus any number of arguments. Behind the scenes, it uses Rust's formatting infrastructure (std::fmt) to generate a string and write it to stdout.

The first argument must be a string literal (not a variable) because the macro inspects it at compile time to generate the right formatting code.

format!

let s = format!("Hello, {}! You are {} years old.", "Bob", 25);
// s is now a String

Same as println! but returns a String instead of printing it. Extremely common in real-world Rust.

vec!

let v1: Vec<i32> = vec![];           // empty
let v2 = vec![1, 2, 3, 4, 5]; // initialized with values
let v3 = vec![0; 10]; // ten zeros

The vec! macro has multiple arms. The second form with ; uses repetition to initialize all elements with the same value — something you couldn't express as a simple function.

assert! and assert_eq!

assert!(2 + 2 == 4);
assert!(2 + 2 == 4, "Math is broken!");
assert_eq!(2 + 2, 4);
assert_eq!(2 + 2, 4, "Expected {} but got {}", 4, 2 + 2);

assert! is special because when it fails, it shows you the code you wrote, not just the values. That's only possible at compile time — a regular function couldn't inspect its own source.

assert_eq! goes further: when it fails, it formats both values so you see:

thread 'main' panicked at 'assertion `left == right` failed
left: 5
right: 4'

panic!

panic!("Something went terribly wrong");
panic!("Expected {}, got {}", expected, actual);

Immediately terminates the current thread with a message. The macro version is needed because it supports formatting — you can include dynamic values in the panic message.

dbg!

let x = 5;
let y = dbg!(x * 2) + 1; // prints: [src/main.rs:2] x * 2 = 10
// y is now 11

A beginner’s best friend. dbg! prints the source expression and its value to stderr, then returns the value. Unlike println!, dbg! shows you the file and line number automatically. It's non-destructive — you can wrap any expression with it without changing the logic.

14. Real-World Practical Use Cases

Let’s look at some real patterns you’ll encounter in production Rust code.

Logging System

macro_rules! log {
(ERROR, $($msg:tt)*) => {
eprintln!("[ERROR] {}", format!($($msg)*));
};
(WARN, $($msg:tt)*) => {
eprintln!("[WARN] {}", format!($($msg)*));
};
(INFO, $($msg:tt)*) => {
println!("[INFO] {}", format!($($msg)*));
};
(DEBUG, $($msg:tt)*) => {
if cfg!(debug_assertions) {
println!("[DEBUG] {}", format!($($msg)*));
}
};
}
log!(INFO, "Server started on port {}", 8080);
log!(ERROR, "Failed to connect: {}", error_message);
log!(DEBUG, "Processing item #{}", item_id);

This gives you a flexible, level-aware logging system in about 15 lines.

Custom Assertions for Testing

macro_rules! assert_between {
($val:expr, $low:expr, $high:expr) => {
assert!(
$val >= $low && $val <= $high,
"Expected {} to be between {} and {}, but it wasn't",
$val, $low, $high
);
};
}
#[test]
fn test_temperature() {
let temp = compute_temperature();
assert_between!(temp, -10.0, 40.0);
}

Builder-Style API

macro_rules! request {
($method:ident $url:expr) => {
Request::new(Method::$method, $url)
};
($method:ident $url:expr, body = $body:expr) => {
Request::new(Method::$method, $url).with_body($body)
};
($method:ident $url:expr, headers = { $($key:expr => $val:expr),* }) => {
{
let mut req = Request::new(Method::$method, $url);
$(req.add_header($key, $val);)*
req
}
};
}
let r1 = request!(GET "/api/users");
let r2 = request!(POST "/api/users", body = json_data);
let r3 = request!(GET "/api/users", headers = { "Auth" => "Bearer token", "Accept" => "application/json" });

Testing Helpers

macro_rules! test_case {
($name:ident: $input:expr => $expected:expr) => {
#[test]
fn $name() {
assert_eq!(process($input), $expected);
}
};
}
test_case!(empty_input: "" => None);
test_case!(single_item: "hello" => Some("hello".to_string()));
test_case!(with_spaces: " hi " => Some("hi".to_string()));

This generates three separate test functions with clean names — no copy-paste required.

15. DSLs Using Macros

One of the most exciting applications of macros is building Domain-Specific Languages (DSLs) — mini-languages tailored to a specific problem.

A DSL is not a full programming language. It’s a set of readable, declarative syntax forms that make expressing solutions in a particular domain feel natural.

Why Macros Are Perfect for DSLs

Functions constrain you to Rust’s function-call syntax. Macros let you define your own syntax forms. This means you can create APIs that read almost like prose.

A Routing DSL

macro_rules! route {
($path:literal => $handler:expr) => {
Router::add(Method::GET, $path, $handler)
};
($method:ident $path:literal => $handler:expr) => {
Router::add(Method::$method, $path, $handler)
};
}
route!("/home" => home_handler);
route!(POST "/login" => login_handler);
route!(DELETE "/users/:id" => delete_user_handler);

An HTML Template DSL

macro_rules! html {
(head { title: $title:literal }) => {
format!("<head><title>{}</title></head>", $title)
};
(p { $content:expr }) => {
format!("<p>{}</p>", $content)
};
(div { $($child:expr)* }) => {
format!("<div>{}</div>", vec![$($child),*].join(""))
};
}
let page = html!(div {
html!(p { "Hello world" })
html!(p { "This is Rust-generated HTML" })
});

An Event System DSL

macro_rules! on_event {
($event:ident => $handler:block) => {
EventBus::subscribe(stringify!($event), |_| $handler);
};
($event:ident ($data:ident: $ty:ty) => $handler:block) => {
EventBus::subscribe(stringify!($event), |raw| {
let $data: $ty = raw.downcast().unwrap();
$handler
});
};
}
on_event!(user_joined => {
println!("Someone joined!");
});
on_event!(message_received(msg: String) => {
println!("Got message: {}", msg);
});

DSL macros make Rust code read more like specifications than implementations. They’re a key reason why Rust frameworks can feel elegant despite Rust’s verbosity in raw form.

16. Procedural Macros: The Next Level

Everything we’ve covered so far has been declarative macros (macro_rules!). There's a more powerful type called procedural macros that work differently.

Instead of pattern-matching token trees, procedural macros are full Rust programs that:

  1. Accept a stream of tokens as input
  2. Process those tokens using Rust code
  3. Return a new stream of tokens as output

There are three kinds:

1. Derive Macros

These are attached to structs and enums to automatically implement traits.

#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}

The #[derive(Debug)] attribute triggers a procedural macro that reads your struct's definition and generates a Debug implementation for it — something that would require many lines of boilerplate by hand.

You can write your own derive macros. The popular serde crate uses this to auto-generate serialization/deserialization code:

#[derive(Serialize, Deserialize)]
struct Config {
host: String,
port: u16,
}

With just one line, serde generates hundreds of lines of custom serialization code.

2. Attribute Macros

These can be placed on any item and transform it:

#[tokio::main]
async fn main() {
// async code here
}

#[tokio::main] is an attribute macro that wraps your main function with Tokio's async runtime setup. Without it, you'd need to write boilerplate to initialize the runtime yourself.

Another common one:

#[route(GET, "/")]
async fn index(req: HttpRequest) -> impl Responder {
"Hello!"
}

Frameworks like Actix-web use attribute macros to register route handlers cleanly.

3. Function-Like Procedural Macros

These look like macro_rules! calls but are backed by Rust code:

let sql = sql!(SELECT * FROM users WHERE id = 1);

A function-like procedural macro can parse the SQL string at compile time, validate it, and generate optimized Rust code — catching SQL errors before your program even runs.

When to Use Procedural vs. Declarative

Use macro_rules! when:

Use procedural macros when:

17. Common Beginner Mistakes

Learning from mistakes saves time. Here are the ones that trip up almost every new Rust macro writer.

Mistake 1: Using a Macro When a Function Would Do

Macros add complexity. If your macro doesn’t need variadic arguments, custom syntax, or compile-time code generation, write a function instead.

// ❌ Don't do this
macro_rules! add_numbers {
($a:expr, $b:expr) => { $a + $b };
}
// ✅ Just write this
fn add_numbers(a: i32, b: i32) -> i32 { a + b }

Mistake 2: Wrong Fragment Specifier

// ❌ Wrong: using expr when you need ident
macro_rules! define_fn {
($name:expr) => {
fn $name() {} // ← error! expr can't be used as a function name
};
}
// ✅ Correct: use ident
macro_rules! define_fn {
($name:ident) => {
fn $name() {}
};
}

When you want a name, use ident. When you want a value, use expr. Mixing these up is the most common fragment specifier mistake.

Mistake 3: Forgetting the Trailing Comma Pattern

Not including $(,)? means your macro will fail for users who habitually add trailing commas:

// ❌ Will reject: my_macro!(a, b, c,)
macro_rules! my_macro {
($($x:expr),*) => { /* ... */ };
}
// ✅ Accept trailing commas gracefully
macro_rules! my_macro {
($($x:expr),* $(,)?) => { /* ... */ };
}

Mistake 4: Unexpected Expansion Behavior with Expressions

macro_rules! double_call {
($x:expr) => {
$x + $x // ← $x is expanded TWICE
};
}
let mut n = 0;
let result = double_call!({ n += 1; n }); // n is incremented TWICE!
// result is 1 + 2 = 3, not 2!

If $x is a side-effectful expression, expanding it multiple times causes the side effects to run multiple times. Fix this by storing in a temporary:

macro_rules! double_call {
($x:expr) => {{
let val = $x; // evaluate once
val + val
}};
}

Mistake 5: Infinite Recursion

// ❌ This will cause a compile error (recursion limit reached)
macro_rules! forever {
() => { forever!() };
}

Macros can call themselves recursively (useful for processing lists), but you must always have a base case that stops the recursion.

// ✅ Recursive macro with base case
macro_rules! count_items {
() => { 0 }; // base case
($first:expr $(, $rest:expr)*) => {
1 + count_items!($($rest),*) // recursive case
};
}
let c = count_items!(a, b, c, d); // 4

Mistake 6: Confusing Macro Scope

Macros defined in a child module are not visible in the parent module unless exported. Beginners often define a macro in a module and wonder why it’s not accessible elsewhere.

18. Best Practices

When to Use Macros

When Not to Use Macros

Naming Conventions

Documentation

Always document macros, especially if they’re public. The /// doc comment works on macros just like on functions:

/// Creates a HashMap from key-value pairs.
///
/// # Example
/// ```
/// let map = map!{ "a" => 1, "b" => 2 };
/// assert_eq!(map["a"], 1);
/// ```
#[macro_export]
macro_rules! map {
($($key:expr => $val:expr),* $(,)?) => {
{
let mut m = std::collections::HashMap::new();
$(m.insert($key, $val);)*
m
}
};
}

Testing Macros

You can test macros just like functions using #[test]:

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum_macro_empty() {
assert_eq!(sum!(), 0);
}
#[test]
fn test_sum_macro_multiple() {
assert_eq!(sum!(1, 2, 3, 4, 5), 15);
}
#[test]
fn test_sum_trailing_comma() {
assert_eq!(sum!(1, 2, 3,), 6);
}
}

Debugging with cargo expand

The most useful tool for debugging macros is cargo-expand. Install it with:

cargo install cargo-expand

Then run:

cargo expand

This shows you your entire source file after all macros have been expanded. When a macro isn’t behaving as expected, expanding it is the fastest way to see what’s actually being generated.

What to Practice Next

  1. Rebuild vec! from scratch. Write a macro that takes comma-separated expressions and creates a Vec. Add support for trailing commas and the [value; count] form.
  2. Write a hashmap! macro. Accept key => value pairs and produce a HashMap.
  3. Create a test_cases! macro. Generate multiple test functions from a compact table of inputs and expected outputs.
  4. Build a simple logging macro. Support log levels (INFO, WARN, ERROR) and format strings.
  5. Explore cargo expand. Take any macro from the standard library, put it in your code, and expand it to see what it generates.

Suggested Mini Projects

Recommended Next Topics

Once you’re comfortable with declarative macros, the natural next step is procedural macros — writing derive macros, building your own #[derive(MyTrait)]. The syn and quote crates are the tools of choice.

From there, explore how major Rust libraries use macros in practice:

Rust macros are one of those features that, once you understand them, you start seeing opportunities everywhere. They’re the reason Rust can be both low-level and expressive — the kind of language where you can define your own ergonomic abstractions without paying runtime costs.

The ! was never just punctuation. It was always an invitation.

This article was originally published on Blockchain Tag and is republished here under RSS syndication for informational purposes. All rights and intellectual property remain with the original author. If you are the author and wish to have this article removed, please contact us at [email protected].

NexaPay — Accept Card Payments, Receive Crypto

No KYC · Instant Settlement · Visa, Mastercard, Apple Pay, Google Pay

Get Started →