Introduction: Why Can Rust Conjure with Code?
\n\nHave you ever written in Rust and felt like you were repeating the same constructs for a dozen structs? Or wanted to automatically generate a trait implementation without copying code manually? Welcome to the world of metaprogramming in Rust — techniques that allow you to write code that writes code.
\n\nRust does not have full runtime reflection (like Java or Python), but it offers something more powerful and safe — macros. They work at compile time, analyzing and transforming the abstract syntax tree (AST). This provides incredible opportunities for optimization and reducing boilerplate code without sacrificing performance.
\n\nIn this article, we will break down advanced metaprogramming techniques: from classic macro_rules! to full-fledged procedural macros. You will learn how to create your own derive attributes and write attribute macros that change the semantics of code. Let's go!
Declarative Macros: macro_rules! Is Not Just "Text Replacement"
\n\nMany people think that macro_rules! is a primitive text preprocessor. In fact, it is a powerful pattern-matching system operating at the token level. It allows you to create DSLs (domain-specific languages) right inside Rust.
Advanced Example: Creating a Builder with a Macro
\n\nImagine you need to create a "builder" for a struct. Instead of writing dozens of methods, you can write one macro:
\n\nmacro_rules! builder {\n ($struct_name:ident, { $($field:ident: $field_type:ty),* $(,)? }) => {\n struct $struct_name {\n $($field: Option<$field_type>),*\n }\n\n impl $struct_name {\n $(\n pub fn $field(mut self, value: $field_type) -> Self {\n self.$field = Some(value);\n self\n }\n )*\n\n pub fn build(self) -> Result<Self, String> {\n $(\n if self.$field.is_none() {\n return Err(format!("Field {} is required", stringify!($field)));\n }\n )*\n Ok(self)\n }\n }\n };\n}\n\nbuilder!(Config, {\n host: String,\n port: u16,\n timeout: u64,\n});\n\nfn main() {\n let config = Config {\n host: None,\n port: None,\n timeout: None,\n }\n .host("localhost".to_string())\n .port(8080)\n .build()\n .unwrap();\n println!("Config ready: {}:{}", config.host.unwrap(), config.port.unwrap());\n}\n\n\nThis macro not only generates fields but also adds validation for required fields. Note the stringify!($field) — it converts the identifier into a string at compile time. This is a typical technique of advanced metaprogramming in Rust.
Repetitions and Iterations: $(...)* in Action
\n\nThe repetition syntax allows you to process argument lists of any length. By combining ident, ty, expr, and other "fragments", you can create complex patterns. For example, a macro for creating an enum with automatic Display implementation:
macro_rules! display_enum {\n ($enum_name:ident, { $($variant:ident => $desc:expr),* $(,)? }) => {\n enum $enum_name {\n $($variant),*\n }\n\n impl std::fmt::Display for $enum_name {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n $(Self::$variant => write!(f, "{}", $desc)),*\n }\n }\n }\n };\n}\n\ndisplay_enum!(Status, {\n Active => "status_active",\n Inactive => "status_inactive",\n Pending => "s