Procedural macros

In Rust a procedural macro is a program that runs during compilation: it receives a token stream and returns another. The three kinds are function-like macros, derives and attribute macros, and all of them live in a crate of their own with a special manifest. The compiler runs that crate first, then compiles the result into the caller.

The lesson builds a crate with two macros, a function-like twice! and a derive named Describe, then a second crate that depends on it by path. The macros here parse tokens by hand, with no helper crates, so the lesson needs no download.

The proc-macro crate

The manifest sets proc-macro = true in the lib section, and the source uses proc_macro::TokenStream, a crate the compiler provides only to this kind of library. The crate is built with the ordinary cargo command.

cargo new --vcs none --lib answer
vi Cargo.toml
vi src/lib.rs

[package]
name = "answer"
version = "0.1.0"
edition = "2024"

[lib]
proc-macro = true

The first version of the library defines one function-like macro. The attribute marks the function as the entry point, and the body parses the input as a number and returns twice that number as tokens.

use proc_macro::TokenStream; #[proc_macro] pub fn twice(input: TokenStream) -> TokenStream { let value: u32 = input.to_string().trim().parse().expect("a number"); (value * 2).to_string().parse().unwrap() }

cargo build

Compiling answer v0.1.0 (/tmp/rw3/L045/stage_a)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s

The build output is the same as for any library, because a proc-macro crate is an ordinary crate with a special target. The difference is that the compiler can load it as a plugin.

A function-like macro

The function takes the tokens between the parentheses, turns them into a string, parses a number and builds the answer. The parse back to tokens is where a real macro would build syntax; here the result is a literal.

The consumer crate declares the dependency with a path and imports the macro. The call looks like a macro from the standard library, and the compiler expands it during the build.

cargo new --vcs none stage_b
vi Cargo.toml
vi src/main.rs

[dependencies]
answer = { path = "../stage_a" }

use answer::twice; fn main() { println!("{}", twice!(21)); }

cargo run

Compiling stage_b v0.1.0 (/tmp/rw3/L045/stage_b)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.36s
Running `target/debug/stage_b`
42

The status line names the binary and the crate, and the program prints forty-two. The compiler built the macro crate first, then the consumer, and the macro ran in between.

A derive macro

A derive receives the item it is attached to and returns new items. The second macro in the library reads the first two words of the item, struct and the name, and generates an inherent impl with a method that prints them. The token parsing is by hand, which is why the name is filtered to letters.

The library now exports both macros, and the build still succeeds. The attribute is #[proc_macro_derive(Describe)], and the function returns the tokens of the impl block.

use proc_macro::TokenStream; #[proc_macro] pub fn twice(input: TokenStream) -> TokenStream { let value: u32 = input.to_string().trim().parse().expect("a number"); (value * 2).to_string().parse().unwrap() } #[proc_macro_derive(Describe)] pub fn describe(input: TokenStream) -> TokenStream { let text = input.to_string(); let mut words = text.split_whitespace(); let kind = words.next().unwrap(); let raw = words.next().unwrap(); let name: String = raw.chars().filter(|c| c.is_alphanumeric() || *c == '_').collect(); format!("impl {name} {{ fn describe(&self) -> &'static str {{ \"{kind} {name}\" }} }}") .parse() .unwrap() }

cargo build

Compiling answer v0.1.0 (/tmp/rw3/L045/answer)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.66s

A real derive uses a parser crate to walk the item properly and keeps the visibility, generics and attributes of the original. The shape is the same: tokens in, items out, and the compiler checks the generated code like any other.

Using the crate

The application imports both macros and derives Describe on a unit struct. The generated method becomes a normal method of the type, and the function-like macro is called like any other.

The program prints the generated description and the doubled number. The derive output is struct Point, which is what the macro read from the item and wrote back.

use answer::{twice, Describe}; #[derive(Describe)] struct Point; fn main() { let point = Point; println!("{}", point.describe()); println!("{}", twice!(21)); }

cargo run

Compiling answer v0.1.0 (/tmp/rw3/L045/answer)
Compiling app v0.1.0 (/tmp/rw3/L045/app)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.66s
Running `target/debug/app`
struct Point
42

The first line of the output comes from the generated method and the second from the function-like macro. Neither is a function call at run time: both are code the compiler wrote before the program existed.

proc_macro in a normal crate

The proc_macro crate is only available to a library whose manifest says proc-macro. Using it in an ordinary library is E0432, and the help suggests adding a dependency that does not exist, because this is a manifest mistake rather than a missing package.

The program below is a normal library with the import at the top. The fix is the [lib] proc-macro = true section from the first section, and then the crate can no longer be used as a normal library.

use proc_macro::TokenStream; pub fn double(input: TokenStream) -> TokenStream { input }

cargo build

Compiling normal v0.1.0 (/tmp/rw3/L045/normal)
error[E0432]: unresolved import `proc_macro`
--> src/lib.rs:1:5
|
1 | use proc_macro::TokenStream;
| ^^^^^^^^^^ use of unresolved module or unlinked crate `proc_macro`
|
= help: if you wanted to use a crate named `proc_macro`, use `cargo add proc_macro` to add it to your `Cargo.toml`

For more information about this error, try `rustc --explain E0432`.
error: could not compile `normal` (lib) due to 1 previous error

The two roles are exclusive: a proc-macro crate exports only macros, and a normal library cannot export them. A project that needs both splits into two crates, which is what the standard derives do.

Exercises

  1. Change twice! to add one instead of doubling, and run the consumer again.
  2. Make the derive read a field and include it in the description. Which tokens does the item contain?
  3. Add a third macro to the crate and use it from the application.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does a procedural macro receive and return?

    Answer

    A token stream in and a token stream out

    The macro works on tokens during compilation and returns the code to expand.

  2. What must a proc-macro crate declare in its manifest?

    Answer

    proc-macro = true in the lib section

    The flag tells cargo to build the crate as a compiler plugin.

  3. How many kinds of procedural macro are there?

    Answer

    Three: function-like, derive and attribute

    The three attributes are proc_macro, proc_macro_derive and proc_macro_attribute.

  4. Where can the proc_macro crate be used?

    Answer

    Only in a crate built as a proc-macro library

    The compiler provides it only to the special library target.

  5. What does this crate print when the consumer calls twice!(21)? use proc_macro::TokenStream; #[proc_macro] pub fn twice(input: TokenStream) -> TokenStream { let value: u32 = input.to_string().trim().parse().expect("a number"); (value * 2).to_string().parse().unwrap() }

    Answer

    42

    The macro parses the number and returns the doubled literal as tokens.

  6. What does a derive macro return?

    Answer

    New items, such as an impl block

    The derive adds items next to the annotated one, and the compiler compiles them.

  7. How does the consumer use a function-like proc macro?

    Answer

    It imports the macro and calls it like a declarative one

    The call site looks the same, and the import brings the macro into scope.

  8. What does a derive know about the item it is attached to?

    Answer

    The tokens of the item, which it can parse

    The input token stream is the whole item, and the macro decides what to read.

  9. What is the first line of the compiler output for this program built as a normal library? use proc_macro::TokenStream; pub fn double(input: TokenStream) -> TokenStream { input }

    Answer

    error[E0432]: unresolved import proc_macro

    The crate is only linked for a proc-macro library, and the help names the manifest flag.

  10. The derive in the lesson generates an impl like this one. What does the program print? struct Point; impl Point { fn describe(&self) -> &'static str { "struct Point" } } fn main() { println!("{}", Point.describe()); }

    Answer

    struct Point

    The generated method returns the kind and the name, and the print shows the string.

The code on this page was built with rustc 1.96.1 and cargo 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev