It’s now time to learn how to write your own macros. Writing macros can be pretty complicated, which is why they are here near the very end of the book. You very rarely need to write them, but sometimes you might want to because they are very convenient—they essentially write code for you. They have a syntax that is pretty different from normal Rust, and they take some getting used to. Well, a lot of getting used to.
Indeed, the book Programming Rust (O’Reilly, 2021) finishes its chapter on macros with a conclusion that sums up the feeling pretty well: “Perhaps, having read all this, you’ve decided that you hate macros.” Hopefully, that’s not the case with you, but we’ll see! Macros offer a power that nothing else can, and they begin to feel friendlier as you use them more and more. We'll start out with the case for macros and why they even exist in the first place.
Macros are extremely common in Rust, as we have already noticed since the beginning of the book. Even println!
itself is a macro. But we haven’t learned yet about what they are, besides mentioning in chapter 1 that a macro is like a function that writes code for you. This is actually a more important point than it might seem: a macro produces code before the compiler has even started looking at it.
Back in chapter 16, we looked at const generics, which removed a lot of pain for Rust users when it came to using arrays (and many other things). The chapter cites a Reddit user back in 2018 before const generics was stabilized and how macros had to be used instead:
By far the single biggest pain point is const_generics. It can’t be implemented and stabilized fast enough. I wrote an elaborate system of macros to solve the issue for our particular system. (http://mng.bz/eE8V)
So, for this user and his team, there was so much manual typing that needed to be done that they had to resort to macros to solve the task. The team needed something that takes some input from the user and produces code that the compiler can then start looking at.
For tasks like this, a regular function simply can’t get the job done. Imagine that we need to create 100 structs. Each has a different name, and each has some parameters that need names. If we tried to make a function to make these structs, we would immediately run into problems, as the following example shows:
fn create_struct(struct_names: ???, struct_parameters: Vec<???>) -> ???? { struct struct_name { struct_parameters.into_iter()??? } }
You can see the problem already. What is the type of struct_names
? It can’t be a String
because a String
is a Vec<u8>
, which has memory allocated to it and later gets dropped. And the parameters would be a Vec
of what? But that doesn’t work either because, once again, a Vec
has memory allocated to it and is dropped. And what does the function even return? Same problem. We have no way to just return a bunch of code from a function.
Everything we write inside a regular function lives inside the Rust compiler’s world. There’s no way that anything we write inside this function would compile, and in fact, we don’t even want it to compile: we want to generate something that can then be compiled.
The compiler wants to turn our code into machine code, but we don’t want the compiler to evaluate what we are typing just yet. We need to take a step back before the Rust compiler takes a look at the code. That’s what macros are for.
Interestingly enough, to write a macro in Rust you use a macro called macro_rules!
. After this, you add your macro name and open a {}
block. Inside is sort of like a match
statement.
Here’s a macro that only takes ()
and returns 6:
macro_rules! give_six { () => { 6 }; } fn main() { let six = give_six!(); println!("{}", six); }
But it’s not the same as a match
statement because nothing here is being checked and compiled—the macro simply takes an input and gives an output. (Technically, it’s called a token parser.) And only afterward the compiler checks to see whether it makes sense.
In fact, we can make a macro that doesn’t make any sense at all to prove that macros work before the compiler takes a look at any code. The following macro only takes an output of Hi Calvin.
and produces an interesting output:
macro_rules! pure_nonsense { (Hi Calvin.) => { GRITTINGS. MA NAM IS KAHLFIN. HEERYOR LUNBOKS. HOFFA GUT TAY ASKOOL. }; } fn main() { }
But if you hit Run, the code compiles! In fact, there isn’t any code because we haven’t called the macro anywhere.
Now, if we call the macro, that’s when we have a problem:
macro_rules! pure_nonsense { (Hi Calvin.) => { GRITTINGS. MA NAM IS KAHLFIN. HEERYOR LUNBOKS. HOFFA GUT TAY ASKOOL. }; } fn main() { let x = pure_nonsense!(Hi Calvin.); }
The compiler tells us that it has no idea what GRITTINGS
is supposed to be and helpfully lets us know that it’s probably the macro’s fault:
error[E0425]: cannot find value `GRITTINGS` in this scope --> src/main.rs:3:9 | 3 | GRITTINGS. MA NAM IS KAHLFIN. HEERYOR LUNBOKS. HOFFA GUT TAY ➥ASKOOL. | ^^^^^^^^^ not found in this scope ... 8 | let x = pure_nonsense!(Hi Calvin.); | -------------------------- in this macro invocation | = note: this error originates in the macro `pure_nonsense` (in Nightly ➥builds, run with -Z macro-backtrace for more info)
A macro is only similar to a match
statement in appearance. We know that a true match
statement needs to return the same type, so this won’t work:
fn main() { let my_number = 10; match my_number { 10 => println!("You got a ten"), _ => 10, } }
It will complain that you want to return ()
in one case and an i32
in the other:
error[E0308]: `match` arms have incompatible types
--> src\main.rs:5:14
|
3 | / match my_number {
4 | | 10 => println!("You got a ten"),
| | ------------------------- this is found to be of type
➥`()`
5 | | _ => 10,
| | ^^ expected `()`, found integer
6 | | }
| |_____- `match` arms have incompatible types
But as we saw previously, a macro has nothing to do with code compilation, so it is fine with producing a completely different output from a different match arm. So, the following code works:
macro_rules! six_or_print { (6) => { 6 }; () => { println!("You didn't give me 6."); }; } fn main() { let my_number = six_or_print!(6); six_or_print!(); }
This is just fine and prints You didn't give me 6.
. Another way that we can see that a macro is not a match statement is that there is no _
wildcard. We can only give it (6)
or ()
. Anything else will make an error. Let’s give the macro the input six_or_ print!(66)
and see what the error looks like:
error: no rules expected the token `66`
--> src/main.rs:11:35
|
1 | macro_rules! six_or_print {
| ------------------------- when calling this macro
...
11 | let my_number = six_or_print!(66);
| ^^ no rules expected this token in
➥macro call
|
note: while trying to match `6`
--> src/main.rs:2:6
|
2 | (6) => {
| ^
You will see this no rules expected the token
a lot when making your macros.
This is another interesting point: the 6
this macro can take as input isn’t even an i32
; it’s just the number 6—a token. A token doesn’t have to be just ascii or numbers either:
macro_rules! might_print { (THis is strange input 하하はは哈哈 but it still works) => { println!("You guessed the secret message!") }; () => { println!("You didn't guess it"); }; } fn main() { might_print!(THis is strange input 하하はは哈哈 but it still works); might_print!(); }
This macro only responds to two things: ()
and (THis is strange input 하하はは哈哈 but it still works
). Nothing else. But the output is correct Rust code, so the code above compiles, and we get the following output:
You guessed the secret message! You didn't guess it
So, it’s pretty clear that a macro isn’t exactly Rust syntax. However, a macro doesn’t just match on raw tokens and nothing else. It can also do something similar to declaring variables in regular Rust code if you indicate what type of token it can expect to see. For example, you can tell a macro that it will receive an expression, a type name, an identifier, and so on. (We will learn what all of these mean shortly.) Here is a simple example of a macro that expects an expression:
macro_rules! might_print { ($input:expr) => { println!("You gave me: {}", $input); } } fn main() { might_print!(6); }
This will print You gave me: 6
. The $input:expr
part is important. With this, you can give the macro any expression, which can then be used inside the macro code block with any name we choose, which, in this case, we decided to call $input
. In macros, variables (technically, they are called arguments) start with a $
. In this macro, if you give it one expression, it will print it. Let’s try it out some more using Debug
print instead of Display
:
macro_rules! might_print { ($input:expr) => { println!("You gave me: {:?}", $input); } } fn main() { might_print!(()); might_print!(6); might_print!(vec![8, 9, 7, 10]); }
You gave me: () You gave me: 6 You gave me: [8, 9, 7, 10]
Note that we wrote {:?}
, but the macro won’t check to see whether &input
implements Debug
. Fortunately, the compiler will check when we try to compile the code that includes the macro output.
We can see that the macro is parsing as expected if we tell it to expect an expression but give it a statement:
macro_rules! wants_expression { ($input:expr) => { println!("You matched the macro input!"); }; } fn main() { wants_expression!(let x = 9); }
The error output shows us clearly that it looked at the input but didn’t find a match from what it expected to see:
error: no rules expected the token `let` --> src/main.rs:8:23 | 1 | macro_rules! wants_expression { | ----------------------------- when calling this macro ... 8 | wants_expression!(let x = 9); | ^^^ no rules expected this token in macro call note: while trying to match meta-variable `$input:expr` --> src/main.rs:2:6 | 2 | ($input:expr) => { | ^^^^^^^^^^^
But if we now tell it to expect a statement and give it the same input, it will match:
macro_rules! wants_statement {
($input:stmt) => { ①
println!("You matched the macro input!");
};
}
fn main() {
wants_statement!(let x = 9);
}
① We change expr to stmt, instructing the macro to expect a statement.
So what can a macro see besides expr
and stmt
? Here is the full list—give it a read, but don’t worry about memorizing it:
block | expr | ident | item | lifetime | literal | meta | pat | path
➥| stmt | tt | ty | vis.
NOTE The words in the list are officially known as fragment specifiers. But you don’t need to know that to understand and write macros.
This is the complicated part. The documentation explains what each fragment specificer means (http://mng.bz/Rmwj). Let’s go over them quickly:
There is another good site called cheats.rs (https://cheats.rs/#macros-attributes) that explains them and gives examples for each.
However, for most macros, you will probably use expr, ident
, and tt
. tt
means token tree, which sort of means any type of input. Let’s try a simple macro with a simple macro using two of them:
macro_rules! check { ($input1:ident, $input2:expr) => { println!( "Is {:?} equal to {:?}? {:?}", $input1, $input2, $input1 == $input2 ); }; } fn main() { let x = 6; let my_vec = vec![7, 8, 9]; check!(x, 6); check!(my_vec, vec![7, 8, 9]); check!(x, 10); }
This will take one ident
(such as a variable name) and an expression and see whether they are the same. It prints
Is 6 equal to 6? true Is [7, 8, 9] equal to [7, 8, 9]? true Is 6 equal to 10? false
Here’s one macro that takes a tt
and prints it. It uses a macro called stringify!
to make a string first:
macro_rules! print_anything { ($input:tt) => { let output = stringify!($input); println!("{}", output); }; } fn main() { print_anything!(ththdoetd); print_anything!(87575oehq75onth); }
ththdoetd 87575oehq75onth
But it won’t print if we give it something with spaces, commas, etc. The macro will think that we are giving it more than one token or extra information and will get confused.
This is where macros start to get difficult. To give a macro more than one item at a time, we have to use a different syntax. Instead of $input
, it will be $($input1),*
. The *
means “zero or more,” while the comma before the * means that the tokens have to be separated by commas. If you want to match on one or more tokens, use +
instead of *
.
Now our macro looks like this:
macro_rules! print_anything { ($($input1:tt),*) => { let output = stringify!($($input1),*); println!("{}", output); }; } fn main() { print_anything!(ththdoetd, rcofe); print_anything!(); print_anything!(87575oehq75onth, ntohe, 987987o, 097); }
So it takes any token tree separated by commas and uses stringify!
to make it into a String. Then it prints the String:
ththdoetd, rcofe 87575oehq75onth, ntohe, 987987o, 097
If we used +
instead of *
, it would give an error because one time we gave it no input. So *
is a bit more flexible. Also, try changing the comma in ($($input1:tt),*)
to a semicolon to see what happens. The macro will generate an error, but only because it is expecting the tokens we give it to be separated by a semicolon now. So, if we change the way we call this macro by entering semicolons instead of commas, it will compile again:
macro_rules! print_anything { ($($input1:tt);*) => { ① let output = stringify!($($input1),*); println!("{}", output); }; } fn main() { print_anything!(ththdoetd; rcofe); ② print_anything!(); print_anything!(87575oehq75onth; ntohe; 987987o; 097); }
① The only difference between the macro now and before is that it expects tokens to be separated by a semicolon.
② If we change the commas to semicolons here, it will accept our input as before.
In this next example, we will make a macro that writes a simple function for us. First, it will match on a single identifier using $name:ident
, after which it checks for repeating tokens using $($input:tt),+
and then prints them all out:
macro_rules! make_a_function { ($name:ident, $($input:tt),+) => { ① fn $name() { let output = stringify!($($input),+); ② println!("{}", output); } }; } fn main() { make_a_function!(print_it, 5, 5, 6, I); ③ print_it(); make_a_function!(say_its_nice, this, is, really, nice); ④ say_its_nice(); }
① First, you give it one name for the function, and then it checks the rest of the input until there are no tokens left to check.
② It makes everything else into a string.
③ We want a function called print_it() that prints everything else we give it.
④ Same here, but we change the function name.
5, 5, 6, I this, is, really, nice
In chapter 19, we learned that the Rust Playground has an Expand
button that expands macros to show us the actual generated output. Let’s click on that to see what the functions. The relevant part is here:
macro_rules! make_a_function { ($name:ident, $($input:tt),+) => { fn $name() { let output = stringify!($($input),+); println!("{}", output); } }; } fn main() { fn print_it() { ① let output = "5, 5, 6, I"; ② { ::std::io::_print(format_args!("{0}\n", output)); }; ③ } print_it(); ④ fn say_its_nice() { let output = "this, is, really, nice"; { ::std::io::_print(format_args!("{0}\n", output)); }; } say_its_nice(); }
① The macro matched on a single identifier here called print_it, which it uses for the name of the function it generates. As you can see, it looks like any other function.
② Next, it looks at the remaining tokens (separated by commas) and stringifies them on this line.
③ Finally, it prints them out. This part is a bit ugly as it shows the internals of the println! macro.
④ Since the function exists in this scope we can call it here.
Let’s use what we know now to see whether we can understand other macros. Some of the macros we’ve already been using in the standard library are pretty simple to read. Let’s take a look at the write!
macro that we used in chapter 18:
macro_rules! write { ($dst:expr, $($arg:tt)*) => ($dst.write_fmt($crate::format_args!($($arg)*))) }
So, to use it, you enter this:
Everything after that. If the macro had written $arg:tt
, it would have only taken one argument, but because it wrote $($arg:tt)*
, it takes zero, one, or any number.
Then it takes $dst
(which stands for “destination”) and uses a method called write_fmt
on it. Inside that, it uses another macro called format_args!
that takes $($arg)*
, or all the arguments we put in, and passes them on to another macro. The format_args!
macro is used internally quite a bit:
macro_rules! format_args { ($fmt:expr) => {{ /* compiler built-in */ }}; ($fmt:expr, $($args:tt)*) => {{ /* compiler built-in */ }}; }
Unfortunately, this macro uses “compiler magic” to work so we can’t look any deeper than this. The standard library has a lot of macros with this /* compiler built-in */
message. But, in any case, format_args!
is an internal macro that allows us to use {}
to capture arguments and format them.
Now let’s take a look at the todo!
macro. That’s the one you use when you want the program to compile but haven’t written your code yet. It looks like this:
macro_rules! todo { () => (panic!("not yet implemented")); ($($arg:tt)+) => (panic!("not yet implemented: {}", $crate::format_args!($($arg)+))); }
This one has two options: you can enter ()
or a number of token trees (tt
):
If you enter ()
, it just uses panic!
with a message. So you could actually just write panic!("not yet implemented")
instead of todo!
and it would be the same.
If you enter some arguments, it will try to make them into a String. You can see the same format_args!
macro inside again.
Having read the code for todo!
, we can now see that this macro can take the same format we use for the println!
macro. Let’s give that a try:
If you write this, it will work, too:
fn not_done() { let time = 8; let reason = "lack of time"; todo!("Not done yet because of {reason}. Check back in {time} hours"); } fn main() { not_done(); }
thread 'main' panicked at 'not yet implemented: Not done yet because of
➥lack of time. Check back in 8 hours', src/main.rs:4:5
So, even if a macro is complex or obscure, we can at least take a look at the possible inputs it can take to get an idea of how to use it.
One interesting thing about macros is that they can even call themselves! Let’s give this a try. See whether you can guess what the output will be for this macro:
macro_rules! my_macro { () => { println!("Let's print this."); }; ($input:expr) => { my_macro!(); }; ($($input:expr),*) => { my_macro!(); } } fn main() { my_macro!(vec![8, 9, 0]); my_macro!(toheteh); my_macro!(8, 7, 0, 10); my_macro!(); }
This one takes ()
, one expression, or many expressions. But take a look at what happens when it receives an expression: it ignores it and calls itself with my_macro!()
. And when my_macro!
gets the input ()
, it will print a message. So the output for the previous code is Let's print this
, four times.
You can see the same thing in the dbg!
macro, which also calls itself:
macro_rules! dbg { () => { $crate::eprintln!("[{}:{}]", $crate::file!(), $crate::line!()); }; ($val:expr) => { match $val { tmp => { $crate::eprintln!("[{}:{}] {} = {:#?}", $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp); tmp } } }; ($val:expr,) => { $crate::dbg!($val) }; ($($val:expr),+ $(,)?) => { ($($crate::dbg!($val)),+,) }; }
We can try this out ourselves:
fn main() { dbg!(); }
With no particular input, it matches the first arm:
() => { $crate::eprintln!("[{}:{}]", $crate::file!(), $crate::line!()); };
So, it will print the filename and line name with the file!
and line!
macros. On the Playground, it prints [src/main.rs:2]
.
fn main() { dbg!(vec![8, 9, 10]); }
This will match the next arm because it’s one expression:
($val:expr) => {
match $val {
tmp => {
$crate::eprintln!("[{}:{}] {} = {:#?}",
$crate::file!(), $crate::line!(), $crate::stringify!
➥($val), &tmp);
tmp
}
}
};
So, it looks like the macro grabs the expression given to it, prints the filename and line number, stringifies the tokens making up the expression, and then prints out the expression itself. For our input, it will write this:
[src/main.rs:2] vec![8, 9, 10] = [ 8, 9, 10, ]
And for other inputs, we can see that it calls dbg!
on itself, even if you put in an extra comma. We know a trailing comma is allowed because of the the $(,)?
inside.
Hold on, how exactly does $(,)?
mean that there might be a trailing comma? Let’s break it down bit by bit. First is the ?
, which is the third of the three repetition operators that can be used in a macro. We already know two of them, so let’s summarize them together:
A ?
inside a macro is sort of like an Option
in regular Rust code. In this case, the dbg!
macro allows a match to happen when there is a trailing comma but doesn’t do anything with it.
We can practice this with our own macro:
macro_rules! comma_check { () => { println!("Got nothing!"); }; ($input:expr) => { println!("One expression!") }; ($input:expr $(,)?) => { println!("One expression with a comma at the end!") }; ($input:expr $(,)? $(,)?) => { println!("One expression with two commas at the end!") }; ($input:expr $(;)? $(,)?) => { println!("One expression with a semicolon and a comma!") }; } fn main() { comma_check!(); comma_check!(8); comma_check!(8,); comma_check!(8,,); comma_check!(8;,); }
Not too hard to read, is it? Here is the output:
Got nothing! One expression! One expression with a comma at the end! One expression with two commas at the end! One expression with a semicolon and a comma!
Now let’s finish by looking at the matches!
macro, which uses the ?
operator quite a bit. This macro is used somewhat frequently in Rust, but we haven’t seen it yet in this book. Let’s look at the code first to see whether we can figure it out:
macro_rules! matches { ($expression:expr, $pattern:pat $(if $guard:expr)? $(,)?) => { match $expression { $pattern $(if $guard)? => true, _ => false } }; }
It looks like it takes an expression and a pattern and matches the expression against the pattern. After that, it has two optional items, but since they are optional, let’s remove them for a moment to make it really easy to read:
macro_rules! matches { ($expression:expr, $pattern:pat) => { match $expression { $pattern => true, _ => false } }; }
Okay, let’s match a few expressions against a few patterns:
fn main() { println!("{}", matches!(9, 9)); println!("{}", matches!(9, 0..=10)); println!("{}", matches!(9, 100..=1000)); }
Easy! This prints true, true
, and false
.
Now let’s look at the optional items again. The one at the end is $(,)?
, which allows a trailing comma. This code will work and produce the same output:
fn main() { println!("{}", matches!(9, 9,)); println!("{}", matches!(9, 0..=10,)); println!("{}", matches!(9, 100..=1000,)); }
Finally comes the other optional item:
$(if $guard:expr)?
This lets us add an if
clause and an expression. It calls this expression $guard
and uses it as follows:
$pattern $(if $guard)? => true,
const ALLOWS_TRUE: bool = false; fn main() { println!("{}", matches!(9, 9 if ALLOWS_TRUE)); }
This code will output false
even though 9
matches 9
because the guard returns false
.
Hopefully, this has made macros a bit less intimidating. To finish up, let’s look at a small yet more real example of where a macro might come in handy.
Let’s imagine that we have three structs that hold a String
. One should be able to hold only small String
s, the next should be able to hold medium-sized String
s, and the last should be able to hold larger ones. Here they are:
struct SmallStringHolder(String); struct MediumStringHolder(String); struct LargeStringHolder(String);
The best way to make sure that these types take a String
that is small enough is to use the TryFrom
trait. We’ll start with SmallStringHolder
and make sure that it can only accept a String
that is up to five characters in length:
#[derive(Debug)] struct SmallStringHolder(String); #[derive(Debug)] struct MediumStringHolder(String); #[derive(Debug)] struct LargeStringHolder(String); impl TryFrom<&str> for SmallStringHolder { type Error = &'static str; fn try_from(value: &str) -> Result<Self, Self::Error> { if value.chars().count() > 5 { Err("Must be no longer than 5") } else { Ok(Self(value.to_string())) } } } fn main() { println!("{:?}", SmallStringHolder::try_from("Hello")); println!("{:?}", SmallStringHolder::try_from("Hello there")); }
This works well! Here is the output:
Ok(SmallStringHolder("Hello")) Err("Must be no longer than 5")
Now, it’s time to do the same for the others. But hold on, this is going to be a lot of repetitive code. This is where you might begin to feel the temptation to use a macro. In this case, a simple macro is pretty easy. We’ll take a type name and a length and implement the trait:
macro_rules! derive_try_from { ($type:ident, $length:expr) => { impl TryFrom<&str> for $type { type Error = String; ① fn try_from(value: &str) -> Result<Self, Self::Error> { let length = $length; ② if value.chars().count() > length { Err(format!("Must be no longer than {length}")) } else { Ok(Self(value.to_string())) } } } }; }
① We change the error type from &str to String because we want to format the error message now.
② The format! macro won’t recognize {$length} (that’s macro syntax, not regular Rust syntax), so here we declare a variable called length that is equal to $length. Now, we can put this into format!, and it will recognize the length.
Now we are able to implement TryFrom
for all three types without having to repeat ourselves over and over. The code now looks like this:
macro_rules! derive_try_from { ($type:ident, $length:expr) => { impl TryFrom<&str> for $type { type Error = String; fn try_from(value: &str) -> Result<Self, Self::Error> { let length = $length; if value.chars().count() > length { Err(format!("Must be no longer than {length}")) } else { Ok(Self(value.to_string())) } } } }; } #[derive(Debug)] struct SmallStringHolder(String); #[derive(Debug)] struct MediumStringHolder(String); #[derive(Debug)] struct LargeStringHolder(String); derive_try_from!(SmallStringHolder, 5); derive_try_from!(MediumStringHolder, 8); derive_try_from!(LargeStringHolder, 12); fn main() { println!("{:?}", SmallStringHolder::try_from("Hello there")); println!("{:?}", MediumStringHolder::try_from("Hello there")); println!("{:?}", LargeStringHolder::try_from("Hello there")); }
The code works and the output is
Err("Must be no longer than 5") Err("Must be no longer than 8") Ok(LargeStringHolder("Hello there"))
Since we are using a macro, why stop here? We can declare the types themselves inside the macro, too. Now we can shrink the code some more:
macro_rules! make_type {
($type:ident, $length:expr) => {
#[derive(Debug)]
struct $type(String); ①
impl TryFrom<&str> for $type {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let length = $length;
if value.chars().count() > length {
Err(format!("Must be no longer than {length}"))
} else {
Ok(Self(value.to_string()))
}
}
}
};
}
make_type!(SmallStringHolder, 5);
make_type!(MediumStringHolder, 8);
make_type!(LargeStringHolder, 12);
fn main() {
println!("{:?}", SmallStringHolder::try_from("Hello there"));
println!("{:?}", MediumStringHolder::try_from("Hello there"));
println!("{:?}", LargeStringHolder::try_from("Hello there"));
}
① Now, we declare the types themselves inside the macro, so derive_try_from! will both create the types and implement TryFrom for them.
If you are feeling up for a challenge, take a look at chapter 16 again, where we looked at const generics. If Rust didn’t have const generics, how would you use a macro to build an array of various sizes and implement some traits like TryFrom
or Display
for each?
#[derive(Debug)] struct Buffers<T, const N: usize> { array_one: [T; N], array_two: [T; N], } fn main() { let buffer_1 = Buffers { array_one: [0u8; 3], array_two: [0; 3], }; let buffer_2 = Buffers { array_one: [0i32; 4], array_two: [10; 4], }; }
As you can see, macros are pretty complicated! Usually, you only want a macro to automatically do something that a simple function can’t do very well. The best way to learn about macros is to look at other macro examples until you get used to the syntax and try modifying them on your own. Macros are frequently used but rarely written, so very few people can sit down and write a complicated macro that works on the first try. Hopefully, this chapter has gotten you comfortable enough with them that you might want to start trying them on your own.
We have now reached the very last section of the book called “Unfinished Projects,” which starts in the next chapter. Over the next two chapters, we will take a look at six small projects that work on their own but that you will probably want to expand yourself. See you there!
Macros are used a lot but rarely written. Very few people are macro experts, but learning to read them is important.
A macro can take any input, but if you tell the macro the kind of input (an expression, statement, etc.), you can give the input a name and use it in a similar way to a variable.
A lot of the macros in the standard library are built into the compiler, and their details can’t be seen.
Most people turn to macros for the first time to save time and reduce code duplication.