Macros Best Practices
Prefer functions, generics, and traits; reach for macros when syntax or codegen demands it. Good macros have great errors, tests, and documented input grammar.
How to Use This List
- Review before adding a new
macro_rules!or proc-macro crate. - Use as checklist for internal DSL/framework macros.
- Pair with Debugging Macros for workflow.
A - When to Macro
- Try
fn,const fn,trait,implfirst. Better errors and IDE support. - Use
macro_rules!for repetitive syntax only. Not for business logic. - Use proc macros for derives and attributes. When AST transformation required.
- Avoid macros to hide ownership hacks. Fix API design instead.
- Document accepted macro input grammar. Examples for every variant.
B - Implementation
- Proc macro in separate
proc-macro = truecrate. Keepsynoff runtime dependency graph. - Use
syn::Error::new_spannedfor diagnostics. Point at user's token, not macro internals. - Support generics with
split_for_impl. Derives must work onstruct Foo<T>. - Re-emit attrs and visibility on transformed items. Do not drop
#[allow]orpub. - Use
$crate::paths in exportedmacro_rules!. Correct resolver from consumer crate.
C - Testing and Debug
-
cargo expandsnapshots for non-trivial macros. Checked in CI viamacrotestor manual review. -
trybuildcompile-fail tests for invalid input. Lock error messages. - Unit test parse/generate logic with
proc_macro2. Without full compiler driver. - Gate
eprintln!debug behind feature or env. No noisy builds for users. - Track compile-time impact. Split huge expansions to
build.rsif needed.
D - API and Maintenance
- Prefer stable public API outside macro. Macro generates thin wrapper around tested fns.
- Version proc-macro crates on semver. Breaking DSL syntax is major bump.
- Pin
syn/quoteversions consciously. Workspace alignment reduces duplicate syn. - README shows before/expand after for one example. Onboarding for macro consumers.
- Avoid leaking internal generated type names. Public surface stable across releases.
E - Security and Correctness
- SQL/DSL macros still parameterize user data at runtime. Compile-time string is not sanitization.
- No filesystem/network in proc macros in CI builds. Deterministic expansion.
- Hygiene: avoid colliding generated idents with user code. Use private modules or unique prefixes.
- Review
unsafein generated code like hand-written unsafe. Macro does not absolve review. - Audit third-party proc macros like any dependency. Supply chain risk.
FAQs
When is macro_rules enough?
Vec-like literals, small assert helpers, repetitive match arms - not whole ORMs.
Should apps ship proc macros?
Usually separate publishable macro crate even if internal monorepo member.
How many derives per crate?
As needed; keep related derives together; split if compile time hurts consumers.
deny unsafe in generated code?
Prefer safe expansion; if unsafe required, document invariants in emitted docs.
Macro in public API?
Yes for frameworks; still offer non-macro builder path when feasible for IDE users.
Edition 2024 macro pitfalls?
Path resolution and use preludes; test expand under 2024 consumer crate.
How teach team macros?
Workshop: expand serde derive, write tiny derive, debug with trybuild.
Replace macro later?
Plan escape hatch: generated code pattern stable enough to hand-write if removing macro.
clippy on generated code?
Clippy lints apply to expansion; fix generator not allow everywhere.
docs.rs for macros?
Show expansion examples in docs; doc_cfg for feature-gated macro paths.
Related
- Macros Basics - introduction
- Debugging Macros - tooling
- syn & quote - implementation
- 50 Rust Rules
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.