DEV Community

Cover image for Rust Macros: Declarative vs Procedural
Sumana
Sumana

Posted on

Rust Macros: Declarative vs Procedural

Rust macros often feel intimidating at first, especially if you’re coming from traditional object-oriented or scripting languages. But macros in Rust exist for one simple reason: to eliminate boilerplate and enable powerful compile-time code generation.

Once you understand why macros exist and the two types Rust provides, they become an essential tool rather than a scary one.


Why Rust Has Macros

Functions in Rust are used to reuse behavior.
Macros, on the other hand, are used to reuse syntax and structure.

Macros:

  • Run at compile time
  • Generate Rust code
  • Have zero runtime cost

This makes them ideal for patterns that functions simply cannot express.


Declarative Macros (macro_rules!)

Declarative macros are the first type of macros most Rust developers encounter.

macro_rules! create_function {
    ($name:ident) => {
        fn $name() {
            println!("Hello from {}", stringify!($name));
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

Using the macro:

create_function!(hello);
Enter fullscreen mode Exit fullscreen mode

At compile time, this expands to:

fn hello() {
    println!("Hello from hello");
}
Enter fullscreen mode Exit fullscreen mode

Key Characteristics

  • Pattern-based (ident, expr, etc.)
  • Simple and predictable
  • Excellent for reducing repetitive code

Generating Multiple Functions with Macros

Declarative macros become more powerful with repetition.

macro_rules! generate_functions {
    ($($name:ident),*) => {
        $(
            fn $name() {
                println!("Hello from {}", stringify!($name));
            }
        )*
    };
}
Enter fullscreen mode Exit fullscreen mode

Usage:

generate_functions!(foo, bar, baz);
Enter fullscreen mode Exit fullscreen mode

This single macro call generates multiple functions automatically.

This kind of code generation is impossible with normal functions, which is exactly why macros exist.


When to Use Declarative Macros

Use macro_rules! when:

  • You’re repeating similar code patterns
  • You need compile-time code generation
  • Functions are not expressive enough

Rule of thumb:

If a function can solve the problem, use a function.
Use macros only when you need syntax generation.


Procedural Macros (Macros You Use Every Day)

Even if you’ve never written a macro, you’ve already used procedural macros.

A common example is Serde:

#[derive(Serialize, Deserialize)]
struct User {
    username: String,
    age: u32,
}
Enter fullscreen mode Exit fullscreen mode

This #[derive] is a procedural macro.

What it does:

  • Reads your struct at compile time
  • Generates serialization and deserialization logic
  • Eliminates massive amounts of boilerplate

Unlike declarative macros, procedural macros:

  • Work on Rust’s syntax tree (AST)
  • Are more powerful
  • Are commonly used by frameworks and libraries

Attribute Macros in Practice

#[serde(rename = "user_name")]
username: String,
Enter fullscreen mode Exit fullscreen mode

This tells Serde how fields should appear in JSON without changing your Rust code.

The behavior is injected at compile time through macros, not runtime logic.


Serialization and Deserialization: Why Macros Matter

Serialization converts Rust data into a transferable format like JSON.
Deserialization converts it back into Rust types.

Rust struct → JSON → Client / DB / File
JSON → Rust struct → Business logic
Enter fullscreen mode Exit fullscreen mode

Rust structs cannot be sent directly across systems. Serialization is how Rust communicates with the outside world.

A Real-World Edge Case

JSON does not support NaN.

score: f64::NAN
Enter fullscreen mode Exit fullscreen mode

This will fail during serialization, even though Rust itself allows it.
Sometimes the limitation isn’t Rust or macros—it’s the data format.


Why Rust’s Macro Design Works

Rust macros:

  • Reduce boilerplate
  • Enforce consistency
  • Move complexity to compile time
  • Keep runtime code clean and fast

Declarative macros help you write less code.
Procedural macros help libraries write code for you.


Final Takeaway

  • macro_rules! → write your own compile-time patterns
  • Procedural macros (derive, attributes) → use powerful library-generated code
  • Macros exist to solve problems functions cannot
  • Serialization is how Rust crosses system boundaries

Top comments (0)