🎓 A1Learner
🔍

Rust Macros

A macro is a stencil for code. It writes code at compile time, following a pattern.

The pain: the same line again and again

Without a macro, each greeting is written out in full. Three greetings mean three copies of the same line:

fn main() {
    println!("Hello, Ana!");
    println!("Hello, Bo!");
    println!("Hello, Cy!");
}

A pattern macro in two lines

macro_rules! names the stencil. The call replaces itself with the code inside it:

macro_rules! say_hello { () => { println!("Hello!"); }; }
say_hello!();

Repeat for a comma list

$( ... ),* reads a comma-separated list, and the same shape in the transcriber writes one copy per piece:

macro_rules! list { ($($x:expr),*) => { vec![$($x),*] }; }
let nums = list!(1, 2, 3);
println!("nums = {:?}", nums);

Define before use

A macro must sit above its call. Move greet!() above the definition and the same program fails:

fn main() { greet!(); }
macro_rules! greet { () => { println!("hi"); }; }
// error: cannot find macro `greet` in this scope

A macro's own variable stays its own

The let inside this expansion is invisible outside it. Code after the call cannot use value — the name simply does not exist there:

macro_rules! set_ten {
    () => { let value = 10; };
}
// set_ten!();
// println!("{}", value); // Error: cannot find value `value`

#[macro_use] lends a module's macros

The attribute on the mod line is what makes hello! visible down in main. Delete that one line and the same program fails with error: cannot find macro `hello` in this scope.

#[macro_use]
mod greet {
    macro_rules! hello {
        () => { println!("hello from a macro") };
    }
}

fn main() {
    hello!();
}

Example

macro_rules! say_hello { () => { println!("Hello from a macro!"); }; ($name:expr) => { println!("Hello, {}!", $name); }; }
fn main() { say_hello!(); say_hello!("Ferris"); }

Key points

  • Procedural macros come in three forms: derive (#[derive(X)]), attribute (#[tokio::main]), and function-like (sql!(...))
  • Expansion comes first.
  • A macro is replaced by its generated code before the compiler reads the meaning of the code.
  • So a macro can write a trait impl.
  • No function call can do that.
  • Macros earn their place in three jobs.
  • They remove repeated code (DRY).
  • They invent a small notation for one job (a DSL).
  • And they accept a variable number of arguments (a variadic interface).
  • Rust functions take a fixed number of arguments — there are no variadic functions — so a flexible call like println! with any number of values must be a macro
  • Hygiene does not cover items and paths.
  • A type or function a macro writes is a real item.
  • Names in paths resolve where the macro is used.
  • That is why exported macros spell their own crate's helpers as $crate::helper.
  • Reach for macros last.
  • A plain function or a generic covers most reuse, with clearer errors.
  • Rust has two kinds of macros, simple pattern macros and procedural macros.
  • A function works with values at run time.
  • println! and vec! are macros.

In this example, say_hello is defined with two pattern arms. The first arm matches a call with nothing inside the parentheses, and expands to a plain greeting. The second arm captures an expression as $name, and expands to a greeting using that value. main calls say_hello!() and then say_hello!("Ferris"). The output shows both lines: Hello from a macro! and Hello, Ferris!. All of this matching and writing happened while compiling — by run time, only ordinary println! calls remain.

Note

Macros earn their keep when the shape of the code itself must vary — like println! accepting any number of arguments.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

What does $name:expr mean in macro_rules!?