Декларативные макросы
В Rust есть два основных вида макросов:
-
Декларативные макросы. Эти макросы обрабатываются после того, как исходный текст программы преобразован в AST (абстрактное синтаксическое дерево). Они манипулируют AST-узлами, что делает их простыми в написании, при этом довольно безопасными.
По сути, декларативные макросы — это функции, которые выполняются на этапе компиляции, и манипулируют не данными, а узлами AST. -
Процедурные макросы. Эти макросы принимают на вход последовательность токенов и выдают на выход тоже последовательность токенов. То есть они отрабатывают до лексического разбора текста программы, поэтому позволяют буквально создать другой язык внутри программы на Rust. Мы не будем изучать процедурные макросы в рамках этой книги, так как они сложны, и обычная бэкенд разработка не подразумевает создание новых процедурных макросов.
Наш первый декларативный макрос
Декларативные макросы объявляются при помощи аннотации #[macro_export], после которой следует описание макроса на специальном Schema-подобном синтаксисе.
#[macro_export]
macro_rules! имя_макроса {
шаблон_1 => { подстановка 1 };
шаблон_2 => { подстановка 2 };
...
шаблон_N => { подстановка N }
}
Для начала, давайте рассмотрим простейший макрос, который суммирует два числа:
#[macro_export]
macro_rules! sum_nums { // объявляем макрос с именем sum_nums
( $x:expr, $y:expr ) => { $x + $y }
}
fn main() {
let res = sum_nums!(1, 2); // вызываем макрос
println!("Sum is: {}", res);
}
При компиляции программы вызов макроса будет “раскрыт” (expanded) в следующее:
fn main() {
let res = 1 + 2;
println!("Sum is: {}", res);
}
Как видно, компилятор просто заменил вызов макроса на сгенерированный им Rust код и дальше продолжил компиляцию.
Tip
Посмотреть, в какие выражения раскрывается вызов макроса, можно с помощью утилиты cargo expand.
Теперь давайте подробнее разберём объявление макроса из нашего примера.
#[macro_export]
macro_rules! sum_nums {
( $x:expr, $y:expr ) => { $x + $y }
}
Здесь ( $x:expr, $y:expr ) — шаблон для того, что мы ожидаем в качестве аргумента макроса. Синтаксис аргументов шаблона имеет вид: $имя_аргумента : тип_аргумента.
( $x:expr, $y:expr ) означает, что мы ожидаем два аргумента, каждый из которых должен быть корректным выражением на языке Rust (тип expr означает выражение — expression).
Именно контроль типов аргументов делает декларативные макросы такими удобными и безопасными. Например, мы можем вызвать этот макрос для любых корректных выражений:
#![allow(unused)]
fn main() {
sum_nums!(1 + 1, 2 + 2); // => 1 + 1 + 2 + 2
sum_nums!({ 1 + 1 }, { 2 + 2 }) // => { 1 + 1 } + { 2 + 2 }
sum_nums!(if 5 > 4 { 1 } else { -1 }, 9); // => if 5 > 4 { 1 } else { -1 } + 9
}
Типы аргументов макросов
Кроме выражений, макросы могут различать целый ряд конструкций на Rust. Ниже приведён полный список допустимых типов аргументов для макросов. Со многими мы пока что не знакомы, поэтому не стоит заострять на них своё внимание. Цель этой главы не научиться писать макросы, а разобраться с ними до того уровня, который позволит комфортно ими пользоваться.
| Фрагмент | Сопоставляется с | после может быть |
|---|---|---|
expr | Выражение на Rust: 2 + 2, "aaa", x.len() | =>, ; |
stmt | Инструкция (то что до точки с запятой) | =>, ; |
ty | Тип данных: String, Vec<u8>, (&str, bool) | =>, = |
path | Путь: crate::module, ::std::sync::mpsc | =>, = |
pat | Деструктурирующий шаблон: _, Some(ref x) | =>, = |
item | Артикул: struct Point {x: f64, y: f64}, mod mymod; | что угодно |
block | Блок кода / скоуп: { s+= "ok"; true } | что угодно |
meta | Тело атрибута: inline, derive(Copy,Clone), doc="3d models." | что угодно |
ident | Идентификатор: std, Json, my_var | что угодно |
tt | Дерево лексем: ;, >=, {}, [0 1 (+ 0 1)] | что угодно |
literal | Литерал: 5, 5u32, 1.0, "Hello" | |
vis | Visibility qualifier: pub, pub (crate) |
Как видите, благодаря системе типов для аргументов макросы в Rust куда безопаснее, чем в C.
Также при неправильном использовании макроса, мы получим осмысленную ошибку компиляции именно для макроса, а не для некорректно-сгенерированного макросом кода, что обычно происходит в C.
Переменное число аргументов
В Rust отсутствуют функции с переменным числом аргументов, однако это ограничение компенсируется макросами.
Давайте рассмотрим макрос, который принимает произвольное количество числовых литералов и конструирует их сумму:
#[macro_export]
macro_rules! sum_nums {
() => { 0 };
( $first:literal $(, $rest:literal )* ) => {
$first $( + $rest )*
};
}
fn main() {
let res = sum_nums!(1, 2, 3, 4, 5);
println!("Sum is: {}", res); // Sum is: 15
}
В этом макросе у нас два шаблона.
С первым шаблоном () => { 0 } всё просто: он обрабатывает случай, если в вызов макроса не было передано ни одного значения. В этом случае мы просто возвращаем 0 (в данном примере мы решили, что сумма никаких аргументов равна нулю).
Второй шаблон ожидает как минимум один аргумент, который будет привязан к имени $first. Далее может следовать произвольное количество чисел предварённых запятой, и вся эта последовательность будет привязана к имени $( $rest )*. Причём сама запятая будет отброшена, так как она не привязывается к какому-то имени аргумента.
Результатом применения этого шаблона будет сначала литерал, привязанный к $first, а затем все элементы последовательности, привязанные к ${$rest}*. Причём перед каждым элементом мы добавляем знак +.
Таким образом вызов sum_nums!(1, 2, 3, 4, 5) превращается в 1 + 2 + 3 + 4 + 5.
Этот макрос можно переписать более простым способом:
#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! sum_nums {
( $( $rest:expr ),* ) => { 0 $( + $rest )* }
}
}
Здесь мы просто воспользовались тем, что сложение любого числа с нулём даёт это же число. Поэтому мы привязываем все аргументы макроса к $( $rest )*, а в теле шаблона мы складываем 0 со всеми аргументами макроса.
sum_nums!(1,); // 0 + 1
sum_nums!(1, 2, 3); // 0 + 1 + 2 + 3
sum_nums!(); // 0
Скобки
Как вы могли заметить, мы вызываем наш макрос используя круглые скобки: sum_nums(1,2). При этом макрос vec! мы вызываем с использованием квадратных скобок. В чём же разница?
На самом деле и наш макрос, и vec! можно вызывать с любыми скобками:
sum_nums!(1, 2);
sum_nums![1, 2];
sum_nums!{1, 2};
vec!(1, 2, 3);
vec![1, 2, 3];
vec!{1, 2, 3};
Какой вариант кажется выразительнее, тот и используйте.
Единственное отличие заключается в необходимости ставить ; после вызова макроса. Если макрос используется для генерации функции или структуры, то после вызова макроса в варианте с фигурными скобками ; ставить не надо.
// Макрос, который создаёт пустую функцию с заданным именем
#[macro_export]
macro_rules! make_empty_func {
($func_name:ident) => {
fn $func_name() {}
}
}
make_empty_func!(function_1); // Нужна точка с запятой после вызова макроса
make_empty_func!{function_2}
fn main() {
function_1();
function_2();
}
Макрос vec!
Теперь, когда мы познакомились с декларативными макросами, становится понятно, почему vec![] оформлен именно в виде макроса, а не функции: функции не поддерживают переменное число аргументов.
Реальная имплементация макроса vec![] такова, что вызов vec![1, 2, 3] раскрывается в <[_]>::into_vec(::alloc::boxed::box_new([1, 2, 3])) (и мы пока что не готовы разбирать что это означает). Однако, в учебных целях, давайте напишем свою, более простую реализацию — классический учебный макрос vec2!.
Код макроса:
#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! vec2 {
() => { Vec::new() };
( $( $x:expr),* ) => {
{
let mut _temp = Vec::new();
$( _temp.push($x); )*
_temp
}
}
}
}
Код вызова макроса:
|
Такой код вызова
|
раскрывается в
|