Skip to main content

Macros in Rust

Macros in Rust is, in the most basic sense, "code that writes code", also known as metaprogramming. By now, you have seen the println!() macro many times, and it illustrates how useful macros may be in everyday coding.

Another prime example that you have seen is the use of the #[derive] macro, which can implement traits on types automatically:

// Automatically implements this type.
#[derive(PartialEq)]
struct SomeType;

Macros are called before the compiler interprets the code, so they can perform these operations (i.e., implementing a trait for you). This is usually called "expanding," as the macro's code expands to actual, usable Rust code the compiler can interpret and use.

Macro Types

There are two primary types of macros:

  • Declarative or "macro_rules!" Macros
  • Procedural Macros - which also have subtypes

Declarative Macros

Declarative macros are the most widely used and often easier to write than procedural ones. They allow programmers to write expressions akin to match statements that "fill in the blank" to make writing Rust more concise. Put simply; declarative macros operate almost like a template where the parameters provided by the programmer fill in the blanks.

Procedural Macros

Procedural macros are more complex, accepting arbitrary code as input and producing code as output. This code, called a TokenStream, represents this input and output. Procedural macros operate more like a function, accepting a TokenStream as a parameter and specifying a return type as a TokenStream. Part of the complexity in creating these macros is that they must be in a separate crate, impacting the Rust project's structure.

There are three primary types of procedural macros:

  • #[derive] macros specify code to add to entities such as structs and enums.
  • Function-like macros which structurally look and work like functions.
  • Attribute-like macros which define custom attributes on a particular entity.

In this course, we won't be reviewing how to write a procedural macro. For more reading, it is encouraged to read the Rust Book's examples, as well as the The Little Book of Rust Macros for more in-depth reading on how macros can be utilized.

Writing a Basic Declarative Macro

Overview

In this example, we will be writing a declarative macro that utilizes macro_rules!. As stated before, a declarative macro similarly works in principle to a match statement, as it declares a set of rules executed in order until the condition is reached. Once the rule is met, the macro generates the corresponding Rust code.

Courtesy of The Little Book of Rust Macros, the following examples help to solidify this concept.

// Each rule looks like the following:
($matcher) => {$expansion}

And in practice:

// This simply returns the expression: "4", aka the result of "1 + 3"
macro_rules! four {
() => { 1 + 3 };
}

fn main() {
let f = four!(); // 4
println!("{f}"); // 4
}

Creating a square! and factor! macro

Macros can also utilize metavariables to capture input and values from outside of the macro. One more commonly used is the expr metavariable, which signifies some expression as an input.

Using these concepts, let's create a macro that takes a number and squares it:

macro_rules! square {
($e:expr) => { $e * $e };
}

fn main() {
let f = square!(10); // 100
println!("{f}"); // 100
}

Slightly more advanced, let's allow our macro to take a number, find all factors, then return a new Vec of those factors:

macro_rules! square {
($e:expr) => {
$e * $e
};
}

macro_rules! find_factors {
($e:expr) => {{
let mut factors = Vec::new();
for multiplier in 1..=$e {
if $e % multiplier == 0 {
factors.push(multiplier);
}
}
factors
}};
}

fn main() {
let f = square!(10);
let factors = find_factors!(24);
println!("{f}");
println!("{:?}", factors);
}

Try it yourself

What is happening here?

Two declarative macros are defined, square! and find_factors!. Both take an expression and return a mutated version of the input. square! simply returns a square version of the number, while find_factors! does a few novel tasks:

  • Takes an expression, $e.
  • Defines a new inner scope to return.
  • Within that scope, creates a Vec of factors to return.
  • Declares a for loop, which iterates from the range of 1 to the value of $e (i.e.,. 1 to 24).
  • Finds if it is a factor via modulo and appends it to the array if it is.