🎓 A1Learner
🔍

Rust Procedural Macros

A procedural macro is an ordinary Rust function that runs inside the compiler and writes code. A procedural macro can read field names and parse a small language.

Derive: the compiler writes the impl

This kind runs from the beginning: #[derive(Name)] on a struct or enum adds an impl for it. The next lesson builds one:

#[derive(Debug)]
struct Point { x: i32, y: i32 }

println!("{:?}", Point { x: 1, y: 2 });

Attribute: rewrites the item below it

An attribute macro can sit on any item, such as a function, and rewrite the whole item. A web framework's #[route(GET, "/")] works this way. So does this one. It rebuilds an async main into a normal fn that starts a runtime:

#[tokio::main]
async fn main() {
    // your async code — the macro rebuilds main around it
}

Function-like: any language inside

This only looks like a function call. The tokens inside may be any language at all — a macro like this can parse and check SQL while compiling:

let query = sql!(SELECT name FROM users WHERE id = 1);
// not a call: a macro reads the SQL tokens at compile time

A proc macro is a Rust function

It takes a TokenStream and returns a TokenStream. It must live in its own crate, with proc-macro = true in its Cargo.toml — an ordinary crate cannot define one:

#[proc_macro_derive(Hello)]
pub fn derive_hello(input: TokenStream) -> TokenStream {
    let parsed = syn::parse(input).unwrap();
    build_impl(&parsed).into()
}

Example

#[derive(Debug, Clone)]
struct Point { x: i32, y: i32 }
fn main() {
    let p = Point { x: 1, y: 2 };
    println!("derive made Debug: {:?}", p);
    println!("attribute macros rewrite an item, like #[tokio::main]");
    println!("function-like macros look like calls, like sql!(...)");
}

Key points

  • TokenStream: your code chopped into its smallest pieces — names, numbers, text, and punctuation marks
  • The macro runs while compiling
  • Its returned tokens are added to (or replace) your code
  • At run time, no trace of it remains
  • One crate may hold several procedural macros, of the same or different kinds
  • A declarative macro only rearranges pieces it matched, like a stencil.

In this example, the first line is a procedural macro at work. #[derive(Debug, Clone)] asks the compiler to generate Debug and Clone impls for Point. That is code you never wrote by hand. Because Debug was generated, {:?} can print the whole point: Point { x: 1, y: 2 }. The last two lines simply name the other two kinds, so you can recognise them in real code. One is an attribute macro such as #[tokio::main], rewriting the function below it. The other is a function-like macro such as sql!(...) that only looks like a call.

Note

You will use procedural macros far more often than you write them

serde, Tokio and most web frameworks ship them

Writing one is a library author's job

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Where must a procedural macro be defined?