A procedural macro is Rust code that runs during compilation. It receives tokens and returns tokens for the compiler to process. The three forms differ in what they receive and where their output goes.
| Form | Example invocation | Input | Output |
|---|---|---|---|
| Function-like | make_value!() |
Tokens inside the delimiters | Replaces the invocation |
| Derive | #[derive(ExtraAnswer)] |
A struct, enum or union definition | Adds items alongside the original definition |
| Attribute | #[replace_answer] |
Attribute arguments and the annotated item | Replaces the annotated item |
A derive does not edit the original fields or body. An attribute macro can
change an item, preserve it or remove it. Not every attribute is a
procedural macro: #[test], for example, is a built-in attribute.
Why there are two crates
Define procedural macros at the root of a library crate configured with
proc-macro = true. Another crate imports and invokes them. The compiler
builds the macro library for the host before running it to expand the
consumer's code, including when the consumer targets another platform.
A proc-macro library cannot invoke its own macros within that same crate. It can contain private helpers, but its public exports are restricted to the supported procedural-macro entry points. Put public runtime types and traits in an ordinary library.
Integration tests in the same Cargo package are separate crates and can
invoke the macros. Unit tests can test ordinary private helper functions;
they cannot directly use a procedural macro defined in the same crate.
Tools such as trybuild can also check examples that should compile or
produce errors.
This lesson is an article because the site's exercise runner compiles a single binary. The following example uses two sibling directories and can be run locally.
A runnable example
Create lesson_macros/Cargo.toml:
[package]
name = "lesson_macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
Put these three definitions in lesson_macros/src/lib.rs:
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_value(_input: TokenStream) -> TokenStream {
"7_u32".parse().expect("valid fixed expression")
}
#[proc_macro_derive(ExtraAnswer, attributes(example_note))]
pub fn extra_answer(_input: TokenStream) -> TokenStream {
"fn derived_answer() -> u32 { 11 }"
.parse()
.expect("valid fixed item")
}
#[proc_macro_attribute]
pub fn replace_answer(
_args: TokenStream,
_item: TokenStream,
) -> TokenStream {
"fn replacement_answer() -> u32 { 13 }"
.parse()
.expect("valid fixed item")
}
These small macros intentionally ignore their inputs. Their fixed names
make the expansion easy to inspect; they are demonstrations, not reusable
generators. Applying ExtraAnswer twice in one module would define
derived_answer twice and cause an error.
Create demo/Cargo.toml beside the lesson_macros directory:
[package]
name = "demo"
version = "0.1.0"
edition = "2024"
[dependencies]
lesson_macros = { path = "../lesson_macros" }
Then put this in demo/src/main.rs:
use lesson_macros::{make_value, replace_answer, ExtraAnswer};
#[derive(ExtraAnswer)]
struct Original {
#[example_note]
value: u32,
}
#[replace_answer]
fn removed_answer() -> u32 {
99
}
fn main() {
assert_eq!(make_value!(), 7);
let original = Original { value: 5 };
assert_eq!(original.value, 5);
assert_eq!(derived_answer(), 11);
assert_eq!(replacement_answer(), 13);
}
From the directory containing both crates, run:
cargo run --manifest-path demo/Cargo.toml
The program exits successfully when all assertions pass. The results show the three expansion behaviors:
make_value!()becomes an expression with value7_u32.ExtraAnswerkeepsOriginaland addsderived_answer.replace_answerremovesremoved_answerand emitsreplacement_answer. Calling the old name would fail to compile.
Inputs and helper attributes
A function-like macro receives the tokens inside its delimiters. These
tokens need not form a Rust expression, but must tokenize successfully
with balanced delimiters. Declarative macro_rules! macros can also
accept custom token syntax. That alone is not a reason to choose a
procedural macro.
A derive receives the annotated definition, including its fields and
relevant attributes. Declaring attributes(example_note) allows the
inert helper attribute used above. It does not implement any behavior:
this demonstration ignores the note. A real derive would parse and
interpret it. Other macros can also see these helper attributes.
An attribute macro receives two streams. For
#[replace_answer(level = "debug")], the first contains
level = "debug"; without arguments it is empty. The second contains
the annotated item, including its other attributes but excluding the
attribute currently being expanded. Our example ignores both streams.
Writing a practical macro
The compiler-provided proc_macro API is sufficient for the example.
Larger macros often use:
proc-macro2for token types that can also be used in ordinary helper libraries and tests.synto parse tokens into syntax structures.quoteto build output tokens, using#nameto interpolate a value.
A real derive usually reads the input name, fields and generics, then generates implementations with the appropriate generic parameters and bounds. Parsing a type's syntax does not reveal the compiler's resolved types or which traits it implements. Generated code is checked afterward.
Procedural macros run with the build process's access to files and other resources. Their dependency and build-time behavior matter when choosing a library, just as build scripts do.
The derive walkthrough explains generated implementations. Macro hygiene covers name resolution, and choosing between generics and macros helps decide when the extra machinery is useful.