🎓 A1Learner
🔍

Rust Macro DSLs

A macro DSL is a tiny domain-specific language built inside Rust with macro arms. DSL stands for domain-specific language.

An invented word

say is not a Rust keyword. It is a literal token. This macro chose it. The eval word in the closing example works the same way. Only the captured piece after it must be real Rust:

macro_rules! announce {
    (say $e:expr) => { println!("{}", $e) };
}
announce!(say "lunch is ready");

Pin the type inside

A type line inside the expansion pins the little language down. let val: usize does the pinning. It makes every eval work with whole numbers:

macro_rules! double_it {
    (eval $e:expr) => {{ let val: usize = $e; val * 2 }};
}
println!("doubled = {}", double_it!(eval 20 + 1));

First plus rest: variadic by recursion

One arm handles a single item. The other handles the first plus at least one more. It hands the rest back to the same macro. That is how a macro accepts arguments. It accepts any number of them:

macro_rules! tally {
    (item $e:expr) => { 1 };
    (item $e:expr, $(item $es:expr),+) => { 1 + tally!($(item $es),+) };
}
println!("count = {}", tally!(item "a", item "b", item "c"));

Example

macro_rules! calculate {
    (eval $e:expr) => {{
        let val: usize = $e;
        println!("{} = {}", stringify!($e), val);
    }};
    (eval $e:expr, $(eval $es:expr),+) => {{
        calculate!(eval $e);
        calculate!($(eval $es),+);
    }};
}
fn main() {
    calculate!(eval 1 + 2);
    calculate!(eval 3 + 4, eval 5 + 6, eval 7 + 8);
}

Key points

  • DSL: domain-specific language — a small invented notation that fits one job, built from macro arms
  • Variadic: accepting a variable number of arguments
  • Rust functions have a fixed argument count — there are no variadic functions
  • Popular crates build small languages this way
  • They build them for configuration
  • They build them for queries and command lines
  • The notation is theirs
  • The engine is macro_rules!

In this example, calculate! invents one word: eval. The first arm matches a single eval followed by an expression. It stores the value as a usize, then prints the expression as text and its value: 1 + 2 = 3. The second arm is the variadic one: it matches the first eval plus at least one more. It runs calculate! on the first, then hands the rest back to calculate! — recursion again. So the three-eval call prints one line per eval. It prints them in order. 3 + 4 = 7 comes first. 5 + 6 = 11 comes next. 7 + 8 = 15 comes last. One invented word, any number of uses.

Note

Keep a DSL tiny and familiar

A notation only its author can read is worse than plain Rust — two or three invented words is usually plenty

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why can println! take any number of arguments, while a Rust function cannot?