Макросы 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 похожие задачи решают декораторы и метаклассы, но во время выполнения.