Attribute Macro Architecture
An attribute procedural macro—declared with #[proc_macro_attribute]—is invoked as an outer attribute #[my_attr] or #[my_attr(args...)] on any Rust item (fn, struct, enum, impl, trait, or mod). Unlike a derive macro, which only appends code after a type definition, an attribute macro receives two TokenStream arguments and completely replaces the annotated item with whatever TokenStream it returns.
The Two Input Streams of #[proc_macro_attribute]
#[proc_macro_attribute]
pub fn trace_calls(attr: TokenStream, item: TokenStream) -> TokenStream
Invocation on a Function:
#[trace_calls(level = "debug")]
^^^^^^^^^^^^^^^
1st parameter: `attr: TokenStream` (contents inside parentheses, or empty)
pub fn compute(x: i32) -> i32 { x * 2 }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2nd parameter: `item: TokenStream` (the entire annotated item, excluding `#[trace_calls]`)
How Attribute Macros Wrap or Transform Functions
Frameworks like #[tokio::main], #[tracing::instrument], and #[actix_web::get("/users")] parse item as a syn::ItemFn, inspect its signature (sig.ident, sig.inputs, sig.output, sig.asyncness, vis, attrs), and wrap its block inside timing, logging, or runtime-initialization code:
// Architecture of `#[proc_macro_attribute] pub fn timed(...)`:
/*
use proc_macro::TokenStream;
use quote::quote;
use syn::{ItemFn, parse_macro_input};
#[proc_macro_attribute]
pub fn timed(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
let vis = &input_fn.vis;
let sig = &input_fn.sig;
let body = &input_fn.block;
let fn_name = sig.ident.to_string();
quote! {
#vis #sig {
let __start = ::std::time::Instant::now();
let __result = (|| #body)();
println!("[timed] {} finished in {:?}", #fn_name, __start.elapsed());
__result
}
}
.into()
}
*/
// Runnable demonstration of the exact transformation performed by `#[timed]` above:
fn compute_fibonacci(n: u32) -> u64 {
let __start = std::time::Instant::now();
// Wrapping the original body in an immediately invoked closure `(|| #body)()`
// ensures early `return` statements inside `#body` still execute the timing log!
let __result = (|| {
if n <= 1 {
return u64::from(n);
}
let (mut a, mut b) = (0_u64, 1_u64);
for _ in 2..=n {
(a, b) = (b, a + b);
}
b
})();
println!(
"[timed] compute_fibonacci finished (elapsed >= 0ns: {})",
__start.elapsed().as_nanos() >= 0
);
__result
}
fn main() {
println!("fib(20) = {}", compute_fibonacci(20));
}
[timed] compute_fibonacci finished (elapsed >= 0ns: true)
fib(20) = 6765
Notice the (|| #body)() closure idiom in the generated function body: if the original function body contains an early return expr;, executing #body directly inline would jump out of the function and skip the post-execution logging statement! Wrapping #body inside (|| #body)() (or an async move #body block for async fn) captures early return statements cleanly so post-call instrumentation always runs.
Preserving IDE Completion When an Attribute Macro Fails to Parse
If the user is midway through typing a function annotated with #[my_attr], returning only err.to_compile_error() completely erases the function from the AST, causing rust-analyzer and rustc to report cascading "unresolved function" errors at every call site!
Instead, when an attribute macro encounters a validation error while inspecting an otherwise syntactically valid item, emit both the original #item (or a stub with matching signature) and #compile_error so downstream call sites continue to type-check cleanly.