425: Proc Macro Attribute
Tutorial Video
Text description (accessibility)
This video demonstrates the "425: Proc Macro Attribute" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Attribute macros transform the item they annotate. Key difference from OCaml: 1. **Arguments**: Rust attribute macros receive arguments as a `TokenStream` and parse them freely; OCaml PPX attributes use a typed declaration system.
Tutorial
The Problem
Attribute macros transform the item they annotate. #[tokio::main] rewrites async fn main() into a synchronous main that creates a Tokio runtime. #[actix_web::get("/path")] registers a handler function with routing metadata. #[cached] wraps a function with memoization. These transformations are impossible with derive macros (which only add implementations) or macro_rules! (which can't inspect or rewrite existing items). Attribute macros receive both the attribute arguments and the annotated item, enabling full code transformation.
Attribute macros are the mechanism behind framework integration: web routing, async runtime setup, middleware injection, retry logic, and profiling annotations.
🎯 Learning Outcomes
(attr: TokenStream, item: TokenStream)#[tokio::main] rewrites async functions using attribute macros#[proc_macro_attribute] registration and how arguments are passedCode Example
#![allow(clippy::all)]
//! Attribute Macro Patterns
//!
//! What attribute macros can do.
/// Example: what #[log_calls] might add
pub fn logged_function(x: i32) -> i32 {
// Generated: println!("Entering logged_function");
let result = x + 1;
// Generated: println!("Exiting logged_function");
result
}
/// Example: what #[test_case] might expand to
pub fn factorial(n: u64) -> u64 {
if n <= 1 {
1
} else {
n * factorial(n - 1)
}
}
/// Simulating #[timed] attribute
pub fn timed_operation() -> std::time::Duration {
let start = std::time::Instant::now();
std::thread::sleep(std::time::Duration::from_millis(1));
start.elapsed()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logged_function() {
assert_eq!(logged_function(5), 6);
}
#[test]
fn test_factorial() {
assert_eq!(factorial(5), 120);
assert_eq!(factorial(0), 1);
}
#[test]
fn test_timed() {
let d = timed_operation();
assert!(d.as_millis() >= 1);
}
#[test]
fn test_factorial_10() {
assert_eq!(factorial(10), 3628800);
}
#[test]
fn test_factorial_1() {
assert_eq!(factorial(1), 1);
}
}Key Differences
TokenStream and parse them freely; OCaml PPX attributes use a typed declaration system.compile_error! or return a TokenStream with errors; OCaml raises exceptions or uses Location.error_extensionf.trybuild for expected outputs; OCaml uses ppx_deriving's test expect tests.OCaml Approach
OCaml's PPX extensions ([@attr] and [%ext ...]) serve the attribute macro role. A [@log_calls] ppx extension receives the function's AST and can wrap it. ppxlib's Attribute.declare creates typed attribute handlers. The ppx_bench and ppx_expect libraries use this to transform functions with benchmarking and expectation test machinery.
Full Source
#![allow(clippy::all)]
//! Attribute Macro Patterns
//!
//! What attribute macros can do.
/// Example: what #[log_calls] might add
pub fn logged_function(x: i32) -> i32 {
// Generated: println!("Entering logged_function");
let result = x + 1;
// Generated: println!("Exiting logged_function");
result
}
/// Example: what #[test_case] might expand to
pub fn factorial(n: u64) -> u64 {
if n <= 1 {
1
} else {
n * factorial(n - 1)
}
}
/// Simulating #[timed] attribute
pub fn timed_operation() -> std::time::Duration {
let start = std::time::Instant::now();
std::thread::sleep(std::time::Duration::from_millis(1));
start.elapsed()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logged_function() {
assert_eq!(logged_function(5), 6);
}
#[test]
fn test_factorial() {
assert_eq!(factorial(5), 120);
assert_eq!(factorial(0), 1);
}
#[test]
fn test_timed() {
let d = timed_operation();
assert!(d.as_millis() >= 1);
}
#[test]
fn test_factorial_10() {
assert_eq!(factorial(10), 3628800);
}
#[test]
fn test_factorial_1() {
assert_eq!(factorial(1), 1);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logged_function() {
assert_eq!(logged_function(5), 6);
}
#[test]
fn test_factorial() {
assert_eq!(factorial(5), 120);
assert_eq!(factorial(0), 1);
}
#[test]
fn test_timed() {
let d = timed_operation();
assert!(d.as_millis() >= 1);
}
#[test]
fn test_factorial_10() {
assert_eq!(factorial(10), 3628800);
}
#[test]
fn test_factorial_1() {
assert_eq!(factorial(1), 1);
}
}
Deep Comparison
OCaml vs Rust: proc macro attribute
See example.rs and example.ml for side-by-side implementations.
Key Points
Exercises
#[timed] that wraps any function with let start = Instant::now(); let result = original_body; println!("{}: {:?}", fn_name, start.elapsed()); result.#[validate_positive] for functions taking i32 that adds an assertion at the start: assert!(arg > 0, "argument must be positive"). Handle functions with multiple parameters by validating only the first i32.#[replace_with("new_function_name")] that emits a deprecation warning #[deprecated(note = "use new_function_name instead")] and adds a log::warn! call at the start of the function body.