DEV Community

loading...
Cover image for First steps with Rust declarative macros!

First steps with Rust declarative macros!

Roger Torres Paes (he/him/ele)
I'm a Brazilian dev who writes technical texts in ordinary language.
・8 min read

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:


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 {
    () => {}
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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) }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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,);
}
Enter fullscreen mode Exit fullscreen mode

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),+ $(,)?) => {};
}
Enter fullscreen mode Exit fullscreen mode

It deals with three kinds of calls:

  • vec![], which creates an empty Vector.
  • vec!["text"; 10], which repeats the first value ("text") n times, where n 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);
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }} 
}
Enter fullscreen mode Exit fullscreen mode

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 | |         )*
   | |_________^
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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),
                    ],
                },
            ));
        };
    };
}
Enter fullscreen mode Exit fullscreen mode

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 {
    () => {};
}
Enter fullscreen mode Exit fullscreen mode

You may also export a module of macros with #[macro_use].

#[macro_use]
mod inner {
    macro_rules! a {
        () => {};
    }

    macro_rules! b {
        () => {};
    }
}

a!();
b!();
Enter fullscreen mode Exit fullscreen mode

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!{}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (2)

Collapse
hannydevelop profile image
Ukpai Ugochi

Thank you, this was a great read!

Collapse
rogertorres profile image
Roger Torres Paes (he/him/ele) Author

Hey, @hannydevelop ! Glad to know :) thanks