A macro can generate code that works in one caller's module but fails in another because a name resolves differently. Hygiene describes how names introduced by a macro interact with names around its invocation.
The macro_rules! hygiene exercise covers its mixed-site rules. Procedural macros generally behave like code inserted at the invocation, but token spans provide some control over name resolution.
Avoid depending on the caller's imports
Suppose a procedural macro emits Option::Some(7_u32). A caller could
define its own Option type, so the generated expression would no longer
refer to the standard enum.
Here is a complete function-like macro for the src/lib.rs of a
proc-macro crate named lesson_macros:
use proc_macro::TokenStream;
#[proc_macro]
pub fn some_seven(_input: TokenStream) -> TokenStream {
"::core::option::Option::Some(7_u32)"
.parse()
.expect("fixed tokens are valid")
}
Set proc-macro = true under [lib] in that crate's Cargo manifest.
A separate binary crate that depends on it can use this src/main.rs:
use lesson_macros::some_seven;
struct Option;
fn main() {
let _local_type = Option;
let value = some_seven!();
assert_eq!(value, ::core::option::Option::Some(7));
}
In Rust 2018 and later, the leading :: resolves this path through the
external-crate namespace, avoiding a local type or module named Option
or core. The generated code still requires the referenced crate to be
available under that name. Paths to other dependencies need care when a
consumer renames a dependency in Cargo.
Qualifying a trait call can help for a similar reason.
<Type as Trait>::method(&value) selects the intended trait method
explicitly, rather than allowing an inherent method with the same name
to take precedence.
Spans affect generated names
A token's Span carries source-location and name-resolution information.
On the stable toolchain used for these lessons:
Span::call_site()gives names call-site resolution, allowing them to interact with the caller's scope.Span::mixed_site()uses the mixed-site behavior associated withmacro_rules!: local variables, labels and$cratereceive definition-site resolution, while other names use the call site.Span::def_site()is an unstable API.
With quote!, tokens written directly in the quoted template normally
receive call-site spans. Interpolated tokens keep their existing spans.
Assigning a mixed-site span to a generated local binding and its matching
uses can prevent it from capturing a caller's local variable.
This does not make every generated name collision-free. A generated item
name can still conflict with another item in the module. Use small scopes,
carefully chosen names and qualified paths where appropriate. An
anonymous const _: () = { ... }; can contain helper items and imports;
ordinary blocks also provide useful scopes.
Custom syntax is available to both macro systems
Function-like procedural macros receive tokens and can parse them into
a custom language. That capability is not exclusive to them:
macro_rules! can match literal tokens and capture token trees with
$name:tt.
This complete declarative example accepts a small query-shaped syntax:
macro_rules! select_column {
(SELECT $column:ident FROM $table:ident) => {
(stringify!($table), stringify!($column))
};
}
fn main() {
assert_eq!(select_column!(SELECT name FROM users), ("users", "name"));
}
The invocation's contents are not a Rust expression. They are still valid
macro input. serde_json::json! is another example of a declarative macro
with custom syntax; it is not a procedural macro.
A procedural macro becomes useful when ordinary Rust code makes the parsing, validation or generation easier to maintain. It can also perform compile-time work such as reading external data. Neither macro system receives the compiler's resolved types or trait-implementation database through its token input.
What can reach a procedural macro?
Input must tokenize successfully and have balanced delimiters. An
unterminated string or unmatched parenthesis is rejected before the macro
can parse it. However, # is a valid punctuation token, and <<< can be
represented by three punctuation tokens. Neither spelling is inherently
forbidden inside a macro invocation.
The macro assigns meaning to those tokens. Accepting a custom delimiter convention does not let it bypass Rust's lexer or balanced-delimiter rules. Its output must also be valid for the Rust syntax position being replaced.
Token trees, punctuation and lifetimes
A procedural TokenStream contains TokenTree values of four kinds:
Group, Ident, Punct and Literal. Groups contain nested token
streams and may have parentheses, braces, brackets or no visible delimiter.
Compared with declarative macro tokens, two differences are especially useful to know:
- Multi-character operators such as
=>are represented by separate punctuation tokens. TheirSpacingrecords whether punctuation can join with what follows. - A lifetime such as
'ais represented by an apostrophe punctuation token followed by an identifier.
Libraries such as syn parse these tokens into syntax trees; quote
helps produce output tokens. They are useful dependencies, not
requirements for every procedural macro. The some_seven example uses
only the compiler-provided proc_macro API.
Choosing an approach
Prefer functions, traits or generics when they express the operation
directly. Use macro_rules! when token matching gives a clear, maintainable
solution. Consider a procedural macro when richer parsing or code generation
justifies a separate crate and additional build work.
Both macro systems contribute to compilation cost. Editor support and diagnostics depend on the macro and tooling, so neither has a universal ranking. Test generated code in several caller contexts, including names that could shadow its dependencies.
Choosing between generics and macros develops that decision further.