Макросы Rust

Материал из llmdocs — справочник разработчика
Портал: Go и Rust
Макросы Rust
ЯзыкRust
Видыдекларативные (macro_rules!), процедурные (derive, атрибутные, function-like)
Работают спотоком токенов, а не текстом
Гигиеналокальные имена макроса не конфликтуют с кодом вызова
Популярные крейтыsyn, quote, proc-macro2
Просмотр раскрытияcargo expand

Макросы Rust — механизм метапрограммирования, который генерирует код во время компиляции. В отличие от макросов препроцессора C, работающих с текстом, макросы Rust оперируют синтаксическими токенами, проверяются компилятором и соблюдают гигиену — имена, объявленные внутри макроса, не сталкиваются с именами в месте вызова. Макрос узнаётся по восклицательному знаку (println!, vec!) или по атрибуту (#[derive(Debug)]).[1]

Зачем макросы

Функции Rust имеют фиксированное число аргументов определённых типов. Макросы нужны там, где этого недостаточно:

  • переменное число аргументов — println!("{} + {} = {}", a, b, a + b), vec![1, 2, 3];
  • проверка на этапе компиляции — format! проверяет, что строка формата совпадает с аргументами;
  • генерация повторяющегося кода — реализации трейтов для многих типов;
  • встроенные предметные языки (DSL) — SQL-запросы, HTML-шаблоны, маршруты;
  • автоматическая реализация трейтов по структуре типа (derive).

Декларативные макросы

Макросы «по образцу» объявляются через macro_rules! и похожи на match: каждое правило сопоставляет входные токены с шаблоном и подставляет результат.[2]

macro_rules! hashmap {
    ($($key:expr => $value:expr),* $(,)?) => {{
        let mut m = ::std::collections::HashMap::new();
        $( m.insert($key, $value); )*
        m
    }};
}

let ages = hashmap! { "alice" => 31, "bob" => 25 };

Элементы шаблона:

Фрагмент Что совпадает
$x:expr выражение
$t:ty тип
$i:ident идентификатор
$p:pat образец
$b:block, $s:stmt блок, оператор
$tt:tt любое дерево токенов
$( ... ),* повторение через запятую, 0 и более раз
$( ... );+ повторение через ;, 1 и более раз

Декларативные макросы можно объявить и использовать прямо в одном файле; экспортируют их атрибутом #[macro_export]. Сложные трюки вроде рекурсии по токенам описаны в «The Little Book of Rust Macros».[4]

Процедурные макросы

Процедурный макрос — функция на Rust, которая получает поток токенов (TokenStream) и возвращает новый. Такие макросы размещаются в отдельном крейте с proc-macro = true в Cargo.toml и выполняются компилятором при сборке.[3] Три вида:

1. Derive-макросы добавляют реализацию трейта к структуре или перечислению:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct User {
    id: u64,
    #[serde(rename = "userName")]
    name: String,
}

Debug, Clone, PartialEq встроены в компилятор, а Serialize и Deserialize предоставляет крейт Serde, который генерирует код преобразования в JSON и десятки других форматов без рефлексии во время выполнения.[6]

2. Атрибутные макросы преобразуют элемент, к которому применены:

#[tokio::main]
async fn main() { /* ... */ }

#[get("/users/{id}")]       // actix-web
async fn user(path: web::Path<u64>) -> impl Responder { /* ... */ }

#[tokio::main] превращает асинхронный main в обычный, создающий рантайм (см. async в Rust и Tokio).

3. Функционально-подобные макросы вызываются как name!(...), но, в отличие от macro_rules!, могут выполнять произвольный код при компиляции. Пример — sqlx::query!, который подключается к базе во время сборки и проверяет SQL-запрос и типы столбцов.

Процедурные макросы почти всегда пишут с помощью крейтов syn (разбор токенов в синтаксическое дерево), quote (генерация токенов из шаблона) и proc-macro2:[5]

#[proc_macro_derive(Hello)]
pub fn derive_hello(input: TokenStream) -> TokenStream {
    let ast = syn::parse_macro_input!(input as syn::DeriveInput);
    let name = &ast.ident;
    quote::quote! {
        impl #name {
            pub fn hello() { println!("Привет от {}", stringify!(#name)); }
        }
    }.into()
}

Отладка

Увидеть, во что раскрывается макрос, позволяет cargo expand (обёртка над флагом nightly-компилятора):[7]

cargo install cargo-expand
cargo expand --lib models::user

Ошибки в процедурных макросах лучше выдавать через syn::Error с указанием места в исходнике — тогда компилятор подсветит нужный фрагмент кода пользователя, а не внутренности макроса.

Недостатки

  • Время компиляции — процедурные макросы и syn заметно замедляют сборку, особенно чистую.
  • Сложность чтения и сопровождения — макрос скрывает сгенерированный код; IDE (rust-analyzer) поддерживает раскрытие, но не всегда идеально.
  • Сообщения об ошибках внутри сложных macro_rules! бывают малопонятными.
  • Процедурные макросы выполняют произвольный код при сборке, что важно для безопасности цепочки поставок — как и build.rs.

Правило сообщества: сначала попробовать функции, обобщённые типы и трейты, а макросы использовать, когда без них не обойтись.

Сравнение с другими языками

Макросы препроцессора C работают с текстом и не гигиеничны. Шаблоны C++ — вычисления над типами. Lisp вдохновил идею макросов над синтаксическим деревом. В Go макросов нет: вместо них используется go generate и генерация кода; в Python похожие задачи решают декораторы и метаклассы, но во время выполнения.

См. также

Источники

  1. The Rust Programming Language — Macros
  2. The Rust Reference — Macros By Example
  3. The Rust Reference — Procedural Macros
  4. The Little Book of Rust Macros
  5. syn crate documentation
  6. Serde — Using derive
  7. cargo-expand