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?