A Beginner’s Guide to Rust Macros ✨

Demystifying one of Rust’s most powerful feature.

One of the most awesome and powerful feature of Rust is its ability to use and create macros. Unfortunately, the syntax for creating macros can be quite intimidating, and the examples can be overwhelming for newcomer developers.

I promise you that Rust Macros are really simple to understand. This article will give you a head-start on how to create your own macros.

What are Macros in Rust? 😮

If you’ve already tried out Rust, you should’ve already used a macro before, println!. This macro allows you to print a line of text with the ability to interpolate variables in the text string.

Macros simply allows you to invent your own syntax and write code that writes more code.

This is called metaprogramming, which allows for syntactic sugars that make your code shorter and make it easier to use your libraries. You could even create your own DSL (Domain-Specific Language) within rust.

Macros works by matching against the specific patterns defined in the macro rule, capturing part of the match as variables, then expand to produce even more code.

It’s okay if you don’t understand some of that. Let’s just dive right in!

How do we create a Macro? 😯

We can create macros by using the macro_rules! macro. Macroception!

This is how you can create a blank hey! macro; it doesn’t do anything for now, obviously.

That () => {} part seems intriguing, isn’t it.

This is an entry for a macro rule, which we can have many rules to match for in a single macro. It’s very similar to pattern matching, where we can also have many cases, delimited by the use of commas.

But, what does it actually mean?

The parentheses part is the matcher, which will allow us to match for patterns and capture part of them as variables. This is how we can invent our own custom syntaxes and DSLs.

The curly braces part is the transcriber, which is where we can make use of the variables we captured from the matcher. The Rust compiler will expand our macro’s code and its variables to an actual Rust code.

Matching and Capturing Patterns ✏️

How do we actually match for a pattern though? Let’s see.

Rust will try to match for the patterns defined within the matcher pairs, which can either be (), {} or []. The characters in the macro rules will be matched against the input to determine if it matches or not.

After the rule’s pattern matches successfully, we can capture parts of the pattern and capture them as a variable to use in the transcriber. Here’s how you can do that.

The first part after the dollar sign is the name of the variable, which will be available as a variable in the transcriber, which is where our code is at. In this example, we’re currently capturing it as $name.

The last part after the semicolon is called a designator, which are types that we can choose to match for. For instance, we are currently using the expression designator, as denoted by the expr after the colon.

This tells Rust to match for an expression, and capture it as $name.

There are many designators we could use, not just expressions. Here’s a quick list of the designators available in Rust:

  • item: an item, such as a function, a struct, a module, etc.
  • block: a block (i.e. a block of statements and/or an expression, surrounded by braces)
  • stmt: a statement
  • pat: a pattern
  • expr: an expression
  • ty: a type
  • ident: an identifier
  • path: a path (e.g. foo::std::mem::replace, transmute::<_, int>, ...)
  • meta: a meta item; the things that go inside #[...] and #![...] attributes
  • tt: a single token tree

Now, how do we use these captured variables in the transcriber?

Simple enough. We just use those captured variables we captured in the matcher, which is prefixed by the dollar sign. It works just like a normal variable; nothing special here.

Our First Macro! 🌈

Cool! We now know just enough stuff to create a simple macro. Let’s create our very own yo! macro.

That’s it, we just created our first macro! This program will print out Yo, Finn! as a result of invoking the macro.

We’ve matched the input expression and captured it as the $name variable. Then, we make use of the captured $name in the transcriber, which will be expanded by the Rust compiler.

That seems straightforward, isn’t it?

Repetitions, Repetitions, Repetitions, Repetitions, Repetitions, Repetitions, Repetitions, Repetitions…

Many of the macros we know and love can take lots of input at once. Take the vec! macro as an example; we can create a Vector with items in it by invoking the vec! macro like so: vec![1, 2, 3, 4, 5].

How does vec! do that? It surely isn’t going to capture a couple thousand variables and pushing them in manually one-by-one, right? This is the secret:

We simply put the patterns we want to repeat inside the $(...) part. Then, insert the separator, which is the comma (,) symbol in this case. This will be the character that will separate the patterns, allowing us to have repetitions.

Finally, we add the star (*) symbol at the end, which will repeatedly match against the pattern inside $() until it ran out of matches.

Confused? Let’s see an example!

In this case, for the hey! macro, we’re capturing all the input expression as $name, and we’re going to keep matching it until we ran out of matches. We can invoke this macro with as many arguments as we like.

Still confused? That’s totally fine! Let’s implement an awesome real-world macro to see how this works.

Implementing Ruby’s HashMap syntax in Rust

If you’ve ever programmed in Ruby before, you would know of its awesome hash syntax, key => value. I would love to have that syntax for creating HashMaps in Rust too!

Luckily, we have macros at our disposal, and we can invent our own syntax and make dreams come true in just a few lines of code. (spoilers: seven)

Let’s start simple. We can use the $key:expr => $value:expr pattern to capture both the $key and the $value expressions, separated by the use of =>.

This already works nicely, but it would only let us match against only one key => value pair, which is not very practical for HashMaps that often has more than one key-value pairs. This is where the star (*) comes in handy.

By putting our pattern inside a $(),*, we’re allowing repetitions for that pattern. This means we can have as many key => value pairs as we desire. Hooray!

When the macro is invoked, the captured variable will be assigned for each repetition.

How are we going to make use of these captured variables that are being repeated, though? It all becomes clear when we use them in the transcriber.

In the transcriber, we can notice the use of $()* in our code. This means that this code will be expanded specifically for each repetition layers!

This will expand to hm.insert() the $key and $value into the HashMap for each repetition layers. Let’s visualize that for a bit.

As you can see, when we invoke map!("name" => "Finn", "gender" => "Boy"), we’re producing two repetition layers.

The key => value pairs will be transcribed to the code specified in the transcriber part, which were specified as hm.insert($key, $value), with the $key and $value being the captured variables.

Phew, that was quite a lot of stuff! Let’s put it all together and create our own map! macro.

In just seven lines of code, we’ve created ourselves a fully-functional macro! Let’s create a program and try to invoke it. See if it works.

The output of this program is User {“gender”: “Boy”, “name”: “Finn”}. There, it worked! This is how we can create macros in Rust.

If you wanted to learn more about how to develop macros for real-world usage in Rust, check out these resources:

That’s it for now! 💖

Thanks everyone for making it to the end! I hope you finally have the courage to create your own macros and fulfill your metaprogramming desires in Rust.

See you in the next article! ✨