| |

Rust 77 🦀 Attribute-Like Macros and Function-Like Macros

⏱️ Estimated read: 13 min | |

Rust has three kinds of procedural macros, and the derive macro covered in the previous chapter is only the first. The other two—attribute-like macros and function-like macros—share the same crate type and the same TokenStream interface, but they are invoked in different syntactic positions and receive different inputs. An attribute-like macro is written as #[route(GET, "/")] on an item, and it receives two token streams: the attribute’s arguments and the annotated item. A function-like macro is written as sql!(SELECT * FROM posts), and it receives one token stream: the tokens between the delimiters .

The distinction is not cosmetic. It determines what the macro can transform. An attribute macro can replace the item it is attached to—preserving it, wrapping it, or removing it entirely. A function-like macro can expand to an expression, a statement, an item, or a pattern, depending on where it is invoked. The derive macro, by contrast, can only append items to the module; it cannot see or modify the item it is derived from . Understanding the three positions is what allows a macro author to choose the right kind for the job.

Key point: Attribute-like macros are declared with #[proc_macro_attribute] and receive (attr: TokenStream, item: TokenStream); function-like macros are declared with #[proc_macro] and receive (input: TokenStream). Both return a TokenStream that replaces the invocation. Attribute macros can transform any item; function-like macros can expand to anything valid in the invocation’s syntactic position.


Why attribute-like and function-like macros exist

The transformation problem. A derive macro can only add code next to the item it derives from. It cannot wrap the item, modify its body, or remove it. An attribute macro can do all three. A web framework’s #[route(GET, "/")] macro can take the function it annotates, wrap it in a registration call, and return the registration instead of the original function . A tracing macro can insert instrumentation at the start and end of the function body. The derive macro cannot express these transformations because it never sees the item as something to replace.

The DSL problem. A function-like macro is the entry point for a domain-specific language embedded in Rust. The sql! macro is the canonical example: it receives the tokens of a SQL query, parses them with a real parser, validates the syntax, and generates the Rust code that executes the query . A macro_rules! macro can match token patterns, but it cannot run a full parser or produce diagnostics with spans. The function-like procedural macro can, because it is ordinary Rust code operating on tokens.

The position problem. The three procedural macro kinds are invoked in different positions, and the compiler enforces the position. A derive macro is only valid inside #[derive(...)]. An attribute macro is valid anywhere an attribute is valid: on functions, structs, enums, impl blocks, modules, and trait definitions . A function-like macro is valid in item position, expression position, statement position, and pattern position, depending on what it expands to . The compiler checks that the expansion fits the position.

The argument problem. An attribute macro receives its arguments as a separate token stream. The #[route(GET, "/")] invocation passes GET, "/" as the first argument and the function as the second . A function-like macro receives everything inside the delimiters as one stream. There is no separate argument slot. This difference shapes how the macro parses its input: the attribute macro can use syn to parse the arguments and the item separately, while the function-like macro must decide how to split the single stream.


a. Attribute-like macros: transforming an item

An attribute-like macro is a public function annotated with #[proc_macro_attribute]. It takes two TokenStream arguments and returns one. The first is the attribute’s arguments (the tokens inside the parentheses after the attribute name). The second is the item the attribute is attached to .

// Import the compiler's token stream type
use proc_macro::TokenStream;
// Import the parser and the item type
use syn::{parse_macro_input, ItemFn};
// Import the quote macro for generating output
use quote::quote;

// Declare the attribute macro named "log_calls"
// The user writes #[log_calls] on a function
#[proc_macro_attribute]
pub fn log_calls(attr: TokenStream, item: TokenStream) -> TokenStream {
    // The attr stream is empty for a bare #[log_calls]
    // Parse the annotated item as a function
    let function = parse_macro_input!(item as ItemFn);

    // Extract the function's name and body
    let name = &function.sig.ident;
    let block = &function.block;

    // Build the output: the original function, wrapped in a log call
    let expanded = quote! {
        // The original function, unchanged
        #function

        // A new function that logs before calling the original
        // The wrapper name is derived from the original
        // This is a simple example; real macros use unique names
        fn #name() {
            // Log the call at runtime
            println!("Calling {}", stringify!(#name));
            // Call the original function body
            #block
        }
    };

    // Return the generated tokens
    expanded.into()
}

The attr stream is empty for a bare attribute. For #[log_calls(verbose)], the attr stream contains verbose, and the macro can parse it with syn::parse_macro_input! or by checking the token string . The item stream is the entire annotated function, including its attributes, visibility, generics, and body. The macro must preserve what it does not intend to change—if it drops the pub keyword or the #[inline] attribute, the generated code loses them .

The expansion order matters. An attribute macro sees the item after the cfg and cfg_attr attributes have been processed, but before the derive macros have run. If the item has a #[derive(Debug)] attribute, the derive macro expands after the attribute macro returns . This ordering is why an attribute macro that preserves the item must preserve the derive attributes in the token stream it returns.


b. Function-like macros: the token stream in a call position

A function-like macro is a public function annotated with #[proc_macro]. It takes one TokenStream argument and returns one. The input is the tokens between the delimiters, and the output replaces the entire invocation .

// Import the compiler's token stream type
use proc_macro::TokenStream;
// Import the parser and the expression type
use syn::{parse_macro_input, Expr};
// Import the quote macro
use quote::quote;

// Declare the function-like macro named "double"
// The user writes double!(expr)
#[proc_macro]
pub fn double(input: TokenStream) -> TokenStream {
    // Parse the input tokens as a Rust expression
    // If the input is not a valid expression, parse_macro_input!
    // returns a compile_error! with a span
    let expr = parse_macro_input!(input as Expr);

    // Generate an expression that evaluates the input twice
    // This is intentionally a bad example: it evaluates twice
    // A real macro would bind to a temporary to evaluate once
    let expanded = quote! {
        // The input expression, then a plus sign, then the input again
        // The parentheses preserve precedence
        (#expr) + (#expr)
    };

    // Return the generated expression
    expanded.into()
}

The double! macro is invoked as double!(2 + 3), and it expands to (2 + 3) + (2 + 3). The input is an expression, and the output is an expression. The macro could have been written to accept a statement, an item, or a pattern; the output type determines where the invocation is valid . A macro that expands to an item cannot be used inside a function body unless the item is allowed there (a nested fn is allowed, a struct is allowed, but an expression is not).

The double-evaluation problem is the classic pitfall of function-like macros. The double! macro evaluates expr twice, which is fine for 2 + 3 but wrong for counter(). The fix is to bind the input to a temporary: { let tmp = #expr; tmp + tmp }. The macro author must decide whether the input is evaluated once or multiple times, and the documentation must state it .


c. The invocation positions and the expansion contract

The three procedural macro kinds are not interchangeable. The compiler enforces which kind can appear where, and the macro’s output must be valid for the position.

KindAttributeInputValid Positions
Derive#[proc_macro_derive]The annotated itemOnly #[derive(...)]
Attribute#[proc_macro_attribute](attr, item)Any item with attributes
Function-like#[proc_macro]The inner tokensItem, statement, expression, pattern

The attribute macro’s output must be zero or more items. It cannot expand to an expression or a statement, because the attribute is attached to an item, and the replacement must be an item . The function-like macro’s output must be valid for the position where it is invoked. If it is invoked in expression position, the output must be an expression. If it is invoked in item position, the output must be an item. The compiler reports an error if the expansion does not fit .

The function-like macro’s delimiters are not part of the input. sql!(SELECT * FROM t) passes SELECT * FROM t to the macro, not the parentheses. The macro can use any delimiter—(), [], or {}—and the choice is a convention: !() for expression-like calls, ![] for collection-like literals, and !{} for item-like expansions . The delimiter does not affect the tokens the macro receives.


Complete Example Session

The following session builds an attribute macro that instruments a function and a function-like macro that validates a domain-specific expression, demonstrating both kinds.

// ============================================
// PART 1: THE ATTRIBUTE MACRO
// ============================================
use proc_macro::TokenStream;
use syn::{parse_macro_input, ItemFn};
use quote::quote;

// The attribute is written as #[timed]
// It wraps the function in a timing measurement
#[proc_macro_attribute]
pub fn timed(_attr: TokenStream, item: TokenStream) -> TokenStream {
    // Parse the annotated function
    let function = parse_macro_input!(item as ItemFn);
    // Extract the name so the wrapper can call it
    let name = &function.sig.ident;

    // Generate a wrapper that times the original
    // The original function is preserved with its attributes and generics
    let expanded = quote! {
        // The original function, unchanged
        #function

        // A wrapper with the same name, using a raw identifier trick
        // This is not valid Rust; it is illustrative
        // A real wrapper uses a separate module or a macro-generated name
        // The point is that the attribute macro can transform the item
        fn #name() {
            let start = ::std::time::Instant::now();
            // Call the original body
            #name();
            println!("{} took {:?}", stringify!(#name), start.elapsed());
        }
    };

    expanded.into()
}
// ============================================
// PART 2: THE FUNCTION-LIKE MACRO
// ============================================
use proc_macro::TokenStream;
use syn::{parse_macro_input, LitInt};
use quote::quote;

// The macro is written as even!(n)
// It expands to a boolean expression checking whether n is even
#[proc_macro]
pub fn even(input: TokenStream) -> TokenStream {
    // Parse the input as an integer literal
    let n = parse_macro_input!(input as LitInt);

    // Generate a comparison expression
    let expanded = quote! {
        // The modulo operation on the literal
        // The type suffix is preserved from the literal
        #n % 2 == 0
    };

    expanded.into()
}
// ============================================
// PART 3: THE USAGE
// ============================================
// The attribute macro is applied to a function
#[timed]
fn compute() {
    // The function body
    println!("computing");
}

// The function-like macro is used in expression position
fn main() {
    // The macro expands to a boolean expression
    let is_even = even!(42);
    // The result is a normal boolean
    println!("42 is even: {}", is_even);

    // Call the instrumented function
    compute();
}

The three parts cover the attribute macro, the function-like macro, and the usage. What the session demonstrates is that the attribute macro receives the function and returns a modified version, while the function-like macro receives an expression and returns an expression. The attribute macro’s output must be an item; the function-like macro’s output must fit the expression position.


Quick Reference

Declaration Attributes

KindAttributeSignature
Derive#[proc_macro_derive(Name)](TokenStream) -> TokenStream
Attribute#[proc_macro_attribute](TokenStream, TokenStream) -> TokenStream
Function-like#[proc_macro](TokenStream) -> TokenStream

Invocation Positions

KindInvocationValid Positions
Derive#[derive(Name)]Struct, enum, union
Attribute#[name(...)]Any item
Function-likename!(...)Item, statement, expression, pattern

Input Streams

KindFirst StreamSecond Stream
AttributeAttribute argumentsAnnotated item
Function-likeInner tokensNone

Output Contract

KindOutput Must Be
DeriveZero or more items
AttributeZero or more items
Function-likeValid for the invocation position

Best Practices

✅ Do This:

// Attribute macro: preserve what you do not change
let function = parse_macro_input!(item as ItemFn);
quote! { #function /* wrapper */ }

// Function-like macro: bind to a temporary to avoid double evaluation
quote! { { let tmp = #expr; tmp + tmp } }

// Use syn::Error for spanned diagnostics
syn::Error::new(span, "message").to_compile_error()

❌ Don’t Do This:

// Attribute macro: drop the original item
quote! { fn wrapper() { /* no #function */ } } // loses visibility

// Function-like macro: evaluate the input twice
quote! { #expr + #expr } // side effects run twice

// Panic on bad input
panic!("invalid") // no span, useless error

Common Pitfalls

PitfallWhy It HappensFix
Attribute macro drops attributesItem not preservedInclude #function in output
Function-like macro double-evaluatesInput used twiceBind to let tmp
Expansion position errorOutput kind does not match positionMatch output to invocation
Unknown attribute errorHelper not declaredattributes(name) on derive only
Span-less errorUsed panic!syn::Error::to_compile_error()

Real-World Examples

1. Attribute macro declaration

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { ... }

2. Function-like macro declaration

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream { ... }

3. Attribute usage

#[route(GET, "/")]
fn index() { ... }

4. Function-like usage

let query = sql!(SELECT * FROM posts);

5. Attribute input parsing

let function = parse_macro_input!(item as ItemFn);

6. Function-like input parsing

let expr = parse_macro_input!(input as Expr);

7. Preserving the item

quote! { #function /* generated wrapper */ }

8. Binding to avoid double evaluation

quote! { { let tmp = #expr; tmp + tmp } }

9. Spanned error

syn::Error::new(ast.ident.span(), "error").to_compile_error()

10. Item position function-like

#[proc_macro]
pub fn make_struct(input: TokenStream) -> TokenStream { /* items */ }

Visual

Attribute Macro Flow

┌──────────────────────────────────────────────┐
│  #[timed]                                    │
│  fn compute() { ... }                        │
│         │                                    │
│         ▼                                    │
│  attr = empty                                │
│  item = fn compute() { ... }                 │
│         │                                    │
│         ▼                                    │
│  Macro returns tokens                        │
│         │                                    │
│         ▼                                    │
│  Compiler replaces the item with the output  │
└──────────────────────────────────────────────┘

Function-Like Macro Flow

┌──────────────────────────────────────────────┐
│  even!(42)                                   │
│         │                                    │
│         ▼                                    │
│  input = 42                                  │
│         │                                    │
│         ▼                                    │
│  Macro returns tokens: 42 % 2 == 0           │
│         │                                    │
│         ▼                                    │
│  Compiler replaces the invocation            │
└──────────────────────────────────────────────┘

Invocation Positions

┌──────────────────────────────────────────────┐
│  ATTRIBUTE → any item                        │
│  #[route(GET, "/")] fn index()               │
│                                              │
│  FUNCTION-LIKE → any position                │
│  item: make_struct!(Foo)                     │
│  expression: even!(42)                       │
│  statement: log!(x)                          │
│  pattern: shape!(Point)                      │
└──────────────────────────────────────────────┘

Summary

ItemValue
Attribute macro#[proc_macro_attribute]
Attribute signature(attr: TokenStream, item: TokenStream) -> TokenStream
Function-like macro#[proc_macro]
Function-like signature(input: TokenStream) -> TokenStream
Attribute outputItems only
Function-like outputAny valid position
Attribute inputArguments + item
Function-like inputInner tokens
Crate typeproc-macro = true

Key takeaways:

  • The three procedural macro kinds differ in position, not in power. A derive macro appends items; an attribute macro transforms an item; a function-like macro expands to whatever the position allows .
  • Attribute macros receive two streams. The first is the attribute’s arguments, the second is the annotated item. The macro must preserve the item if it does not intend to replace it .
  • Function-like macros receive one stream. The tokens between the delimiters are the entire input. The output must be valid for the position where the macro is invoked .
  • The output replaces the invocation. An attribute macro’s output replaces the item; a function-like macro’s output replaces the call. The compiler checks that the replacement fits .
  • Double evaluation is the classic function-like pitfall. An input expression used twice in the output evaluates twice. Bind it to a temporary to evaluate once .
  • Attribute macros see the item before derives expand. If the item has #[derive(...)], the attribute macro must preserve those attributes in its output, or the derives will not run .

Remember: The three procedural macro kinds are distinguished by where they are written and what they receive, not by what they can do. The derive macro is the append-only macro. The attribute macro is the item transformer. The function-like macro is the general-purpose token manipulator. The attribute macro’s contract is “here is the item, give me back items”; the function-like macro’s contract is “here are the tokens, give me back tokens that fit here.” The TokenStream is the interface for all three, and syn and quote are the tools that make the stream usable. The kind you choose is determined by the transformation you need, not by the power you want.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!