7.1 KiB
Macros
The term macro refers to a family of features in Rust: declarative macros with macro_rules!
and three kinds of procedural macros:
- Custom
#[derive]
macros that specify code added with thederive
attribute used on structs and enums. - Attribute-like macros that define custom attributes usable on any item
- Function-like macros that look like function calls but operate on the tokens specified as their argument Each will be discussed, but let's first look at why we even need macros when we already have functions.
The Difference Between Macros and Functions
Fundamentally macros are a way of writing code that writes other code, which is known as metaprogramming.
In Appendix C, we discuss the derive
attribute, which generates an implementation of various traits for you.
All of these macros expand to produce more code than the code you have written manually.
Metaprogramming is useful for reducing the amount of code you have to write and maintain, which is also one of the roles of functions.
Macros have some additional powers that functions don't have.
A function signature must declare the number and type of parameters the function has.
Macros can take a variable number of parameters.
To show this: we can call println!("hello")
with one argument or println!("hello {}", name)
with two arguments.
Macros are also expanded before the compiler interprets the meaning of the code, so a macro can.
For example, implement a trait on a give type.
A function cannot, because it gets called at runtime and a trait needs to be implemented at compile time.
The downside to implementing a macro instead of a function is that macro definition are more complex than function definitions because you are writing Rust code that writes Rust code.
This indirection of macro definitions are generally more difficult to read, understand and maintain than function definitions.
Another important difference between macros and functions is that you must define macros or bring them into scope before you call them in a file.
This is opposed to functions where you can define anywhere and call anywhere.
Declarative Macros with macro_rules!
for General Metaprogramming
The most widely used form of macros in Rust is the declarative macro.
These are sometimes also referred to as "macros by example," "macro rules
macros" or just plain "macros."
At their core, declarative macros allow you to write something similar to a Rust match
expression.
match
expressions are control structures that take an expression, compare the resulting value of the expression to patterns, and then run the code associated with the matching pattern.
Macros also compare a value to patterns that are associated with particular code.
In this situation, the value is the literal Rust source code passed to the macro, the patterns are compared with the structure of that source code and the code associated with each pattern, when matched, replaces the code passed to the macro.
This happens during compilation.
In order to define a macro, you use the macro_rules!
construct.
Now lets explore how to use macro_rules!
by looking at how the vec!
macro is defined.
Ch8 covered how we can use the vec!
macro to create a new vector with particular values.
For example, the following macro creates a new vector containing three integers:
let v: Vec<u32> = vec![1, 2, 3];
We could also use the vec!
macro to make a vector of two integers or a vector of five string slices.
We wouldn't be able to use a function to do the same because we wouldn't know the number or type values up front.
Here shows a slightly simplified definition of the vec!
macro.
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
Note: The actual definition of the vec!
macro in std library includes code to preallocate the correct amount of memory up front.
That code is an optimization that we don't include here to make the example simpler.
The #[macro_export]
annotation indicates that this macro should be made available whenever the crate in which the macro is defined is brought into scope.
We then start the macro definition with macro_rules!
and the name of the macro we are defining without the exclamation mark.
Then name here vec
is followed by curly brackets denoting the body of the macro definition.
The structure in the vec!
body is similar to the structure of a match
expression.
Here we have one arm with the pattern ( $( $x:expr ),* )
, followed by =>
and the block of code associated with this pattern.
If the pattern matches, the associated block of code will be emitted.
Given this is the only pattern in this macro, there is only one valid way to match; any other pattern will result in an error.
More complex macros will have more than one arm.
Valid pattern syntax in macro definitions is different than the pattern syntax covered in Ch19.
This is because macro patterns are matched against Rust code structure rather than values.
Now lets go over what the pattern pieces in the previous examples mean.
The full macro pattern syntax can be seen in the Rust Reference.
First, we use a set of parentheses to encompass the whole pattern.
We use a dollar sign ($
) to declare a variable in the macro system that will contain the Rust code matching the pattern.
The dollar sign makes it clear this a macro variable as opposed to a regular Rust variable.
Next comes a set of parentheses that captures values that match the pattern within the parentheses for use in the replacement code.
Within $()
is $x: expr
, this matches any Rust expression and gives the expression the name $x
.
The comma following $()
indicates that a literal comma separator character must appear between each instance of the code that matches the code within $()
.
The *
specifies that the pattern matches zero or more of whatever precedes the *
.
When we call this macro with vec![1, 2, 3];
, the $x
pattern matches three times with the three expressions 1
, 2
, and 3
.
Now lets look at the pattern in the body of the code associated with this arm: temp_vec.push()
within $()*
is generated for each part that matches $()
in the pattern zero or more times depending on how many times the pattern matches.
The $x
is replaced with each expression matched.
When we call this macro with vec![1, 2, 3];
the code generated that replaces this macro call will be this:
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
Here we dined a macro that can take any number of arguments of any type and can generate code to create a vector containing the specified elements.
To learn more about how to write macros, read online documentation or other resources like "The Little Book of Rust Macros" started by Daniel Keep and continued by Lukas Wirth.