Procedural Macro Crate Structure
Put procedural macro entry points in a dedicated proc-macro crate, keep reusable runtime APIs in a normal crate, and test the macro from a separate caller crate or integration test.
What it is
A procedural macro crate is a library target whose manifest sets [lib] proc-macro = true.
That crate exports macro namespace items such as #[derive(MyTrait)], #[my_attr], or my_macro!(...).
The Rust Reference requires procedural macro functions to live in the root of the proc-macro crate.
This shape is different from normal library organization. A proc-macro crate is compiled as a compiler plugin-like dynamic library and is run by rustc during compilation of downstream crates. It cannot use its own procedural macros from inside the same crate. Its public exports are the macro entry points, not arbitrary runtime helpers.
The usual production layout is therefore split:
my_cratecontains traits, runtime support types, and ordinary APIs.my_crate_macroscontains#[proc_macro],#[proc_macro_derive], or#[proc_macro_attribute]entry points.my_cratemay optionally re-export macros frommy_crate_macrosbehind a feature.- Integration tests or examples use the macro the way a downstream crate would.
This keeps Procedural Macros isolated from ordinary runtime code and makes the semver surface explicit.
How it works
Cargo enables the crate type with:
[lib]
proc-macro = trueThe proc-macro crate typically depends on proc-macro2, syn, and quote.
Use the unversioned docs.rs /latest/ URLs to verify the latest compatible versions before pinning exact versions.
As of 2026-06-21, docs.rs shows syn 2.0.118 and quote 1.0.45.
Inside src/lib.rs, keep only thin public macro functions at the root.
Move parsing, validation, and generation into private modules.
That style gives each entry point a predictable shape:
- Convert
proc_macro::TokenStreaminto parsed data. - Validate input and build an internal model.
- Generate
proc_macro2::TokenStream. - Convert errors into
compile_error!tokens. - Return
proc_macro::TokenStreamto rustc.
The runtime crate should not depend on the proc-macro crate unless it is only re-exporting macros as an optional convenience.
The proc-macro crate may depend on the runtime crate to generate paths such as ::my_crate::TraitName, but that can create version-coupling if not designed deliberately.
Example
pub trait Describe {
fn describe(&self) -> String;
}
pub struct User {
pub name: String,
}
impl Describe for User {
fn describe(&self) -> String {
format!("User({})", self.name)
}
}
fn main() {
let user = User {
name: String::from("Ada"),
};
assert_eq!(user.describe(), "User(Ada)");
}This is the ordinary runtime API that a derive macro would target.
The generated impl should call public paths such as ::my_crate::Describe rather than private helpers in the macro crate.
Layout sketch
my_crate/
Cargo.toml
src/lib.rs # trait, runtime helpers, optional macro re-export
tests/derive.rs # downstream-style integration test
my_crate_macros/
Cargo.toml # [lib] proc-macro = true
src/lib.rs # public macro entry points only
src/derive_describe.rs # parsing, validation, expansion
tests/ui.rs # trybuild harness
tests/ui/*.rs # pass and compile-fail casesThis sketch is not a language requirement, but it matches how complex macro crates stay reviewable.
Best practice
- ✅ Keep the
#[proc_macro*]functions insrc/lib.rssmall and rooted in the proc-macro crate. - ✅ Put reusable traits and runtime helpers in a normal library crate, not in the proc-macro crate.
- ✅ Generate paths to public runtime APIs, usually with absolute paths such as
::my_crate::Trait. - ✅ Use internal modules for parsing, semantic validation, expansion, and Macro Diagnostics.
- ✅ Exercise public macros from integration tests, examples, or Testing Macros with trybuild cases.
- ✅ Re-export macros from the runtime crate only when it improves the public API intentionally.
- ✅ Keep macro implementation dependencies out of runtime users’ dependency graph when possible.
Pitfalls
- ⚠️ A proc-macro crate cannot use its own procedural macros internally; test through another crate boundary.
- ⚠️ Do not make the macro crate the home of runtime traits that generated code needs to name.
- ⚠️ Do not expose private helper functions through generated output; callers cannot access them cross-crate.
- ⚠️ Do not let
src/lib.rsbecome a large parser and code generator; diagnostics degrade quickly. - ⚠️ Do not assume
syn,quote, ortrybuildlatest versions stay fixed; verify docs.rs before updating pins. - ⚠️ Do not forget that procedural macros run at build time with build-script-like trust concerns.
See also
Macros · Procedural Macros · Derive Macros · Attribute Macros · Function-like Macros · syn and quote · Macro Diagnostics · Testing Macros with trybuild · Unhygienic Procedural Macro Output · Cargo.toml Manifest
Sources
- The Rust Reference, “Procedural macros” — the-reference, https://doc.rust-lang.org/reference/procedural-macros.html
- docs.rs,
syncrate docs, latest verified 2026-06-21 as 2.0.118, https://docs.rs/syn/latest/syn/ - docs.rs,
quotecrate docs, latest verified 2026-06-21 as 1.0.45, https://docs.rs/quote/latest/quote/
