๐Ÿฆ€ Functional Rust

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

Key Differences

ConceptOCamlRust
Macro visibility`ppx` transformations apply globally during compilation`macro_rules!` textually scoped; must define before use
Export to other modulesFunctions/types follow module system naturally`#[macro_export]` required โ€” bypasses module tree
Cross-crate references inside macrosN/AUse `$crate::` to reference the defining crate
Importing macrosModule 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"