TL;DR: A procedural macro is a Rust function that takes a
TokenStreamas input and returns aTokenStreamas output; it transforms or generates code at compile time. There are three kinds:#[derive(MyTrait)](adds trait impls),#[my_attribute](transforms items), andmy_macro!(...)(function-like, expands anywhere). Proc-macros live in their own crate withproc-macro = trueinCargo.toml. Usesynto parse input andquote!to generate output. Proc-macros have been stable since Rust 1.15 (derive) and Rust 1.45 (attribute and function-like), and are the backbone of nearly every major Rust framework in 2026.
What Are the Three Types of Proc-Macros?
Derive macros add trait implementations. Attribute macros transform items. Function-like macros expand like declarative macros.
// 1. Derive macro; adds impl block for a struct/enum
#[derive(Serialize, Deserialize, Debug)]
struct User { name: String, age: u32 }
// 2. Attribute macro; transforms the annotated item
#[route(GET, "/users")]
async fn get_users() -> impl Responder { ... }
// 3. Function-like macro; called with macro syntax
let query = sql!(SELECT * FROM users WHERE id = $1);#[derive(...)] is by far the most common; serde, thiserror, clap, and diesel all use it.
How Do You Create a Proc-Macro Crate?
Proc-macros must live in a separate crate with proc-macro = true set in Cargo.toml.
# my-derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"// my-derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
quote! {
impl MyTrait for #name {
fn hello(&self) -> &str { stringify!(#name) }
}
}.into()
}How Do syn and quote Work Together?
syn parses a TokenStream into a Rust AST. quote! turns Rust syntax back into a TokenStream.
syn::parse_macro_input!(input as DeriveInput): parse the struct/enum the macro is applied tosyn::DeriveInput: gives you the name, generics, and fieldsquote! { ... }: write Rust code with#interpolationslike a template#name,#(#fields),*: interpolate parsed values into generated code
Together they form the standard proc-macro toolkit: parse with syn; generate with quote.
Proc-Macro vs macro_rules! in 2026
macro_rules! does textual pattern matching. Proc-macros operate on token trees and have full Rust logic.
| Feature | macro_rules! | Proc-macro |
|---|---|---|
| Power | Pattern matching | Full Rust code |
| AST access | ❌ | ✅ via syn |
| Error messages | Basic | Precise spans via syn::Error |
| Separate crate | ❌ not required | ✅ Required |
| Compile-time cost | Low | Higher (parses full AST) |
| Maintainer | Rust core team (built-in) | Community (syn, quote by dtolnay) |
| Best for | Repetition, simple patterns | Derives, frameworks, codegen |
If your macro only needs to repeat or substitute tokens, choose macro_rules! (it is simpler and faster to compile). If you need to inspect struct fields, generate trait impls conditionally, or build a framework like Axum or Leptos, use a proc-macro. In 2026, proc-macros are the default choice for any crate that ships a #[derive(...)] or #[route(...)] annotation.
Frequently Asked Questions
Yes, but it is strongly discouraged. Proc-macros run at compile time on the developer's machine; any I/O must be deterministic and fast. Build scripts (build.rs) are the proper place for I/O-heavy codegen.
The Rust compiler links proc-macro crates as native libraries and loads them during compilation. This requires a clean separation from regular library code to avoid circular dependencies and to ensure the macro binary is compiled for the host platform, not the target.
proc-macro2 re-exports the proc_macro types in a form usable outside of a proc-macro context; enabling unit testing of proc-macro logic without invoking the compiler. Most proc-macro authors use proc-macro2::TokenStream internally and convert at the boundary.
Partially. Derive macros operate on the items passed to them and do not introduce new identifiers into the caller's scope by default. Attribute and function-like macros can be non-hygienic depending on how they generate code, so authors must be careful with generated identifier names.
serde (derive), thiserror (derive), tokio::main (attribute), clap (derive), sqlx::query! (function-like), leptos::component (attribute), and axum routing macros all rely on proc-macros. They are foundational to the Rust ecosystem.
Sources
- Rust Reference; Procedural Macros
- syn on crates.io
- quote on crates.io
- The Little Book of Rust Macros: covers both
macro_rules!and proc-macros with worked examples
Related Glossary Terms
- macro: Overview of all macro types in Rust
- macro-rules: Declarative macros as an alternative
- derive: The
#[derive(...)]attribute proc-macros power - trait: Derive proc-macros generate trait implementations
- serde: The most widely used proc-macro crate in the ecosystem
Keep Reading
- Learn Rust in 2026: Proc-macros are a mid-to-advanced Rust topic worth understanding after ownership
- Rust vs Go in 2026: See how Rust's metaprogramming compares to Go's code generation approach

