Declarative Macros in Rust

C…
crusty.rustacean
Published on November 17, 2025

Some notes from Claude about declarative macros in Rust

notes
meta-programming rust macros declarative-macros
0 likes

Core Concept

Macros are code that writes code at compile time. They use pattern matching on Rust syntax to generate repetitive code.

Basic Structure

macro_rules! macro_name {
    (pattern) => {
        // code to generate
    };
}

Fragment Specifiers

  • $name:ident - matches an identifier (function name, variable name)
  • $name:expr - matches any expression (literals, variables, function calls, blocks)
  • $name:tt - matches any token tree (most flexible)

Key Insight: Everything is an Expression

A single number like 5 is an expression because it evaluates to itself. This is why $x:expr can match literals, variables, or complex expressions.

The Process

  1. Write boilerplate 2-3 times - solve the problem first
  2. Notice the pattern - what varies vs what stays the same
  3. Extract to macro - capture the varying parts as parameters
  4. Use multiple match arms - handle different patterns

Example: Static File Handler

Started with repetitive functions:

pub async fn get_css_file() -> impl IntoResponse {
    let css_file = Asset::get("styles.css").unwrap();
    let contents = std::str::from_utf8(css_file.data.as_ref())
        .unwrap()
        .to_string();

    Response::builder()
        .status(StatusCode::OK)
        .header("content-type", "text/css; charset=utf-8")
        .body(contents)
        .unwrap()
}

Extracted to macro with multiple patterns:

macro_rules! static_file_handler {
    // Text files
    ($fn_name:ident, $filename:expr, $content_type:expr, text) => {
        pub async fn $fn_name() -> impl IntoResponse {
            let asset = Asset::get($filename).unwrap();
            let contents = std::str::from_utf8(asset.data.as_ref())
                .unwrap()
                .to_string();

            Response::builder()
                .status(StatusCode::OK)
                .header("content-type", $content_type)
                .body(contents)
                .unwrap()
        }
    };
    
    // Binary files
    ($fn_name:ident, $filename:expr, $content_type:expr, binary) => {
        pub async fn $fn_name() -> impl IntoResponse {
            let asset = Asset::get($filename).unwrap();
            let contents = asset.data.as_ref().to_vec();

            Response::builder()
                .status(StatusCode::OK)
                .header("content-type", $content_type)
                .body(Body::from(contents))
                .unwrap()
        }
    };
}

Usage:

static_file_handler!(get_css_file, "styles.css", "text/css; charset=utf-8", text);
static_file_handler!(get_scripts_file, "scripts.js", "text/javascript", text);
static_file_handler!(get_image_file, "favicon.png", "image/png", binary);

When to Use Macros

  • You’ve copy-pasted similar code 2-3 times
  • The pattern is clear: some parts vary, most stays the same
  • The varying parts can be captured as parameters
  • Don’t try to design macros upfront - write the boilerplate first

Types of Macros

Declarative macros (macro_rules!):

  • Pattern matching on tokens
  • 80% of macro use cases
  • What we learned here

Procedural macros (for later):

  • Derive macros: #[derive(Debug)]
  • Function-like macros: sqlx::query!()
  • Attribute macros: #[tokio::main]
  • More complex, require separate crate
  • Learn these after you’re comfortable with declarative macros

Build Up, Then Sand Down

Same principle as learning other Rust concepts:

  1. Write working code (even if repetitive)
  2. Notice patterns
  3. Refactor with macros
  4. Don’t try to be perfect upfront
C…
crusty.rustacean

Comments

Loading comments...