Skip to content
← All Rust articles

Rust explainer

Function-like proc macros and proc-macro hygiene

About 5 min to read. Allow extra time to work through examples.

How generated names resolve, when absolute paths help, and what procedural macros add beyond macro_rules!.

This article uses Rust. You can make it your practice mode to browse related content across the site.

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 with macro_rules!: local variables, labels and $crate receive 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. Their Spacing records whether punctuation can join with what follows.
  • A lifetime such as 'a is 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.

step 19 of 28

Macros, FFI and Type-Driven Design

Up next Choosing between generics and macros