Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
189: The syn and quote Crates
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 = trueinCargo.toml. - Use
synto parse theDeriveInputand iterate through the fields of the struct. - Use
quoteto generate animplblock for the struct. - The generated
full_concatmethod 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");
There are no comments for now.