429: Macro Scoping and Visibility
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
#[macro_export] and how it places macros at crate root#[macro_export]) are restricted to their moduleuse crate::macro_name path for importing macros in Rust 2018+pub use dependency::macro_name re-exports macros from dependenciesCode 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
use crate::macro_name; older Rust required #[macro_use] extern crate name.#[macro_export] always places macros at the crate root regardless of module nesting; OCaml exports are always at the module they're defined in.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());
}
}#[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
Exercises
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.#[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.#[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.