🎓 A1Learner
🔍

Rust Macro Internal Rules

An internal rule is a helper arm inside a macro that only the macro itself should call. The trick is a name tag no ordinary call would use.

The problem: a helper macro for one job

This tiny helper exists only so another macro can use it. But it now sits in the open. Anyone can call it:

macro_rules! as_text { ($e:expr) => { format!("{}", $e) } }
macro_rules! shout_it { ($e:expr) => { as_text!($e).to_uppercase() } }

Hide the helper inside, as an @ rule

Move the helper in as an extra arm. Its matcher starts with @as_text, so plain calls never match it:

macro_rules! shout_it {
    (@as_text $e:expr) => { format!("{}", $e) };
    ($e:expr) => { shout_it!(@as_text $e).to_uppercase() };
}

The macro calls its own tagged arm

The plain arm does no work itself. It hands the value to the internal arm and finishes what comes back:

($e:expr) => { shout_it!(@as_text $e).to_uppercase() };

Example

macro_rules! twice {
    (@run $e:expr) => { $e + $e };
    (num $e:expr) => { twice!(@run $e) };
    (text $e:expr) => { format!("{0} {0}", $e) };
}
fn main() {
    println!("double = {}", twice!(num 21));
    println!("echo = {}", twice!(text "ha"));
}

Key points

  • Internal rule: an arm whose matcher starts with a special token, most often @ followed by a name, like (@run $e:expr)
  • Only the macro itself calls its internal rules.
  • It calls its own name.
  • It puts the @ tag in front of the arguments.
  • The @ tag works because no ordinary call starts that way, so the arm can never be picked by accident
  • List internal rules first: arms are tried in order, and a wide-open arm written earlier could swallow an internal call
  • One macro with internal rules replaces two or three separate helper macros — and only one name is published
  • The @ is a convention, not a lock.
  • A determined caller could type the @ form too.

In this example, twice has three arms. The first is internal: its matcher starts with @run, so a normal call never chooses it. The num arm matches a number call. It forwards the value to the internal arm. That arm adds the value to itself: 21 + 21. The text arm needs no help: it formats the word twice with {0} {0}. main prints double = 42 from the internal arm's work, and echo = ha ha from the text arm. One macro, one published name, and the plumbing stays inside.

Note

It says "this arm is plumbing", and readers honour it.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

What marks an arm as an internal rule?