Macros in Rust: How to Make Them Work with Modules?
Have you ever written a useful macro in Rust, then tried to use it in a neighboring module — and got a mysterious compiler error? If so, you are not alone. The question "How do I use a macro across module files?" has gathered thousands of views on Stack Overflow, and for good reason.
In Rust, macros are a powerful metaprogramming tool. They allow you to generate code on the fly, avoid duplication, and write DSLs (domain-specific languages). But here's the catch: macros don't behave like regular functions. They have their own visibility rules, and beginners often stumble over this.
In this tutorial, I will show you three proven ways to organize macros between module files. Let's go!
Why Don't Macros Work "Out of the Box"?
Before diving into the solution, let's understand the reason. In Rust, functions and types are only visible within their own module by default (unless marked pub). The same story applies to macros, but there's a nuance: pub for macros doesn't work the same way as for functions.
Consider a simple example. Suppose we have a project with this structure:
src/├── main.rs├── helpers/│ ├── mod.rs│ └── macros.rsIn macros.rs we declared a macro:
// src/helpers/macros.rsmacro_rules! greet { ($name:expr) => { println!("Hello, {}!", $name); };}Now we want to use it in main.rs:
// src/main.rsmod helpers;
fn main() { greet!("World"); // Error! Macro is not visible.}The compiler will output something like: error: macro `greet` is not imported. Why? Because macros are not exported from a module by default, even if the module is declared as pub.
Method 1: The #[macro_use] Attribute for Importing Macros
The simplest way is to use the #[macro_use] attribute before the module declaration. This forces Rust to import all macros from that module into the current scope.
Let's fix our example:
// src/main.rs#[macro_use]mod helpers;
fn main() { greet!("World"); // Now it works!}But there's a catch: #[macro_use] imports all macros from the module. If you have many macros, this can lead to name conflicts. Additionally, this approach is considered outdated in modern Rust (starting from edition 2018).
Where is this still useful? In small projects or when you're writing a prototype and want to quickly include a couple of macros without extra effort.
Important Nuance: Declaration Order
If you use #[macro_use], the macros become available immediately after the module declaration. This means the following code will not work:
// src/main.rsfn use_greet() { greet!("Test"); // Error! Module not yet declared.}
#[macro_use]mod helpers;
fn main() { use_greet();}Order matters. Keep this in mind.
Method 2: Explicit Macro Export with #[macro_export]
A more modern and controlled way is to use the #[macro_export] attribute directly in the macro definition. It makes the macro available at the crate level, not just the module level.
Here's how it works:
// src/helpers/macros.rs#[macro_export]macro_rules! greet { ($name:expr) => { println!("Hello, {}!", $name); };}Now in main.rs we can explicitly import the macro using use:
// src/main.rsmod helpers;
use helpers::greet;
fn main() { greet!("World"); // It works!}Note: we use use helpers::greet, not use hel