Introduction : pourquoi Rust sait-il faire de la magie avec le code ?
\n\nAvez-vous déjà écrit du code en Rust en ayant l'impression de répéter les mêmes constructions pour une dizaine de structures ? Ou auriez-vous aimé générer automatiquement l'implémentation d'un trait sans copier le code manuellement ? Bienvenue dans le monde de la métaprogrammation en Rust — des techniques qui permettent d'écrire du code qui écrit du code.
\n\nRust ne dispose pas d'une réflexion complète à l'exécution (comme Java ou Python), mais il propose quelque chose de plus puissant et plus sûr — les macros. Elles fonctionnent au moment de la compilation, en analysant et en transformant l'arbre syntaxique abstrait (AST). Cela offre des possibilités incroyables pour optimiser et réduire le code boilerplate sans perte de performance.
\n\nDans cet article, nous allons explorer les techniques avancées de métaprogrammation : des classiques macro_rules! aux macros procédurales complètes. Vous apprendrez à créer vos propres attributs derive et à écrire des macros d'attribut qui modifient la sémantique du code. C'est parti !
Macros déclaratives : macro_rules! — ce n'est pas qu'un "remplacement de texte"
\n\nBeaucoup pensent que macro_rules! est un préprocesseur textuel primitif. En réalité, c'est un puissant système de correspondance de motifs, fonctionnant au niveau des tokens. Il permet de créer des DSL (langages dédiés) directement dans Rust.
Exemple avancé : création d'un builder avec une macro
\n\nImaginez que vous deviez créer un "constructeur" (builder) pour une structure. Au lieu d'écrire des dizaines de méthodes, vous pouvez écrire une seule 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!("Le champ {} est obligatoire", 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 prête : {}:{}", config.host.unwrap(), config.port.unwrap());\n}\n\n\nCette macro ne se contente pas de générer les champs, elle ajoute également une validation des champs obligatoires. Notez l'utilisation de stringify!($field) — il convertit l'identifiant en chaîne de caractères au moment de la compilation. C'est une technique typique de métaprogrammation avancée en Rust.
Répétitions et itérations : $(...)* en action
\n\nLa syntaxe de répétition permet de traiter des listes d'arguments de n'importe quelle longueur. En combinant ident, ty, expr et d'autres "fragments", vous pouvez créer des motifs complexes. Par exemple, une macro pour créer un enum avec une implémentation automatique de Display :
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_actif",\n Inactive => "status_inactif",\n Pending => "s