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]