Макросы
К этому моменту вы узнали о многих инструментах Rust, которые нацелены на абстрагирование и повторное использование кода. Эти единицы повторно использованного кода имеют богатую смысловую структуру. Например, функции имеют сигнатуры типа, типы параметров могут имеют ограничения по типажам, перегруженные функции также могут принадлежать к определенному типажу.
Эта структура означает, что ключевые абстракции Rust имеют мощный механизм проверки времени компиляции. Но это достигается за счет снижения гибкости. Если вы визуально определите структуру повторно используемого кода, то вы можете найти трудным или громоздким выражение этой схемы в виде обобщённой функции, типажа, или чего-то еще в семантике Rust.
Макросы позволяют абстрагироваться на синтаксическом уровне. Вызов макроса является сокращением для «расширенной» синтаксической формы. Это расширение происходит в начале компиляции, до начала статической проверки. В результате, макросы могут охватить много шаблонов повторного использования кода, которые невозможны при использовании лишь ключевых абстракций Rust.
Недостатком является то, что код, основанный на макросах, может быть трудным для понимания, потому что к нему применяется меньше встроенных правил. Подобно обычной функции, качественный макрос может быть использован без понимания его реализации. Тем не менее, может быть трудно разработать качественный макрос! Кроме того, ошибки компилятора в макро коде сложнее интерпретировать, потому что они описывают проблемы в расширенной форме кода, а не в исходной сокращенной форме кода, которую используют разработчики.
Эти недостатки делают макросы чем-то вроде «возможности последней инстанции». Это не означает, что макросы это плохо; они являются частью Rust, потому что иногда они все же нужны для по-настоящему краткой записи хорошо абстрагированной части кода. Просто имейте этот компромисс в виду.
Определение макросов (Макроопределения)
Вы, возможно, видели макрос vec!
, который используется для инициализации
вектора с произвольным количеством элементов.
let x: Vec<u32> = vec![1, 2, 3];
# assert_eq!(x, [1, 2, 3]);
Его нельзя реализовать в виде обычной функции, так как он принимает любое количество аргументов. Но мы можем представить его в виде синтаксического сокращения для следующего кода
let x: Vec<u32> = {
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
};
# assert_eq!(x, [1, 2, 3]);
Мы можем реализовать это сокращение, используя макрос: [^actual]
[^actual]: Фактическое определение vec!
в libcollections отличается от
представленного здесь по соображениям эффективности и повторного
использования.
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
# fn main() {
# assert_eq!(vec![1,2,3], [1, 2, 3]);
# }
Ого, тут много нового синтаксиса! Давайте разберем его.
macro_rules! vec { ... }
Тут мы определяем макрос с именем vec
, аналогично тому, как fn vec
определяло бы функцию с именем vec
. При вызове мы неформально пишем имя
макроса с восклицательным знаком, например, vec!
. Восклицательный знак
является частью синтаксиса вызова и служит для того, чтобы отличать макрос от
обычной функции.
Сопоставление (Matching) (Синтаксис вызова макрокоманды)
Макрос определяется с помощью ряда правил, которые представляют собой варианты сопоставления с образцом. Выше у нас было
( $( $x:expr ),* ) => { ... };
Это очень похоже на конструкцию match
, но сопоставление происходит на уровне
синтаксических деревьев Rust, на этапе компиляции. Точка с запятой не является
обязательной для последнего (только здесь) варианта. «Образец» слева от =>
известен как шаблон совпадений (образец) (обнаружитель совпадений)
(matcher). Он имеет свою собственную грамматику в
рамках языка.
Образец $x:expr
будет соответствовать любому выражению Rust, связывая его
дерево синтаксиса с метапеременной $x
. Идентификатор expr
является
спецификатором фрагмента; полные возможности перечислены далее в этой главе.
Образец, окруженный $(...),*
, будет соответствовать нулю или более выражениям,
разделенным запятыми.
За исключением специального синтаксиса сопоставления с образцом, любые другие элементы Rust, которые появляются в образце, должны в точности совпадать. Например,
macro_rules! foo {
(x => $e:expr) => (println!("mode X: {}", $e));
(y => $e:expr) => (println!("mode Y: {}", $e));
}
fn main() {
foo!(y => 3);
}
выведет
mode Y: 3
А с
foo!(z => 3);
мы получим ошибку компиляции
error: no rules expected the token `z`
Развертывание (Expansion) (Синтаксис преобразования макрокоманды)
С правой стороны макро правил используется, по большей части, обычный синтаксис Rust. Но мы можем соединить кусочки раздробленного синтаксиса, захваченные при сопоставлении с соответствующим образцом. Из предыдущего примера:
$(
temp_vec.push($x);
)*
Каждое соответствующее выражение $x
будет генерировать одиночный оператор
push
в развернутой форме макроса. Повторение в развернутой форме происходит
синхронно с повторением в форме образца (более подробно об этом чуть позже).
Поскольку $x
уже объявлен в образце как выражение, мы не повторяем :expr
с
правой стороны. Кроме того, мы не включаем разделителяющую запятую в качестве
части оператора повторения. Вместо этого, у нас есть точка с запятой в пределах
повторяемого блока.
Еще одна деталь: макрос vec!
имеет две пары фигурных скобках правой части.
Они часто сочетаются таким образом:
macro_rules! foo {
() => {{
...
}}
}
Внешние скобки являются частью синтаксиса macro_rules!
. На самом деле, вы
можете использовать ()
или []
вместо них. Они просто разграничивают правую
часть в целом.
Внутренние скобки являются частью расширенного синтаксиса. Помните, что макрос
vec!
используется в контексте выражения. Мы используем блок, для записи
выражения с множественными операторами, в том числе включающее let
привязки.
Если ваш макрос раскрывается в одно единственное выражение, то дополнительной
слой скобок не нужен.
Обратите внимание, что мы никогда не говорили, что макрос создает выражения. На самом деле, это не определяется, пока мы не используем макрос в качестве выражения. Если соблюдать осторожность, то можно написать макрос, развернутая форма которого будет валидна сразу в нескольких контекстах. Например, сокращенная форма для типа данных может быть валидной и как выражение, и как шаблон.
Повторение (Repetition) (Многовариантность)
Операции повтора всегда сопутствуют два основных правила:
$(...)*
проходит через один «слой» повторений, для всех$name
, которые он содержит, в ногу, и- каждое
$name
должно быть под, по крайней мере, стольким количеством$(...)*
, сколько было использовано при сопоставлении. Если оно под большим числом$(...)*
,$name
будет дублироваться, при необходимости.
Этот причудливый макрос иллюстрирует дублирования переменных из внешних уровней повторения.
macro_rules! o_O {
(
$(
$x:expr; [ $( $y:expr ),* ]
);*
) => {
&[ $($( $x + $y ),*),* ]
}
}
fn main() {
let a: &[i32]
= o_O!(10; [1, 2, 3];
20; [4, 5, 6]);
assert_eq!(a, [11, 12, 13, 24, 25, 26]);
}
Это наибольшая синтаксиса совпадений. Эти примеры используют конструкцию
$(...)*
, которая означает «ноль или более» совпадений. Также вы можете
написать $(...)+
, что будет означать «одно или более» совпадений. Обе формы
записи включают необязательный разделитель, располагающийся сразу за закрывающей
скобкой, который может быть любым символом, за исключением +
или *
.
Эта система повторений основана на «Macro-by-Example» (PDF ссылка).
Гигиена (Hygiene)
Некоторые языки реализуют макросы с помощью простой текстовой замены, что
приводит к различным проблемам. Например, нижеприведенная C программа напечатает
13
вместо ожидаемого 25
.
#define FIVE_TIMES(x) 5 * x
int main() {
printf("%d\n", FIVE_TIMES(2 + 3));
return 0;
}
После развертывания мы получаем 5 * 2 + 3
, но умножение имеет больший
приоритет чем сложение. Если вы часто использовали C макросы, вы, наверное,
знаете стандартные идиомы для устранения этой проблемы, а также пять или шесть
других проблем. В Rust мы можем не беспокоиться об этом.
macro_rules! five_times {
($x:expr) => (5 * $x);
}
fn main() {
assert_eq!(25, five_times!(2 + 3));
}
Метапеременная $x
обрабатывается как единый узел выражения, и сохраняет свое
место в дереве синтаксиса даже после замены.
Другой распространенной проблемой в системе макросов является захват переменной (variable capture). Вот C макрос, использующий GNU C расширение, который эмулирует блоки выражениий в Rust.
#define LOG(msg) ({ \
int state = get_log_state(); \
if (state > 0) { \
printf("log(%d): %s\n", state, msg); \
} \
})
Вот простой случай использования, применение которого может плохо кончиться:
const char *state = "reticulating splines";
LOG(state)
Он раскрывается в
const char *state = "reticulating splines";
int state = get_log_state();
if (state > 0) {
printf("log(%d): %s\n", state, state);
}
Вторая переменная с именем state
затеняет первую. Это проблема, потому что
команде печати требуется обращаться к ним обоим.
Эквивалентный макрос в Rust обладает требуемым поведением.
# fn get_log_state() -> i32 { 3 }
macro_rules! log {
($msg:expr) => {{
let state: i32 = get_log_state();
if state > 0 {
println!("log({}): {}", state, $msg);
}
}};
}
fn main() {
let state: &str = "reticulating splines";
log!(state);
}
Это работает, потому что Rust имеет систему макросов с соблюдением
гигиены. Раскрытие каждого макроса происходит в
отдельном контексте синтаксиса, и каждая переменная обладает меткой контекста
синтаксиса, где она была введена. Это как если бы переменная state
внутри
main
была бы окрашена в другой «цвет» в отличае от переменной state
внутри
макроса, из-за чего они бы не конфликтовали.
Это также ограничивает возможности макросов для внедрения новых связываний переменных на месте вызова. Код, приведенный ниже, не будет работать:
macro_rules! foo {
() => (let x = 3);
}
fn main() {
foo!();
println!("{}", x);
}
Вместо этого вы должны передавать имя переменной при вызове, тогда она будет обладать меткой правильного контекста синтаксиса.
macro_rules! foo {
($v:ident) => (let $v = 3);
}
fn main() {
foo!(x);
println!("{}", x);
}
Это справедливо для let
привязок и меток loop, но не для элементов.
Код, приведенный ниже, компилируется:
macro_rules! foo {
() => (fn x() { });
}
fn main() {
foo!();
x();
}
Рекурсия макросов
Раскрытие макроса также может включать в себя вызовы макросов, в том числе вызовы того макроса, который раскрывается. Эти рекурсивные макросы могут быть использованы для обработки древовидного ввода, как показано на этом (упрощенном) HTML сокращение:
# #![allow(unused_must_use)]
macro_rules! write_html {
($w:expr, ) => (());
($w:expr, $e:tt) => (write!($w, "{}", $e));
($w:expr, $tag:ident [ $($inner:tt)* ] $($rest:tt)*) => {{
write!($w, "<{}>", stringify!($tag));
write_html!($w, $($inner)*);
write!($w, "</{}>", stringify!($tag));
write_html!($w, $($rest)*);
}};
}
fn main() {
# // FIXME(#21826)
use std::fmt::Write;
let mut out = String::new();
write_html!(&mut out,
html[
head[title["Macros guide"]]
body[h1["Macros are the best!"]]
]);
assert_eq!(out,
"<html><head><title>Macros guide</title></head>\
<body><h1>Macros are the best!</h1></body></html>");
}
Отладка макросов
Чтобы увидеть результаты расширения макросов, выполните команду rustc --pretty expanded
. Вывод представляет собой целый контейнер, так что вы можете подать
его обратно в rustc
, что иногда выдает лучшие сообщения об ошибках, чем при
обычной компиляции. Обратите внимание, что вывод --pretty expanded
может иметь
разное значение, если несколько переменных, имеющих одно и то же имя (но разные
контексты синтаксиса), находятся в той же области видимости. В этом случае
--pretty expanded,hygiene
расскажет вам о контекстах синтаксиса.
rustc
, поддерживает два синтаксических расширения, которые помогают с отладкой
макросов. В настоящее время, они неустойчивы и требуют feature gates.
-
log_syntax!(...)
будет печатать свои аргументы в стандартный вывод во время компиляции, и «развертываться» в ничто. -
trace_macros!(true)
будет выдавать сообщение компилятора каждый раз, когда макрос развертывается. Используйтеtrace_macros!(false)
в конце развертывания, чтобы выключить его.
Требования синтаксиса
Код на Rust может быть разобран в синтаксическое дерево, даже когда он содержит неразвёрнутые макросы. Это свойство очень полезно для редакторов и других инструментов, обрабатывающих исходный код. Оно также влияет на вид системы макросов Rust.
Как следствие, когда компилятор разбирает вызов макроса, ему необходимо знать, во что развернётся данный макрос. Макрос может разворачиваться в следующее:
- ноль или больше элементов;
- ноль или больше методов;
- выражение;
- оператор;
- образец.
Вызов макроса в блоке может представлять собой элементы, выражение, или оператор. Rust использует простое правило для разрешения этой неоднозначности. Вызов макроса, производящего элементы, должен либо
- ограничиваться фигурными скобками, т.е.
foo! { ... }
; - завершаться точкой с запятой, т.е.
foo!(...);
.
Другое следствие разбора перед раскрытием макросов — это то, что вызов макроса
должен состоять из допустимых лексем. Более того, скобки всех видов должны быть
сбалансированы в месте вызова. Например, foo!([)
не является разрешённым
кодом. Такое поведение позволяет компилятору понимать где заканчивается вызов
макроса.
Говоря более формально, тело вызова макроса должно представлять собой последовательность деревьев лексем. Дерево лексем определяется рекурсивно и представляет собой либо:
- последовательность деревьев лексем, окружённую согласованными круглыми,
квадратными или фигурными скобками (
()
,[]
,{}
); - любую другую одиночную лексему.
Внутри сопоставления каждая метапеременная имеет указатель фрагмента, определяющий синтаксическую форму, с которой она совпадает. Вот список этих указателей:
ident
: идентификатор. Например:x
;foo
.path
: квалифицированное имя. Например:T::SpecialA
.expr
: выражение. Например:2 + 2
;if true then { 1 } else { 2 }
;f(42)
.ty
: тип. Например:i32
;Vec<(char, String)>
;&T
.pat
: образец. Например:Some(t)
;(17, 'a')
;_
.stmt
: единственный оператор. Например:let x = 3
.block
: последовательность операторов, ограниченная фигурными скобками. Например:{ log(error, "hi"); return 12; }
.item
: элемент. Например:fn foo() { }
;struct Bar;
.meta
: «мета-элемент», как в атрибутах. Например:cfg(target_os = "windows")
.tt
: единственное дерево лексем.
Есть дополнительные правила относительно лексем, следующих за метапеременной:
- за
expr
должно быть что-то из этого:=> , ;
; - за
ty
иpath
должно быть что-то из этого:=> , : = > as
; - за
pat
должно быть что-то из этого :=> , =
; - за другими лексемами могут следовать любые символы.
Приведённые правила обеспечивают развитие синтаксиса Rust без необходимости менять существующие макросы.
И ещё: система макросов никак не обрабатывет неоднозначность разбора. Например,
грамматика $($t:ty)* $e:expr
всегда будет выдавать ошибку, потому что
синтаксическому анализатору пришлось бы выбирать между разбором $t
и разбором
$e
. Можно изменить синтаксис вызова так, чтобы грамматика отличалась в начале.
В данном случае можно написать $(T $t:ty)* E $e:exp
.
Области видимости, импорт и экспорт макросов
Макросы разворачиваются на ранней стадии компиляции, перед разрешением имён. Один из недостатков такого подхода в том, что правила видимости для макросов отличны от правил для других конструкций языка.
Компилятор определяет и разворачивает макросы при обходе графа исходного кода
контейнера в глубину. При этом определения макросов включаются в граф в порядке
их встречи компилятором. Поэтому макрос, определённый на уровне модуля, виден во
всём последующем коде модуля, включая тела всех вложенных модулей (mod
).
Макрос, определённый в теле функции, или где-то ещё не на уровне модуля, виден только внутри этого элемента (например, внутри одной функции).
Если модуль имеет атрибут macro_use
, то его макросы также видны в его
родительском модуле после элемента mod
данного модуля. Если родитель тоже
имеет атрибут macro_use
, макросы также будут видны в модуле-родителе родителя,
после элемента mod
родителя. Это распространяется на любое число уровней.
Атрибут macro_use
также можно поставить на подключение контейнера extern crate
. В этом контексте оно управляет тем, какие макросы будут загружены из
внешнего контейнера, т.е.
#[macro_use(foo, bar)]
extern crate baz;
Если атрибут записан просто как #[macro_use]
, будут загружены все
макросы. Если атрибута нет, никакие макросы не будут загружены. Загружены могут
быть только макросы, объявленные с атрибутом #[macro_export]
.
Чтобы загрузить макросы из контейнера без компоновки контейнера в выходной
артефакт, можно использовать атрибут #[no_link]
.
Например:
macro_rules! m1 { () => (()) }
// здесь видны: m1
mod foo {
// здесь видны: m1
#[macro_export]
macro_rules! m2 { () => (()) }
// здесь видны: m1, m2
}
// здесь видны: m1
macro_rules! m3 { () => (()) }
// здесь видны: m1, m3
#[macro_use]
mod bar {
// здесь видны: m1, m3
macro_rules! m4 { () => (()) }
// здесь видны: m1, m3, m4
}
// здесь видны: m1, m3, m4
# fn main() { }
Когда эта библиотека загружается с помощью #[macro_use] extern crate
, виден
только макрос m2
.
Атрибуты, относящиеся к макросам, перечислены в справочнике Rust.
Переменная $crate
Если макрос используется в нескольких контейнерах, всё становится ещё
сложнее. Допустим, mylib
определяет
pub fn increment(x: u32) -> u32 {
x + 1
}
#[macro_export]
macro_rules! inc_a {
($x:expr) => ( ::increment($x) )
}
#[macro_export]
macro_rules! inc_b {
($x:expr) => ( ::mylib::increment($x) )
}
# fn main() { }
inc_a
работает только внутри mylib
, а inc_b
— только снаружи. Более того,
inc_b
сломается, если пользователь импортирует mylib
под другим именем.
В Rust пока нет гигиеничных ссылок на контейнеры, но есть простой способ обойти
эту проблему. Особая макро-переменная $crate
раскроется в ::foo
внутри
макроса, импортированного из контейнера foo
. А когда макрос определён и
используется в одном и том же контейнере, $crate
станет пустой. Это означает,
что мы можем написать
#[macro_export]
macro_rules! inc {
($x:expr) => ( $crate::increment($x) )
}
# fn main() { }
чтобы определить один макрос, который будет работать и внутри, и снаружи
библиотеки. Имя функции раскроется или в ::increment
, или в
::mylib::increment
.
Чтобы эта система работала просто и правильно, #[macro_use] extern crate ...
может быть написано только в корне вашего контейнера, но не внутри mod
. Это
обеспечивает, что $crate
раскроется в единственный идентификатор.
Во тьме глубин
Вводная глава упоминала рекурсивные макросы, но она не рассказывала всей истории. Рекурсивные макросы полезны ещё по одной причине: каждый рекурсивный вызов даёт нам ещё одну возможность сопоставить с образцом аргументы макроса.
Приведём такой радикальный пример использования данной возможности. С помощью рекурсивных макросов можно реализовать конечный автомат типа Bitwise Cyclic Tag. Стоит заметить, что мы не рекомендуем такой подход, а просто иллюстрируем возможности макросов.
macro_rules! bct {
// cmd 0: d ... => ...
(0, $($ps:tt),* ; $_d:tt)
=> (bct!($($ps),*, 0 ; ));
(0, $($ps:tt),* ; $_d:tt, $($ds:tt),*)
=> (bct!($($ps),*, 0 ; $($ds),*));
// cmd 1p: 1 ... => 1 ... p
(1, $p:tt, $($ps:tt),* ; 1)
=> (bct!($($ps),*, 1, $p ; 1, $p));
(1, $p:tt, $($ps:tt),* ; 1, $($ds:tt),*)
=> (bct!($($ps),*, 1, $p ; 1, $($ds),*, $p));
// cmd 1p: 0 ... => 0 ...
(1, $p:tt, $($ps:tt),* ; $($ds:tt),*)
=> (bct!($($ps),*, 1, $p ; $($ds),*));
// halt on empty data string
( $($ps:tt),* ; )
=> (());
}
В качестве упражнения предлагаем читателю определить ещё один макрос, чтобы уменьшить степень дублирования кода в определении выше.
Распространённые макросы
Вот некоторые распространённые макросы, которые вы увидите в коде на Rust.
panic!
Этот макрос вызывает панику текущего потока. Вы можете указать сообщение, с которым поток завершится:
panic!("о нет!");
vec!
Макрос vec!
используется по всей книге, поэтому вы наверняка уже видели его.
Он упрощает создание Vec<T>
:
let v = vec![1, 2, 3, 4, 5];
Он также позволяет вам создавать векторы с повторяющимися значениями. Например, вот сто нолей:
let v = vec![0; 100];
assert! and assert_eq!
Эти два макроса используются в тестах. assert!
принимает логическое значение.
assert_eq!
принимает два значения и проверяет, что они равны. true
засчитывается как успех, а false
вызывает панику и проваливает тест. Вот так:
// Работает!
assert!(true);
assert_eq!(5, 3 + 2);
// а это нет :(
assert!(5 < 3);
assert_eq!(5, 3);
try!
try!
используется для обработки ошибок. Он принимает нечто возвращающее
Result<T, E>
и возвращает T
если было возвращено Ok<T>
; иначе он делает
возврат из функции со значением Err(E)
. Вроде такого:
use std::fs::File;
fn foo() -> std::io::Result<()> {
let f = try!(File::create("foo.txt"));
Ok(())
}
Такой код читается легче, чем этот:
use std::fs::File;
fn foo() -> std::io::Result<()> {
let f = File::create("foo.txt");
let f = match f {
Ok(t) => t,
Err(e) => return Err(e),
};
Ok(())
}
unreachable!
Этот макрос применяется, когда вы хотите пометить какой-то код, который никогда не должен исполняться:
if false {
unreachable!();
}
Иногда вам придётся определять ветви условных конструкций, которые точно никогда не исполнятся. В таком случае, используйте этот макрос, чтобы в случае ошибки программа запаниковала:
let x: Option<i32> = None;
match x {
Some(_) => unreachable!(),
None => println!("Я знаю, что x — это None!"),
}
unimplemented!
Макрос unimplemented!
можно использовать, когда вы хотите, чтобы ваш код
прошёл проверку типов, но пока не хотите реализовывать его настоящую логику.
Один из примеров — это реализация типажа с несколькими требуемыми методами.
Возможно, вы хотите разбираться с типажом постепенно — по одному методу за раз.
В таком случае, определите остальные методы как unimplemented!
, пока не
захотите наконец реализовать их.
Процедурные макросы
Если система макросов не может сделать того, что вам нужно, вы можете написать плагин к компилятору. По сравнению с макросами, это гораздо труднее, там ещё более нестабильные интерфейсы, и ещё сложнее найти ошибки. Зато вы получаете гибкость — внутри плагина может исполняться произвольный код на Rust. Иногда плагины расширения синтаксиса называются процедурными макросами.