First steps with Rust declarative macros!
Roger Torres (he/him/ele)
Posted on July 26, 2021
Macros are one of the ways to extend Rust syntax. As The Book calls it, “a way of writing code that writes other code”. Here, I will talk about declarative macros, or as it is also called, macros by example. Examples of declarative macros are vec!
, println!
and format!
.
The macros I will not talk about are the procedural macros, but you can read about them here.
As usual, I am writing for beginners. If you want to jump to the next level, check:
- The Book's section on macros.
- The chapter about macros in Rust by Example
- The Little Book of Rust Macros, which is the most complete material I found about the topic (the second chapter is specially amusing).
Why do I need macros?
The actual coding start in the next section.
Declarative macros (from now on, just “macros”) are not functions, but it would be silly to deny the resemblance. Like functions, we use them to perform actions that would otherwise require too many lines of code or quirky commands (I am thinking about vec!
and println!
, respectively). These are (two of) the reasons to use macros, but what about the reasons to create them?
Well, maybe you are developing a crate and want to offer this feature to the users, like warp did with path!
. Or maybe you want to use a macro as a boilerplate, so you don't have to create several similar functions, as I did here. It might be also the case that you need something that cannot be delivered by usual Rust syntax, like a function with initial values or structurally different parameters (such as vec!
, that allows calls like vec![2,2,2]
or vec![2;3]
—more on this later).
That being said, I believe that the best approach is to learn how to use them, try them a few times, and when the time comes when they might be useful, you will remember this alternative.
Declaring macros
This is how you declare a macro:
macro_rules! etwas {
() => {}
}
You could call this macro with the commands etwas!()
, etwas![]
or etwas!{}
. There's no way to force one of those. When we call a macro always using one or the other—like parenthesis in println!("text")
or square-brackets in vec![]
—it is just a convention of usage (a convention that we should keep).
But what is happening in this macro? Nothing. Let's add something to make it easier to visualize its structure:
macro_rules! double {
($value:expr) => { $value * 2 }
}
fn main() {
println!("{}", double!(7));
}
The left side of =>
is the matcher, the rules that define what the macro can receive as input. The right side is the transcriber, the output processing.
Not very important, but both matcher and transcriber could be writen using either
()
,[]
or{}
.
The matching
This will become clear later, but let me tell you right the way: the matching resembles regex. You may ask for specific arguments, fixed values, define acceptable repetition, etc. If you are familiar with it, you should have no problems picking this up.
Let's go through the most important things you should know about the matching.
Variable argument
Variable arguments begin with $
(e.g., $value:expr
). Their structure is: $
name
:
designator
.
- Both
$
and:
are fixed. - The
name
follows Rust variables convention. When used in the transcriber (see below), they will be called metavariables. - Designators are not variable types. You may think of them as “syntax categories”. Here, I will stick with expressions (
expr
), since Rust is “primarily an expression language”. A list of possible designators can be found here.
Note: There seems to be no consensus on the name "designator". The little book calls it "capture"; The Rust reference calls it "fragment-specifier"; and you will also find people referring them as "types". Just be aware of that when jumping from source to source. Here, I will stick with designator, as proposed in Rust by example.
Fixed arguments
No mystery here. Just add them without $
. For example:
macro_rules! power {
($value:expr, squared) => { $value.pow(2) }
}
fn main() {
println!("{}", power!(3_i32, squared));
}
I know there are things here that I have not explained yet. I will talk about them now.
Separator
Some designators require some specific follow up. Expressions require one of these: =>
, ,
or ;
. That is why I had to add a comma between $value:expr
and the fixed-value squared
. You will find a complete list of follow-ups here.
Multiple matching
What if we want our macro to not only calculate a number squared, but also a number cubed? We do this:
macro_rules! power {
($value:expr, squared) => { $value.pow(2_i32) }
($value:expr, cubed) => { $value.pow(3_i32) }
}
Multiple matching can be used to capture different levels of specificity. Usually, you will want to write the matching rules from the most-specific to the least-specific, so your call doesn't fall in the wrong matching. A more technical explanation can be found here.
Repetition
Most macros that we use allow for a flexible number of inputs. For example, we may call vec![2]
or vec![1, 2, 3]
. This is where the matching resembles Regex the most. Basically, we wrap the variable inside $()
and follow up with a repetition operator:
-
*
— indicates any number of repetitions. -
+
— indicates any number, but at least one. -
?
— indicates an optional, with zero or one occurrence.
Let's say we want to add n
numbers. We need at least two addends, so we will have a single first value, and one or more (+
) second value. This is what such a matching would look like.
macro_rules! adder {
($left:expr, $($right:expr),+) => {}
}
fn main() {
adder!(1, 2, 3, 4);
}
We will work on the transcriber latter.
Repetition separator
As you can see in the example above, I added a comma before the repetition operator +
. That's how we add a separator for each repetition without a trailing separator. But what if we want a trailing separator? Or maybe we want it to be flexible, allowing the user to have a trailing separator or not? You may have any of the three possibilities like this:
macro_rules! no_trailing {
($($e:expr),*) => {}
}
macro_rules! with_trailing {
($($e:expr,)*) => {}
}
macro_rules! either {
($($e:expr),* $(,)*) => {}
}
fn main() {
no_trailing!(1, 2, 3);
with_trailing!(1, 2, 3,);
either!(1, 2, 3);
either!(1, 2, 3,);
}
Versatility
Unlike functions, you may pass rather different arguments to macros. Let's consider the vec!
macro example. For that, I will omit the transcriber.
macro_rules! vec {
() => {};
($elem:expr; $n:expr) => {};
($($x:expr),+ $(,)?) => {};
}
It deals with three kinds of calls:
-
vec![]
, which creates an empty Vector. -
vec!["text"; 10]
, which repeats the first value ("text")n
times, wheren
is the second value (10). -
vec![1,2,3]
, which creates a vector with all the listed elements.
If you want to see the implementation of the
vec!
macro, check Jon's stream about macros.
The transcriber
The magic happens after the =>
. Most of what you are going to do here is regular Rust, but I would like to bring your attention to some specificities.
Type
When I called the exponentiation macro power!
, I did this:
power!(3_i32, squared);
I had to specify the type i32
because I used the pow()
function, which cannot be called on ambiguous numeric type; and as we do not define types in macros, I had to let the compiler know this information somehow. This is something to be aware when dealing with macros. Of course, I could have forced it by declaring a variable and passing the metavariable value to it and thus fixing the variable type. However, to do such a thing, we need multiple statements.
Multiple statements
To have more than one line in your transcriber, you have to use double curly brackets:
macro_rules! etwas {
//v --- this one
($value:expr, squared) => {{
let x: u32 = $value;
x.pow(2)
}}
//^ --- and this one
};
Easy.
Using repetition
Let us finish our adder!
macro.
macro_rules! adder {
($($right:expr),+) => {{
let mut total: i32 = 0;
$(
total += $right;
)+
total
}};
}
fn main() {
assert_eq!(adder!(1, 2, 3, 4), 10);
}
To handle repetition, all we have to do is to place the statement we want to repeat within $()+
(the repetition operator should match, that is why I am using +
here as well).
But what if we have multiple repetitions? Consider the following code.
macro_rules! operations {
(add $($addend:expr),+; mult $($multiplier:expr),+) => {{
let mut sum = 0;
$(
sum += $addend;
)*
let mut product = 1;
$(
product *= $multiplier;
)*
println!("Sum: {} | Product: {}", sum, product);
}}
}
fn main() {
operations!(add 1, 2, 3, 4; mult 2, 3, 10);
}
How does Rust know that it must repeat four times during the first repetition block and only three times in the second one? By context. It checks the variable that is being use and figure out what to do. Clever, huh?
Sure, you can make things harder to Rust. In fact, you may turn them indecipherable, like this:
macro_rules! operations {
(add $($addend:expr),+; mult $($multiplier:expr),+) => {{
let mut sum = 0;
let mut product = 1;
$(
sum += $addend;
product *= $multiplier;
)*
println!("Sum: {} | Product: {}", sum, product);
}}
}
What does “clever Rust” do with something like this? Well, one of the things it does best: it gives you a clear compile error:
error: meta-variable 'addend' repeats 4 times, but 'multiplier' repeats 3 times
--> src/main.rs:43:10
|
43 | $(
| __________^
44 | | sum += $addend;
45 | | product *= $multiplier;
46 | | )*
| |_________^
Neat! 🦀
Expand
As mentioned earlier, macros are syntax extensions, which means that Rust will turn them into regular Rust code. Sometimes, to understand what is going wrong on, it is very helpful to see how rust pull that transformation off. To do so, use the following command.
$ cargo rustc --profile=check -- -Zunstable-options --pretty=expanded
This command, however, is not only verbose, but it will also call for the nightly compiler. To avoid this and get the same result, you may install cargo-expand
:
$ cargo install cargo-expand
Once it is installed, you just have to run the command cargo expand
.
Note: Although you don't have to be using the nightly compiler, I guess (and you may call me on this) you got to have it installed. To do so, run the command
rustup instal nightly
.
Look at how the macro operations!
is expanded.
fn main() {
{
let mut sum = 0;
sum += 1;
sum += 2;
sum += 3;
sum += 4;
let mut product = 1;
product *= 2;
product *= 3;
product *= 10;
{
::std::io::_print(::core::fmt::Arguments::new_v1(
&["Sum: ", " | Product: ", "\n"],
&match (&sum, &product) {
(arg0, arg1) => [
::core::fmt::ArgumentV1::new(arg0, ::core::fmt::Display::fmt),
::core::fmt::ArgumentV1::new(arg1, ::core::fmt::Display::fmt),
],
},
));
};
};
}
As you can see, even println!
was expanded.
Export and import
To use a macro outside the scope it was defined, you got to export it by using #[macro_export]
.
#[macro_export]
macro_rules! etwas {
() => {};
}
You may also export a module of macros with #[macro_use]
.
#[macro_use]
mod inner {
macro_rules! a {
() => {};
}
macro_rules! b {
() => {};
}
}
a!();
b!();
To use a macro that a crate exports, you also use #[macro_use]
.
#[macro_use(lazy_static)] // Or #[macro_use] to import all macros.
extern crate lazy_static;
lazy_static!{}
The example above is from The Rust Reference.
And that's all for today. There is certainly more to cover, but I will leave you with the readings I recommended earlier.
Cover image by Thom Milkovic.
Posted on July 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.