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
derivesMyTrait
, it automatically gets themy_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 namedMyGeneratedStruct
with two fields:field1
andfield2
. - 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.