429: Macro Scoping and #[macro_export]
Difficulty: 3 Level: Advanced `macro_rules!` macros follow textual scoping within a file, but `#[macro_export]` hoists them to the crate root โ understanding this distinction prevents confusing "macro not found" errors.The Problem This Solves
Macros in Rust have a scoping model that surprises everyone the first time. Unlike functions and types, which are scoped to modules, `macro_rules!` macros are visible only from the point of definition downward in the source file. Define a macro at the bottom of a file and try to use it at the top โ you get a "cannot find macro" error. Move it up and it works. This ordering dependency feels arbitrary and trips up developers used to module-based scoping. The second surprise is cross-module use. A macro defined in `mod inner` is not automatically visible in the parent module. And when you want to export a macro for use by other crates, `pub use` doesn't work โ you need `#[macro_export]`, which bypasses the module tree entirely and puts the macro at the crate root. Understanding these rules is essential for library authors. A library that exports macros must either use `#[macro_export]` (the modern way) or the old `#[macro_use]` extern crate pattern.The Intuition
Think of `macro_rules!` as a textual find-replace rule that the compiler registers as it reads the file top to bottom. Once registered, it's available for the rest of the file. If a macro is in a submodule, it's registered in that module's namespace only โ unreachable from outside unless exported. `#[macro_export]` teleports the macro to the crate's top-level namespace, as if you had defined it in `lib.rs`. Other crates can then use it with `use your_crate::your_macro!` (Rust 2018+) or the older `#[macro_use] extern crate your_crate`. `$crate::` inside a macro is the counterpart: it refers to the defining crate regardless of where the macro is used, ensuring that helper functions the macro calls are correctly resolved.How It Works in Rust
// Local macro โ visible only from here downward in this file/module
macro_rules! add {
($a:expr, $b:expr) => { $a + $b };
}
// Exported macro โ hoisted to crate root, importable from other crates
#[macro_export]
macro_rules! assert_approx_eq {
($a:expr, $b:expr, $tolerance:expr) => {{
let diff = ($a - $b).abs();
assert!(diff < $tolerance);
}};
($a:expr, $b:expr) => {
assert_approx_eq!($a, $b, 1e-9f64); // calls itself โ same crate
};
}
// $crate:: resolves to THIS crate even when macro is used from another crate
#[macro_export]
macro_rules! my_debug {
($val:expr) => {
// Without $crate::, 'log_impl' would be looked up in the CALLER's crate
$crate::log_impl($val)
};
}
mod inner {
// Visible only inside 'inner' module:
macro_rules! inner_only { ($x:expr) => { $x + 1 }; }
// #[macro_export] hoists even from a nested module to crate root:
#[macro_export]
macro_rules! inner_exported { ($x:expr) => { $x * 2 }; }
pub fn compute(x: i32) -> i32 {
inner_only!(x) // fine โ same module
}
}
// inner_only!(5); // ERROR: not visible outside 'inner'
inner_exported!(5); // fine โ hoisted to crate root
What This Unlocks
- Library macros โ export utility macros (`assert_approx_eq!`, `bail!`, `ensure!`) for users of your crate with `#[macro_export]`.
- Cross-crate safety โ use `$crate::` to reference your crate's items inside exported macros, preventing broken lookups when users import the macro.
- Structured macro organisation โ keep macros in dedicated files, use `#[macro_use] mod macros;` (with `#[macro_export]` inside) to make them available crate-wide.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Macro visibility | `ppx` transformations apply globally during compilation | `macro_rules!` textually scoped; must define before use |
| Export to other modules | Functions/types follow module system naturally | `#[macro_export]` required โ bypasses module tree |
| Cross-crate references inside macros | N/A | Use `$crate::` to reference the defining crate |
| Importing macros | Module opens (`open`) | Rust 2018+: `use crate::my_macro!`; older: `#[macro_use]` |
// Macro scoping and #[macro_export] in Rust
// Macros follow textual scoping โ must define before use in file
// Local macro (visible only in this module and below)
macro_rules! add {
($a:expr, $b:expr) => { $a + $b };
}
// Exported macro โ available at crate root
#[macro_export]
macro_rules! assert_approx_eq {
($a:expr, $b:expr, $tolerance:expr) => {
{
let diff = ($a - $b).abs();
assert!(diff < $tolerance,
"assert_approx_eq failed: |{} - {}| = {} >= {}",
$a, $b, diff, $tolerance);
}
};
($a:expr, $b:expr) => {
assert_approx_eq!($a, $b, 1e-9f64);
};
}
// Using $crate:: for correct cross-crate references
#[macro_export]
macro_rules! my_debug {
($val:expr) => {
// $crate:: ensures we refer to this crate's items
// even when macro is used from another crate
println!("[{}:{}] {} = {:?}",
file!(), line!(), stringify!($val), $val)
};
}
mod inner {
// Macro defined here is only visible within this module
macro_rules! inner_add {
($a:expr, $b:expr) => { $a + $b };
}
pub fn compute(x: i32, y: i32) -> i32 {
inner_add!(x, y) // valid: same module
}
// #[macro_export] would hoist to crate root:
#[macro_export]
macro_rules! inner_exported {
($x:expr) => { $x * 2 };
}
}
// Using #[macro_use] for compatibility with older macro pattern
// mod compat {
// #[macro_use]
// mod macros {
// macro_rules! old_style { ($x:expr) => { $x }; }
// }
// fn use_it() { let _ = old_style!(42); }
// }
fn main() {
// Local macro
println!("add!(3, 4) = {}", add!(3, 4));
// Exported macros (available at crate root)
assert_approx_eq!(1.0f64, 1.0 + 1e-12, 1e-9);
println!("approx_eq test passed");
my_debug!(42);
my_debug!(vec![1, 2, 3]);
my_debug!("hello");
// Macro from inner module (exported)
println!("inner_exported!(5) = {}", inner_exported!(5));
// Function using inner macro
println!("inner compute: {}", inner::compute(3, 7));
}
#[cfg(test)]
mod tests {
// Exported macros are available here via #[macro_export]
#[test]
fn test_assert_approx_eq() {
assert_approx_eq!(1.0f64, 1.0 + 1e-12);
assert_approx_eq!(3.14f64, 3.14000001, 1e-5);
}
#[test]
fn test_inner_exported() {
assert_eq!(inner_exported!(6), 12);
}
}
(* Macro scoping in OCaml via module visibility *)
(* Macros (or macro-like helpers) scoped to module *)
module Internal = struct
let debug_log msg = Printf.printf "[DEBUG] %s\n" msg
let assert_ok condition msg =
if not condition then failwith msg
end
(* Exported to users *)
module Public = struct
let log = Internal.debug_log
let assert_equal a b =
Internal.assert_ok (a = b)
(Printf.sprintf "Expected %d but got %d" b a)
end
(* Textual scoping: helpers must come before uses *)
let helper x = x * 2 (* defined first *)
let main_logic () = helper 21 (* uses helper โ works *)
(* let bad = early_use () -- would fail: undefined *)
(* let early_use () = 42 -- comes after bad *)
let () =
Public.log "Application started";
Public.assert_equal (main_logic ()) 42;
Printf.printf "All assertions passed\n"