Skip to Content
Course content

186: Macro Hygiene Explained

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've spent any time with C or C++, you probably have a visceral reaction to the word "macro." In those languages, macros are essentially sophisticated find-and-replace tools. They are "unhygienic," meaning they just dump text into your source code. If your macro defines a variable called tmp and the function using that macro also happens to have a variable called tmp, you've just walked into a naming collision that can take hours to debug.

The chaos of name capture

To understand why hygiene matters, let's imagine Rust's macro_rules! didn't have it. Imagine I wrote a macro to help you calculate the average of a few numbers. Inside the macro, I'd need a variable to hold the running total. In an unhygienic world, it would look something like this:

// WARNING: This is a conceptual "unhygienic" example, NOT how Rust actually works.
macro_rules! calculate_avg {
    ($($x:expr),*) => {
        let total = 0; // Internal variable
        $( total += $x; )*
        total / count()
    };
}

Now, imagine you're using this macro in a function where you've already defined a variable named total to track something entirely different, like your bank balance. The macro would either shadow your balance or, worse, accidentally add your numbers to your bank balance. It's a nightmare because the macro "leaks" its internal implementation details into your local scope, or "captures" your local variables.

How Rust isolates the macro's internals

Rust solves this by making macro_rules! hygienic. When the compiler expands a macro, it doesn't just paste text. It assigns a unique "color" or identity to the variables created inside the macro. Even if you name a variable total inside the macro and the user names a variable total in their function, Rust treats them as two completely different entities.

I'll show you what I mean with a real example. We can define a macro that uses a local variable, and then call it from a scope that uses the exact same name. In any other language, this would be a disaster. In Rust, it's a Tuesday.

macro_rules! sum_and_print {
    ($($val:expr),*) => {
        let accumulator = 0; // This is "hygienic"
        $(
            accumulator += $val;
        )*
        println!("The sum is: {}", accumulator);
    };
}

fn main() {
    let accumulator = 100; // This is a totally different 'accumulator'
    sum_and_print!(1, 2, 3); 
    println!("The original accumulator is still: {}", accumulator);
}

When you run this, the macro doesn't touch the accumulator in main. It creates its own internal version that exists only for the duration of the expansion. You don't have to worry about naming your internal macro variables __my_macro_internal_var_sum_123 just to avoid collisions. You can use clean, descriptive names.

When you actually want to break the wall

Now, here is the trade-off. Hygiene is great until you want the macro to interact with the outside world. For instance, what if your macro needs to call a function that the user provides, or a specific function from a crate that the user has imported?

If the macro refers to Vec::new(), but the user hasn't imported Vec (hypothetically) or has shadowed Vec with their own type, the macro might fail because it's looking for the "macro's version" of Vec, not the "user's version." This is why you'll often see experienced Rust engineers use fully qualified paths inside macros, like ::std::vec::Vec::new(). By using the absolute path starting from the crate root (::), you bypass the ambiguity of the local scope and ensure the macro works regardless of what the user has named their variables or which traits they've imported.




📋 Practical Task

Implementing a Hygienic Guarded-Execution Macro

Your goal is to create a macro called track_execution!. This macro should take a block of code, execute it, and print a message indicating that the block has finished. To test the hygiene, you must ensure that the macro uses an internal variable (e.g., let status = "Done";) and that this variable does not clash with a variable named status in the main function.

Requirements:

  • Define macro_rules! track_execution which accepts a block of code { ... }.
  • Inside the macro, define a local variable status and print it after the block executes.
  • In main, define a variable status with a different value (e.g., "Waiting").
  • Call the macro and then print the main function's status variable to prove it remained unchanged.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.