Common Use Cases for Procedural Macros in Rust

Procedural macros in Rust are a powerful feature that allows developers to write code that generates other code at compile time. They enable advanced metaprogramming capabilities, making it possible to reduce boilerplate, implement custom derive functionality, and create domain-specific languages (DSLs). This guide will explore some common use cases for procedural macros, along with examples to illustrate their usage.

1. Custom Derive Macros

One of the most common use cases for procedural macros is to create custom derive macros. These macros allow you to automatically implement traits for your types based on their fields.

Example of a Custom Derive Macro


// In your procedural macro crate
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;

let expanded = quote! {
impl MyTrait for #name {
fn my_method(&self) {
println!("This is a method for {}", stringify!(#name));
}
}
};

TokenStream::from(expanded)
}

// In your main crate
#[derive(MyTrait)]
struct MyStruct;

fn main() {
let my_struct = MyStruct;
my_struct.my_method(); // This will print: This is a method for MyStruct
}

Explanation of the Example

  • In this example, we define a procedural macro named MyTrait that generates an implementation of a trait for any struct that derives it.
  • We use the quote crate to generate the implementation code dynamically based on the input type.
  • When MyStruct derives MyTrait, it automatically gets the my_method method implemented, which prints the name of the struct.

2. Attribute-like Macros

Procedural macros can also be used as attribute-like macros, allowing you to annotate items with additional behavior or metadata.

Example of an Attribute-like Macro


// In your procedural macro crate
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn log(_attr: TokenStream, item: TokenStream) -> TokenStream {
let output = quote! {
fn #item {
println!("Function called: {}", stringify!(#item));
#item
}
};

TokenStream::from(output)
}

// In your main crate
#[log]
fn my_function() {
println!("Inside my_function");
}

fn main() {
my_function(); // This will log the function call
}

Explanation of the Example

  • In this example, we define an attribute-like macro named log that wraps a function with logging behavior.
  • When the macro is applied to my_function, it generates code that prints a log message every time the function is called.
  • This allows you to easily add logging to multiple functions without repeating code.

3. Domain-Specific Languages (DSLs)

Procedural macros can be used to create DSLs, allowing you to define custom syntax for specific tasks or domains. This can make your code more expressive and easier to read.

Example of a Simple DSL


// In your procedural macro crate
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro]
pub fn html(input: TokenStream) -> TokenStream {
let input = input.to_string();
let output = format!("<div>{}</div>", input);

TokenStream::from(quote! {
#output
})
}

// In your main crate
fn main() {
let content = html!("Hello, world!"); // This will generate <div>Hello, world!</div>
println!("{}", content);
}

Explanation of the Example

  • In this example, we define a procedural macro named html that generates HTML markup from a string input.
  • The macro takes a string and wraps it in a <div> tag, allowing you to write HTML-like syntax in your Rust code.
  • When you call html!("Hello, world!");, it generates the output <div>Hello, world!</div>, demonstrating how procedural macros can create more expressive code.

4. Code Generation and Boilerplate Reduction

Procedural macros can help reduce boilerplate code by generating repetitive code patterns automatically. This is particularly useful in large projects where similar structures are common.

Example of Code Generation


// In your procedural macro crate
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro]
pub fn generate_structs(input: TokenStream) -> TokenStream {
let struct_name = input.to_string();
let output = quote! {
struct #struct_name {
field1: i32,
field2: String,
}
};

TokenStream::from(output)
}

// In your main crate
generate_structs!(MyGeneratedStruct);

fn main() {
let instance = MyGeneratedStruct {
field1: 10,
field2: String::from("Hello"),
};
println!("Field1: {}, Field2: {}", instance.field1, instance.field2);
}

Explanation of the Example

  • In this example, we define a procedural macro named generate_structs that generates a struct with predefined fields.
  • When you call generate_structs!(MyGeneratedStruct);, it creates a struct named MyGeneratedStruct with two fields: field1 and field2.
  • This reduces the need to manually define similar structs, streamlining the codebase.

5. Conclusion

Procedural macros in Rust provide powerful capabilities for metaprogramming, enabling developers to create custom derive macros, attribute-like macros, DSLs, and reduce boilerplate code. By leveraging procedural macros, you can enhance code readability, maintainability, and expressiveness in your Rust projects.