В Rust макросы позволяют генерировать код на этапе компиляции, обеспечивая мощные инструменты для метапрограммирования, уменьшения boilerplate и реализации DSL. Главные виды макросов:
macro_rules!.#[derive], атрибутные (#[some_macro]) и function-like (custom_macro!()).Declarative проще в использовании для шаблонного кода, procedural дают больше контроля над синтаксисом и анализом токенов.
Пример declarative макроса:
macro_rules! vec_of_strings { ($($x:expr),*) => { { let mut v = Vec::new(); $(v.push($x.to_string());)* v } }; } let v = vec_of_strings!("a", "b"); // => Vec<String>
Процедурный макрос (пример derive):
#[derive(Debug, Clone)] struct MyStruct; // сам derive Debug реализован процедурным макросом в std.
Могут ли макросы Rust генерировать синтаксически некорректный код или код с ошибками времени выполнения?
Ответ: Да, макросы не проверяют корректность развёртки на момент написания; ошибки могут проявиться только после подстановки макроса компилятором. Декларативные макросы могут привести к неочевидным синтаксическим ошибкам. Процедурные макросы могут генерировать некорректный или уязвимый код, поэтому критически важно тщательно тестировать их работу.
Пример:
macro_rules! make_error { () => { let x = ; // синтаксическая ошибка возникнет при использовании макроса } }
История
В большом проекте для редукции boilerplate использовали macro_rules!, не покрыв все случаи паттернов. Пользователь случайно передал в макрос неподдерживаемое выражение, что привело к непонятной ошибке компиляции, причину которой было трудно отследить.
История
При переносе процедурных макросов между crate возникли проблемы несовместимости версий TokenStream API, из-за чего IDE зависала, а ошибка проявлялась только при no_std-сборках.
История
При написании DSL для конфигов с procedural macro был реализован небезопасный разбор входных токенов (без валидации типов), из-за чего в рантайме возникали странные баги, уязвимости и невозможность корректно деплоить часть новой функциональности.