-
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
187: Procedural Macro Types: Derive, Attribute, Function-Like
A few years ago, I was working on a large-scale telemetry system where we had dozens of different event structs. Every single one of them needed to be converted into a specific internal log format. At first, I just implemented a trait for each one manually. Then I got lazy and wrote a declarative macro (macro_rules!) to handle it. But as the requirements grew—like needing to skip certain fields based on a custom attribute—the declarative macro became a nightmare of recursive patterns and confusing syntax. I spent an entire afternoon staring at a compiler error that basically said "something went wrong in the macro expansion," but didn't tell me where.
That was the moment I realized I needed procedural macros. Unlike declarative macros, which are essentially sophisticated find-and-replace tools, procedural macros are actual Rust functions that run during compilation. They take a stream of tokens as input, manipulate them using a full programming language, and spit out a new stream of tokens. It's essentially writing a program that writes your program.
Automating Boilerplate with Custom Derives
The most common procedural macro you've likely already used is the derive macro. When you write #[derive(Debug)], you're telling the compiler to find the Debug derive macro and run it on your struct. As a developer, you can write your own to eliminate repetitive impl blocks.
A derive macro is additive. It doesn't change the struct it's attached to; it just generates additional code (usually a trait implementation) alongside it. For example, imagine you have a Validate trait. Instead of manually writing the validation logic for every single DTO in your API, you can create a #[derive(Validate)] macro. The macro reads the fields of your struct and generates the impl Validate for MyStruct { ... } block automatically. I usually prefer these for 90% of my automation because they are the least intrusive and the easiest for other teammates to understand.
Rewriting Logic via Attribute Macros
Attribute macros are a bit more powerful—and a bit more dangerous. While a derive macro only adds code, an attribute macro can actually transform the item it's attached to. It takes the token stream of the function or struct it's decorating and can return an entirely different token stream, effectively replacing the original code.
You've seen this in action with #[tokio::main]. That attribute isn't just adding something to your main function; it's wrapping your entire function body inside an async runtime initialization block. I use attribute macros when I need to implement "aspect-oriented" programming, like adding a #[log_execution_time] attribute to a function that automatically inserts timing logic at the start and end of the method body without cluttering the business logic.
Flexible Syntax with Function-like Macros
Finally, we have function-like macros. These look and feel like println! or vec!. They are called with an exclamation mark and can take almost any syntax you can imagine as their input. They aren't attached to a specific item; they are invoked wherever you need them in your code.
I find these incredibly useful for creating Domain Specific Languages (DSLs). If you're building a tool that requires a complex configuration tree or a custom routing table for a web server, a function-like macro allows you to define a syntax that is much more readable than nested function calls. Just be careful: because they can take arbitrary tokens, they can be harder to debug if you don't provide clear error messages using the proc_macro_error crate.
📋 Practical Task
Building a Custom Loggable Derive Macro
Your task is to create a procedural macro crate that implements a Loggable trait for any struct it is applied to. This is a classic real-world use case for reducing boilerplate when debugging complex data structures.
Requirements:
- Create a new library crate with
proc-macro = truein itsCargo.toml. - Define a trait named
Loggablewith a methodlog_info(&self)that prints the name of the struct. - Implement a derive macro
#[derive(Loggable)]. - The macro should identify the name of the struct it is attached to and generate an implementation of
Loggablethat prints:"Logging info for struct [StructName]". - Test the macro by applying it to two different structs (e.g.,
UserandOrder) and callinglog_info()on them.
Hint: You will need to add syn and quote to your dependencies to parse the token stream and turn it back into Rust code.
There are no comments for now.