Macros in Rust let you write code that writes code—giving you the power to simplify, reuse, and expand your logic with precision and flexibility.
In Rust, macros offer a powerful way to write code that can generate other code, enabling flexibility and efficiency in programming. By diving into macros, you’ll learn how to engage in metaprogramming in Rust using declarative macros—the kind most commonly encountered and utilized in the Rust community. As we unravel their syntax, rules, and usage, you’ll see how they enable cleaner, more concise code.
Basic Syntax
The basic syntax of declarative macros is quite similar to a Rust match expression. Consider the basic syntax of macros as shown in this listing.
macro_rules! macro_name {
// |--- Match rules
(...) => { ... };
(...) => { ... };
(...) => { ... };
}
The syntax starts with macro_rules!, which is also the called macro declaration, followed by a name for the macro and then the body of the macro. Inside the body of the macro, we have match rules. Each macro must have at least one rule and may contain many rules. The rules have a similar syntax to that of a match statement. The left side is a matching pattern, and the right side indicates the code substitution that should be made when a pattern is matched.
We’ll examine both the matching pattern and the code substitution in the next section. Each rule ends with a semicolon. A semicolon is optional for the last rule.
The code shown below defines a simple macro.
macro_rules! our_macro {
() => {
1 + 1
};
}
fn main() {
our_macro!();
}
The name of the macro in this case is our_macro, and it contains a simple rule. The pattern is empty, and for the code substitution part, we have 1+1. In the main function, we can invoke the macro by writing its name, similar to that of a function call. The macro will execute but some warning messages may arise, which you can just ignore for now.
our_macro has no associated pattern, and therefore, its invocation in main will be substituted by its code, which is 1+1. In other words, the calling of the macro does nothing but substitute the code of 1+1 into the main program. We can confirm this substitution by enclosing the invocation inside a print statement, as in the following example:
fn main() {
println!("{}", our_macro!());
}
When executed, the result is an output of 2.
Macros can be invoked using any type of brackets. For instance, all the invocations shown below are valid.
fn main() {
our_macro!();
our_macro![];
our_macro! {};
}
The same is also true for the left sides and right sides of the rules themselves. For instance, consider the code shown here.
macro_rules! our_macro {
[] => { // you can use (), {} or [] for left side of the rule
1 + 1
};
() => [
1 + 1
]; // you can use (), {} or [] for right side of the rule
}
The general convention, however, is to use parentheses for the left side of the rule and curly brackets for the right side of the rule. We’ll stick to this convention in our remaining examples.
Before we move further, note that macro invocation is not strictly the same as a function call. To see this distinction clearly, recall that a function returns some expression that has no semicolon at the end of the function. However, let’s add a semicolon to the end statement of the expression 1+1, which is the last statement in the macro body, as shown in this listing.
macro_rules! our_macro {
() => {
1 + 1;
};
}
The code in main will still produce a value of 2 when executed.
Matching Pattern in the Rule
The left side of the rule may contain any type of matching expression for containing anything that can be parsed and matched. Or, should we say, almost anything. Let’s modify the macro shown earlier and add another rule with some random pattern:
macro_rules! our_macro {
() => {
1 + 1;
};
(something@_@) => {
println!("You found nonsense here")
};
}
fn main() {
our_macro!(something@_@);
}
The pattern in the added rule does not make any sense but is something that can be matched. The right side of the rule is the substitution code, which must be valid Rust code. This is because it is something that the macro will be expanded to. In this case, when the rule matches, the invocation of the macro will be replaced with the substitution part which is the print statement. Executing the code in will print statement inside the second rule.
The summary so far is that each rule inside a macro consists of two main parts: the left side, which holds the matching pattern, and the right side, which defines the Rust code to be expanded. The key point to note here is that the left side, representing the matching pattern, can include nearly any syntactically correct expression, meaning anything that can be parsed by the compiler. On the other hand, the right side, also known as the expansion or body of the rule, must contain valid Rust code written with correct syntax.
Captures
The patterns we’ve just seen don’t make any sense. More useful patterns can be constructed by making use of captures, which are variables from the surrounding scope that the macro can refer to and use within its body. Captures allow a macro to include dynamic values or data from outside the macro’s pattern, making the macro more flexible and powerful. Captures have the following syntax:
$name: (expression or type or identifier)
The $ in $name denotes a capture variable within a macro pattern. In Rust, an expression is any piece of code that produces a value. Therefore, expressions could be function calls, arithmetic operations, even whole blocks of code. Let’s walk through an example of expression. Consider the code shown.
macro_rules! our_macro {
...
($e1:expr, $e2:expr) => {
$e1 + $e2
};
}
fn main() {
println!("{}", our_macro!(2, 2));
}
The left side of the rule will match any two expressions. The rule, when matched, will be expanded to the addition of the two expressions. In this case, the result is a value of 4. Recall that an expression can be anything that produces a value; therefore, the following invocation is also valid:
println!("{}", our_macro!(2 + 2 + (2 * 2), 2));
In this case, the output is a value of 10.
In general, you can match on any number of expressions in the left side of the rule. Consider another rule in a macro, as shown here.
macro_rules! our_macro {
...
($a:expr, $b:expr, $c:expr) => {
$a * ($b + $c)
};
}
fn main() {
println!("{}", our_macro!(5, 6, 3));
}
Strict Matching
The left side of the macro needs to strictly match in the invocation. For instance, if we change the commas in the last rule shown in Listing 15.8 to that of semicolons, then the invocation in main will throw an error, as shown in Listing 15.9.
macro_rules! our_macro {
...
($a:expr, $b:expr; $c:expr) => {
$a * ($b + $c)
};
}
fn main() {
println!("{}", our_macro!(5, 6, 3)); // Error
}
This error arises because invocation does not match any rule. The error can be fixed by changing the comma in the invocation to a semicolon, as shown in this listing.
macro_rules! our_macro {
...
($a:expr, $b:expr; $c:expr) => {
$a * ($b + $c)
};
}
fn main() {
println!("{}", our_macro!(5, 6; 3)); // fixed by changing comma to
// semicolon
}
The essential point to note is that you must provide something in the invocation itself that would match at least one rule.
Macro Expansion
Macro expansion in Rust is the process by which a macro’s code is transformed into valid Rust code before compile time. This step is necessary because macros allow you to write more concise and reusable code, and when the macro is invoked, it expands into the actual code that the compiler will process. Expanding the macro code ensures that the logic defined by the macro is applied in the correct context, allowing for flexible and efficient code generation.
The cargo expand command provides particularly useful insights into the macro’s expansion. It basically displays the expanded code. To enable the command, we need to install cargo-expand using the following command:
c:\> cargo install cargo-expand
This command works on the nightly version. To install the nightly version, the following command is used:
c:\> rustup install nightly
If nightly is already installed, then you’ll you enable it using the following command:
c:\> rustup override set nightly
After installing switching to the nightly version, everything will probably be fine; however, if for any reason the commands in the remaining of the same section are not working, then you may consider running the following commands:
c:\> rustup component add rustfmt
C:\> rustup component add rustfmt --toolchain nightly
Now let’s see the usage of the command by considering the following single invocation of the macro in main:
fn main() {
our_macro!();
}
The following command will expand the code in main:
c:\> cargo expand
Running the command will produce the output shown in this listing.
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() {
1 + 1;
}
The expansion shows us the underlying code that the macro generates, allowing us to see what the compiler will work with after the macro has been expanded. The prelude and the standard library crate are included by default by almost all the Rust programs. In main, we only have a single statement which corresponds to the expansion of the macro invocation. Note that there are also some extensions that expand the code such as Rust Macro Expand.
It is important to highlight that we have been using macros from the very beginning of the book. The print statement is also a macro. Considering the following code in main:
fn main() {
println!("Hello to macros world");
}
Running the cargo expand for the preceding code will generate the output shown in this listing.
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
fn main() {
{
::std::io::_print(format_args!("Hello to macros world\n"));
};
}
This shows us the long syntax for the print statement.
Macros for Reducing Complexity
Macros provide strong motivation for simplifying code. Without macros, you would need to write separate code for each type, resulting in extensive duplication and unnecessary complexity. While generics can sometimes reduce this redundancy, in some scenarios, macros are especially effective in removing unwanted complexity, ultimately making your code more readable and compact.
Take the print! macro, for example. If we had to rewrite the full underlying code each time we wanted to print something, our code would quickly become unwieldy. Macros thus help by extracting repetitive details, allowing the core logic to be clearer and more manageable.
Editor’s note: This post has been adapted from a section of the book Rust: The Practical Guide by Nouman Azam. Dr. Azam is an associate professor of computer science at the National University of Computer and Emerging Sciences. He also teaches online programming courses about Rust antd MATLAB, and reaches a community of more than 50,000 students.
This post was originally published 6/2025.
Comments