ПрограммированиеRust приложение/библиотека архитектор

Что из себя представляют макросы в Rust? Виды макросов, отличие procedural и declarative, когда лучше выбирать один из них и какие опасности могут возникать при их использовании?

Проходите собеседования с ИИ помощником Hintsage

Ответ

В Rust макросы позволяют генерировать код на этапе компиляции, обеспечивая мощные инструменты для метапрограммирования, уменьшения boilerplate и реализации DSL. Главные виды макросов:

  • Declarative macros (макро-правила, macro_rules!): определяются с помощью match-подобного синтаксиса, работают по принципу шаблон->замена. Самый типичный вид — macro_rules!.
  • Procedural macros: объявляются как внешние функции в crate, получают AST (TokenStream) и возвращают модифицированный код. Поделены на #[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 был реализован небезопасный разбор входных токенов (без валидации типов), из-за чего в рантайме возникали странные баги, уязвимости и невозможность корректно деплоить часть новой функциональности.