Skip to Content
Course content

189: The syn and quote Crates

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years back, I worked with a developer who was building a custom serialization layer for a high-performance trading system. He had about forty different configuration structs, and for every single one, he was manually writing a to_json method that looked almost identical: iterate through the fields, format the strings, and push them into a buffer. Every time he added a field to a struct, he had to remember to update the manual implementation. He was exhausted, and the bugs were starting to creep in because of the sheer repetition. He didn't need a library; he needed a way to make the compiler write the boring parts for him.

This is exactly why syn and quote exist. If you've dabbled in procedural macros, you know they operate on TokenStreams. But raw tokens are an absolute nightmare to work with—it's like trying to edit a document by manipulating the individual bytes of a PDF. You don't want to manually check if the next token is a comma or a brace. You want to know, "Is this a struct? What are its fields? What is its name?"

Parsing the AST with syn

The syn crate is the industry standard for parsing Rust tokens into a structured Abstract Syntax Tree (AST). When you write a derive macro, syn takes that raw TokenStream and turns it into a DeriveInput enum. This is where the magic happens. Instead of guessing what the user wrote, you get a clean data structure that tells you whether you're looking at a struct, an enum, or a union.

For example, if you're targeting a struct, you can drill down into the DataStruct to access the fields. You can iterate over these fields, grab their identifiers (the variable names), and check their types. I usually tell people to think of syn as the "reader" of your macro: it converts the chaos of source code into a format your Rust logic can actually reason about.

Generating Code with quote

Once you've used syn to understand what the user wrote, you need to output the actual Rust code you want to inject into the program. This is where quote comes in. Writing Rust code as strings is a recipe for disaster—you'll miss a semicolon or a closing brace and spend an hour debugging an incomprehensible compiler error.

The quote! macro allows you to write Rust code that looks like Rust, but it's actually producing a TokenStream. The real power lies in interpolation. By using the # symbol, you can drop variables from your syn parsing phase directly into the generated code. If you have a variable name that holds the identifier of the struct, quote! { impl MyTrait for #name { ... } } will expand to the actual name of the struct in the final binary.

// A simplified look at how they work together
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Describe)]
pub fn describe_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // syn parses the input tokens into a structured AST
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;

    // quote generates the new tokens using interpolation
    let expanded = quote! {
        impl Describe for #name {
            fn describe(&self) {
                println!("This is a struct named {}", stringify!(#name));
            }
        }
    };

    // Convert the quote output back into a TokenStream
    proc_macro::TokenStream::from(expanded)
}

It's a symbiotic relationship: syn reads the code, you process the logic in standard Rust, and quote writes the code back. Just remember that procedural macros live in their own crate (of type proc-macro = true), because they run at compile time, not runtime. It's a weird mental shift at first, but once it clicks, you'll stop writing boilerplate and start writing code that writes code.




📋 Practical Task

Exercise: Building a Struct-Field Concatenator Macro

Your goal is to create a procedural macro called ConcatFields. When applied to a struct where all fields are Strings, this macro should generate a method called full_concat() that returns a single String containing all the field values joined by a space.

Requirements:

  • Create a new library crate with proc-macro = true in Cargo.toml.
  • Use syn to parse the DeriveInput and iterate through the fields of the struct.
  • Use quote to generate an impl block for the struct.
  • The generated full_concat method should look something like this internally: format!("{} {} {}", self.field1, self.field2, self.field3).
  • Ensure you handle the interpolation of the struct name and the field identifiers correctly using the # operator.

Test Case:

#[derive(ConcatFields)]
struct User {
    first_name: String,
    last_name: String,
}

// This should now work:
let user = User { first_name: "Jane".into(), last_name: "Doe".into() };
assert_eq!(user.full_concat(), "Jane Doe");
Rating
0 0

There are no comments for now.

to be the first to leave a comment.