ExamplesBy LevelBy TopicLearning Paths
429 Fundamental

429: Macro Scoping and Visibility

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "429: Macro Scoping and Visibility" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Macros have complex scoping rules in Rust that differ from both functions and types. Key difference from OCaml: 1. **Path

Tutorial

The Problem

Macros have complex scoping rules in Rust that differ from both functions and types. A macro_rules! without #[macro_export] is only visible within the file it's defined in and below in the same module tree. #[macro_export] exports to the crate root, making it accessible as crate::my_macro!. The 2018 edition introduced module-path importing (use crate::my_macro). Understanding these rules is essential for structuring crates with macros and for importing macros from dependencies.

Macro scoping rules explain why use std::collections::HashMap doesn't export macros, why #[macro_use] extern crate was the old way to import macros, and how pub use crate_name::macro_name re-exports work.

🎯 Learning Outcomes

  • • Understand #[macro_export] and how it places macros at crate root
  • • Learn that macro visibility is lexical — macros defined later in a file are visible earlier (unlike functions)
  • • See how module-local macros (without #[macro_export]) are restricted to their module
  • • Understand the use crate::macro_name path for importing macros in Rust 2018+
  • • Learn how pub use dependency::macro_name re-exports macros from dependencies
  • Code Example

    #![allow(clippy::all)]
    //! Macro Scoping
    //!
    //! How macros are imported and exported.
    
    /// #[macro_export] makes macro public.
    /// #[macro_use] imports macros from crate.
    
    #[macro_export]
    macro_rules! public_macro {
        () => {
            "public"
        };
    }
    
    /// Not exported - only usable in this crate.
    macro_rules! private_macro {
        () => {
            "private"
        };
    }
    
    /// Use private macro.
    pub fn use_private() -> &'static str {
        private_macro!()
    }
    
    /// Use public macro.
    pub fn use_public() -> &'static str {
        public_macro!()
    }
    
    /// Module with local macro.
    mod inner {
        macro_rules! local_macro {
            () => {
                "local"
            };
        }
    
        pub fn use_local() -> &'static str {
            local_macro!()
        }
    }
    
    pub use inner::use_local;
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_public_macro() {
            assert_eq!(public_macro!(), "public");
        }
    
        #[test]
        fn test_use_private() {
            assert_eq!(use_private(), "private");
        }
    
        #[test]
        fn test_use_public() {
            assert_eq!(use_public(), "public");
        }
    
        #[test]
        fn test_use_local() {
            assert_eq!(use_local(), "local");
        }
    
        #[test]
        fn test_exported_available() {
            let s = public_macro!();
            assert!(!s.is_empty());
        }
    }

    Key Differences

  • Path-based import: Rust 2018 macros use use crate::macro_name; older Rust required #[macro_use] extern crate name.
  • Crate root export: #[macro_export] always places macros at the crate root regardless of module nesting; OCaml exports are always at the module they're defined in.
  • No runtime scope: Macros don't exist at runtime; OCaml module functions do. The scoping rules serve compile-time resolution only.
  • Re-export: Rust re-exports macros with pub use crate_dep::macro_name; OCaml re-exports with include Module or explicit let fn = Mod.fn.
  • OCaml Approach

    OCaml modules naturally scope functions. A module Private = struct let helper = ... end creates a private scope. module type S = sig val public_fn : unit -> string end restricts what's exported. OCaml doesn't distinguish macros from functions in scoping — PPX extensions are build-time only and don't have runtime scope. Library users access PPX features through dune configuration, not use statements.

    Full Source

    #![allow(clippy::all)]
    //! Macro Scoping
    //!
    //! How macros are imported and exported.
    
    /// #[macro_export] makes macro public.
    /// #[macro_use] imports macros from crate.
    
    #[macro_export]
    macro_rules! public_macro {
        () => {
            "public"
        };
    }
    
    /// Not exported - only usable in this crate.
    macro_rules! private_macro {
        () => {
            "private"
        };
    }
    
    /// Use private macro.
    pub fn use_private() -> &'static str {
        private_macro!()
    }
    
    /// Use public macro.
    pub fn use_public() -> &'static str {
        public_macro!()
    }
    
    /// Module with local macro.
    mod inner {
        macro_rules! local_macro {
            () => {
                "local"
            };
        }
    
        pub fn use_local() -> &'static str {
            local_macro!()
        }
    }
    
    pub use inner::use_local;
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_public_macro() {
            assert_eq!(public_macro!(), "public");
        }
    
        #[test]
        fn test_use_private() {
            assert_eq!(use_private(), "private");
        }
    
        #[test]
        fn test_use_public() {
            assert_eq!(use_public(), "public");
        }
    
        #[test]
        fn test_use_local() {
            assert_eq!(use_local(), "local");
        }
    
        #[test]
        fn test_exported_available() {
            let s = public_macro!();
            assert!(!s.is_empty());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_public_macro() {
            assert_eq!(public_macro!(), "public");
        }
    
        #[test]
        fn test_use_private() {
            assert_eq!(use_private(), "private");
        }
    
        #[test]
        fn test_use_public() {
            assert_eq!(use_public(), "public");
        }
    
        #[test]
        fn test_use_local() {
            assert_eq!(use_local(), "local");
        }
    
        #[test]
        fn test_exported_available() {
            let s = public_macro!();
            assert!(!s.is_empty());
        }
    }

    Deep Comparison

    OCaml vs Rust: macro scoping

    See example.rs and example.ml for side-by-side implementations.

    Key Points

  • Rust macros operate at compile time
  • OCaml uses ppx for similar metaprogramming
  • Both languages support powerful code generation
  • Rust's macro_rules! is built into the language
  • OCaml's approach requires external tooling
  • Exercises

  • Module-gated macro: Define a macro inside a mod internal { macro_rules! my_macro { ... } } and verify it's not accessible outside the module. Then add #[macro_export] and verify it becomes accessible via crate::my_macro.
  • Re-export chain: Create a crate A with #[macro_export] macro_rules! helper!. In crate B, pub use crate_a::helper. In crate C using crate B, verify use crate_b::helper makes the macro available.
  • Conditional export: Use #[cfg(feature = "macros")] #[macro_export] to make a macro only available when a feature is enabled. Write a doc comment explaining the feature flag to users.
  • Open Source Repos