Demystifying Procedural Macros: Unleashing Power in Rust

Murat Aslan
2 min readMay 14, 2024

we strive to write expressive and concise code. Procedural macros offer a powerful tool to achieve this, allowing us to define custom functionality that operates directly at compile time. This article explores procedural macros, with a specific focus on custom derive, empowering you to extend Rust’s capabilities and write elegant code.

Beyond Simple Macros: The Magic of Procedural Macros

Procedural macros differ from traditional macros in Rust. They are full-fledged functions that operate on a stream of tokens representing your code. These functions can analyze the code, generate new code, and ultimately transform your source code at compile time.

There are three main categories of procedural macros:

  • Custom Derive: Modify code based on the derive attribute. (This article's focus)
  • Attribute-like Macros: Invoked as item attributes (e.g., #[foo(...)]).
  • Function-like Macros: Resemble function calls but work with arbitrary tokens.

Custom Derive: Supercharging the derive Attribute

The derive attribute in Rust allows us to automatically generate code based on existing traits. Custom derive macros unlock the power to define new behaviors for this powerful feature.

Benefits of Custom Derive:

  • Reduced Boilerplate: Eliminate the need to write repetitive code for common functionalities.
  • Improved Readability: Express complex logic concisely with custom derive macros.
  • Extensibility: Create new traits with custom derive implementations for specific use cases.

Example: Custom Debug Derive

Imagine a Displayable trait that requires formatting a struct for display. We can create a custom derive macro to implement Display automatically:

// Custom derive macro for Displayable trait
macro_rules! impl_displayable {
($name:ident, $fields:expr) => {
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", format!($fields, self.field1, self.field2))
}
}
};
}

// Struct with Displayable trait
#[derive(impl_displayable)]
struct User {
field1: String,
field2: u32,
}

fn main() {
let user = User {
field1: "Alice".to_string(),
field2: 30,
};
println!("{}", user); // Output: User { field1: "Alice", field2: 30 }
}

In this example, the impl_displayable macro automatically generates the Display implementation based on the provided fields.

Beyond the Basics: Additional Considerations

  • Complexity: Custom derive macros can become intricate. Prioritize clarity and maintainability.
  • Testing: Thoroughly test your custom derive macros to ensure they generate the intended code.
  • Third-Party Macros: Explore existing custom derive macros in popular crates for inspiration.

Conclusion

Procedural macros, particularly custom derive, provide immense power for crafting elegant and expressive Rust code. By leveraging these techniques, you can extend the language, reduce boilerplate, and write more maintainable codebases. However, remember to use them judiciously, prioritizing readability and clear intent. Let’s embrace procedural macros to push the boundaries of Rust development!

--

--