Course contentsShow
← Rust
Lesson 1546 of 1,91013. Metaprogramming with MacrosFree lesson

Inter-Macro Communication

You will pass state between different macros using hidden internal attributes or shared file buffers.

In this lesson, you will learn how to orchestrate state sharing between decoupled procedural macros by leveraging the compiler’s filesystem-based side channels.

The "Stateless" Challenge

In Rust, procedural macros are isolated; each expansion invocation happens in a clean, stateless environment. They cannot store variables in global memory or share mutable state across different crate components. However, when building complex frameworks—such as a distributed service where an #[entity] macro needs to inform a #[registry] macro about its existence—you need a way to pass data between them.

The File-System Side Channel

Since we cannot rely on memory, we use the OUT_DIR environment variable to create a shared buffer. The strategy works in two steps:

  1. The Producer: An attribute macro emits code and writes metadata (like a JSON blob or a simple symbol) into a file inside OUT_DIR.
  2. The Consumer: A separate macro (often a custom derive or a function-like macro) reads that specific file during compilation to aggregate the gathered data.
// Inside the procedural macro crate:
let out_dir = std::env::var("OUT_DIR").expect("No OUT_DIR");
let path = std::path::Path::new(&out_dir).join("macro_registry.json");

// Append the entity's data to the shared file
let mut file = std::fs::OpenOptions::new()
    .create(true)
    .append(true)
    .open(path)?;
writeln!(file, "{}", entity_name)?;

Bridging via Hidden Attributes

To ensure the consumer macro knows where to look or what to include, you can have the producer inject a "hidden" attribute—a #[doc(hidden)] entry—that the consumer then parses using syn. By emitting a dummy item with an attribute containing the serialized state, you can force the compiler to pass the information through the TokenStream itself, effectively "communicating" the state without needing a permanent database.

While file-based communication is powerful, it is inherently sensitive to compilation order. Always ensure your build script or top-level entry point guarantees that the producer macros have finished writing their buffers before the final consumer macro attempts to read them, typically by anchoring the consumer to a mod.rs scan.

Key Takeaway: Because procedural macros lack shared memory, you must treat the OUT_DIR as a temporary state store or inject metadata into the TokenStream to communicate context between separate expansion phases.