Lesson 79 of 94
Declarative Macros
You've used macros since the first lesson: println!, vec!, format!, assert_eq!, #[derive(Debug)]. A macro is code that writes code at compile time. The ! (or the #[...] attribute) tells you a macro is involved.
Rust's derive macros (#[derive(Debug, Clone, Serialize)]) generate code from attributes at compile time, and macros in general often do at compile time what other approaches do at runtime.
Rust has two kinds of macros:
| Kind | Defined with | Used as | Typical use |
|---|---|---|---|
| declarative | macro_rules! in normal code |
name!(...) |
small syntax helpers, repetitive code, DSLs |
| procedural | a separate proc-macro crate, Rust functions over token streams |
#[derive(Name)], #[name], name!(...) |
derives (serde, thiserror), frameworks (tokio, axum) |
This lesson covers macro_rules!, which you can write in any file. The next lesson, Procedural Macros, covers derive, attribute, and function-like macros.
When not to write a macro: if a function or a generic does the job, use it. Macros are harder to read, debug, and document. Reach for one when you need something functions can't do: variable numbers of arguments, generating items (functions, structs, impls, tests), or custom syntax.
Part 1: macro_rules!
1. The simplest macro
A macro_rules! definition has one or more rules, each (pattern) => { expansion }. This one has an empty pattern.
macro_rules! banner {
() => {
println!("==========");
println!(" REPORT ");
println!("==========");
};
}
fn main() {
banner!();
println!("all systems nominal");
}==========
REPORT
==========
all systems nominalAt compile time, banner!() is replaced by the three println! lines. The macro must be defined above its first use in the file.
2. Taking an expression: $x:expr
Patterns capture input into metavariables like $x, each with a fragment specifier saying what kind of syntax it matches. expr matches any expression.
macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
println!("{}", square!(7));
println!("{}", square!(2 + 3)); // 25: `$x` is substituted as a whole expression
let side = 1.5;
println!("{}", square!(side));
}49
25
2.25Unlike a C preprocessor macro, square!(2 + 3) gives 25, not 2 + 3 * 2 + 3 = 11: an expr fragment behaves like a parenthesized unit.
3. Evaluate arguments once
The expansion pastes the expression wherever $x appears, so square!(next_id()) would call next_id() twice. Bind it to a variable first.
use std::cell::Cell;
thread_local! {
static CALLS: Cell<u32> = const { Cell::new(0) };
}
fn next_value() -> i32 {
CALLS.with(|c| c.set(c.get() + 1));
4
}
macro_rules! square_twice_evaluated {
($x:expr) => {
$x * $x
};
}
macro_rules! square {
($x:expr) => {{
let v = $x; // evaluate once
v * v
}};
}
fn main() {
println!("{}", square_twice_evaluated!(next_value()));
println!("calls so far: {}", CALLS.with(|c| c.get()));
println!("{}", square!(next_value()));
println!("calls so far: {}", CALLS.with(|c| c.get()));
}16
calls so far: 2
16
calls so far: 3The double braces {{ ... }} make the expansion a block expression, so it can contain statements and still produce a value.
4. Fragment specifiers
| Specifier | Matches | Example input |
|---|---|---|
expr |
an expression | a + 1, f(x), vec![1] |
ident |
an identifier | user_id, Point |
ty |
a type | u32, Vec<String>, &'a str |
pat |
a pattern | Some(x), 1..=5, _ |
literal |
a literal | 42, "text", 'c', true |
block |
a { ... } block |
{ let x = 1; x } |
path |
a path | std::fmt::Display, Self::new |
lifetime |
a lifetime | 'a |
vis |
a visibility (possibly empty) | pub, pub(crate) |
item |
an item | fn f() {}, struct S; |
tt |
a single token tree | +, foo, ( ... ), [ ... ] |
5. ident: generating a function
With an ident fragment, a macro can define new items with names you choose. Here each call creates a validation function.
macro_rules! range_validator {
($name:ident, $min:expr, $max:expr) => {
fn $name(value: i64) -> Result<i64, String> {
if ($min..=$max).contains(&value) {
Ok(value)
} else {
Err(format!("{} must be between {} and {}, got {}", stringify!($name), $min, $max, value))
}
}
};
}
range_validator!(check_age, 0, 150);
range_validator!(check_port, 1, 65535);
range_validator!(check_percent, 0, 100);
fn main() {
println!("{:?}", check_age(34));
println!("{:?}", check_port(80800));
println!("{:?}", check_percent(-5));
}Ok(34)
Err("check_port must be between 1 and 65535, got 80800")
Err("check_percent must be between 0 and 100, got -5")stringify!($name) turns the tokens into a string literal at compile time: "check_port".
6. Several rules: overloading by shape
A macro can have several rules; the first one whose pattern matches is used. That gives you overloading by syntax.
macro_rules! greet {
() => {
String::from("Hello!")
};
($name:expr) => {
format!("Hello, {}!", $name)
};
($name:expr, formal) => {
format!("Good day, {}.", $name)
};
}
fn main() {
println!("{}", greet!());
println!("{}", greet!("Ana"));
println!("{}", greet!("Dr. Lee", formal));
}Hello!
Hello, Ana!
Good day, Dr. Lee.The third rule matches the literal word formal: patterns can contain fixed tokens, not just metavariables.
7. Repetition: $( ... ),*
$( $x:expr ),* matches zero or more comma-separated expressions. In the expansion, $( ... )* repeats once per match. This is how vec! accepts any number of elements.
macro_rules! sum {
( $( $x:expr ),* ) => {
0 $( + $x )*
};
}
macro_rules! strings {
( $( $s:expr ),* ) => {
vec![ $( String::from($s) ),* ]
};
}
fn main() {
println!("{}", sum!());
println!("{}", sum!(1, 2, 3, 4));
println!("{:?}", strings!("a", "b", "c"));
}0
10
["a", "b", "c"]The separators and repetition operators: * (zero or more), + (one or more), ? (zero or one). The separator (, or ; or others) goes before the operator.
8. Allowing a trailing comma
Users will write trailing commas in multi-line calls. $(,)? at the end accepts an optional one.
macro_rules! csv_row {
( $( $field:expr ),+ $(,)? ) => {
[ $( $field.to_string() ),+ ].join(",")
};
}
fn main() {
println!("{}", csv_row!(1, "ana", 34.5));
println!(
"{}",
csv_row!(
2,
"ben",
true, // trailing comma is fine
)
);
}1,ana,34.5
2,ben,true9. A hashmap! literal
Repetition can capture several fragments per item. $( $k:expr => $v:expr ),* matches key => value pairs, giving you a map literal.
use std::collections::HashMap;
macro_rules! hashmap {
( $( $k:expr => $v:expr ),* $(,)? ) => {{
let mut map = HashMap::new();
$( map.insert($k, $v); )*
map
}};
}
fn main() {
let status_text = hashmap! {
200 => "OK",
404 => "Not Found",
500 => "Internal Server Error",
};
let mut codes: Vec<_> = status_text.iter().collect();
codes.sort();
println!("{codes:?}");
}[(200, "OK"), (404, "Not Found"), (500, "Internal Server Error")]10. Recursion: processing one argument at a time
A macro can call itself. A common pattern peels off the first argument and recurses on the rest. Here max! works for any number of arguments.
macro_rules! max {
($x:expr) => {
$x
};
($x:expr, $( $rest:expr ),+) => {{
let a = $x;
let b = max!($( $rest ),+);
if a > b { a } else { b }
}};
}
fn main() {
println!("{}", max!(3));
println!("{}", max!(3, 9, 2));
println!("{}", max!(1.5, -2.0, 0.25, 1.75));
}3
9
1.75The compiler has a recursion limit (128 by default), so this style is for modest inputs.
11. Counting arguments at compile time
Counting is a classic recursion trick: replace each argument with 1 +. The result is a constant expression, usable for array sizes.
macro_rules! count {
() => { 0usize };
($head:tt $( $tail:tt )*) => { 1usize + count!($( $tail )*) };
}
macro_rules! const_list {
($name:ident: $( $item:literal ),* ) => {
const $name: [&str; count!($( $item )*)] = [ $( $item ),* ];
};
}
const_list!(WEEKDAYS: "Mon", "Tue", "Wed", "Thu", "Fri");
fn main() {
println!("{}", count!(a b c d));
println!("{} weekdays: {:?}", WEEKDAYS.len(), WEEKDAYS);
}4
5 weekdays: ["Mon", "Tue", "Wed", "Thu", "Fri"]12. Hygiene: macro variables don't leak
Variables created inside a macro live in their own scope, so they can't accidentally clash with, or capture, variables at the call site. This is called hygiene.
macro_rules! double_it {
($x:expr) => {{
let value = $x * 2; // this `value` is private to the macro
value
}};
}
fn main() {
let value = 10;
let doubled = double_it!(value + 1); // `value` here is the caller's
println!("value = {value}, doubled = {doubled}");
}value = 10, doubled = 22Identifiers passed in by the caller (via $name:ident) do refer to the caller's scope, which is how example 5 could define a function the caller then calls.
13. Generating a newtype with impls
Macros shine at removing boilerplate that generics can't. Here one line per type generates a newtype struct, a constructor, and Display.
use std::fmt;
macro_rules! id_type {
($name:ident, $prefix:literal) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name(u64);
impl $name {
pub fn new(raw: u64) -> Self {
$name(raw)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}-{:05}", $prefix, self.0)
}
}
};
}
id_type!(UserId, "USR");
id_type!(OrderId, "ORD");
fn ship(order: OrderId, to: UserId) -> String {
format!("shipping {order} to {to}")
}
fn main() {
let user = UserId::new(42);
let order = OrderId::new(7);
println!("{}", ship(order, user));
// ship(user, order); // error: mismatched types, the macro made them distinct
}shipping ORD-00007 to USR-0004214. Generating an enum with string conversions
An enum whose variants map to and from strings is tedious to keep in sync by hand. The macro lists each variant once.
macro_rules! string_enum {
($name:ident { $( $variant:ident => $text:literal ),+ $(,)? }) => {
#[derive(Debug, Clone, Copy, PartialEq)]
enum $name {
$( $variant ),+
}
impl $name {
const ALL: &'static [$name] = &[ $( $name::$variant ),+ ];
fn as_str(self) -> &'static str {
match self {
$( $name::$variant => $text ),+
}
}
fn parse(s: &str) -> Option<$name> {
match s {
$( $text => Some($name::$variant), )+
_ => None,
}
}
}
};
}
string_enum!(Status {
Active => "active",
Suspended => "suspended",
Deleted => "deleted",
});
fn main() {
println!("{:?}", Status::ALL);
println!("{}", Status::Suspended.as_str());
println!("{:?} {:?}", Status::parse("deleted"), Status::parse("archived"));
}[Active, Suspended, Deleted]
suspended
Some(Deleted) None15. $vis:vis and attributes pass-through
To let callers choose visibility and add their own attributes (#[derive], doc comments), capture them: $vis:vis and $( #[$meta:meta] )*.
mod model {
macro_rules! plain_struct {
( $( #[$meta:meta] )* $vis:vis struct $name:ident { $( $field:ident : $ty:ty ),* $(,)? } ) => {
$( #[$meta] )*
$vis struct $name {
$( pub $field: $ty ),*
}
impl $name {
pub fn field_names() -> &'static [&'static str] {
&[ $( stringify!($field) ),* ]
}
}
};
}
plain_struct! {
/// A product in the catalog.
#[derive(Debug, Clone)]
pub struct Product {
sku: String,
price_cents: u32,
in_stock: bool,
}
}
}
fn main() {
let p = model::Product { sku: String::from("BK-1"), price_cents: 1500, in_stock: true };
println!("{p:?}");
println!("{:?}", model::Product::field_names());
}Product { sku: "BK-1", price_cents: 1500, in_stock: true }
["sku", "price_cents", "in_stock"]16. Generating tests from a table
The Testing lesson mentioned this: a macro that turns a table of cases into one named #[test] per case, so each shows up separately in the test report.
pub fn is_valid_isbn10(s: &str) -> bool {
let digits: Vec<u32> = s.chars().filter(|c| *c != '-').map(|c| if c == 'X' { 10 } else { c.to_digit(10).unwrap_or(99) }).collect();
digits.len() == 10 && digits.iter().all(|&d| d <= 10) && digits.iter().enumerate().map(|(i, d)| (10 - i as u32) * d).sum::<u32>() % 11 == 0
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! isbn_tests {
( $( $name:ident: $input:literal => $expected:literal, )* ) => {
$(
#[test]
fn $name() {
assert_eq!(is_valid_isbn10($input), $expected, "input {:?}", $input);
}
)*
};
}
isbn_tests! {
valid_plain: "0306406152" => true,
valid_with_dashes: "0-306-40615-2" => true,
valid_with_x: "0-8044-2957-X" => true,
wrong_checksum: "0306406153" => false,
too_short: "030640615" => false,
letters: "03064A6152" => false,
}
}running 6 tests
test tests::letters ... ok
test tests::too_short ... ok
test tests::valid_plain ... ok
test tests::valid_with_dashes ... ok
test tests::valid_with_x ... ok
test tests::wrong_checksum ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s17. compile_error! for helpful messages
A catch-all rule that expands to compile_error!("...") turns invalid input into a clear error instead of a confusing "no rules expected this token".
macro_rules! http_status {
(ok) => { 200 };
(not_found) => { 404 };
($other:ident) => {
compile_error!(concat!("unknown status `", stringify!($other), "`; expected `ok` or `not_found`"))
};
}
fn main() {
let a = http_status!(ok);
let b = http_status!(teapot);
println!("{a} {b}");
}error: unknown status `teapot`; expected `ok` or `not_found`
--> src/main.rs:5:9
|
5 | compile_error!(concat!("unknown status `", stringify!($other), "`; expected `ok` or `not_found`"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
11 | let b = http_status!(teapot);
| -------------------- in this macro invocation
|18. A small DSL with token trees
tt (token tree) fragments accept almost anything, which lets macros define mini-languages. This one builds routing rules from a readable syntax.
#[derive(Debug)]
struct Route {
method: &'static str,
path: &'static str,
handler: fn(&str) -> String,
}
macro_rules! routes {
( $( $method:ident $path:literal => $handler:expr ; )* ) => {
vec![ $( Route { method: stringify!($method), path: $path, handler: $handler } ),* ]
};
}
fn list_users(_: &str) -> String {
String::from("[ana, ben]")
}
fn main() {
let table = routes! {
GET "/users" => list_users;
POST "/users" => |body| format!("created {body}");
DELETE "/users" => |_| String::from("forbidden");
};
for (method, path, body) in [("GET", "/users", ""), ("POST", "/users", "cy"), ("PUT", "/users", "")] {
match table.iter().find(|r| r.method == method && r.path == path) {
Some(r) => println!("{method} {path} -> {}", (r.handler)(body)),
None => println!("{method} {path} -> 405 Method Not Allowed"),
}
}
}GET /users -> [ana, ben]
POST /users -> created cy
PUT /users -> 405 Method Not Allowed19. Exporting macros: #[macro_export] and $crate
Inside a crate, a macro_rules! macro is visible after its definition. To make it usable from other crates, add #[macro_export]; it then lives at the crate root (mycrate::my_macro!). Paths inside the expansion should start with $crate::, which always refers to the macro's own crate, whatever the caller has imported.
mod metrics {
pub fn record(name: &str, value: f64) -> String {
format!("metric {name}={value}")
}
#[macro_export]
macro_rules! metric {
($name:literal, $value:expr) => {
// `$crate` makes this path work from any module or crate.
$crate::metrics::record($name, $value as f64)
};
}
}
mod handlers {
pub fn checkout() -> String {
crate::metric!("checkout.latency_ms", 42) // exported macros live at the crate root
}
}
fn main() {
println!("{}", handlers::checkout());
println!("{}", metric!("queue.depth", 7));
}metric checkout.latency_ms=42
metric queue.depth=720. Seeing the expansion
When a macro misbehaves, look at what it expands to. cargo expand (a Cargo plugin) prints your code with every macro expanded, including derives.
cargo install cargo-expand
cargo expand # the whole crate
cargo expand handlers # one moduleInside a macro, stringify! is useful for printing what was matched while you debug:
macro_rules! show {
($e:expr) => {
println!("{} = {:?}", stringify!($e), $e)
};
}
fn main() {
let prices = [120, 80, 45];
show!(prices.iter().sum::<i32>());
show!(prices.iter().max());
show!(prices.len() > 2);
}prices.iter().sum::<i32>() = 245
prices.iter().max() = Some(120)
prices.len() > 2 = trueThat's essentially the standard dbg! macro, which prints to stderr with the file and line.
Part 2: Built-in macros worth knowing
21. Assertions and "not yet" markers
| Macro | Does |
|---|---|
assert!(cond, "msg {}", x) |
panics if false (always) |
debug_assert!(...) |
same, but only in debug builds |
assert_eq! / assert_ne! |
compares and prints both sides |
matches!(value, pattern) |
pattern test that returns bool |
todo!() |
panics with "not yet implemented"; type-checks as any type |
unimplemented!() |
panics with "not implemented" |
unreachable!() |
panics with "entered unreachable code" |
#[derive(Debug)]
enum Shape {
Circle(f64),
Square(f64),
Polygon(Vec<(f64, f64)>),
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Square(side) => side * side,
Shape::Polygon(_) => todo!("polygon area"), // compiles now, panics if reached
}
}
fn main() {
let shapes = [Shape::Circle(1.0), Shape::Square(2.0)];
let round = shapes.iter().filter(|s| matches!(s, Shape::Circle(r) if *r > 0.5)).count();
println!("round shapes: {round}");
println!("{:.2}", area(&shapes[0]));
debug_assert!(area(&shapes[1]) == 4.0, "square area is wrong");
let _ = Shape::Polygon(vec![]);
}round shapes: 1
3.1422. Compile-time information: file!, line!, concat!, env!, include_str!
These expand to constants known during compilation.
macro_rules! here {
() => {
concat!(file!(), ":", line!())
};
}
const VERSION: &str = concat!("v", 2, ".", 4); // concat! joins literals at compile time
const README: &str = include_str!("main.rs"); // embeds a file's text in the binary
fn main() {
println!("logged at {}", here!());
println!("version {VERSION}");
println!("module {}", module_path!());
println!("this source file has {} lines", README.lines().count());
println!("built for {}", if cfg!(target_pointer_width = "64") { "64-bit" } else { "32-bit" });
}logged at src/main.rs:11
version v2.4
module main
this source file has 16 lines
built for 64-bitenv!("NAME") reads an environment variable at compile time (Cargo sets CARGO_PKG_VERSION and friends), and include_bytes! embeds binary files. Rust bakes them into the executable.
23. format_args! and writing your own println!-style macro
All formatting macros are built on format_args!, which checks the format string at compile time. Forwarding $($arg:tt)* to it gives your own macros the full {} syntax, with compile-time checking.
use std::fmt::Write;
macro_rules! log_info {
($($arg:tt)*) => {
println!("[INFO {}] {}", module_path!(), format_args!($($arg)*))
};
}
macro_rules! appendln {
($buf:expr, $($arg:tt)*) => {
writeln!($buf, $($arg)*).expect("writing to a String cannot fail")
};
}
fn main() {
let user = "ana";
log_info!("user {user} logged in from {}", "10.0.0.7");
log_info!("{} items in cart, total {:.2}", 3, 59.9);
let mut report = String::new();
appendln!(report, "{:<6}|{:>5}", "name", "qty");
appendln!(report, "{:<6}|{:>5}", "pen", 12);
print!("{report}");
}[INFO main] user ana logged in from 10.0.0.7
[INFO main] 3 items in cart, total 59.90
name | qty
pen | 12A typo like log_info!("{} {}", 1) is a compile error, not a runtime error.
Part 3: Patterns from real codebases
24. Internal rules: private helpers with @
A macro often needs a helper, such as the counting trick from example 11. Instead of defining a second macro, real code puts the helper inside the same macro as a rule that starts with a marker like @count. Callers never type @count, so these internal rules act like private methods. Put them first, so the public rule doesn't try to match them.
macro_rules! status_enum {
// Internal rules.
(@count) => { 0 };
(@count $head:ident $($tail:ident)*) => { 1 + status_enum!(@count $($tail)*) };
// The public rule.
($name:ident { $($variant:ident),* $(,)? }) => {
#[derive(Debug, Clone, Copy, PartialEq)]
enum $name {
$($variant),*
}
impl $name {
const COUNT: usize = status_enum!(@count $($variant)*);
const ALL: [$name; status_enum!(@count $($variant)*)] = [$($name::$variant),*];
}
};
}
status_enum!(OrderStatus { Pending, Paid, Shipped, Delivered });
fn main() {
println!("{} statuses", OrderStatus::COUNT);
for status in OrderStatus::ALL {
println!("{status:?}");
}
println!("{}", OrderStatus::ALL.contains(&OrderStatus::Paid));
}4 statuses
Pending
Paid
Shipped
Delivered
true25. TT munchers: consuming input one piece at a time
When the input has several shapes mixed together, a single repetition can't match it. A TT muncher matches the first piece, handles it, and calls itself on the remaining tokens ($($rest:tt)*). An accumulator in brackets carries the output built so far. Here a query string accepts both key = value pairs and bare flags:
macro_rules! query {
// Nothing left: join the accumulated parts.
(@acc [$($parts:expr),*]) => {
vec![$($parts),*].join("&")
};
// `key = value, ...`
(@acc [$($parts:expr),*] $key:ident = $value:expr $(, $($rest:tt)*)?) => {
query!(@acc [$($parts,)* format!("{}={}", stringify!($key), $value)] $($($rest)*)?)
};
// `flag, ...`
(@acc [$($parts:expr),*] $flag:ident $(, $($rest:tt)*)?) => {
query!(@acc [$($parts,)* stringify!($flag).to_string()] $($($rest)*)?)
};
// Entry point: start with an empty accumulator.
($($input:tt)+) => {
query!(@acc [] $($input)+)
};
}
fn main() {
let page = 2;
println!("{}", query!(q = "rust", page = page, verbose, sort = "date"));
println!("{}", query!(dry_run));
println!("{}", query!(limit = 10 * page, archived,));
}q=rust&page=2&verbose&sort=date
dry_run
limit=20&archivedEvery step re-parses the remaining input, so munchers get slow on very long inputs. They are common in crates that accept rich syntax, such as serde_json::json!.
26. Implementing a trait for many types
Crates often need the same impl for a list of types. The standard library does this for its number types with private macros, which you'll see throughout its source.
trait Describe {
fn describe(&self) -> String;
}
macro_rules! impl_describe {
($($t:ty => $kind:literal),* $(,)?) => {
$(
impl Describe for $t {
fn describe(&self) -> String {
format!("{} {} (size {})", $kind, self, std::mem::size_of::<$t>())
}
}
)*
};
}
impl_describe! {
u8 => "unsigned",
i32 => "signed",
u64 => "unsigned",
f64 => "float",
}
fn main() {
let values: [&dyn Describe; 4] = [&255u8, &-40i32, &18_000_000_000u64, &2.5f64];
for value in values {
println!("{}", value.describe());
}
}unsigned 255 (size 1)
signed -40 (size 4)
unsigned 18000000000 (size 8)
float 2.5 (size 8)27. Where a macro is visible
macro_rules! macros don't follow the normal module rules. By default they are textually scoped: usable after the definition, in the same module and in child modules written after it. There are two ways to use a macro elsewhere in the crate:
#[macro_use]on amodkeeps the module's macros visible after themoditem.pub(crate) use name;after the definition turns the macro into a normal item that you import by path.
macro_rules! greet {
($name:expr) => {
format!("hello, {}", $name)
};
}
mod shop {
// A child module written after the definition can use it.
pub fn welcome() -> String {
greet!("shop")
}
}
#[macro_use]
mod text {
macro_rules! shout {
($s:expr) => {
$s.to_uppercase()
};
}
}
mod math {
macro_rules! double {
($x:expr) => {
$x * 2
};
}
pub(crate) use double;
}
fn main() {
println!("{}", shop::welcome());
println!("{}", shout!("sale")); // visible thanks to #[macro_use]
println!("{}", math::double!(21)); // imported by path
}hello, shop
SALE
42Newer code prefers pub(crate) use, because a reader can follow the path to the definition. #[macro_export] (example 19) is for macros that other crates use.
28. Realistic: an ensure! macro for validation
anyhow provides ensure!(condition, "message"), which returns an error when the condition is false. Writing our own shows how macros can use return to exit the calling function, something a helper function can't do.
macro_rules! ensure {
($cond:expr, $($fmt:tt)+) => {
if !$cond {
return Err(format!($($fmt)+));
}
};
}
#[derive(Debug)]
struct Transfer {
from: String,
to: String,
cents: i64,
}
fn validate(t: &Transfer) -> Result<(), String> {
ensure!(t.cents > 0, "amount must be positive, got {}", t.cents);
ensure!(t.from != t.to, "cannot transfer from {} to itself", t.from);
ensure!(t.cents <= 1_000_000, "amount {} exceeds the daily limit", t.cents);
Ok(())
}
fn main() {
let cases = [
Transfer { from: "ana".into(), to: "ben".into(), cents: 5_000 },
Transfer { from: "ana".into(), to: "ana".into(), cents: 100 },
Transfer { from: "ana".into(), to: "ben".into(), cents: -3 },
Transfer { from: "ana".into(), to: "ben".into(), cents: 5_000_000 },
];
for t in &cases {
println!("{:?}", validate(t));
}
}Ok(())
Err("cannot transfer from ana to itself")
Err("amount must be positive, got -3")
Err("amount 5000000 exceeds the daily limit")Summary
- Macros generate code at compile time. Prefer functions and generics; use macros for variable arguments, generating items, or custom syntax.
macro_rules!matches input against patterns with fragment specifiers (expr,ident,ty,tt, ...), supports multiple rules, repetition ($( ... ),*), and recursion.- Bind
exprarguments to a local to evaluate them once. Macro-internal variables are hygienic. - Macros can generate functions, structs, enums, impls, and tests;
$vis:visand#[$meta:meta]pass visibility and attributes through;compile_error!gives clear errors. #[macro_export]publishes a macro at the crate root; use$crate::for paths in the expansion.cargo expandshows what macros produce.- Built-ins such as
matches!,todo!,concat!,include_str!, andformat_args!cover common compile-time needs. - Internal
@ruleskeep helpers private to one macro, a TT muncher consumes its input a piece at a time, and one macro can implement a trait for a whole list of types. - A macro is visible textually after its definition;
#[macro_use]extends that past a module, andpub(crate) use name;lets you import it by path.
Next: Procedural Macros: derive, attribute, and function-like macros written with syn and quote.
