macro_rules!
What is it?
macro_rules! is a way of writing macros in Rust. Macros produce code as output, typically derived from code they receive as input.
macro_rules! in particular offers a declarative style for defining these macros.
The output code has fixed placeholders into which input is templated.
And you can select different output templates by matching on the structure of the input.
Here is an example of a macro_rules! definition with two output templates.
One of them has a placeholder $name, which accepts an expression.
macro_rules! hello { () => { println!("Hello, World!"); }; ($name:expr) => { println!("Hello, {}!", $name); }; } fn main() { hello!(); //equivalent to `println!("Hello, World!");` hello!("Alice"); //equivalent to `println!("Hello, Alice!");` }
macro_rules! are less powerful than the procedural proc_macros, but often simpler to read and write.
Mind that function-like proc_macros also use the exclamation mark syntax!(), so not everything with an exclamation mark is macro_rules!.
What is macro_rules! useful for?
-
Reducing boilerplate
macro_rules!can be used for deduplicating code, similar to functions. But unlike functions, it can be used when the code defines types, modules or similar. Reducing boilerplate is whatmacro_rules!are most commonly used for.
Examples:vec![],dbg!(),uuid!(),shadow!()
The standard library also makes heavy use ofmacro_rules!for declaring types with small variations, like for example integers. -
Simple domain-specific languages
You can match on fixed words, symbols or even formats. This way, you can create a small syntax to configure even complex output templates.
Examples:clap::arg!(),format!() -
Structural validation
macro_rules!allows you to reject inputs that don’t match a certain format, meaning you can verify the user made no mistake when entering them. However, you cannot use a traditional if-else inmacro_rules!(without generating it into the output), meaning you likely want proc_macros for more complex cases.
Examples:json!(),toml! { } -
Variadic interfaces
Unlike functions,macro_rules!can accept a variable number of parameters. This is useful for libraries in particular, where it can allow you to build a cleaner interface, especially when you combine it with the points above.
Examples:vec![],format!()
How to write macro_rules!
For an in-depth explanation, you can read the page in the Rust Book. For simple macros, however, you can often figure out how to write them from looking at examples.
A good explanation for the different kinds of placeholders can be found in The Little Book of Rust Macros.
Tips
-
To split up your logic, you can generate code which calls further macros. This can also be used for recursion.
-
Macros are evaluated early during compilation.
As such, you cannot match on the value or type of a variable, unless you generate code which does this at runtime. On the other hand, this early evaluation allows generating type definitions and modules. -
IDEs or text editors with LSP integration can typically ‘expand’ macros, which shows you the output code that will get templated.
-
You cannot declare whether your macro is called with parentheses
(), brackets[]or braces{}. All of them are allowed. For example, these calls are perfectly legal:
vec!(1, 2, 3)
vec! { 1, 2, 3 } -
macro_rules!visibility is somewhat unusual:- Within the module where the macro is defined, it is only visible after the definition.
- To use a macro in other modules, you need either
pub(crate) use my_macro;below the macro definition or you can annotate it with#[macro_export].
The#[macro_export]annotation makes it available at the module pathcrate::my_macro. - To use a macro in other crates, you have to annotate it with
#[macro_export].
macro_rules! in a library
-
When your macro is called from another crate, the
cratekeyword will refer to that crate (since that’s where the code is templated into). If you need to refer to elements in your own crate, use the$cratevariable instead. -
You want to avoid using imports in the generated code, as they will affect the user’s code, too. Instead, reference elements with their absolute module path.
You should also make it explicit that it is an absolute path by prepending a double-colon, e.g.::std::path::Path. If you use juststd::path::Pathinstead, a user having a modulestdin scope would prevent your macro from working. -
If your macro generates items which don’t have to be used, you should mark them as
#[allow(unused)]. -
If you need to generate a module, which isn’t intended to be accessed by the user, use a name which is unlikely to collide with the user’s modules, like for example
__my_library_macro. -
You can expose
format!()-like string formatting by callingformat_args!(). -
You can reject input with an explicit message by generating a
compile_error!(). -
Documentation comments can be templated with placeholders by using the
#[doc]attribute.
The normal///syntax does not work, because the$placeholdersyntax will be deemed part of the comment text. -
trybuildcan be used for testing, whether your macro correctly rejects inputs.