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 scopeA 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!?