5 min read #rust #Macro System
On this page

Procedural Macros

Procedural macros run Rust code at compile time, receiving a TokenStream and outputting a TokenStream—derive macros automatically generate impls for structs/enums, attribute macros modify arbitrary items, and function-like macros are called like functions but process token streams. syn parses TokenStreams into ASTs, quote converts ASTs back into TokenStreams, and cargo expand lets you view the expansion results—the three form the standard workflow for writing procedural macros.

Three Types of Procedural Macros

// 1. derive: #[derive(MyTrait)] — automatically generates impls for types
#[proc_macro_derive(MyTrait)]
pub fn my_derive(input: TokenStream) -> TokenStream { ... }

// 2. attribute: #[my_attr] — modifies entire items (fn, struct, impl)
#[proc_macro_attribute]
pub fn my_attr(attr: TokenStream, item: TokenStream) -> TokenStream { ... }

// 3. function-like: my_macro!(...) — similar to macro_rules! but uses a Rust function to process input
#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream { ... }

Procedural macros are Rust code that runs at compile time, receiving and producing TokenStreams—a sequence of tokens. This is more flexible than macro_rules!—you can perform arbitrarily complex compile-time computations.

syn + quote: The De Facto Standard

use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
use quote::quote;

#[proc_macro_derive(Hello)]
pub fn hello_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);   // TokenStream → AST
    let name = input.ident;
    let gen = quote! {
        impl Hello for #name {
            fn hello() { println!("Hello, {}!", stringify!(#name)); }
        }
    };
    gen.into()                                               // back to TokenStream
}

syn parses TokenStreams into ASTs (similar to the Rust compiler's frontend). quote! converts Rust code back into a token stream, supporting interpolation (#name).

Limitations of proc-macro Crates

  • Must be a separate crate (Cargo.toml: [lib] proc-macro = true)
  • Can only export proc macro functions, not regular functions, types, or constants
  • Dependencies cannot form cycles—the proc-macro crate can depend on other crates, but cannot be depended upon by crates in the same workspace

cargo expand: Expanding Macros

cargo install cargo-expand
cargo expand                          # expand all macros in the current crate
cargo expand my_function              # expand a specific function

This is the fastest way to understand macro behavior—seeing the actual code the compiler processes.

References

  • syn: docs.rs/syn, quote: docs.rs/quote
  • cargo-expand: github.com/dtolnay/cargo-expand

Keywords: proc macro, derive, syn, quote, TokenStream, cargo expand, attribute macro, function-like macro