Procedural Macro Crates
A procedural macro (proc macro) is a Rust function compiled into a host dynamic library that executes inside the compiler process during compilation, consuming a proc_macro::TokenStream as input and returning a generated proc_macro::TokenStream as output.
Why Procedural Macros Require a Dedicated proc-macro = true Crate
Because procedural macros are compiled for the host machine (the developer's or CI machine running rustc) and dynamically loaded by the compiler before the main application crate is compiled (which may be cross-compiling for a different target like wasm32 or aarch64-unknown-none), Cargo requires procedural macros to live in their own separate crate with proc-macro = true in Cargo.toml:
# my_macros/Cargo.toml
[package]
name = "my_macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
A proc-macro = true crate can only export procedural macros—it cannot export ordinary runtime structs, traits, or functions. In practice, libraries use a two-crate workspace pattern and re-export the proc macro from the main library crate so users only depend on one crate:
Two-Crate Re-Export Architecture (Used by `serde`, `tokio`, `thiserror`, `clap`):
+---------------------------------------+
| `my_lib_macros` (`proc-macro = true`)|
| Defines `#[proc_macro_derive(MyTrait)]`|
+-------------------+-------------------+
| (Dependency)
v
+---------------------------------------+
| `my_lib` (Normal Library Crate) |
| 1. Defines `pub trait MyTrait` |
| 2. Re-exports `pub use my_lib_macros::MyTrait;` |
+---------------------------------------+
(Users import BOTH the trait AND the derive macro via `use my_lib::MyTrait;`!)
The Three Kinds of Procedural Macros
All three kinds of procedural macros are public functions in the root of a proc-macro = true crate using types from the compiler-provided extern crate proc_macro; (available automatically without a Cargo.toml dependency):
| Proc-Macro Kind | Defining Attribute | Function Signature | Invocation Syntax |
|---|---|---|---|
| 1. Derive macro | #[proc_macro_derive(Name, attributes(helper))] |
pub fn my_derive(item: TokenStream) -> TokenStream |
#[derive(Name)] on a struct, enum, or union |
| 2. Attribute macro | #[proc_macro_attribute] |
pub fn my_attr(attr: TokenStream, item: TokenStream) -> TokenStream |
#[my_attr(args)] on any item (fn, struct, impl, mod) |
| 3. Function-like macro | #[proc_macro] |
pub fn my_macro(input: TokenStream) -> TokenStream |
my_macro!(...) in item, statement, expression, or type position |
Inspecting proc_macro::TokenStream and TokenTree Directly
Even without third-party crates, the built-in proc_macro crate provides four TokenTree variants (Group, Ident, Punct, Literal) and FromStr parsing:
// Inside a `proc-macro = true` crate (`src/lib.rs`):
/*
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_input: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
*/
// Demonstrating the exact same token-generation concept in a runnable binary:
fn generate_rust_fn_source(fn_name: &str, value: u32) -> String {
format!("fn {fn_name}() -> u32 {{ {value} }}")
}
fn main() {
let generated = generate_rust_fn_source("answer", 42);
println!("generated token stream source: {}", generated);
}
generated token stream source: fn answer() -> u32 { 42 }