Macros

Function-Like Procedural Macros

A function-like procedural macro (#[proc_macro]) is invoked with the same ! syntax as a declarative macro (my_macro!(...), my_macro![...], or my_macro! { ... }), but processes its input token stream using arbitrary compile-time Rust code rather than declarative macro_rules! pattern matching.

Signature and Execution Model

// Inside a `proc-macro = true` crate:
use proc_macro::TokenStream;

#[proc_macro]
pub fn sql_check(input: TokenStream) -> TokenStream {
    // Parse `input` tokens, validate syntax/schema, and emit Rust tokens:
    input
}
Caller Invocation:
    let rows = html! { <div class="card">"Hello"</div> };
                         │
                         ▼  Passed as `proc_macro::TokenStream` to `#[proc_macro] pub fn html(...)`
                  Custom Rust Parser
                         │
                         ▼  Emits valid Rust expression tokens
    let rows = HtmlNode::element("div").attr("class", "card").text("Hello");

When to Choose #[proc_macro] Over macro_rules!

While macro_rules! is simpler and does not require a separate crate, a function-like procedural macro can perform compile-time tasks that are impossible or intractable in macro_rules!:

Requirement macro_rules! #[proc_macro]
Needs a separate proc-macro = true crate? No Yes
Inspect contents inside a string literal ("SELECT ...") No (string literal is an atomic token) Yes (parses string value at compile time)
Custom compile-time computation (hashing, regex/DFA compilation, sorting tables) No Yes (runs arbitrary Rust code)
Custom domain-specific syntax with custom error spans Limited to compile_error! Yes (pinpoints exact sub-tokens via Span)

Compile-Time String Parsing and Lookup Table Generation

For example, a function-like procedural macro can parse a string literal or embedded DSL at compile time (such as a hex byte string hex!("48656c6c6f") or a SQL query) and expand directly into a fixed-size byte array literal [0x48, 0x65, 0x6c, 0x6c, 0x6f] with zero runtime parsing overhead:

// Inside `hex_literal_macro/src/lib.rs` (`proc-macro = true`):
/*
use proc_macro::TokenStream;

#[proc_macro]
pub fn hex_bytes(input: TokenStream) -> TokenStream {
    let raw = input.to_string();
    let hex_str = raw.trim().trim_matches('"');
    let mut bytes = Vec::new();
    for i in (0..hex_str.len()).step_by(2) {
        let byte = u8::from_str_radix(&hex_str[i..i + 2], 16)
            .expect("invalid hex byte in hex_bytes!()");
        bytes.push(format!("{byte}u8"));
    }
    format!("[{}]", bytes.join(", ")).parse().unwrap()
}
*/

// What `const MAGIC: [u8; 4] = hex_bytes!("CAFEBABE");` expands to at compile time:
const MAGIC: [u8; 4] = [202u8, 254u8, 186u8, 190u8];

fn main() {
    println!("Compile-time decoded bytes: {MAGIC:02X?}");
}
Compile-time decoded bytes: [CA, FE, BA, BE]