DEV Community

Cover image for Procedural Macros in Rust
kaioh33
kaioh33

Posted on

 

Procedural Macros in Rust

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:

  1. Custom Derive
  2. Function-like
  3. Attribute-like

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

print_name_macro/src/lib.rs

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.

TokenTree 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

DeriveInput Struct

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 into() method.

print_name_macro_derive/src/lib.rs
Image description

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.

print_macro/cargo.toml
Image description

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.

print_macro/main.rs
print_macro/main.rs

If you cargo run you should get this output
Hello, My name is Example
Hello, My name is ExampleEnum

Top comments (0)

Regex for lazy developers

regex for lazy devs

You know who you are. Sorry for the callout 😆