In this lesson, you will learn how to structure complex procedural macro crates into decoupled, cacheable modules to prevent compilation bottlenecks.
When building expansive macro systems—like the ones we’ve explored for automatic mocking or type-safe SQL—a common trap is monolithic design. Placing all TokenStream manipulation logic into a single lib.rs file forces the compiler to re-process the entire macro suite whenever a single helper function changes. By partitioning your macro logic into granular, library-internal modules, you significantly improve incremental compilation and maintainability.
The Strategy: Decoupling Logic from Implementation
To modularize effectively, separate your procedural macro crate into three distinct layers:
- The Entry Point (
lib.rs): Strictly contains the#[proc_macro]attribute or derive declarations. Its only job is to receive the inputTokenStreamand delegate it to the logic crate. - The Logic Crate (
core): A standard Rust library crate that processesTokenStreamtypes (usingproc-macro2for platform independence). Because this is a standard library, it can be unit-tested and incrementally compiled. - The Back-end (
gen): Modules responsible strictly for code emission or AST transformation.
Illustration
Instead of embedding complex logic inside your derive entry point, delegate to a structured backend:
// logic_module.rs
pub fn generate_impl(input: DeriveInput) -> TokenStream {
let name = &input.ident;
// Perform complex AST manipulation here
quote! { impl #name { ... } }
}
// lib.rs
#[proc_macro_derive(MyTrait)]
pub fn my_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
logic_module::generate_impl(ast).into()
}
By isolating the generate_impl logic, the Rust compiler can cache the results of your logic crate independently of the proc-macro interface. If you change a small helper function in logic_module.rs, the compiler only needs to re-evaluate the transformation logic, rather than re-registering the entire macro entry point. Furthermore, using proc-macro2 allows you to write standard integration tests for your macro logic without the overhead of the proc-macro proc-macro crate, enabling faster iteration cycles and cleaner build artifacts.
Key Takeaway: Decouple your macro entry points from your transformation logic into separate, testable crates to ensure the compiler can cache your AST processing steps efficiently.