ExamplesBy LevelBy TopicLearning Paths
426 Advanced

426: Function-like Proc Macros

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "426: Function-like Proc Macros" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Function-like proc macros look like function calls (`my_macro!(...)`) but unlike `macro_rules!`, they receive their entire argument as a `TokenStream` and can generate arbitrary Rust code. Key difference from OCaml: 1. **Syntax freedom**: Rust function

Tutorial

The Problem

Function-like proc macros look like function calls (my_macro!(...)) but unlike macro_rules!, they receive their entire argument as a TokenStream and can generate arbitrary Rust code. This enables domain-specific languages embedded in Rust source: SQL queries (sql!("SELECT * FROM users WHERE id = $1") with type-checked parameters), HTML templates, CSS-in-Rust, regex patterns compiled at build time. The sql! macro can verify query syntax at compile time and generate typed query structs — impossible with macro_rules!.

Function-like proc macros power quote! itself, pest's grammar macros, type-checked SQL in sqlx, and compile-time regex compilation in regex_lite.

🎯 Learning Outcomes

  • • Understand function-like proc macros as the most flexible macro type (arbitrary input parsing)
  • • Learn the #[proc_macro] registration and the TokenStream in/out signature
  • • See how function-like macros differ from macro_rules! (full tokenstream access, external crate)
  • • Understand compile-time validation use cases: SQL, regex, URIs, JSON schemas
  • • Learn the ergonomic difference: function-like macros allow custom syntax without $ sigils
  • Code Example

    #![allow(clippy::all)]
    //! Function-like Proc Macros
    //!
    //! Macros invoked like function calls.
    
    /// Example: what sql!("SELECT...") might do
    pub fn parse_sql(query: &str) -> Vec<&str> {
        query.split_whitespace().collect()
    }
    
    /// Example: what html!(<div>Hello</div>) might generate
    pub struct HtmlElement {
        pub tag: String,
        pub content: String,
    }
    
    impl HtmlElement {
        pub fn new(tag: &str, content: &str) -> Self {
            HtmlElement {
                tag: tag.to_string(),
                content: content.to_string(),
            }
        }
    
        pub fn render(&self) -> String {
            format!("<{}>{}</{}>", self.tag, self.content, self.tag)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_sql() {
            let parts = parse_sql("SELECT * FROM users");
            assert_eq!(parts, vec!["SELECT", "*", "FROM", "users"]);
        }
    
        #[test]
        fn test_html_render() {
            let el = HtmlElement::new("div", "Hello");
            assert_eq!(el.render(), "<div>Hello</div>");
        }
    
        #[test]
        fn test_html_span() {
            let el = HtmlElement::new("span", "World");
            assert_eq!(el.render(), "<span>World</span>");
        }
    
        #[test]
        fn test_sql_count() {
            let parts = parse_sql("SELECT COUNT(*) FROM items WHERE active = true");
            assert_eq!(parts.len(), 8);
        }
    
        #[test]
        fn test_html_empty() {
            let el = HtmlElement::new("br", "");
            assert_eq!(el.render(), "<br></br>");
        }
    }

    Key Differences

  • Syntax freedom: Rust function-like proc macros accept any token sequence; OCaml extension points must contain valid OCaml expressions inside [%name ...].
  • Error location: Rust can attach errors to specific spans within the input; OCaml PPX errors appear at the extension point location.
  • Schema access: Both can read external files (SQL schemas, OpenAPI specs) during compilation; Rust uses build scripts to pass paths via env vars.
  • Caching: Rust proc macros run on every compilation (unless incremental cache hits); OCaml PPX runs on every file containing the extension.
  • OCaml Approach

    OCaml's equivalent is the [%ppx_name expr] extension point syntax. [%sql "SELECT * FROM users"] with a custom PPX validates SQL at compile time. [%re "regex"] compiles regular expressions. OCaml's ppxlib extension points provide the same power as function-like proc macros, with the same compile-time validation benefits. The ocaml-re library uses this for compile-time regex compilation.

    Full Source

    #![allow(clippy::all)]
    //! Function-like Proc Macros
    //!
    //! Macros invoked like function calls.
    
    /// Example: what sql!("SELECT...") might do
    pub fn parse_sql(query: &str) -> Vec<&str> {
        query.split_whitespace().collect()
    }
    
    /// Example: what html!(<div>Hello</div>) might generate
    pub struct HtmlElement {
        pub tag: String,
        pub content: String,
    }
    
    impl HtmlElement {
        pub fn new(tag: &str, content: &str) -> Self {
            HtmlElement {
                tag: tag.to_string(),
                content: content.to_string(),
            }
        }
    
        pub fn render(&self) -> String {
            format!("<{}>{}</{}>", self.tag, self.content, self.tag)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_sql() {
            let parts = parse_sql("SELECT * FROM users");
            assert_eq!(parts, vec!["SELECT", "*", "FROM", "users"]);
        }
    
        #[test]
        fn test_html_render() {
            let el = HtmlElement::new("div", "Hello");
            assert_eq!(el.render(), "<div>Hello</div>");
        }
    
        #[test]
        fn test_html_span() {
            let el = HtmlElement::new("span", "World");
            assert_eq!(el.render(), "<span>World</span>");
        }
    
        #[test]
        fn test_sql_count() {
            let parts = parse_sql("SELECT COUNT(*) FROM items WHERE active = true");
            assert_eq!(parts.len(), 8);
        }
    
        #[test]
        fn test_html_empty() {
            let el = HtmlElement::new("br", "");
            assert_eq!(el.render(), "<br></br>");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_sql() {
            let parts = parse_sql("SELECT * FROM users");
            assert_eq!(parts, vec!["SELECT", "*", "FROM", "users"]);
        }
    
        #[test]
        fn test_html_render() {
            let el = HtmlElement::new("div", "Hello");
            assert_eq!(el.render(), "<div>Hello</div>");
        }
    
        #[test]
        fn test_html_span() {
            let el = HtmlElement::new("span", "World");
            assert_eq!(el.render(), "<span>World</span>");
        }
    
        #[test]
        fn test_sql_count() {
            let parts = parse_sql("SELECT COUNT(*) FROM items WHERE active = true");
            assert_eq!(parts.len(), 8);
        }
    
        #[test]
        fn test_html_empty() {
            let el = HtmlElement::new("br", "");
            assert_eq!(el.render(), "<br></br>");
        }
    }

    Deep Comparison

    OCaml vs Rust: proc macro function like

    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

  • Compile-time JSON: Implement json!({ "key": "value", "num": 42 }) as a function-like macro (or simulate with macro_rules!) that validates the JSON structure at compile time and generates a serde_json::Value constructor.
  • Unit literal: Create meters!(5.3) that expands to Meters(5.3f64) and kilometers!(1.2) to Kilometers(1.2f64). Use function-like proc macros (or macro_rules!) to make unit conversion values ergonomic.
  • Compile-time UUID: Implement uuid!("550e8400-e29b-41d4-a716-446655440000") that validates the UUID format at compile time and generates a [u8; 16] constant. Compile-error on malformed input.
  • Open Source Repos