426: Function-like Proc Macros
Difficulty: 4 Level: Expert Macros that look like function calls but run at compile time with full AST access โ for DSLs, compile-time validation, and code generation from structured input.The Problem This Solves
`macro_rules!` works on token patterns but can't understand their meaning. `sql!("SELECT * FROM users WHERE id = ?")` needs to parse SQL, validate it against a schema, and generate type-safe query code. `html! { <div class="main">...</div> }` needs to parse HTML syntax and generate `VNode` constructor calls. These require semantic understanding of structured input, not just token shuffling. Function-like proc macros look like `name!(...)` โ identical syntax to declarative macros โ but the implementation is a Rust function with full access to `syn`, `quote`, and anything else you can import. The entire content inside the parens arrives as a `TokenStream` for you to interpret however you need. Real examples: `regex!("[a-z]+")` compiles the regex at compile time; `include_proto!("schema.proto")` generates Rust types from protobuf; `query!("SELECT id FROM users")` in `sqlx` validates SQL and generates typed structs. All of these are function-like proc macros.The Intuition
A function-like proc macro receives everything inside `macro!(...)` as a token stream and returns any Rust code โ it's a compile-time code generator with full Rust as its implementation language.How It Works in Rust
// In proc-macro crate:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, LitStr, LitInt};
// Simple: generate a constant from a string literal
#[proc_macro]
pub fn make_greeting(input: TokenStream) -> TokenStream {
let name = parse_macro_input!(input as LitStr);
let greeting = format!("Hello, {}!", name.value());
quote! { #greeting }.into()
}
// Usage: const MSG: &str = make_greeting!("World");
// Expands to: const MSG: &str = "Hello, World!";
// Custom syntax: parse key=value pairs
#[proc_macro]
pub fn config(input: TokenStream) -> TokenStream {
// Parse custom DSL: config!(host = "localhost", port = 8080)
// Validate at compile time, generate Config struct construction
// ...
input // simplified
}
// Compile-time regex compilation
#[proc_macro]
pub fn regex(input: TokenStream) -> TokenStream {
let pattern = parse_macro_input!(input as LitStr);
let pat = pattern.value();
// Validate regex at compile time โ build error if invalid!
if let Err(e) = regex_syntax::Parser::new().parse(&pat) {
return syn::Error::new(pattern.span(), e.to_string())
.to_compile_error().into();
}
quote! { ::regex::Regex::new(#pat).unwrap() }.into()
}
1. `#[proc_macro]` on a public function in a proc-macro crate.
2. Function signature: `pub fn name(input: TokenStream) -> TokenStream`.
3. Parse `input` with `syn` โ could be a literal, a custom syntax, or any token sequence.
4. Validate at compile time โ emit `syn::Error::to_compile_error()` for user-friendly build errors.
5. Generate output with `quote!`.
What This Unlocks
- Compile-time DSLs: SQL, HTML, regex, configuration formats โ validate and generate at build time.
- Type-safe interfaces: Generate typed structs from schemas at compile time (protobuf, SQL, GraphQL).
- Custom syntax: Any token stream you can parse โ create embedded mini-languages.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Compile-time DSL | Camlp4 / `[%extension_point]` via PPX | `#[proc_macro]` function-like macro |
| Custom syntax | Camlp4 grammar extensions | Parse `TokenStream` with `syn` |
| Compile-time validation | PPX with `Location.raise_errorf` | `syn::Error::new(span, msg).to_compile_error()` |
| Meta-programming entry point | PPX driver (`ppxlib.runner`) | `#[proc_macro]` โ registered by crate type |
| Usage syntax | `[%myext ...]` | `myext!(...)` โ indistinguishable from built-ins |
//! 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>");
}
}
(* Function-like macro concepts in OCaml *)
(* PPX-based custom syntax for SQL-like DSL *)
(* Simulate sql! macro *)
let validate_sql sql =
(* Simple validation *)
let required = ["SELECT"; "FROM"] in
let upper = String.uppercase_ascii sql in
List.for_all (fun kw -> try
let _ = Str.search_forward (Str.regexp_string kw) upper 0 in true
with Not_found -> false) required
(* Simulate html! macro *)
let make_element tag content =
Printf.sprintf "<%s>%s</%s>" tag content tag
let () =
let sql = "SELECT id, name FROM users WHERE active = true" in
if validate_sql sql then
Printf.printf "Valid SQL: %s\n" sql
else
Printf.printf "Invalid SQL!\n";
let html = make_element "div"
(make_element "h1" "Hello" ^
make_element "p" "World") in
Printf.printf "%s\n" html