October 25, 2018·6 min read·engineeringrustmacrosmodules

Rust 1.30 expands procedural macros beyond derive

Attribute and function-like procedural macros become stable, with explicit macro imports and clearer paths. Token streams keep generated code connected to source structure and diagnostics.

A derive macro can generate an implementation for a data type, but it cannot express every transformation a library wants to apply to source. A framework may need to interpret an attribute on a function or accept a dedicated token syntax. Rust 1.30 stabilizes procedural macro forms for those cases. Its release announcement also shows macros entering scope through ordinary use declarations.

The expansion builds on existing custom derives. The announcement traces those to Rust 1.15, so this is not the first stable procedural-macro capability. Today's change widens the places a procedural macro can operate and makes its use fit more closely with other imported items. It gives library authors additional means to generate code on stable Rust.

I regard the connection to ordinary imports as especially useful. A reader should be able to identify where a macro comes from without reconstructing an entire crate's import conventions. More powerful code generation increases the value of making its dependencies visible.

Attribute macros receive the annotated item

An attribute-like procedural macro has two token-stream inputs. One contains the attribute's arguments. The other contains the item to which the attribute is attached. It returns a token stream representing the generated replacement. That division gives the macro both its local configuration and the source it is intended to transform.

The announcement uses a route attribute on a function as an illustration. A framework can interpret an HTTP method and path in the attribute while examining the function item separately. The syntax is an example of what a framework can define, not a built-in routing feature supplied by the language.

This differs from treating an annotation as inert metadata. An attribute macro can generate code based on the item it receives. A reader understanding the resulting program therefore needs the macro's contract as well as the visible function body. The annotation names a transformation that runs as part of compiling the source.

A library exposing such an attribute has an API to document. It needs to explain accepted arguments and the effect on the annotated item. Good diagnostics matter because a mistake in that small annotation can affect a larger amount of generated code. Reporting only an error deep inside the expansion gives the user less useful information than identifying the relevant input.

Function-like macros receive their invocation tokens

A function-like procedural macro uses the familiar macro invocation form and receives one token stream. Its result supplies generated code. The declaration uses #[proc_macro], while an attribute macro uses #[proc_macro_attribute]. These forms identify how the compiler supplies the input and where the generated result belongs.

The name function-like should not obscure when the transformation occurs. A procedural macro does not behave like an ordinary runtime function receiving already evaluated Rust values. Its input is source tokens. The generated Rust code subsequently participates in the program's compilation.

This difference permits a macro to accept a syntax designed for a particular problem. The announcement illustrates generating GObject classes from a dedicated declaration. Such syntax can reduce repetitive binding code, but users still need to understand what declarations it produces. A compact invocation can represent substantial work by the macro implementation.

There is also a distinction from declarative pattern-based macros. A procedural macro author writes Rust code that receives and produces tokens. This offers an implementation mechanism for transformations that are inconvenient to express as matching and substitution rules. The release adds that mechanism to stable Rust in more invocation positions.

The public exchange is a token stream

The Rust 1.30 proc_macro source describes TokenStream as a sequence of token trees. The interface supports iteration over the trees and construction of a stream from them. It is the shared input and output type for the procedural macro forms.

This is more structured than treating the entire source as an undifferentiated string. Tokens preserve distinctions the macro can use while generating Rust syntax. The API's groups also represent delimited sequences. A macro consuming the interface needs to respect that structure instead of assuming every visible character belongs to one flat token list.

The versioned documentation says iteration is shallow. A delimited group appears as a token tree rather than automatically exposing all nested tokens in one sequence. That is a useful implementation detail because a parser may need to choose when to descend into a group. Flattening everything indiscriminately would discard a distinction the interface supplies.

The source also documents string conversion. Parsing a string into a stream can fail for unbalanced delimiters or invalid characters, and the display representation has qualifications around round trips. Those details make direct token manipulation and arbitrary string rewriting different techniques. A macro author should not infer that formatting a stream gives a permanent textual representation suitable for every parser shortcut.

The stable annotations record a longer history than a blanket claim that the whole crate becomes stable today. TokenStream already has stable support from earlier releases, while additional operations have their own stabilization versions. For example, extending a token stream has a Rust 1.30 stability annotation. A macro's minimum compiler requirement depends on the operations it actually uses.

Source locations affect the quality of generated code

The release announcement demonstrates improved diagnostics around serialization derives. The newer output can identify a field whose type lacks the required trait, rather than pointing only at the derive annotation and exposing a cumbersome generated name. That changes how a user finds the source of a failed expansion.

The compiler still checks the generated program. A procedural macro cannot make a field's type implement a required behavior merely by generating a call that assumes it does. The diagnostic has to relate that failed requirement back to useful source. A successful macro API therefore includes the quality of errors for unsupported inputs.

This is especially relevant when macros promise to remove boilerplate. Removing handwritten code also removes places where a user might otherwise see the requirement directly. If the expansion fails, the tool needs to restore enough context for the user to act. The release's example shows a concrete improvement in that relationship.

I prefer code generation that keeps failures connected to the user's declarations. A macro can make a routine implementation shorter without making its type requirements mysterious. Better error locations help preserve that property as the transformation becomes more capable.

Macro imports identify a specific dependency

Rust 1.30 permits bringing a macro into scope with use. The announcement contrasts the earlier #[macro_use] extern crate approach with importing a specific macro from the crate. The selected name can then be called through the usual macro invocation syntax.

That makes an import communicate which capability the module uses. A broad macro import asks the reader to infer more of the available names from another crate. An explicit import places the relationship near the rest of the module's dependencies. This is useful even when the generated program's behavior is unchanged.

The release examples still include extern crate in their Rust 2015 setup. Removing it from every example would assume edition rules that this announcement has not yet made the universal default. The edition transition needs to remain distinct from features stable in today's compiler.

Likewise, use imports do not make every procedural macro operation behave like an ordinary function. The import identifies the macro. Its invocation still asks for a compile-time token transformation. Keeping those two observations separate prevents an improvement in naming from obscuring the macro's execution model.

Module paths become more explicit

The announcement also introduces paths starting with crate to identify the current crate root. It explains how earlier path behavior differed between imports and direct item references. An explicit root marker gives code a way to state where lookup begins without relying on that contextual distinction.

For example, the path in this import starts at the current crate's root.

use crate::config::Settings;

The example assumes that the crate defines the corresponding accessible module and type. The declaration names an origin. It does not grant access to private items or create a dependency that does not exist. A clear starting point simplifies reasoning about lookup while leaving visibility rules in force.

Raw identifiers provide another compatibility mechanism in this release. The r# prefix allows certain keyword spellings to be used as identifiers, which matters when code written under different keyword sets needs to refer to the same API. This is preparation for cross-edition use, not evidence that the upcoming edition has already shipped.

Rust 1.30 gives macro authors more stable invocation forms and gives callers more explicit ways to name what they use. An attribute receives its arguments and annotated item separately. A function-like macro receives invocation tokens, and an import identifies its defining crate. Those contracts let libraries generate more code while preserving a source-level explanation of where the transformation begins and which inputs it interprets.