A proc-macro crate¶
Level: 201 · working knowledge
One line: proc-macro = true makes a library the compiler loads and runs while it compiles other crates. It may export its macros and nothing else, it cannot use them itself, and a crate that wants them depends on it like any other library.
Two crates in a workspace¶
The macro crate says one thing its neighbours do not, under [lib]:
[package]
name = "hello_macro"
version = "0.1.0"
edition = "2024"
publish = false
[lib]
proc-macro = true
The crate that uses it names it as an ordinary dependency:
[package]
name = "hello_app"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
hello_macro = { path = "../hello_macro" }
The macro is the smallest derive worth writing: it implements a trait for the type it sits on. It needs no syn and no quote, and it has no dependencies at all:
//! The smallest useful derive: `#[derive(HelloMacro)]` implements a trait for
//! the type it is on. No `syn`, no `quote`, no dependencies at all; the
//! `proc_macro` crate comes with the compiler.
use proc_macro::{TokenStream, TokenTree};
#[proc_macro_derive(HelloMacro)]
pub fn derive_hello_macro(item: TokenStream) -> TokenStream {
let name = type_name(item);
format!(
r#"impl HelloMacro for {name} {{
fn hello_macro() -> &'static str {{ "Hello, Macro! My name is {name}!" }}
}}"#
)
.parse()
.unwrap()
}
/// The identifier after `struct`, `enum` or `union`. A private function is
/// fine: only the macros themselves may be `pub`.
fn type_name(item: TokenStream) -> String {
let is_keyword = |token: &TokenTree| {
matches!(token, TokenTree::Ident(i) if ["struct", "enum", "union"].contains(&i.to_string().as_str()))
};
let mut tokens = item.into_iter().skip_while(|token| !is_keyword(token));
match (tokens.next(), tokens.next()) {
(Some(_keyword), Some(TokenTree::Ident(name))) => name.to_string(),
_ => panic!("HelloMacro goes on a struct, an enum or a union"),
}
}
Finding the name after struct by walking the tokens is as far as a macro gets without a parser. Tokens and token streams shows what it is walking, and Parsing with syn replaces it.
The program:
use hello_macro::HelloMacro;
/// The trait lives here, not in `hello_macro`: a proc-macro crate cannot export
/// one. The generated `impl HelloMacro for …` finds it because it is in scope.
pub trait HelloMacro {
fn hello_macro() -> &'static str;
}
#[derive(HelloMacro)]
struct Pancakes;
#[derive(HelloMacro)]
enum Waffles {}
fn main() {
println!("{}", Pancakes::hello_macro());
println!("{}", Waffles::hello_macro());
}
Verified output of cargo run -q -p hello_app — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
main.rs has two things named HelloMacro in scope, and that is not a clash. use hello_macro::HelloMacro brings in the derive, which lives in the macro namespace, and pub trait HelloMacro is in the type namespace, so #[derive(HelloMacro)] and impl HelloMacro for … each find the one they mean. serde does the same: its Serialize is a trait and, with the derive feature, also the derive it re-exports from serde_derive (lib.rs ↗).
The generated impl HelloMacro for Pancakes names the trait without a path, so it resolves wherever the derive is used, and it compiles here only because main.rs declares the trait. The re-export pattern is how a real library avoids depending on that.
What proc-macro = true does¶
This script builds hello_macro once through Cargo, printing just the two flags that matter, and then three times with rustc alone.
#!/bin/sh
# hello_macro built by Cargo, printing only the flags that make it a
# proc-macro crate, and then three times by rustc alone. Everything happens in
# a temporary directory, and nothing below prints a path.
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
echo '1. The flags Cargo passes to rustc for hello_macro'
CARGO_TARGET_DIR="$tmp/target" cargo build --locked -v -p hello_macro 2>&1 |
grep -o -e '--crate-type proc-macro' -e '--extern proc_macro'
cp hello_macro/src/lib.rs "$tmp/lib.rs"
cd "$tmp" || exit 1
echo
echo '2. rustc --edition 2024 --crate-type proc-macro lib.rs'
rustc --edition 2024 --crate-type proc-macro lib.rs 2>&1
echo "exit status $?"
echo
echo '3. rustc --edition 2024 --crate-type proc-macro --extern proc_macro lib.rs'
rustc --edition 2024 --crate-type proc-macro --extern proc_macro lib.rs 2>&1
echo "exit status $?"
echo
echo '4. the same file with extern crate proc_macro; added, and no --extern'
{ cat lib.rs; echo 'extern crate proc_macro;'; } > with_extern.rs
rustc --edition 2024 --crate-type proc-macro --crate-name hello_macro with_extern.rs 2>&1
echo "exit status $?"
ls libhello_macro.*
Verified output of sh bare_rustc.sh — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
1. The flags Cargo passes to rustc for hello_macro
--crate-type proc-macro
--extern proc_macro
2. rustc --edition 2024 --crate-type proc-macro lib.rs
error[E0432]: unresolved import `proc_macro`
--> lib.rs:5:5
|
5 | use proc_macro::{TokenStream, TokenTree};
| ^^^^^^^^^^ use of unresolved module or unlinked crate `proc_macro`
|
= help: you might be missing a crate named `proc_macro`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0432`.
exit status 1
3. rustc --edition 2024 --crate-type proc-macro --extern proc_macro lib.rs
exit status 0
4. the same file with extern crate proc_macro; added, and no --extern
exit status 0
libhello_macro.dylib on macOS, libhello_macro.so on Linux
- Cargo passes
--crate-type proc-macroand--extern proc_macro. The first is what the manifest line turns into. The second makes theproc_macrocrate visible. - Without
--extern proc_macro, the same file is E0432.proc_macroships with the compiler, likestd, but a crate does not see it unless something names it, and the help line does not say what that something is. --extern proc_macrowith no path fixes it. rustc finds the crate among its own libraries.extern crate proc_macro;in the source does the same job without the flag. It is the older spelling, and you will still meet it: thiserror-impl 2.0.20'slib.rs↗ has it. A crate built by Cargo does not need it.
The last line is what rustc wrote: a dynamic library, which is the form the compiler can load and call into. The filter in cargo_runs.toml writes out the file name for both platforms, since the extension is the only part that differs; the Linux run of this key checks the .so half.
Why it has to be a crate of its own¶
A derive runs while the compiler is compiling the crate that uses it, so it has to be a finished library before that compilation starts. A crate cannot be that library and the code being compiled at the same time:
//! A derive, used in the crate that defines it.
use proc_macro::TokenStream;
#[proc_macro_derive(HelloMacro)]
pub fn derive_hello_macro(_item: TokenStream) -> TokenStream {
TokenStream::new()
}
#[derive(HelloMacro)]
struct Pancakes;
Verified output of cargo build -q -p uses_its_own_derive, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
error: can't use a procedural macro from the same crate that defines it
--> uses_its_own_derive/src/lib.rs:10:10
|
10 | #[derive(HelloMacro)]
| ^^^^^^^^^^
error: could not compile `uses_its_own_derive` (lib) due to 1 previous error
So the macros live in one crate and everything that uses them in another, and Cargo's dependency order builds the macro crate first.
What it may export¶
Everything pub in a proc-macro crate has to be a macro:
//! The trait and its derive in one crate, the way you would lay out an
//! ordinary library.
use proc_macro::TokenStream;
pub trait HelloMacro {
fn hello_macro() -> &'static str;
}
pub fn greeting(name: &str) -> String {
format!("Hello, Macro! My name is {name}!")
}
#[proc_macro_derive(HelloMacro)]
pub fn derive_hello_macro(_item: TokenStream) -> TokenStream {
TokenStream::new()
}
Verified output of cargo build -q -p exports_a_trait, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
error: `proc-macro` crate types currently cannot export any items other than functions tagged with `#[proc_macro]`, `#[proc_macro_derive]`, or `#[proc_macro_attribute]`
--> exports_a_trait/src/lib.rs:6:1
|
6 | pub trait HelloMacro {
| ^^^^^^^^^^^^^^^^^^^^
error: `proc-macro` crate types currently cannot export any items other than functions tagged with `#[proc_macro]`, `#[proc_macro_derive]`, or `#[proc_macro_attribute]`
--> exports_a_trait/src/lib.rs:10:1
|
10 | pub fn greeting(name: &str) -> String {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: could not compile `exports_a_trait` (lib) due to 2 previous errors
The trait and the helper function are both refused, and the macro between them is fine. Private items are allowed, which is why hello_macro can keep type_name as a plain fn. So a trait and its derive are two crates, and the trait's crate is an ordinary library: serde has the Serialize trait, serde_derive has #[derive(Serialize)], and serde re-exports the derive so a user depends on one crate. In the demo the trait sits in hello_app instead, which is the smallest thing that compiles.
If you are coming from another language¶
- C and C++. GCC's
-fplugin=and Clang's-fplugin=load a shared library into the compiler, which is the same arrangement: a separately built.sothe compiler calls into while it compiles your code, and one that cannot be part of the code it is compiling. The difference is the interface. A compiler plugin is built against that compiler's internal headers, so it is tied to the version that loads it; a proc-macro crate sees only tokens, through the stableproc_macroAPI, and Cargo builds it and wires it in from one line of a manifest. - Python. A decorator can be defined on line 1 and used on line 5 of the same file, because nothing is compiled ahead of time: the decorator is an ordinary object by the time line 5 runs. A Rust macro has to be machine code before the code that uses it is compiled, which is the whole reason for the second crate.
- Java. An annotation processor is the closest match. It is compiled before the code it processes and handed to
javacon the processor path, and it does not run on its own compilation. Java does let one jar carry the annotation types, the runtime classes and the processor together; a proc-macro crate cannot export anything but macros, so Rust splits what Java packages as one.
See also¶
- A function,
macro_rules!, or a procedural macro — the previous page: whether you need this crate at all - Tokens and token streams — the next page: what
type_nameis walking - Three kinds of procedural macro — what the crate's macros can be
- The re-export pattern — how a library ships a trait and its derive as one dependency
- Running a scratch program:
rustcalone — the other things Cargo passes to rustc for you - Cargo targets: the
proc-macrofield ↗ - The Rust Reference: Procedural macros ↗
Po polsku¶
Crate makr proceduralnych (proc-macro crate) to biblioteka z proc-macro = true w sekcji [lib] pliku Cargo.toml. Kompilator ładuje ją jako bibliotekę dynamiczną (.dylib na macOS, .so na Linuksie) i wywołuje w trakcie kompilacji innego crate'a. Stąd dwa ograniczenia: crate nie może użyć własnego makra (can't use a procedural macro from the same crate that defines it), a publicznie może eksportować wyłącznie funkcje oznaczone #[proc_macro], #[proc_macro_derive] albo #[proc_macro_attribute] — cecha (trait) czy funkcja pomocnicza z pub zostaną odrzucone. Dlatego serde i serde_derive to dwa crate'y.
Cargo przekazuje kompilatorowi --crate-type proc-macro i --extern proc_macro. Przy samym rustc, bez tej flagi, use proc_macro::… kończy się błędem E0432; zamiast flagi można dopisać w kodzie extern crate proc_macro;.
Szukaj po polsku: crate makr proceduralnych · proc-macro = true · extern crate proc_macro · rust proc-macro crate cannot export · rust can't use a procedural macro from the same crate