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?