This is sort of a beginner post explaining procedural macros and also illustrating how to write them.
First we would define what a macro is and what functions they serve.Macros are a way to write code that basically writes code, a term referred to as meta-programming. There are different reasons why you would need such functionality. Such as to reduce boilerplate code, that is to reduce the amount code you need to write and maintain.
For example you might have a type that you want to implement different traits on, and to do so you will have to write multiple
impl blocks to implement the trait functionality
Instead of writing numerous
impl blocks to implement the functionality you can just annotate your type with the traits you want just like
#[derive(Copy, Clone)] and it automatically implements the traits for you. Such is the power of macros!
There are two types of macros in rust: Declarative and Procedural macros.
Procedural macros consist of 3 different types which are:
- Custom Derive
The focus of this tutorial is on custom derive macros specifically, especially since I have come across a lot of them in my rust journey such as the Copy, Clone, Debug traits to name a few.
If you want to write your own custom derive macro, it is as simple as follows:
First you create a lib crate with the cargo command
cargo new print_name_macro --lib. When you do this you clear the default code written in src/lib.rs. Then define the trait
Next is to define the actual procedural macro. Rust conventions for doing so is to first create the crate as we have just done above and then the procedural crate needs to be in the project crate with derive appended to it like print_name_macro_derive.
To create the procedural macro crate you change directory to the already created
print_name_macro crate and then use the command
cargo new print_name_macro_derive --lib.
In this library crate you would have to make some changes to its cargo.toml for it to be able to offer macro functionality. This is done by adding
[lib] proc-macro = true and then the syn and quote dependencies. These dependencies are very important as they allow us to generate an abstract syntax tree that can be further manipulated to then write other code.
Next we define the actual procedural macro by first bringing the necessary dependencies into scope. The proc_macro crate is the compiler’s API that allows us to read and manipulate Rust code from our code. We then annotate the
fn print_name_macro_derive with
#[proc_macro_derive(PrintNameMacro)]so that when someone specifies
#[derive(PrintNameMacro)] on their type they can implement it which also means having access to the function.
Before I explain the body of the function, I will try to explain what a TokenStream is. A TokenStream is a basically a sequence of TokenTrees, and a TokenTree is an enum.
Back to body of the function, we call the
parse_macro_input! macro and convert the TokenStream struct to a DeriveInput struct which looks like what we have below
Then we access the identifier which is also the name of the type and then call the quote macro to replace the name with whichever name of the type that the users of our macro supply. The quote macro outputs a TokenStream from the proc_macro2 crate rather than the proc_macro crate that we have added in this example. Therefore we store the quote macro in the gen variable and then convert it back to the TokenStream from the proc_macro with the
To test our macro functionality we can create a new binary with
cargo new print_name and then first ensure our macros are added as dependencies in the cargo.toml like so.
Then in the main.rs file we import our crates and then annotate our types which are a struct named Example and enum named ExampleEnum with
#[derive(PrintNameMacro)] to access our macro functionality, which basically just prints out the name of the types.
If you cargo run you should get this output
Hello, My name is Example
Hello, My name is ExampleEnum
Top comments (0)