ExamplesBy LevelBy TopicLearning Paths
425 Advanced

425: Proc Macro Attribute

Functional Programming

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

  • • Understand the attribute proc macro lifecycle: receives (attr: TokenStream, item: TokenStream)
  • • Learn how attribute macros can transform, wrap, or replace the annotated item
  • • See the difference from derive macros: attribute macros modify existing items, derives add new impls
  • • Understand how #[tokio::main] rewrites async functions using attribute macros
  • • Learn the #[proc_macro_attribute] registration and how arguments are passed
  • Code 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

  • Arguments: Rust attribute macros receive arguments as a TokenStream and parse them freely; OCaml PPX attributes use a typed declaration system.
  • Item types: Rust attribute macros can annotate any item (fn, struct, impl, mod); OCaml PPX extensions are declared for specific AST node types.
  • Error handling: Rust macros can call compile_error! or return a TokenStream with errors; OCaml raises exceptions or uses Location.error_extensionf.
  • Testing: Rust attribute macro tests use 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Rust macros operate at compile time
  • OCaml uses ppx for similar metaprogramming
  • Both languages support powerful code generation
  • Rust's macro_rules! is built into the language
  • OCaml's approach requires external tooling
  • Exercises

  • Timing attribute: Implement #[timed] that wraps any function with let start = Instant::now(); let result = original_body; println!("{}: {:?}", fn_name, start.elapsed()); result.
  • Validate input: Create #[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.
  • Deprecated with replacement: Implement #[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.
  • Open Source Repos