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
188: Building a Custom Derive Macro
A few years ago, I was working on a massive telemetry system with dozens of different event types. Each event was a struct, and for every single one, we had to implement a ToTelemetry trait that mapped the struct fields into a specific key-value format for our database. Every time a junior dev added a new event, they'd either forget to implement the trait or, more often, make a typo in the string key that didn't get caught until the data hit production. I spent a whole Tuesday manually auditing 60 structs. I remember staring at the screen thinking, "The compiler already knows the names of these fields; why am I typing them again?"
That's where custom derive macros come in. They allow you to write code that generates other code at compile time. Instead of manually implementing a trait, you just slap #[derive(MyTrait)] on your struct, and the macro handles the boilerplate. Itβs essentially a plugin for the Rust compiler.
The Machinery of syn and quote
Before you write a single line of macro logic, you need to understand that a derive macro doesn't actually "see" your Rust code as text. It sees it as a TokenStreamβa sequence of tokens like struct, User, {, and String. Trying to manipulate this raw stream is a nightmare. That's why we almost always use two crates: syn and quote.
syn is the parser. It takes that raw TokenStream and turns it into a Syntax Tree (AST) that we can actually work with. It lets us ask questions like, "Is this a struct? What are its field names? Are they public?" quote is the inverse; it allows us to write Rust-like code in a macro and "quote" it back into a TokenStream that the compiler can then integrate into the final binary.
One critical detail: derive macros must live in their own crate. You cannot define a proc-macro in the same crate where you use it. In your Cargo.toml, you have to explicitly tell Rust this is a procedural macro library by adding proc-macro = true under the [lib] section.
Mapping Struct Fields to Implementation
Let's look at how we'd build a Loggable derive macro. The goal is to automatically implement a log_me method that prints the name of every field and its value. Here is how the implementation logic actually flows:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Loggable)]
pub fn loggable_derive(input: TokenStream) -> TokenStream {
// Parse the input tokens into a syntax tree
let input = parse_macro_input!(input as DeriveInput);
// Get the name of the struct (e.g., "User")
let name = input.ident;
// We only care about structs with named fields for this example
let fields = if let syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(fields), .. }) = input.data {
fields.named
} else {
panic!("Loggable can only be derived for structs with named fields");
};
// Generate the actual implementation code
let field_logs = fields.iter().map(|f| {
let name = &f.ident;
quote! {
println!("{}: {:?}", stringify!(#name), self.#name);
}
});
let expanded = quote! {
impl Loggable for #name {
fn log_me(&self) {
println!("Logging struct {}:", stringify!(#name));
#(#field_logs)*
}
}
};
// Convert the quote back into a TokenStream
TokenStream::from(expanded)
}
Notice the #(#field_logs)* syntax. That's quote's way of handling iteration. Since field_logs is an iterator, the repetition operator expands it into a sequence of statements. I personally find this the most "magical" part of the process, but once you get used to it, it's incredibly powerful. You're essentially templating Rust code using the actual AST as your data source.
Keep in mind that because this happens at compile time, if your macro panics (like the panic! call above), the user will see that error message as a compilation error. It's a bit jarring, but it's the most direct way to enforce constraints on who can use your derive macro.
π Practical Task
Build a "JsonKey" Derive Macro
Your task is to create a custom derive macro called JsonKey. Instead of logging the values, this macro should implement a trait called JsonSerializable which has a single method: fn to_json_keys(&self) -> Vec<String>.
The to_json_keys method should return a vector containing the names of all the fields in the struct as strings. For example, if you have a struct User { username: String, email: String }, calling to_json_keys() should return vec!["username".to_string(), "email".to_string()].
Requirements:
- Create a separate crate for the macro with
proc-macro = true. - Use
synto extract the field identifiers. - Use
quoteto generate theVecof strings. - Ensure the macro only works on structs with named fields; otherwise, provide a descriptive compile-time panic.
- Test it on a struct with at least three different fields.
There are no comments for now.