In this lesson, you will learn how to enable Rust modules to perform cross-type validation by having structs introspect their siblings during expansion.
The Problem: Encapsulated Blindness
In standard Rust, a struct definition is essentially an island. If you are building a complex system—like a distributed protocol or a database schema—you often need to ensure that a Message struct and its corresponding Response struct share compatible fields. Using standard macros, these structs usually don’t know about each other, leading to runtime failures if an API contract changes.
The Solution: Introspective Generation
Introspective Code Generation allows a procedural macro to "reach out" into the surrounding module scope. By leveraging the proc_macro metadata or custom registry attributes, we can make the compiler treat a collection of structs as a unified graph rather than independent units.
Instead of writing:
#[derive(Validate)]
struct Request { id: u32 }
#[derive(Validate)]
struct Response { id: u64 } // Oops, types don't match
We introduce an introspective attribute that scans the crate’s internal registry:
#[registry_module]
mod protocol {
#[sync_with(Response)]
pub struct Request { pub id: u32 }
pub struct Response { pub id: u32 }
}
How it works under the hood
When the #[registry_module] attribute macro processes the protocol module, it does not just expand the structs immediately. It first scans the TokenStream to construct an in-memory representation (a map) of all structs marked with identifiable labels.
When it encounters #[sync_with(Response)], the macro performs an introspective lookup:
- It queries its cached registry of the current module.
- It fetches the AST (Abstract Syntax Tree) of the sibling
Response. - It compares the field types of
RequestagainstResponseat compile time. - If a mismatch is detected, it emits a
compile_error!pointing exactly to the divergent field.
This moves the burden of consistency from manual runtime tests to the compilation phase, ensuring that your system’s internal contracts are mathematically sound before the binary is even produced.
Key Takeaway: Introspective code generation turns your modules into self-aware registries, allowing macros to enforce cross-type invariants by scanning the local AST at expansion time.