Macro Repetitions in Rust
Rust macro is also useful when we need to generate repetitive code. We can define a macro to accept arguments and repeat the generated code based on those arguments.
The
macro_rules!
macro supports repetition using the $(...)*
syntax. The ...
inside the parentheses can be any valid Rust expression or a pattern.Here's an example that demonstrates macro repetition:
Output
Here, the macro
repeat_print
takes a single argument, ($($x:expr),*)
, which is a repeating pattern.The pattern consists of zero or more expressions, separated by commas, that are matched by the macro. The star (
*
) symbol at the end will repeatedly match against the pattern inside $()
.The code inside the curly braces
println!("{}", $x);
, is repeated zero or more times, once for each expression in the list of arguments as it is wrapped around $(...)*
in the body of the macro definition. The $x
in the code refers to the matched expressions.Each iteration of the generated code will print a different expression. Now, when we call
repeat_print!(1, 2, 3);
the macro will generate this code:Thus, this macro
repeat_print!
can print multiple expressions in a concise and convenient manner, without having to write out the println!
macro every time.Procedural macros
Types of Procedural Macros
Derive macros
Derive macros are, generally speaking, applied to data types in Rust. They are a way to extend the type declaration to also automatically "derive" functionality for it.
You can use them to generate "derived" types from a type, or as a way to implement methods on the target data type automatically. This should make sense once you look at the following example below.
There's a standard way of debug-printing each type of data structure in Rust that it uses for its internal types. The
Debug
macro allows you to automatically implement the Debug
trait for your custom types, while following the same rules and style guide as the implementation for internal data types.The
Debug
derive macro will result in the following code (presentational, not exact):During actual compilation, the same code would give the following as the result:
Notice how the original type declaration is preserved in the output code. This is one of the main differences between derive macros vs others. Derive macros preserve the input type without modifications. They only add additional code to the output. On the other hand, all the other macros do not behave the same way. They only preserve the target when the output for macro itself includes the target as well.
Functional macros
Functional macros are macros disguised as functions. These are the least restrictive type of procedural macros, as they can be used literally anywhere, as long as they output code that's valid in the context that they're used in.
Attribute macros(we used in embassy)
Attribute macros define new outer attributes which can be attached to items, including items in
extern
blocks, inherent and trait implementations, and trait definitions.Attribute macros are defined by a public function with the
proc_macro_attribute
attribute that has a signature of (TokenStream, TokenStream) -> TokenStream
. The first TokenStream
is the delimited token tree following the attribute's name, not including the outer delimiters. If the attribute is written as a bare attribute name, the attribute TokenStream
is empty. The second TokenStream
is the rest of the item including other attributes on the item. The returned TokenStream
replaces the item with an arbitrary number of items.For example, this attribute macro takes the input stream and returns it as is, effectively being the no-op of attributes.
This following example shows the stringified
TokenStream
s that the attribute macros see. The output will show in the output of the compiler. The output is shown in the comments after the function prefixed with "out:".