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 typegenerics— any generic parametersdata— the body (struct fields or enum variants)attrs— attributes on the typevis— 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.