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
66: Procedural Macros Overview
Procedural macros are often the most intimidating part of Rust because they feel less like writing code and more like writing a program that writes other programs. If declarative macros (macro_rules!) are like find-and-replace on steroids, procedural macros are full-blown compiler plugins. They take a stream of tokens as input, manipulate them, and spit out a new stream of tokens.
To keep this concrete, we're going to build a simple attribute macro called #[log_call]. The goal is to be able to slap this on any function, and have Rust automatically inject a print statement at the start and end of that function. It's a classic "cross-cutting concern" that saves us from typing the same boilerplate in fifty different functions.
Setting up the specialized crate
Here is the first thing you need to know: procedural macros must reside in their own separate crate. You can't define them in the same crate where you use them. This is because the compiler needs to compile the macro first, then run it to generate the code for your main application.
I'll start by creating a new library crate called my_macros. In the Cargo.toml, I have to explicitly tell Rust this is a proc-macro library:
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
I'm pulling in syn to parse the Rust code into a data structure I can actually work with, and quote to turn those data structures back into Rust code. Without these, you'd be manually manipulating token streams, which is a nightmare I wouldn't wish on anyone.
Parsing the function input
Now, let's write the macro. I'll define the function and use syn::parse_macro_input! to turn the input tokens into an ItemFn, which represents a function definition in Rust.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn log_call(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
// We'll handle the code generation here
}
At this point, input is a rich object. I can see the function's name, its arguments, and its body. I've grabbed the fn_name because I want to use it in my log message.
The "Invisible Function" mistake
This is where I usually trip up when I first start writing these. I'll try to wrap the function body, but I'll make a classic mistake: I'll generate the logging code but forget to include the original function signature and body in the output.
// MISTAKE: I'm only returning the print statements!
let expanded = quote! {
println!("Entering function: {}", stringify!(#fn_name));
println!("Exiting function: {}", stringify!(#fn_name));
};
TokenStream::from(expanded)
When I try to use this in my main project, I'll get a bizarre compiler error saying the function I just called doesn't exist. Why? Because an attribute macro replaces the item it's attached to. By only returning the println! calls, I literally deleted my function from the source code and replaced it with two floating print statements that aren't even inside a function. Oops.
Correctly wrapping the logic
To fix this, I need to reconstruct the entire function. I'll use the quote! macro to recreate the function signature and then inject my logging calls around the original block of code.
#[proc_macro_attribute]
pub fn log_call(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
let fn_vis = &input.vis;
let fn_sig = &input.sig;
let fn_block = &input.block;
let expanded = quote! {
#fn_vis #fn_sig {
println!("Entering function: {}", stringify!(#fn_name));
let result = (|| #fn_block)();
println!("Exiting function: {}", stringify!(#fn_name));
result
}
};
TokenStream::from(expanded)
}
I used a little trick here: wrapping the original #fn_block in an immediately-invoked closure (|| #fn_block)(). This ensures that if the original function had a return statement or returned a value, it still works perfectly and the "Exiting" log always runs. Now, when I apply #[log_call] to a function, it actually behaves like a wrapper.
📋 Practical Task
Build a #[track_usage] Attribute Macro
Create a procedural macro crate that implements an attribute macro called #[track_usage]. This macro should wrap a function so that every time the function is called, it prints a message to the console in the following format: "Executing [function_name]...", followed by the actual execution of the function.
Your implementation must:
- Use
synto parse the input as anItemFn. - Use
quoteto rebuild the function so that it is not deleted from the final binary. - Correctly handle the function's visibility and signature so it remains usable by the rest of the program.
- Successfully print the name of the function dynamically using
stringify!or by accessing the identifier.
There are no comments for now.