mirror of
https://github.com/darkicewolf50/RustBrock.git
synced 2025-06-15 13:04:18 -06:00
147 lines
7.1 KiB
Markdown
147 lines
7.1 KiB
Markdown
# 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 the `derive` 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:
|
|
```rust
|
|
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.
|
|
```rust
|
|
#[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](https://doc.rust-lang.org/reference/macros-by-example.html).
|
|
|
|
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:
|
|
```rust
|
|
{
|
|
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"](https://veykril.github.io/tlborm/) started by Daniel Keep and continued by Lukas Wirth.
|
|
|
|
## Procedural Macros for Generating Code from Attributes
|