Attribute Macros
An attribute macro is a procedural macro used like this:
#[my_attribute]
fn my_function() { ... }
or with arguments:
#[my_attribute(key = "value")]
fn my_function() { ... }
It:
- Receives the annotated item as input.
- Optionally receives arguments from the attribute.
- Returns transformed Rust code that replaces the original item.
When to Use Attribute Macros
Use them when you want to:
- Modify function behavior (logging, timing, retries, async setup).
- Add validation or instrumentation.
- Generate extra code around structs, modules, or impl blocks.
- Create ergonomic APIs (e.g.,
#[tokio::main],#[test],#[wasm_bindgen]).
Attribute Macros vs Custom Derives
| Feature | Attribute Macro | Custom Derive |
|---|---|---|
| Syntax | #[my_attr] | #[derive(MyTrait)] |
| Applies to | Any item | Structs/enums/unions |
| Can modify original item | ✅ Yes | ❌ No (only adds code) |
| Receives arguments | ✅ Yes | ❌ No |
How Attribute Macros Work
- Compiler encounters #[my_attr(...)].
- It passes:
- The attribute arguments as a TokenStream.
- The annotated item as another TokenStream.
- Your macro:
- Parses both.
- Generates new code.
- The new code replaces the original item.
Setting Up an Attribute Macro Crate
Procedural macros must live in a proc-macro crate.
cargo new my_attr_macros --lib
Cargo.toml:
[lib]
proc-macro = true
[dependencies]
syn = "2"
quote = "1"
proc-macro2 = "1"
Example 1: Simple Logging Attribute for Functions
We’ll write #[log_calls] to log entry and exit of a function.
Macro Implementation
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn log_calls(_attr: TokenStream, item: TokenStream) -> TokenStream {
// Parse the input as a function
let input = parse_macro_input!(item as ItemFn);
let vis = &input.vis;
let sig = &input.sig;
let block = &input.block;
let name = &sig.ident;
let expanded = quote! {
#vis #sig {
println!("Entering {}", stringify!(#name));
let result = (|| #block)();
println!("Exiting {}", stringify!(#name));
result
}
};
TokenStream::from(expanded)
}
Usage
use my_attr_macros::log_calls;
#[log_calls]
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let x = add(2, 3);
}
What Gets Generated
fn add(a: i32, b: i32) -> i32 {
println!("Entering add");
let result = (|| { a + b })();
println!("Exiting add");
result
}
This preserves return values, early returns, and ? behavior.
Example 2: Attribute Macro With Arguments
Let’s create:
#[route(method = "GET", path = "/users")]
fn get_users() { ... }
Macro Implementation
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, Meta, NestedMeta, Lit};
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let mut method = None;
let mut path = None;
let meta = parse_macro_input!(attr as syn::AttributeArgs);
for arg in meta {
if let NestedMeta::Meta(Meta::NameValue(nv)) = arg {
if nv.path.is_ident("method") {
if let Lit::Str(lit) = nv.lit {
method = Some(lit.value());
}
}
if nv.path.is_ident("path") {
if let Lit::Str(lit) = nv.lit {
path = Some(lit.value());
}
}
}
}
let method = method.expect("Missing method");
let path = path.expect("Missing path");
let name = &input.sig.ident;
let vis = &input.vis;
let sig = &input.sig;
let block = &input.block;
let expanded = quote! {
// original function
#vis #sig #block
// generated metadata
fn #name##_route_info() {
println!("Route: {} {}", #method, #path);
}
};
TokenStream::from(expanded)
}
Usage
use my_attr_macros::route;
#[route(method = "GET", path = "/users")]
fn get_users() {
println!("Fetching users");
}
Conceptual Expansion
fn get_users() {
println!("Fetching users");
}
fn get_users_route_info() {
println!("Route: GET /users");
}
Example 3: Attribute Macro on Structs
#[auto_new]
struct Point {
x: i32,
y: i32,
}
Generate a constructor:
Macro Implementation
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_attribute]
pub fn auto_new(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let name = &input.ident;
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => panic!("auto_new only supports named fields"),
},
_ => panic!("auto_new only supports structs"),
};
let params = fields.iter().map(|f| {
let name = &f.ident;
let ty = &f.ty;
quote! { #name: #ty }
});
let inits = fields.iter().map(|f| {
let name = &f.ident;
quote! { #name }
});
let expanded = quote! {
#input
impl #name {
pub fn new(#(#params),*) -> Self {
Self { #(#inits),* }
}
}
};
TokenStream::from(expanded)
}
Usage
#[auto_new]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point::new(1, 2);
}
Expansion
struct Point {
x: i32,
y: i32,
}
impl Point {
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
Hygiene and Name Resolution
Attribute macros are hygienic:
- They won’t accidentally capture local variables.
- Use
stringify!, fully qualified paths (::std::println!), and careful naming.
Error Handling in Attribute Macros
Use compile-time errors instead of panic!:
return syn::Error::new_spanned(
input,
"This attribute only supports functions",
)
.to_compile_error()
.into();
Best Practices
- Keep attribute macros focused and predictable.
- Provide clear compiler errors.
- Preserve user-written code as much as possible.
- Prefer derives for trait implementations and attributes for behavior modification.