Custom Derive Macros in Rust: A Practical Guide 🎯

beginner
6 min

Custom Derive Macros in Rust: A Practical Guide 🎯

Welcome to our in-depth guide on Custom Derive Macros in Rust! By the end of this tutorial, you'll be able to create your own macro derivation traits and simplify your Rust code significantly.

What are Macros? 📝

Macros are a powerful feature in Rust that allows you to define code generation rules to automate repetitive tasks. They can help in making the code more concise and efficient. There are two types of macros: macro_rules and proc_macro. In this lesson, we'll focus on proc_macro and proc_macro2 which are used to create derive macros.

What are Derive Macros? 💡

Derive macros are macros that automatically generate code for a given data type. For example, the derive attribute allows you to generate implementation for Debug, Copy, Clone, and more without writing any code.

Creating a Custom Derive Macro ✅

Let's create a simple custom derive macro that generates serde serialization code for our structs. First, let's create a new library named custom_derive.

bash
cargo new custom_derive --lib

Now, open the src/lib.rs file and let's start creating our custom derive macro.

Step 1: Creating a Proc Macro 📝

First, we'll create a procedural macro, which will be used to generate code during the compile time.

rust
use proc_macro::TokenStream; use syn::parse::{Parse, ParseStream}; use syn::Ident; use syn::Token; #[derive(Serialize)] struct Example { name: String, age: u32, } #[proc_macro] pub fn serialize(input: TokenStream) -> TokenStream { // Our macro implementation will go here unimplemented!(); }

In the above code, we've defined a serialize macro that takes a TokenStream as input and returns a TokenStream. We also have a sample Example struct that we'll use later for demonstration.

Step 2: Parsing the Input 📝

Next, we'll parse the input TokenStream to extract the struct definition and its fields. We'll use syn crate for this purpose.

rust
use syn::parse::{Parse, ParseStream}; use syn::{Data, DataStruct, Field, Ident}; fn parse_struct(input: ParseStream) -> syn::Result<DataStruct> { let struct_ident = input.current(); input.call(DataStruct::ident(&struct_ident.ident()))?; Ok(struct_ident.into()) }

In the above function, we parse the input TokenStream and extract the struct definition.

Step 3: Generating the Output 💡

Now, we'll generate the serialization code for the struct and its fields. We'll use quote crate to generate the code.

rust
use quote::quote; use syn::spanned::Spanned; use syn::{DataStruct, Field, Fields, FieldsNamed, Ident}; fn serialize_fields(fields: &FieldsNamed) -> TokenStream { let mut field_stream = Vec::new(); for field in &fields.named { let ident = &field.ident; let ty = &field.ty; field_stream.push(quote! { #ident: #ty, }); } field_stream.into() }

In the above function, we generate the serialization code for the struct fields.

Step 4: Implementing the Macro 🎯

Now, we'll implement our serialize macro using the functions we've created.

rust
#[proc_macro] pub fn serialize(input: TokenStream) -> TokenStream { let ast = syn::parse_macro_input!(input as syn::MacroInput); let parsed_struct = parse_struct(&ast.parse_stream()).unwrap(); let field_stream = serialize_fields(&parsed_struct.fields); quote! { impl serde::Serialize for #parsed_struct { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { serializer.serialize_struct("Example", 2)?; serializer.serialize_field("name")?; serializer.serialize_value(&self.name)?; serializer.serialize_field("age")?; serializer.serialize_value(&self.age)?; serializer.serialize_struct_end() } } #( #field_stream )* } .into() }

In the above implementation, we generate the serialization code for the struct and its fields. We also include the generated code for the struct fields.

Step 5: Using the Custom Derive Macro 💡

Finally, we can use our custom derive macro in our project.

rust
#[macro_use] extern crate custom_derive; #[derive(Serialize)] struct Example { name: String, age: u32, } fn main() { let example = Example { name: String::from("John Doe"), age: 30, }; // Our serialized data }

In the above code, we use our custom derive macro #[derive(Serialize)] to generate the serialization code for the Example struct.

Wrapping Up 🎯

You've now learned how to create a custom derive macro in Rust! By using derive macros, you can significantly simplify your code and make it more concise.

Quick Quiz
Question 1 of 1

What are derive macros used for in Rust?

We hope you enjoyed this tutorial and found it helpful. Stay tuned for more advanced Rust topics! 😊