In this lesson, you will learn how to write macros that generate code involving references without violating Rust’s strict borrowing rules.
The Challenge of Macro Expansion
When you write a macro in Rust, you are effectively performing text substitution before the compiler performs its borrow checking. Because macros operate on abstract syntax trees (AST) or tokens rather than evaluated logic, it is easy to accidentally generate code that creates a dangling reference or breaks the aliasing rules—errors that the compiler will only catch after the macro has finished expanding.
Hygiene and Captured Variables
The most common way reference safety issues arise in macros is through "captured" variables. When your macro consumes an expression passed as an argument, it is often tempting to use that expression in multiple places within the generated code.
Consider a simple macro designed to inspect a value:
macro_rules! inspect {
($val:expr) => {
{
let r = &$val;
println!("Value: {}", *r);
println!("Address: {:p}", r);
}
};
}
If you call inspect!(x), the macro generates a reference to x. If x is a local variable, this is safe. However, if you pass a temporary expression like inspect!(String::from("hello")), the macro expands to a block where a reference is taken to a temporary object. In this specific case, the reference r will be valid for the duration of the block, but if you were to return that reference or store it, you would create a dangling pointer.
The Macro Safety Rule
To keep your macros safe, you must treat references generated within them exactly as you would if you had written the code manually. Specifically:
- Avoid "Double Evaluation": If your macro uses
$valtwice (e.g.,let x = &$val; let y = &$val;), and$valhas side effects or creates a temporary, you may accidentally create two distinct references where you intended to share one. - Respect Scope: Ensure that any reference created inside the macro does not outlive the scope of the generated block.
- Use
stmtorexprcorrectly: Understand that macros operate at the token level; if your generated code attempts to borrow something that is being moved, the compiler will flag it during the final compilation phase, not during the macro expansion.
Key Takeaway: Macros are just templates for code, so if the expanded result violates borrow checker rules, the macro is unsafe; always verify that your macro expansion creates references that adhere to standard ownership and lifetime constraints.