macro_rules! matches patterns. Procedural macros go further: they're Rust functions that take a stream of tokens and return a stream of tokens — arbitrary code transforming code. The most important kind is one you've used constantly: derive.
The three kinds
Derive macros — #[derive(Debug, Clone)] — generate trait impls from a struct or enum definition. Attribute macros — #[tokio::main], #[test] — transform the item they annotate. Function-like macros — sql!(...) — look like macro_rules! calls but run arbitrary Rust to produce the expansion.
You consume far more than you write
Writing a procedural macro is advanced — it lives in its own crate, parses tokens with the syn crate, and generates code with quote. Most Rustaceans never write one. But you use them constantly: every #[derive(...)], every #[tokio::main], every #[derive(Serialize)] is a procedural macro doing compile-time code generation for you.
#[derive(Debug, Clone, PartialEq)] isn't built-in syntax — it's a procedural macro that reads your struct and writes the trait impls. Understanding that demystifies the whole attribute-and-derive ecosystem: it's all code generation, triggered by annotations.The cost-benefit
Procedural macros can eliminate enormous boilerplate — serde derives serialization for a 50-field struct from one line. The cost is compile time (they run a mini-compiler) and opacity (the generated code is invisible). That trade is almost always worth it for the well-tested ecosystem macros; think harder before writing your own.