Macros you'll read
Going further · Lesson 51
A macro is code that writes code. The compiler runs it, puts what it produced where the call was, and only then checks types. You met the macros you call every day in Macros, attributes and naming. This page is for reading the others: a macro_rules! block in a dependency, a #[derive(...)] that writes code you never see, an attribute such as #[tokio::main]. You will read far more macros than you write.
Coming from GoGo has no macros. map[string]int{...} is syntax the language provides. Rust has no map literal, and the hashmap! macro below fills the gap. go generate is the nearest idea: it runs a program you wrote and leaves source files behind. A Rust macro runs inside the compiler whenever the crate is compiled and leaves nothing in your source tree.
Coming from KotlinKotlin has no macros either: mapOf("a" to 1) is an ordinary function. Code generators for Kotlin, such as KSP, are compiler plugins. Rust's derive and attribute macros fill that role, and the ones you add yourself (serde's, for example) arrive as a crate in Cargo.toml.
Coming from PythonPython has no macro stage: ordinary code cannot rewrite other code before it runs. @dataclass rewrites a class when the module is imported; #[derive] writes its code while the program compiles, so a mistake is reported before anything runs.
Coming from TypeScriptTypeScript has no macros. A Babel plugin is the nearest mechanism: it rewrites code in a build step, outside the language, for the whole build. Decorators run when the program runs. A Rust macro is part of the compiler's own pipeline and runs only where the code names it.
Coming from JavaAn annotation is metadata, and an annotation processor reads it inside javac while your code compiles and generates code. #[derive] and attribute macros are the nearest match, and macro_rules! has no Java counterpart at all.
Coming from C#A source generator runs inside the C# compiler and adds code, as a derive macro does. A macro_rules! macro has no C# counterpart, and [CallerArgumentExpression] covers one small job macros do, capturing the text of an argument.
Coming from CA #define substitutes tokens before the compiler parses anything, which is where SQUARE(x) x*x goes wrong. macro_rules! works on parsed pieces of code (an expr, a ty), and the variables a macro makes do not collide with yours.
Coming from C++Templates and constexpr do much of what you would reach for a macro to do, and Rust's generics do the same jobs. What templates cannot do (generate a function whose name you pass in, capture an expression's text) is the work of macros.
The quick comparison
use std::collections::HashMap;
// A macro: a pattern on the left of `=>`, the code it becomes on the right.macro_rules! hashmap { ($($key:expr => $value:expr),* $(,)?) => {{ let mut map = HashMap::new(); $(map.insert($key, $value);)* map }};}
fn main() { let ages = hashmap! { "Ada" => 36, "Linus" => 28, "Grace" => 45 };
let mut names: Vec<_> = ages.keys().collect(); names.sort(); for name in names { println!("{name}: {}", ages[name]); } println!("entries: {}", ages.len());}Ada: 36 Grace: 45 Linus: 28 entries: 3
package main
import ( "fmt" "sort")
func main() { // A map literal is part of the language: no macro needed ages := map[string]int{"Ada": 36, "Linus": 28, "Grace": 45}
names := make([]string, 0, len(ages)) for name := range ages { names = append(names, name) } sort.Strings(names) for _, name := range names { fmt.Printf("%s: %d\n", name, ages[name]) } fmt.Println("entries:", len(ages))}Ada: 36 Grace: 45 Linus: 28 entries: 3
fun main() { // mapOf is an ordinary function; `to` makes a pair val ages = mapOf("Ada" to 36, "Linus" to 28, "Grace" to 45)
for (name in ages.keys.sorted()) { println("$name: ${ages[name]}") } println("entries: ${ages.size}")}Ada: 36 Grace: 45 Linus: 28 entries: 3
# A dict literal is part of the language: no macro neededages = {"Ada": 36, "Linus": 28, "Grace": 45}
for name in sorted(ages): print(f"{name}: {ages[name]}")print(f"entries: {len(ages)}")Ada: 36 Grace: 45 Linus: 28 entries: 3
// Object and Map literals are part of the language: no macro neededconst ages = new Map([ ["Ada", 36], ["Linus", 28], ["Grace", 45],]);
for (const name of [...ages.keys()].sort()) { console.log(`${name}: ${ages.get(name)}`);}console.log(`entries: ${ages.size}`);Ada: 36 Grace: 45 Linus: 28 entries: 3
import java.util.Map;import java.util.TreeSet;
public class Main { public static void main(String[] args) { // Map.of is an ordinary method; the language has no map literal Map<String, Integer> ages = Map.of("Ada", 36, "Linus", 28, "Grace", 45);
for (String name : new TreeSet<>(ages.keySet())) { System.out.println(name + ": " + ages.get(name)); } System.out.println("entries: " + ages.size()); }}Ada: 36 Grace: 45 Linus: 28 entries: 3
// A collection initializer is part of the language: no macro neededvar ages = new Dictionary<string, int> { ["Ada"] = 36, ["Linus"] = 28, ["Grace"] = 45 };
foreach (var name in ages.Keys.Order()){ Console.WriteLine($"{name}: {ages[name]}");}Console.WriteLine($"entries: {ages.Count}");Ada: 36 Grace: 45 Linus: 28 entries: 3
#include <stdio.h>#include <stdlib.h>#include <string.h>
// C has no map type and no macro-free literal for one: an array of pairsstruct Entry { const char *name; int age;};
static int by_name(const void *a, const void *b) { return strcmp(((const struct Entry *)a)->name, ((const struct Entry *)b)->name);}
int main(void) { struct Entry ages[] = {{"Ada", 36}, {"Linus", 28}, {"Grace", 45}}; size_t count = sizeof ages / sizeof ages[0];
qsort(ages, count, sizeof ages[0], by_name); for (size_t i = 0; i < count; i++) { printf("%s: %d\n", ages[i].name, ages[i].age); } printf("entries: %zu\n", count); return 0;}Ada: 36 Grace: 45 Linus: 28 entries: 3
#include <iostream>#include <map>#include <string>
int main() { // An initializer list is part of the language: no macro needed std::map<std::string, int> ages{{"Ada", 36}, {"Linus", 28}, {"Grace", 45}};
for (const auto &[name, age] : ages) { // std::map iterates in key order std::cout << name << ": " << age << "\n"; } std::cout << "entries: " << ages.size() << "\n"; return 0;}Ada: 36 Grace: 45 Linus: 28 entries: 3
What's different
- A name followed by
!is a macro call, and it may use(),[]or{}. The rules of the macro decide what may go inside, sovec![1, 2]andhashmap! { "a" => 1 }are different grammars. macro_rules!is a list of rules, each(pattern) => { code }. To read a call, find the first rule whose pattern fits, then swap the pieces the pattern captured into the code.$x:exprcaptures one expression,$name:identa name,$t:tya type, and$( ... ),*repeats whatever is inside it, separated by commas.- Derives and attributes such as
#[tokio::main]are macros too. Those from a crate are Rust functions that read and write tokens, in a crate of their own. - A function is the right tool until it is not. A macro is for what a function cannot do: take a list of any length, generate items, capture the text of an expression.
A macro call has the syntax its author gave it
Looks fine: in Go, Kotlin, Python, TypeScript, Java, C#, C and C++ it runs fine. Rust won't compile it.
Works in Go. In Rust it's just a rule.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.Works in TypeScript. In Rust it's just a rule.Works in Java. In Rust it's just a rule.Works in C#. In Rust it's just a rule.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.use std::collections::HashMap;
macro_rules! hashmap { ($($key:expr => $value:expr),* $(,)?) => {{ let mut map = HashMap::new(); $(map.insert($key, $value);)* map }};}
fn main() { let ages = hashmap! { "Ada": 36, "Linus": 28 }; println!("entries: {}", ages.len());}package main
import "fmt"
func main() { ages := map[string]int{"Ada": 36, "Linus": 28} fmt.Println("entries:", len(ages))}entries: 2
fun main() { val ages = mapOf("Ada" to 36, "Linus" to 28) println("entries: ${ages.size}")}entries: 2
ages = {"Ada": 36, "Linus": 28}print("entries:", len(ages))entries: 2
const ages = { Ada: 36, Linus: 28 };console.log("entries:", Object.keys(ages).length);entries: 2
import java.util.Map;
public class Main { public static void main(String[] args) { Map<String, Integer> ages = Map.of("Ada", 36, "Linus", 28); System.out.println("entries: " + ages.size()); }}entries: 2
var ages = new Dictionary<string, int> { ["Ada"] = 36, ["Linus"] = 28 };Console.WriteLine($"entries: {ages.Count}");entries: 2
#include <stdio.h>
struct Entry { const char *name; int age;};
int main(void) { struct Entry ages[] = {{"Ada", 36}, {"Linus", 28}}; printf("entries: %zu\n", sizeof ages / sizeof ages[0]); return 0;}entries: 2
#include <iostream>#include <map>#include <string>
int main() { std::map<std::string, int> ages{{"Ada", 36}, {"Linus", 28}}; std::cout << "entries: " << ages.size() << "\n"; return 0;}entries: 2
What does the compiler say? Guess first, then open.
error: no rules expected `:` --> src/main.rs:12:32 | 3 | macro_rules! hashmap { | -------------------- when calling this macro ... 12 | let ages = hashmap! { "Ada": 36, "Linus": 28 }; | ^ no rules expected this token in macro call | note: while trying to match `=>` --> src/main.rs:4:18 | 4 | ($($key:expr => $value:expr),* $(,)?) => {{ | ^^
Go runs this. A map literal is part of the language, and key: value is how its entries are written.
Kotlin runs this, but the pairs are written "Ada" to 36: mapOf is a function and to makes a pair. There is no colon form.
Python runs this. A dict literal is part of the language, with the colon.
TypeScript runs this. An object literal takes key: value. A Map has no literal and takes an array of pairs instead.
Java runs this, with Map.of("Ada", 36, ...), an ordinary method that takes keys and values in turn. Java has no map literal.
C# runs this. A collection initializer is part of the language and takes ["Ada"] = 36 entries.
C runs this, with an array of structs: the standard library has no map type to write a literal for.
C++ runs this. A std::map takes a list of {key, value} pairs through an initializer list.
hashmap! is not part of the language. It has one rule, and that rule says each pair is key => value. A : fits no rule, so the compiler stops at it.
The macro's own patterns decide what a call may look like. The error names the token that fits no rule and points at what the macro was trying to match when it gave up. When you meet a macro you do not know, its documentation or its macro_rules! block is where the grammar is written.
The fix
Compiles and runs.
use std::collections::HashMap;
macro_rules! hashmap { ($($key:expr => $value:expr),* $(,)?) => {{ let mut map = HashMap::new(); $(map.insert($key, $value);)* map }};}
fn main() { let ages = hashmap! { "Ada" => 36, "Linus" => 28 }; println!("entries: {}", ages.len());}entries: 2
The toolkit
Macros, attributes and naming taught you to call macros such as println! and vec!. This page is for reading the others: a macro_rules! block in a dependency, the #[derive(...)] above a struct, the #[tokio::main] above main. The aim is to open one, say what it accepts and what it becomes, and know when a function would do. Outputs: Linux on 64-bit ARM, Rust 1.99.1, a debug build.
A macro runs inside the compiler. It reads the tokens of your call (the words, numbers and symbols of source, such as x, + or () and writes new tokens in place of the call. The Book says “macros are expanded before the compiler interprets the meaning of the code” (chapter 20.5), so a macro can produce what a function cannot, such as a trait implementation.
Four shapes of macro
| What you see | What it is | Examples |
|---|---|---|
name!(...), name![...], name!{...} |
Declarative: macro_rules!, written in ordinary Rust |
vec!, println! |
| the same | Procedural, function-like: a Rust function over tokens, in a crate of its own | make_answer! in the procedural section |
#[derive(Name)] |
Derive: a procedural macro; the built-in ones ship with the compiler | Debug, serde’s Serialize (serde) |
#[name], #[path::name(...)] |
Attribute: a procedural macro, or built into the compiler | #[tokio::main] (tokio), #[test] |
To tell which you are looking at:
- A
!after a name is a function-like macro, and the brackets are not an array or an index:vec![1, 2]is a call. What it accepts is written in its docs or itsmacro_rules!block (vec!shows its declaration). #[derive(A, B)]calls one derive macro per name.- Another
#[...]is built into the compiler (#[inline],#[cfg(...)],#[test]) or an attribute macro from a crate, which a path such astokio::maingives away. - An attribute inside a derived type, such as
#[serde(rename = "x")]or#[default], is a helper attribute: the derive reads it.
Reading a macro_rules! block
A macro_rules! block is a list of rules, like the arms of a match, except the patterns match source code. Each rule is (pattern) => { code }. This one follows std’s vec! without its [x; n] form:
// The rules of std's vec!, without the `[x; n]` formmacro_rules! my_vec { () => { Vec::new() }; ($($x:expr),+ $(,)?) => {{ let mut v = Vec::new(); $(v.push($x);)+ v }};}
fn main() { let empty: Vec<i32> = my_vec![]; let nums = my_vec![1, 2 + 3, 4,]; let words = my_vec!["a", "b"]; println!("empty: {empty:?}"); println!("nums: {nums:?}"); println!("words: {words:?}");}empty: [] nums: [1, 5, 4] words: ["a", "b"]
To read a call, find the first rule whose pattern fits. my_vec![] fits the first, Vec::new(). For my_vec![1, 2 + 3, 4,] the second fits:
$x:exprcaptures one whole expression, so2 + 3is one capture, not three tokens.$( ... ),+means “one or more, separated by commas”;*is zero or more and?zero or one, so$(,)?allows a trailing comma.$(v.push($x);)+is written once per capture.- The doubled braces: the outer pair delimits the code, the inner pair makes a block, so the expansion is one expression.
The $name:kind part is a fragment specifier, the kind of code to capture (all of them).
| Specifier | Captures | Example |
|---|---|---|
expr |
an expression | 1 + 2 * x.len() |
ident |
a name | total |
ty |
a type | Vec<&str> |
pat |
a pattern | Some(1 | 2) |
literal |
a literal, with an optional - |
-3.5 |
tt |
one token, or one bracketed group | + or (b, c) |
block stmt item path lifetime meta vis |
rarer: look them up | - |
macro_rules! show_expr { ($e:expr) => { stringify!($e) };}macro_rules! show_ident { ($i:ident) => { stringify!($i) };}macro_rules! show_ty { ($t:ty) => { stringify!($t) };}macro_rules! show_pat { ($p:pat) => { stringify!($p) };}macro_rules! show_literal { ($l:literal) => { stringify!($l) };}macro_rules! count_tt { ($($t:tt)*) => { [$(stringify!($t)),*].len() };}
fn main() { println!("expr: {}", show_expr!(1 + 2 * x.len())); println!("ident: {}", show_ident!(total)); println!("ty: {}", show_ty!(Vec<&str>)); println!("pat: {}", show_pat!(Some(1 | 2))); println!("literal: {}", show_literal!(-3.5)); println!( "tt count of `a + (b, c) [d] 4`: {}", count_tt!(a + (b, c) [d] 4) );}expr: 1 + 2 * x.len() ident: total ty: Vec<&str> pat: Some(1 | 2) literal: -3.5 tt count of `a + (b, c) [d] 4`: 5
$($t:tt)* takes any tokens and hands them on untouched, which is how println! passes its arguments on. Its second rule is in the std source (macros.rs): ($($arg:tt)*) => {{ $crate::io::_print($crate::format_args_nl!($($arg)*)); }}. $crate stands for the crate the macro was written in, so the expansion works wherever it is called.
Other things you will see
| You see | It means |
|---|---|
#[macro_export] |
The macro is available from the crate’s root path, so other crates can use it. |
A leading ::, as in ::std::collections::HashMap |
An absolute path: not something of the same name in the caller’s scope. |
A pattern starting with @inner |
An internal rule, by convention: the macro calls itself with that marker. Read the public rules first (Little Book of Rust Macros). |
Seeing an expansion
rustc -Zunpretty=expanded prints code after expansion, the gcc -E of Rust. Unlike gcc -E, only the nightly compiler accepts the flag. RUSTC_BOOTSTRAP=1 makes the stable compiler accept it, as the program below shows. That is an escape hatch for reading code in a scratch directory: the rustc developer guide says the variable “should never be used except when bootstrapping the compiler” (guide), so keep it out of a project you ship. cargo expand wraps the same flag; it is not installed in the runner, so it is not run. The Rust in the program is only a harness; read the output.
use std::process::{Command, Output};
const SOURCE: &str = r#"use std::collections::HashMap;
macro_rules! hashmap { ($($key:expr => $value:expr),* $(,)?) => {{ let mut map = HashMap::new(); $(map.insert($key, $value);)* map }};}
macro_rules! sum { ($x:expr) => { $x }; ($head:expr, $($tail:expr),+) => { $head + sum!($($tail),+) };}
fn main() { let ages = hashmap! { "Ada" => 36, "Linus" => 28 }; let total = sum!(1, 2, 3); println!("{} entries, total {total}", ages.len());}"#;
// The Rust below is only a harness: it saves SOURCE and asks rustc for the expansion.// -Zunpretty=expanded is a nightly flag; RUSTC_BOOTSTRAP=1 makes this stable compiler accept it.fn rustc(bootstrap: bool) -> Output { let path = std::env::temp_dir().join("expand-demo.rs"); std::fs::write(&path, SOURCE).unwrap(); let mut command = Command::new("rustc"); command .args(["--edition", "2024", "-Zunpretty=expanded"]) .arg(&path); if bootstrap { command.env("RUSTC_BOOTSTRAP", "1"); } let out = command.output().unwrap(); std::fs::remove_file(&path).unwrap(); out}
fn main() { let refused = String::from_utf8_lossy(&rustc(false).stderr).into_owned(); println!( "without RUSTC_BOOTSTRAP: {}", refused.lines().next().unwrap() );
let text = String::from_utf8_lossy(&rustc(true).stdout).into_owned(); // The macro definitions stay in the output; the interesting part is `main`. let main_at = text.find("fn main()").unwrap(); println!( "with RUSTC_BOOTSTRAP=1, from `fn main`:\n{}", &text[main_at..] );}without RUSTC_BOOTSTRAP: error: the option `Z` is only accepted on the nightly compiler
with RUSTC_BOOTSTRAP=1, from `fn main`:
fn main() {
let ages =
{
let mut map = HashMap::new();
map.insert("Ada", 36);
map.insert("Linus", 28);
map
};
let total = 1 + (2 + 3);
{
::std::io::_print(format_args!("{0} entries, total {1}\n", ages.len(),
total));
};
}hashmap! became insert calls in a block. sum!(1, 2, 3) became 1 + (2 + 3): the second rule’s $head + sum!(...) was expanded again for the tail. println! became a call to ::std::io::_print with a format_args!, as its rules said, and {total} became argument 1.
Hygiene
Macros, attributes and naming showed that a macro’s variable does not collide with the caller’s. The Reference calls this “mixed-site hygiene”: locals and labels belong to where the macro was written. So a macro’s locals are hidden from the caller:
macro_rules! make { () => { let local = 5; };}
fn main() { make!(); println!("local = {}", local);}error[E0425]: cannot find value `local` in this scope --> src/main.rs:9:28 | 9 | println!("local = {}", local); | ^^^^^ not found in this scope | help: an identifier with the same name is defined here, but is not accessible due to macro hygiene --> src/main.rs:3:13 | 3 | let local = 5; | ^^^^^ ... 8 | make!(); | ------- in this macro invocation = note: this error originates in the macro `make` (in Nightly builds, run with -Z macro-backtrace for more info)
Why a macro and not a function
A function signature “must declare the number and type of parameters the function has” (the Book):
fn sum(values: &[i32]) -> i32 { values.iter().sum()}
fn main() { println!("{}", sum(&[1, 2, 3])); println!("{}", sum(1, 2, 3));}error[E0061]: this function takes 1 argument but 3 arguments were supplied --> src/main.rs:7:20 | 7 | println!("{}", sum(1, 2, 3)); | ^^^ - - - unexpected argument #3 of type `{integer}` | | | | | unexpected argument #2 of type `{integer}` | expected `&[i32]`, found integer | note: function defined here --> src/main.rs:1:4 | 1 | fn sum(values: &[i32]) -> i32 { | ^^^ -------------- help: remove the extra arguments | 7 - println!("{}", sum(1, 2, 3)); 7 + println!("{}", sum(/* &[i32] */));
A function can take a slice, as sum(&[1, 2, 3]) does. A macro makes the call itself look like a list, with a type per argument, as in println!. A macro also sees the code, not only its value, which gives it five powers:
| A macro can | Shown in |
|---|---|
| take a list of any length, written as a call | my_vec!, sum! above |
| take a name or type and generate items from it | the first program below |
| capture the text of an expression | the second program below |
return from the function that called it |
the third program below |
check its input while compiling: println! rejects a format string that does not fit its arguments |
Macros, attributes and naming |
trait Describe { fn describe(&self) -> String;}
// One impl block for each type in the listmacro_rules! describe_all { ($($t:ty),+ $(,)?) => { $(impl Describe for $t { fn describe(&self) -> String { format!("{} {}", stringify!($t), self) } })+ };}describe_all!(i32, u8, f64, bool);
// A function whose name the caller choosesmacro_rules! make_adder { ($name:ident, $n:expr) => { fn $name(x: i32) -> i32 { x + $n } };}make_adder!(add_one, 1);make_adder!(add_ten, 10);
fn main() { println!("{}", 7_i32.describe()); println!("{}", 7_u8.describe()); println!("{}", 2.5_f64.describe()); println!("{}", true.describe()); println!("add_one(5) = {}, add_ten(5) = {}", add_one(5), add_ten(5));}i32 7 u8 7 f64 2.5 bool true add_one(5) = 6, add_ten(5) = 15
describe_all! wrote four impl blocks and make_adder! two functions named by the caller.
macro_rules! show { ($e:expr) => { println!("{} = {:?}", stringify!($e), $e) };}
// rustfmt would respace `a+b`, which is the point of the second line#[rustfmt::skip]fn main() { let (a, b) = (1, 2); show!(a + b); show!(a+b); show!(a * (b + 3)); show!(vec![a, b].len());}a + b = 3 a+b = 3 a * (b + 3) = 5 vec![a, b].len() = 2
stringify! turns tokens into text, as assert! does for its message. The std docs warn that “the expanded results of the input tokens may change in the future”, so use it for messages, not logic.
macro_rules! parse_or_zero { ($s:expr) => { match $s.parse::<i32>() { Ok(n) => n, Err(_) => return 0, } };}
fn sum_pair(a: &str, b: &str) -> i32 { let x = parse_or_zero!(a); let y = parse_or_zero!(b); println!(" both parsed: {x} and {y}"); x + y}
fn main() { println!("sum_pair(\"1\", \"2\") = {}", sum_pair("1", "2")); println!("sum_pair(\"1\", \"x\") = {}", sum_pair("1", "x"));} both parsed: 1 and 2
sum_pair("1", "2") = 3
sum_pair("1", "x") = 0The return inside parse_or_zero! leaves sum_pair, so the second call prints no “both parsed” line. For errors, ? is the usual answer (Errors).
The Book says macro definitions are “generally more difficult to read, understand, and maintain than function definitions”. And a macro pastes your argument wherever $x appears, so it can run twice. A function evaluates it once:
use std::cell::Cell;
macro_rules! square_twice { ($x:expr) => { $x * $x };}macro_rules! square_once { ($x:expr) => {{ let v = $x; v * v }};}fn square(x: i32) -> i32 { x * x}
fn main() { let calls = Cell::new(0); let next = || { calls.set(calls.get() + 1); calls.get() };
let a = square_twice!(next()); println!("square_twice!(next()) = {a} after {} calls", calls.get()); calls.set(0); let b = square_once!(next()); println!("square_once!(next()) = {b} after {} calls", calls.get()); calls.set(0); let c = square(next()); println!("square(next()) = {c} after {} calls", calls.get());}square_twice!(next()) = 2 after 2 calls square_once!(next()) = 1 after 1 calls square(next()) = 1 after 1 calls
When a macro call fails
A call fits no rule, or the code the rule wrote does not compile.
macro_rules! my_vec { () => { Vec::new() }; ($($x:expr),+ $(,)?) => {{ let mut v = Vec::new(); $(v.push($x);)+ v }};}
fn main() { let v = my_vec![0; 3]; println!("{v:?}");}error: no rules expected `;` --> src/main.rs:13:22 | 1 | macro_rules! my_vec { | ------------------- when calling this macro ... 13 | let v = my_vec![0; 3]; | ^ no rules expected this token in macro call | = note: while trying to match sequence start
“No rules expected ;” names the first token that fits nothing: this my_vec! has no [x; n] rule, where std’s does. In the gotcha the note said “while trying to match =>”, what the rule wanted next. Read the message against the macro_rules! block or the docs.
struct Age(u32);
macro_rules! age { ($years:expr) => { Age($years) };}
fn main() { let ada = age!(36); let linus = age!("twenty-eight"); println!("{} {}", ada.0, linus.0);}error[E0308]: mismatched types --> src/main.rs:11:22 | 5 | Age($years) | --- arguments to this struct are incorrect ... 11 | let linus = age!("twenty-eight"); | ^^^^^^^^^^^^^^ expected `u32`, found `&str` | note: tuple struct defined here --> src/main.rs:1:8 | 1 | struct Age(u32); | ^^^
This one fit a rule and failed later: an ordinary type error, E0308, pointing at your argument and at the line inside the macro that received it. To see what the macro wrote, expand it as above.
Going deeper
Why hashmap! uses =>
The gotcha gave hashmap! a :. A rule with $key:expr cannot accept it: after an expr fragment only =>, , and ; may follow (Reference, “follow-set ambiguity restrictions”).
// The gotcha's call used `"Ada": 36`. Can a rule be written to accept it?macro_rules! pair { ($key:expr : $value:expr) => { ($key, $value) };}
fn main() { let p = pair!("Ada": 36); println!("{p:?}");}error: `$key:expr` is followed by `:`, which is not allowed for `expr` fragments --> src/main.rs:3:16 | 3 | ($key:expr : $value:expr) => { | ^ not allowed after `expr` fragments | = note: allowed there are: `=>`, `,` or `;`
A literal key has no such limit, so another macro could take "Ada": 36. This one takes any expression as a key, so it uses =>.
Derive macros: what the one line writes
#[derive(Debug, Clone, PartialEq)] is a macro call. It reads the struct and writes impl blocks. This is the code after the derives ran, for a struct and for an enum with a helper attribute:
use std::process::Command;
const POINT: &str = "#[derive(Debug, Clone, PartialEq)]struct Point { x: i32, y: i32,}";
const MODE: &str = "#[derive(Default)]enum Mode { #[default] Fast, Slow,}";
// A harness: it asks rustc (RUSTC_BOOTSTRAP=1, as above) for the source after macros ran.fn expand(source: &str) -> String { let path = std::env::temp_dir().join("derive-demo.rs"); std::fs::write(&path, source).unwrap(); let out = Command::new("rustc") .args([ "--edition", "2024", "--crate-type", "lib", "-Zunpretty=expanded", ]) .arg(&path) .env("RUSTC_BOOTSTRAP", "1") .output() .unwrap(); std::fs::remove_file(&path).unwrap(); let text = String::from_utf8_lossy(&out.stdout).into_owned(); // Skip the prelude lines every expansion starts with. text.lines().skip(4).collect::<Vec<_>>().join("\n")}
fn main() { println!("--- you write ---\n{POINT}"); println!("--- rustc sees ---\n{}\n", expand(POINT)); println!("--- you write ---\n{MODE}"); println!("--- rustc sees ---\n{}", expand(MODE));}--- you write ---
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
--- rustc sees ---
struct Point {
x: i32,
y: i32,
}
#[automatically_derived]
impl ::core::fmt::Debug for Point {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
::core::fmt::Formatter::debug_struct_field2_finish(f, "Point", "x",
&self.x, "y", &&self.y)
}
}
#[automatically_derived]
impl ::core::clone::Clone for Point {
#[inline]
fn clone(&self) -> Point {
Point {
x: ::core::clone::Clone::clone(&self.x),
y: ::core::clone::Clone::clone(&self.y),
}
}
}
#[automatically_derived]
impl ::core::marker::StructuralPartialEq for Point { }
#[automatically_derived]
impl ::core::cmp::PartialEq for Point {
#[inline]
fn eq(&self, other: &Point) -> bool {
self.x == other.x && self.y == other.y
}
}
--- you write ---
#[derive(Default)]
enum Mode {
#[default]
Fast,
Slow,
}
--- rustc sees ---
enum Mode {
#[default]
Fast,
Slow,
}
#[automatically_derived]
impl ::core::default::Default for Mode {
#[inline]
fn default() -> Mode { Self::Fast }
}Each impl is plain code: Clone clones each field, PartialEq compares each field, Debug calls a helper with the name and fields. #[automatically_derived] marks the block as generated; the Reference says it “has no direct effect”. #[default] survives expansion: it only marks the variant for Default.
The built-in derives add a bound for every type parameter, whether the fields need it or not (Reference: impl<T: Clone> Clone for Foo<T>). PhantomData<T>, a field that stores no data, is Clone for any T (docs), yet the derive demands T: Clone:
use std::marker::PhantomData;
#[derive(Clone)]struct Handle<T> { id: u32, _kind: PhantomData<T>,}
struct Printer; // no Clone
fn main() { let h: Handle<Printer> = Handle { id: 1, _kind: PhantomData, }; let again = h.clone(); println!("{}", again.id);}error[E0599]: the method `clone` exists for struct `Handle<Printer>`, but its trait bounds were not satisfied --> src/main.rs:16:19 | 4 | struct Handle<T> { | ---------------- method `clone` not found for this struct because it doesn't satisfy `Handle<Printer>: Clone` ... 9 | struct Printer; // no Clone | -------------- doesn't satisfy `Printer: Clone` ... 16 | let again = h.clone(); | ^^^^^ method cannot be called on `Handle<Printer>` due to unsatisfied trait bounds | note: trait bound `Printer: Clone` was not satisfied --> src/main.rs:4:15 | 3 | #[derive(Clone)] | ----- in this derive macro expansion 4 | struct Handle<T> { | ^ type parameter would need to implement `Clone` = help: consider manually implementing the trait to avoid undesired bounds help: consider annotating `Printer` with `#[derive(Clone)]` | 9 + #[derive(Clone)] 10 | struct Printer; // no Clone
The note says “consider manually implementing the trait to avoid undesired bounds”; Common traits covers when.
Attribute macros
An attribute macro receives the item it sits on and returns what replaces it. #[test] is built into the compiler but works like one: it keeps your function, adds a constant that describes it, and adds a main that runs the list. This is what cargo test builds (it passes --test to the compiler):
use std::process::Command;
const SOURCE: &str = "#[test]fn adds() { assert_eq!(1 + 1, 2);}";
// A harness: `--test` is the flag `cargo test` passes to rustc; RUSTC_BOOTSTRAP=1 as above.fn expand(test_mode: bool) -> String { let dir = std::env::temp_dir(); std::fs::write(dir.join("test-demo.rs"), SOURCE).unwrap(); let mut command = Command::new("rustc"); command.args([ "--edition", "2024", "--crate-type", "lib", "-Zunpretty=expanded", ]); if test_mode { command.arg("--test"); } let out = command .arg("test-demo.rs") .current_dir(&dir) .env("RUSTC_BOOTSTRAP", "1") .output() .unwrap(); std::fs::remove_file(dir.join("test-demo.rs")).unwrap(); String::from_utf8_lossy(&out.stdout).into_owned()}
fn main() { let text = expand(true); // From the generated constant to your function, then the generated `main`. let start = text.find("extern crate test;").unwrap(); let body = text.find("fn adds()").unwrap(); let main_at = text.find("#[rustc_main]").unwrap(); println!("--- with --test ---"); println!("{}", text[start..body].trim_end()); println!("fn adds() {{ ... }} // your function, with assert_eq! expanded"); println!("{}", text[main_at..].trim_end());
println!("--- without --test ---"); println!( "expansion mentions `adds`: {}", expand(false).contains("adds") );}--- with --test ---
extern crate test;
#[rustc_test_marker = "adds"]
#[doc(hidden)]
pub const adds: test::TestDescAndFn =
test::TestDescAndFn {
desc: test::TestDesc {
name: test::StaticTestName("adds"),
ignore: false,
ignore_message: ::core::option::Option::None,
source_file: "test-demo.rs",
start_line: 2usize,
start_col: 4usize,
end_line: 2usize,
end_col: 8usize,
compile_fail: false,
no_run: false,
should_panic: test::ShouldPanic::No,
test_type: test::TestType::Unknown,
},
testfn: test::StaticTestFn(#[coverage(off)] ||
test::assert_test_result(adds())),
};
fn adds() { ... } // your function, with assert_eq! expanded
#[rustc_main]
#[coverage(off)]
#[doc(hidden)]
pub fn main() -> () {
extern crate test;
test::test_main_static(&[&adds])
}
--- without --test ---
expansion mentions `adds`: falseWithout --test the expansion does not mention adds: test functions are not part of a normal build (Reference).
| Attribute | Written by | What it does |
|---|---|---|
#[tokio::main] |
the tokio crate | “helps set up a Runtime” for async fn main (docs; Async with Tokio uses it) |
#[serde(rename = "x")] |
serde’s derive | a helper attribute that configures the generated code (serde) |
Procedural macro crates
A procedural macro is a Rust function that takes tokens in and returns tokens. The Reference says it “must be defined in the root of a crate with the crate type of proc-macro” and “may not be used from the crate where” it is defined; with Cargo the crate sets proc-macro = true under [lib] (Reference). So it lives in a different crate from its users.
The lesson runner compiles one file, so this harness writes a two-crate workspace to a temporary folder and builds it with Cargo; the macros are in the string SHOUT_LIB. It needs no dependencies, because proc_macro ships with the compiler. One macro of each procedural kind; the attribute prints what it receives during compilation:
use std::fs;use std::path::Path;use std::process::Command;
// A harness: it writes a two-crate workspace to a temporary folder and builds it with Cargo.// No dependencies: `proc_macro` comes with the compiler.const WORKSPACE: &str = "[workspace]\nmembers = [\"shout\", \"app\"]\nresolver = \"3\"\n";
// The crate that holds the macros. `proc-macro = true` is what makes it one.const SHOUT_TOML: &str = "[package]\nname = \"shout\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\nproc-macro = true\n";
const SHOUT_LIB: &str = r#"use proc_macro::TokenStream;
// make_answer!(): ignore the input and write a function.#[proc_macro]pub fn make_answer(_input: TokenStream) -> TokenStream { "fn answer() -> u32 { 42 }".parse().unwrap()}
// #[derive(TypeName)]: read the name after `struct`, write an impl block.#[proc_macro_derive(TypeName)]pub fn derive_type_name(input: TokenStream) -> TokenStream { let text = input.to_string(); let name = text.split_whitespace().nth(1).unwrap().trim_end_matches(';'); format!("impl {name} {{ fn type_name() -> &'static str {{ \"{name}\" }} }}") .parse() .unwrap()}
// #[show_streams(...)]: print what arrives, at compile time, and return the item unchanged.#[proc_macro_attribute]pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream { println!("attr: \"{attr}\""); println!("item: \"{item}\""); item}"#;
// The crate that uses them.const APP_TOML: &str = "[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nshout = { path = \"../shout\" }\n";
const APP_MAIN: &str = r#"use shout::{TypeName, make_answer, show_streams};
make_answer!();
#[derive(TypeName)]struct Pet;
#[show_streams(loud, times = 2)]fn greet() -> &'static str { "hi"}
fn main() { println!("Pet::type_name() = {}", Pet::type_name()); println!("greet() = {}", greet()); println!("answer() = {}", answer());}"#;
fn main() { let root = std::env::temp_dir().join("proc-macro-demo"); let _ = fs::remove_dir_all(&root); for (file, text) in [ ("Cargo.toml", WORKSPACE), ("shout/Cargo.toml", SHOUT_TOML), ("shout/src/lib.rs", SHOUT_LIB), ("app/Cargo.toml", APP_TOML), ("app/src/main.rs", APP_MAIN), ] { let path = root.join(file); fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, text).unwrap(); }
let build = Command::new("cargo") .args(["build", "--quiet", "--offline", "--package", "app"]) .env("CARGO_TARGET_DIR", root.join("target")) .current_dir(&root) .output() .unwrap(); println!("--- cargo build: success = {} ---", build.status.success()); print!("{}", String::from_utf8_lossy(&build.stdout)); print!("{}", String::from_utf8_lossy(&build.stderr));
println!("--- target/debug/app ---"); let run = Command::new(Path::new(&root).join("target/debug/app")) .output() .unwrap(); print!("{}", String::from_utf8_lossy(&run.stdout)); let _ = fs::remove_dir_all(&root);}--- cargo build: success = true ---
attr: "loud, times = 2"
item: "fn greet() -> &'static str { "hi" }"
--- target/debug/app ---
Pet::type_name() = Pet
greet() = hi
answer() = 42The two attr/item lines came from compile time: the attribute received loud, times = 2 and the whole function as tokens, and returned it unchanged. The function-like macro ignored its input and wrote answer. The derive read the name after struct and wrote an impl. Project structure covers workspaces. The Book’s examples use syn to parse the tokens and quote to write them.
Two Reference facts matter when you read someone else’s code. Procedural macros “are unhygienic”: they act as if their output was written inline. And they have “the same security concerns that Cargo’s build scripts have”: a dependency with one runs code on your machine when you build. A panic in one becomes a compiler error.
When to write one
Writing one is rare. In order of preference:
- A function, the easiest to read, test and debug.
- Generics and traits (Generics, Traits) for code that must work for many types.
- A
macro_rules!macro, for one of the five powers above, such as one implementation repeated for a list of types. - A procedural macro, when it must read a struct’s fields or parse its own syntax. It costs a separate crate.
What you write today, in Rust
Practice
Try it
The first exercise reads macros; the others finish small ones. Replace each todo!() and run until the checks print all checks passed.
Which rule fires, and what does it become?
Say which rule of rule! each of four calls matches, then write by hand what twice!(next()) expands to. The checks compare your answers with the compiler’s; one call is not what its shape suggests.
use std::cell::Cell;
// `rule!` answers with the number of the rule that matched. Read the patterns top to bottom.macro_rules! rule { ($a:expr) => { 1 }; ($a:expr, $b:expr) => { 2 }; ($a:ident = $b:expr) => { 3 }; ($($t:tt)*) => { 4 };}
// `twice!(e)` pastes its argument in twice.macro_rules! twice { ($e:expr) => { $e + $e };}
// Part 1: which rule does each call match? Answer from the patterns, in this order:// `2 + 3`, `1, 2`, `x = 5`, `a, b, c`.fn which_rules() -> [u32; 4] { todo!("read the patterns: the first rule that fits the whole call wins")}
// Part 2: write out by hand what `twice!(next())` becomes.fn twice_by_hand(next: &dyn Fn() -> i32) -> i32 { todo!("the macro pastes the argument as written, so what is called, and how often?")}
fn main() { let actual = [rule!(2 + 3), rule!(1, 2), rule!(x = 5), rule!(a, b, c)]; let answer = which_rules(); for (i, call) in ["2 + 3", "1, 2", "x = 5", "a, b, c"].iter().enumerate() { assert_eq!( answer[i], actual[i], "wrong rule for the call `rule!({call})`" ); }
let calls = Cell::new(0); let next = || { calls.set(calls.get() + 1); calls.get() }; let by_macro = twice!(next()); let macro_calls = calls.get(); calls.set(0); assert_eq!( twice_by_hand(&next), by_macro, "the value differs from the macro's" ); assert_eq!( calls.get(), macro_calls, "the macro calls `next` more than once" ); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::cell::Cell;
// `rule!` answers with the number of the rule that matched. Read the patterns top to bottom.macro_rules! rule { ($a:expr) => { 1 }; ($a:expr, $b:expr) => { 2 }; ($a:ident = $b:expr) => { 3 }; ($($t:tt)*) => { 4 };}
// `twice!(e)` pastes its argument in twice.macro_rules! twice { ($e:expr) => { $e + $e };}
// Part 1: which rule does each call match? Answer from the patterns, in this order:// `2 + 3`, `1, 2`, `x = 5`, `a, b, c`.fn which_rules() -> [u32; 4] { [1, 2, 1, 4]}
// Part 2: write out by hand what `twice!(next())` becomes.fn twice_by_hand(next: &dyn Fn() -> i32) -> i32 { next() + next()}
fn main() { let actual = [rule!(2 + 3), rule!(1, 2), rule!(x = 5), rule!(a, b, c)]; let answer = which_rules(); for (i, call) in ["2 + 3", "1, 2", "x = 5", "a, b, c"].iter().enumerate() { assert_eq!( answer[i], actual[i], "wrong rule for the call `rule!({call})`" ); }
let calls = Cell::new(0); let next = || { calls.set(calls.get() + 1); calls.get() }; let by_macro = twice!(next()); let macro_calls = calls.get(); calls.set(0); assert_eq!( twice_by_hand(&next), by_macro, "the value differs from the macro's" ); assert_eq!( calls.get(), macro_calls, "the macro calls `next` more than once" ); println!("all checks passed");}all checks passed
Wrap a type, with its name
Finish the Display that newtype!(Meters, f64) writes so it prints Meters(2.5). Two more types are checked, so the name must come from the call.
use std::fmt;
// newtype!(Meters, f64) defines `struct Meters(f64)` with `new`, `get`, and a Display that// prints `Meters(2.5)`: the type's own name, then the value in parentheses.macro_rules! newtype { ($name:ident, $inner:ty) => { struct $name($inner);
impl $name { fn new(value: $inner) -> Self { $name(value) }
fn get(&self) -> &$inner { &self.0 } }
impl fmt::Display for $name { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { todo!("write the name, then the value in parentheses") } } };}
newtype!(Meters, f64);newtype!(UserId, u32);newtype!(Label, String);
fn main() { let m = Meters::new(2.5); assert_eq!(*m.get(), 2.5); assert_eq!(m.to_string(), "Meters(2.5)");
let id = UserId::new(7); assert_eq!(*id.get(), 7); assert_eq!(format!("{id}"), "UserId(7)");
let label = Label::new(String::from("hi")); assert_eq!(label.get(), "hi"); assert_eq!(label.to_string(), "Label(hi)"); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::fmt;
// newtype!(Meters, f64) defines `struct Meters(f64)` with `new`, `get`, and a Display that// prints `Meters(2.5)`: the type's own name, then the value in parentheses.macro_rules! newtype { ($name:ident, $inner:ty) => { struct $name($inner);
impl $name { fn new(value: $inner) -> Self { $name(value) }
fn get(&self) -> &$inner { &self.0 } }
impl fmt::Display for $name { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}({})", stringify!($name), self.0) } } };}
newtype!(Meters, f64);newtype!(UserId, u32);newtype!(Label, String);
fn main() { let m = Meters::new(2.5); assert_eq!(*m.get(), 2.5); assert_eq!(m.to_string(), "Meters(2.5)");
let id = UserId::new(7); assert_eq!(*id.get(), 7); assert_eq!(format!("{id}"), "UserId(7)");
let label = Label::new(String::from("hi")); assert_eq!(label.get(), "hi"); assert_eq!(label.to_string(), "Label(hi)"); println!("all checks passed");}all checks passed
The largest of any number of values
Finish the second rule of max_of!: compare the first argument with the maximum of the rest. The checks count evaluations.
use std::cell::Cell;
// max_of!(a, b, c, ...) is the largest of one or more values, written as a macro because a// function cannot take any number of arguments.macro_rules! max_of { ($x:expr $(,)?) => { $x }; ($head:expr, $($tail:expr),+ $(,)?) => {{ let first = $head; // The starter hands back `first` so the types work out; the rest is yours. if false { first } else { todo!("compare first with the maximum of the rest, evaluating each argument once") } }};}
fn main() { assert_eq!(max_of!(7), 7); assert_eq!(max_of!(3, 9, 4), 9); assert_eq!(max_of!(-5, -2), -2); assert_eq!(max_of!(1, 2 + 3 * 4, 5), 14); assert_eq!(max_of!("pear", "apple", "fig"), "pear"); assert_eq!(max_of!(1.5, 0.5,), 1.5); // a trailing comma is allowed
// Each argument is evaluated exactly once, left to right. let calls = Cell::new(0); let next = || { calls.set(calls.get() + 1); calls.get() }; let largest = max_of!(next(), next(), next()); assert_eq!(calls.get(), 3, "each argument must be evaluated once"); assert_eq!(largest, 3); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::cell::Cell;
// max_of!(a, b, c, ...) is the largest of one or more values, written as a macro because a// function cannot take any number of arguments.macro_rules! max_of { ($x:expr $(,)?) => { $x }; ($head:expr, $($tail:expr),+ $(,)?) => {{ let first = $head; let rest = max_of!($($tail),+); if first > rest { first } else { rest } }};}
fn main() { assert_eq!(max_of!(7), 7); assert_eq!(max_of!(3, 9, 4), 9); assert_eq!(max_of!(-5, -2), -2); assert_eq!(max_of!(1, 2 + 3 * 4, 5), 14); assert_eq!(max_of!("pear", "apple", "fig"), "pear"); assert_eq!(max_of!(1.5, 0.5,), 1.5); // a trailing comma is allowed
// Each argument is evaluated exactly once, left to right. let calls = Cell::new(0); let next = || { calls.set(calls.get() + 1); calls.get() }; let largest = max_of!(next(), next(), next()); assert_eq!(calls.get(), 3, "each argument must be evaluated once"); assert_eq!(largest, 3); println!("all checks passed");}all checks passed
Return from the caller
Write ensure!(cond): when false, the calling function returns an Err naming the condition as written. One check has a side effect in the condition.
use std::cell::Cell;
// ensure!(condition) returns early from the surrounding function with an Err that names the// condition, as written, when it is false. A function cannot do that: only a macro can// return from its caller, and only a macro can see the condition as text.macro_rules! ensure { ($cond:expr) => { todo!("if the condition is false, return Err(String) that says which condition failed") };}
fn check(age: i32, name: &str) -> Result<(), String> { ensure!(age >= 0); ensure!(!name.is_empty()); ensure!(age + 1 < 150); Ok(())}
fn side_effect(calls: &Cell<i32>) -> Result<(), String> { ensure!({ calls.set(calls.get() + 1); true }); Ok(())}
fn main() { assert_eq!(check(30, "Ada"), Ok(())); assert_eq!(check(-1, "Ada"), Err("ensure failed: age >= 0".to_string())); assert_eq!( check(30, ""), Err("ensure failed: !name.is_empty()".to_string()) ); assert_eq!( check(200, "Ada"), Err("ensure failed: age + 1 < 150".to_string()) ); // The first failing check ends the function: the later ones never run. assert_eq!(check(-1, ""), Err("ensure failed: age >= 0".to_string()));
let calls = Cell::new(0); assert_eq!(side_effect(&calls), Ok(())); assert_eq!(calls.get(), 1, "the condition must be evaluated once"); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
use std::cell::Cell;
// ensure!(condition) returns early from the surrounding function with an Err that names the// condition, as written, when it is false. A function cannot do that: only a macro can// return from its caller, and only a macro can see the condition as text.macro_rules! ensure { ($cond:expr) => { if !$cond { return Err(format!("ensure failed: {}", stringify!($cond))); } };}
fn check(age: i32, name: &str) -> Result<(), String> { ensure!(age >= 0); ensure!(!name.is_empty()); ensure!(age + 1 < 150); Ok(())}
fn side_effect(calls: &Cell<i32>) -> Result<(), String> { ensure!({ calls.set(calls.get() + 1); true }); Ok(())}
fn main() { assert_eq!(check(30, "Ada"), Ok(())); assert_eq!(check(-1, "Ada"), Err("ensure failed: age >= 0".to_string())); assert_eq!( check(30, ""), Err("ensure failed: !name.is_empty()".to_string()) ); assert_eq!( check(200, "Ada"), Err("ensure failed: age + 1 < 150".to_string()) ); // The first failing check ends the function: the later ones never run. assert_eq!(check(-1, ""), Err("ensure failed: age >= 0".to_string()));
let calls = Cell::new(0); assert_eq!(side_effect(&calls), Ok(())); assert_eq!(calls.get(), 1, "the condition must be evaluated once"); println!("all checks passed");}all checks passed
Read more
- The Rust Book, chapter 20.5 and Rust by Example: macro_rules!.
- The Reference: macros by example (fragments, repetition, hygiene, scope), procedural macros and derive.
- The Little Book of Rust Macros, the long guide to
macro_rules!(recursion, tt-munchers, internal rules), and std::stringify!. - syn parses tokens and quote writes them, in most procedural macros.
-Zunprettyin the Unstable Book.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.