сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Экосистема Rust

Макросы в Rust

Макросы

Про макросы спрашивают дважды: сначала чем декларативные отличаются от процедурных, потом - когда их вообще стоит писать. Второй вопрос важнее, потому что чаще ошибаются именно в выборе инструмента.

Стержень: macro_rules! сопоставляет образцы, процедурный макрос это программа над токенами; и то и другое разворачивается при компиляции.

// Формулировки: «чем macro_rules отличается от proc-macro?», «что такое гигиена?», «когда макрос уместен?»

Два вида и гигиена

macro_rules! - набор правил «такой образец превращается в такой код». Он декларативный, живёт в обычном крейте и хорош для коротких удобств вроде vec!. Процедурный макрос - настоящая программа, получающая поток токенов и возвращающая новый; так работают derive, атрибуты вроде tokio::main и функциональные макросы. Живёт он в отдельном крейте и заметно дороже по времени сборки.

Гигиена означает, что имена, введённые внутри макроса, не конфликтуют с именами места вызова: объявленный там let x не затрёт ваш x. В C-препроцессоре это классический источник багов, здесь его нет. Гигиена частичная: типы и функции резолвятся в контексте вызова, поэтому внутри макросов пишут полные пути вроде ::std::vec::Vec.

macro_rules!
декларативный макрос по образцам
гигиена
изоляция имён макроса от имён места вызова

Когда макрос действительно нужен

Оправданных случаев немного: переменное число аргументов (vec!, println!), генерация кода по структуре типа (derive), проверки на этапе компиляции - например, разбор строки формата, где лишний аргумент становится ошибкой сборки.

Всё остальное лучше выражать функцией или дженериком. Макрос хуже читается, ломает подсказки IDE, усложняет диагностику ошибок и удлиняет сборку. И он ничего не даёт по скорости: обычные вызовы и так инлайнятся.

// Когда ошибка приходит из сгенерированного кода и непонятна, помогает cargo expand - он показывает, во что развернулся макрос.

proc-macro
процедурный макрос: программа над токенами
cargo expand
показ раскрытого кода макросов

Как отвечать: «Почему println! - макрос, а не функция?»

По двум причинам. Функция принимает фиксированный набор аргументов известных типов, а println! берёт сколько угодно значений разных типов. И главное - он проверяет строку формата на этапе компиляции: лишний аргумент, пропущенный плейсхолдер или тип без Display становятся ошибкой сборки, а не сюрпризом в рантайме. Обычной функцией такое не выразить. По той же причине макрос у vec!: переменный список элементов плюс форма с повтором.

Ты называешь не только переменную арность, но и проверку формата при компиляции - вторую половину ответа обычно забывают.

На чём валятся

  • Пишут макрос там, где хватило бы функции или дженерика.
  • Считают, что макрос ускоряет код, убирая накладные расходы вызова.
  • Забывают полные пути внутри макроса и ловят ошибку в чужом модуле.
  • Не знают, что процедурные макросы требуют отдельного крейта.
  • Разбираются в ошибке сгенерированного кода без cargo expand.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 11, остальные разбираются в тренажёре.

  1. #rs_macros1 / 5
    Когда макрос — правильный инструмент, а когда лучше обойтись без него?
    A)Макрос — когда важна скорость: он убирает накладные расходы вызова
    B)Макрос — для переменного синтаксиса и генерации по типу
    C)Макрос — когда нужно обойти ограничения системы типов
    D)Макрос — когда код повторяется больше трёх раз, независимо от вида повтора
    показать ответ и разбор
    +B)Макрос — для переменного синтаксиса и генерации по типу

    // разбор: Оправданные случаи: переменное число аргументов (vec!, println!), генерация impl по структуре (derive), встраивание проверок на этапе компиляции. Всё, что выражается функцией или дженериком, лучше писать ими: макрос хуже читается, ломает подсказки IDE, усложняет диагностику и удлиняет сборку. Скорости он сам по себе не добавляет — обычные вызовы и так инлайнятся.

  2. #rs_macros2 / 5
    Почему println! и vec! — макросы, а не функции?
    A)Функции не умеют печатать в стандартный вывод без unsafe
    B)Так быстрее: макрос разворачивается в прямой системный вызов
    C)Им нужно переменное число аргументов и проверка формата
    D)Функции не могут возвращать значения разных типов
    показать ответ и разбор
    +C)Им нужно переменное число аргументов и проверка формата

    // разбор: Функция в Rust принимает фиксированный набор аргументов известных типов, а println! берёт сколько угодно и проверяет соответствие строке формата ещё при компиляции: лишний аргумент или неверный тип — ошибка сборки, а не сюрприз в рантайме. vec! так же принимает произвольный список элементов и форму с повтором.

  3. #rs_macros3 / 5
    Что означает запись $($x:expr),* в macro_rules!?
    A)Ровно один аргумент-выражение, который можно использовать многократно в теле
    B)Список типов, по которому макрос сгенерирует по реализации на каждый тип
    C)Необязательный аргумент, который подставляется только при его наличии
    D)Повторение: ноль или больше выражений, разделённых запятыми
    показать ответ и разбор
    +D)Повторение: ноль или больше выражений, разделённых запятыми

    // разбор: Конструкция описывает повторяющуюся группу: $x — метапеременная с типом фрагмента expr, запятая — разделитель, звёздочка — ноль или больше повторов. В теле макроса ту же группу разворачивают через $(...)*. Плюс к этому обычно добавляют $(,)? — необязательную хвостовую запятую, чтобы vec![1, 2, 3,] тоже собирался.

  4. #rs_macros4 / 5
    Какие три вида процедурных макросов бывают?
    A)Отладочный, релизный и универсальный — по профилю сборки крейта
    B)Внутренний, экспортируемый и переэкспортируемый — по области видимости
    C)derive, атрибутный и функциональный (вызываемый как макрос с восклицательным знаком)
    D)Синтаксический, семантический и оптимизирующий — по стадии компиляции крейта
    показать ответ и разбор
    +C)derive, атрибутный и функциональный (вызываемый как макрос с восклицательным знаком)

    // разбор: derive добавляет реализации по объявлению типа (#[derive(Serialize)]), атрибутный оборачивает и переписывает помеченный элемент (#[tokio::main], #[get("/")]), функциональный вызывается как макрос и получает произвольный поток токенов (sqlx::query!). Все три живут в отдельном крейте с типом proc-macro и работают с TokenStream на этапе компиляции.

  5. #rs_macros5 / 5
    Проект собирается медленно, и профиль сборки показывает много времени в proc-macro крейтах. Почему так выходит?
    A)Процедурные макросы выполняются в рантайме программы и замедляют её запуск
    B)Они компилируются под хост-платформу и запускаются на каждый обрабатываемый элемент, порождая много кода
    C)Они пересобираются при каждом запуске cargo, потому что их результат не кэшируется
    D)Они блокируют параллельную сборку крейтов, выстраивая их в одну общую очередь
    показать ответ и разбор
    +B)Они компилируются под хост-платформу и запускаются на каждый обрабатываемый элемент, порождая много кода

    // разбор: Макрос — это программа, которую нужно собрать (вместе с syn и quote), а потом выполнить на каждом помеченном элементе; результат её работы — исходный код, который тоже надо разобрать и скомпилировать. Отсюда двойная цена: сборка самого макро-крейта и раздувание кода на выходе. Лечится точечно — убрать derive там, где он не нужен, или заменить генерацию на обычный код.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.