Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

A minimal derive macro

So far we’ve either returned an empty token stream or hardcoded the output. To write a useful derive macro, we need to read the input — at minimum, we need to know the name of the type we’re deriving for.

Parsing the input (the hard way)

The TokenStream you receive is a sequence of tokens. You could convert it to a string and try to extract the struct name manually:

#![allow(unused)]
fn main() {
use proc_macro::TokenStream;

#[proc_macro_derive(Greet)]
pub fn greet(input: TokenStream) -> TokenStream {
    let input = input.to_string();
    // Crude: find the word after "struct"
    let name = input
        .split_whitespace()
        .skip_while(|t| *t != "struct")
        .nth(1)
        .expect("expected a struct");

    format!(
        "impl {name} {{ pub fn greet() -> &'static str {{ \"Hello from {name}!\" }} }}"
    )
    .parse()
    .unwrap()
}
}

This works for struct Foo {}, but it’s fragile. It breaks on generics (struct Foo<T>), visibility modifiers (pub struct Foo), attributes, and many other valid Rust constructs.

Parsing the input with syn

The syn crate provides a full Rust parser. It can parse a TokenStream into a structured representation that’s easy to work with:

#![allow(unused)]
fn main() {
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Greet)]
pub fn greet(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    format!(
        "impl {name} {{ pub fn greet() -> &'static str {{ \"Hello from {name}!\" }} }}"
    )
    .parse()
    .unwrap()
}
}

DeriveInput is a syn type that represents the input to a derive macro. It gives you:

  • ident — the name of the type
  • generics — any generic parameters
  • data — the body (struct fields or enum variants)
  • attrs — attributes on the type
  • vis — the visibility (pub, pub(crate), etc.)

For now, we only need ident. We’ll use the other fields in later chapters.

parse_macro_input!

Let’s look again at the line that does the parsing:

#![allow(unused)]
fn main() {
let input = parse_macro_input!(input as DeriveInput);
}

The parse_macro_input! macro from syn parses a TokenStream into any type that implements syn::parse::Parse. If parsing fails, it automatically emits a compiler error and returns early from the function.

The as DeriveInput part tells parse_macro_input! which type to parse the tokens into. A TokenStream is just a sequence of tokens — it has no structure. By specifying as DeriveInput, you’re saying “parse these tokens as a struct/enum/union definition.” syn provides other target types too — for example, you could parse as syn::ItemFn to parse a function definition (ItemFn). The as syntax here is specific to parse_macro_input!; it’s not the same as Rust’s type-casting as operator.

Exercise

The exercise for this section is located in 03_basics/03_minimal_derive

Write a derive macro that reads the type name from the input using syn and generates an impl block. This time, the generated code must work for any struct name, not just a hardcoded one.