The macro that vanishes across crate boundaries
You write a clean macro_rules! macro inside your library. The local tests pass. You publish the crate. A friend adds it to their project, types my_crate::say_hi!(), and the compiler rejects them with error: cannot find macro say_hi in this scope. The macro exists. The code compiles fine inside your own workspace. Yet the moment it crosses the crate boundary, it disappears.
The missing piece is a single attribute: #[macro_export]. Without it, declarative macros stay locked inside their defining file. With it, they become visible to dependents. The catch is that this attribute changes where the macro lives in your crate's namespace, and that shift trips up nearly everyone who exports their first macro.
How macros live outside the module tree
Regular Rust items follow the module tree. You declare a function in src/util/format.rs, mark it pub, and callers reach it at my_crate::util::format::my_func. The path matches the file structure. Macros ignore this layout entirely.
Declarative macros were designed before the modern module system stabilized. They live in a separate namespace that the compiler resolves at parse time, not at type-check time. By default, a macro_rules! macro is only visible in the same file, and only after the line where you defined it. It does not respect mod declarations. It does not respect pub.
When you attach #[macro_export], the compiler performs two operations. First, it marks the macro as visible to external crates. Second, it hoists the macro to the crate root. The file path you wrote it in becomes irrelevant. A macro defined in src/internal/helpers.rs becomes reachable as my_crate::my_macro!, not my_crate::internal::helpers::my_macro!.
Think of regular items as books organized on shelves by genre. Macros are flyers pinned to the front door. #[macro_export] is what puts the flyer up. The shelf it was drafted on no longer matters.
The smallest working export
Here is the minimal pattern that makes a macro cross a crate boundary.
// src/lib.rs
// #[macro_export] does two things:
// 1. Grants cross-crate visibility.
// 2. Hoists the macro to the crate root namespace.
#[macro_export]
macro_rules! say_hi {
($name:expr) => {
// Expansion happens at the call site.
// The caller's $name substitutes directly into the println.
println!("Hello, {}!", $name);
};
}
A dependent crate brings it into scope with a standard use statement.
// In the dependent crate's src/main.rs
// Modern Rust resolves macros through the regular use system.
// This is cleaner than the old #[macro_use] extern crate pattern.
use my_crate::say_hi;
fn main() {
say_hi!("Ferris");
}
The use statement works because Rust 2018 unified macro resolution with item resolution. You no longer need special syntax to import macros. Treat them like functions during imports.
Why your macro needs a safety harness
Macros do not execute at runtime. They expand at compile time, injecting raw tokens into the caller's source code. That injection happens in the caller's lexical context. Every identifier inside your macro body is resolved where the macro is invoked, not where it is defined.
This creates a hygiene problem. If your macro body references Vec::new(), and the caller happens to have a type alias or a local function named Vec in scope, your macro silently picks up the wrong definition. The compiler will not catch it until the injected code fails to type-check, and the error message will point to the caller's file, not your macro.
The community standard for fixing this is the $crate token. $crate expands to the absolute path of the crate where the macro is defined. It anchors your references to your own code, regardless of the caller's namespace.
#[macro_export]
macro_rules! safe_vec {
( $( $x:expr ),* ) => {{
// $crate guarantees we call our own helper, not a shadowed one.
// Absolute paths (::std) protect against stdlib shims.
let mut tmp = ::std::vec::Vec::new();
$(
tmp.push($x);
)*
tmp
}};
}
Two rules keep macros robust. Use $crate::item when referencing functions, types, or other macros from your own crate. Use absolute paths starting with ::std, ::core, or ::alloc for standard library items. Never rely on unqualified names like Vec, String, or println! inside a cross-crate macro.
Treat $crate as a mandatory seatbelt. Skip it and you will eventually crash on a name collision you never anticipated.
A realistic cross-crate helper
Here is a practical example that combines $crate hygiene with a re-exported function.
// src/lib.rs
// A panic helper that formats a custom message.
// We will call this from the macro using $crate.
pub fn panic_with_detail(msg: &str) -> ! {
panic!("macro assertion failed: {msg}");
}
#[macro_export]
macro_rules! assert_close {
($a:expr, $b:expr, $eps:expr) => {{
let a = $a;
let b = $b;
let eps = $eps;
// Evaluate expressions once to avoid side effects.
// Use $crate to lock in our panic helper.
if (a - b).abs() > eps {
$crate::panic_with_detail(&format!("|{a} - {b}| > {eps}"));
}
}};
}
A consumer imports and invokes it like any other tool.
use my_crate::assert_close;
fn check_floating_point_math() {
// The macro expands here, injecting the if-check and panic call.
assert_close!(0.1 + 0.2, 0.3, 1e-9);
}
Even if the consumer defines their own panic_with_detail function in the same module, the macro calls yours. The $crate prefix resolves the ambiguity at expansion time.
Keep your macro bodies lean. Every extra token you inject increases the surface area for name collisions and makes downstream debugging harder.
The traps that catch most developers
Forgetting #[macro_export] and calling the macro from a dependent crate triggers error: cannot find macro name in this scope. The compiler sees the macro as a local artifact that never left your workspace.
Adding #[macro_export] but searching for the macro inside its original module triggers error: cannot find macro name in module my_crate::submodule. The hoisting behavior moves the macro to the root. The old path ceases to exist for external callers.
Importing a macro without a use statement in Rust 2018+ triggers error: cannot find macro name in this scope with a helpful suggestion to use my_crate::name. The compiler will not auto-import macros across crate boundaries.
Historically, developers used #[macro_use] extern crate my_crate; to pull all exported macros into scope. That pattern floods the namespace with names you may not need and makes dependency graphs harder to trace. The modern convention is explicit use my_crate::macro_name; statements. It matches how you import functions, keeps imports visible, and plays nicely with cargo fmt and IDE auto-imports.
Stick to explicit use statements. Namespace pollution is a silent productivity tax.
When to export a macro versus alternatives
Use #[macro_export] when you have a macro_rules! macro that external crates must invoke. It is the only mechanism that makes declarative macros visible across crate boundaries. Use #[macro_export] when the macro performs compile-time code generation that cannot be expressed with regular functions, such as repeating patterns, generating match arms, or wrapping expressions in custom control flow. Leave the attribute off when the macro is purely internal to your crate; rely on module-relative use crate::path::to::macro; to share it across your own files. Reach for procedural macros when you need to parse arbitrary syntax, generate items from attributes, or build derive macros that attach to structs and enums. Pick regular functions when the operation can be expressed with generics, traits, or closures; macros add compile-time complexity that functions avoid.