Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Макросы по примеру

Syntax
MacroRulesDefinition
    macro_rules ! IDENTIFIER MacroRulesDef

MacroRulesDef
      ( MacroRules ) ;
    | [ MacroRules ] ;
    | { MacroRules }

MacroRules
    MacroRule ( ; MacroRule )* ;?

MacroRule
    MacroMatcher => MacroTranscriber

MacroMatcher
      ( MacroMatch* )
    | [ MacroMatch* ]
    | { MacroMatch* }

MacroMatch
      Tokenкроме $ и разделителей
    | MacroMatcher
    | $ ( IDENTIFIER_OR_KEYWORDкроме crate | RAW_IDENTIFIER ) : MacroFragSpec
    | $ ( MacroMatch+ ) MacroRepSep? MacroRepOp

MacroFragSpec
      block | expr | expr_2021 | ident | item | lifetime | literal
    | meta | pat | pat_param | path | stmt | tt | ty | vis

MacroRepSepTokenкроме разделителей и MacroRepOp

MacroRepOp* | + | ?

MacroTranscriberDelimTokenTree

macro_rules позволяет пользователям определять расширения синтаксиса декларативным способом. Мы называем такие расширения “макросами по примеру” или просто “макросами”.

Каждый макрос по примеру имеет имя и одно или несколько правил. Каждое правило состоит из двух частей: сопоставителя (matcher), описывающего синтаксис, который он сопоставляет, и транскрайбера (transcriber), описывающего синтаксис, который заменит успешно сопоставленный вызов. Как сопоставитель, так и транскрайбер должны быть окружены разделителями. Макросы могут раскрываться в выражения, инструкции, элементы (включая трейты, реализации и внешние элементы), типы или шаблоны.

Транскрибирование

При вызове макроса, раскрыватель макросов ищет вызовы макросов по имени и пробует каждое правило макроса по очереди. Он транскрибирует первое успешное совпадение; если это приводит к ошибке, то последующие совпадения не проверяются.

При сопоставлении не выполняется упреждающий просмотр (lookahead); если компилятор не может однозначно определить, как разбирать вызов макроса по одному токену за раз, то это ошибка. В следующем примере компилятор не заглядывает вперед после идентификатора, чтобы увидеть, является ли следующий токен ), даже если это позволило бы ему однозначно разобрать вызов:

#![allow(unused)]
fn main() {
macro_rules! ambiguity {
    ($($i:ident)* $j:ident) => { };
}

ambiguity!(error); // Ошибка: локальная неоднозначность
}

Как в сопоставителе, так и в транскрайбере, токен $ используется для вызова специального поведения механизма макросов (описано ниже в Метапеременные и Повторы). Токены, не являющиеся частью такого вызова, сопоставляются и транскрибируются буквально, за одним исключением. Исключение состоит в том, что внешние разделители для сопоставителя будут соответствовать любой паре разделителей. Таким образом, например, сопоставитель (()) будет соответствовать {()} но не {{}}. Символ $ не может быть сопоставлен или транскрибирован буквально.

Передача сопоставленного фрагмента

При передаче сопоставленного фрагмента в другой макрос по примеру, сопоставители во втором макросе увидят непрозрачное AST типа фрагмента. Второй макрос не может использовать литеральные токены для сопоставления фрагментов в сопоставителе, только спецификатор фрагмента того же типа. Типы фрагментов ident, lifetime и tt являются исключением и могут быть сопоставлены литеральными токенами. Следующий пример иллюстрирует это ограничение:

#![allow(unused)]
fn main() {
macro_rules! foo {
    ($l:expr) => { bar!($l); }
// ОШИБКА:               ^^ никакие правила не ожидают этот токен в вызове макроса
}

macro_rules! bar {
    (3) => {}
}

foo!(3);
}

Следующий пример иллюстрирует, как токены могут быть непосредственно сопоставлены после сопоставления фрагмента tt:

#![allow(unused)]
fn main() {
// компилируется OK
macro_rules! foo {
    ($l:tt) => { bar!($l); }
}

macro_rules! bar {
    (3) => {}
}

foo!(3);
}

Метапеременные

В сопоставителе $ имя : спецификатор-фрагмента сопоставляет фрагмент синтаксиса Rust указанного вида и связывает его с метапеременной $имя.

Допустимые спецификаторы фрагментов:

В транскрайбере на метапеременные ссылаются просто как на $имя, поскольку вид фрагмента указан в сопоставителе. Метапеременные заменяются элементом синтаксиса, который им соответствует. Метапеременные могут транскрибироваться более одного раза или вообще не транскрибироваться.

Ключевое слово-метапеременная $crate может использоваться для ссылки на текущий крейт.

2021 Edition differences

Начиная с редакции 2021, спецификаторы фрагментов pat сопоставляют OR-шаблоны верхнего уровня (то есть они принимают Pattern).

До редакции 2021 они сопоставляют точно такие же фрагменты, как pat_param (то есть они принимают PatternNoTopAlt).

Соответствующая редакция - это та, которая действует для определения macro_rules!.

2024 Edition differences

До редакции 2024 спецификаторы фрагментов expr не сопоставляют Выражение с подчеркиванием или Выражение-константный блок на верхнем уровне. Они разрешены внутри подвыражений.

Спецификатор фрагмента expr_2021 существует для сохранения обратной совместимости с редакциями до 2024.

Повторы

Как в сопоставителе, так и в транскрайбере, повторы указываются путем помещения токенов, которые нужно повторить, внутри $(), за которым следует оператор повтора, опционально с разделителем между ними.

Разделителем может быть любой токен, кроме разделителя или одного из операторов повтора, но ; и , являются наиболее распространенными. Например, $( $i:ident ),* представляет любое количество идентификаторов, разделенных запятыми. Вложенные повторы разрешены.

Операторы повтора:

  • * — указывает любое количество повторений.
  • + — указывает любое количество, но не менее одного.
  • ? — указывает на необязательный фрагмент с нулем или одним вхождением.

Поскольку ? представляет не более одного вхождения, он не может использоваться с разделителем.

Повторяющийся фрагмент как сопоставляется, так и транскрибируется в указанное количество фрагментов, разделенных токеном-разделителем. Метапеременные сопоставляются с каждым повторением их соответствующего фрагмента. Например, приведенный выше пример $( $i:ident ),* сопоставляет $i со всеми идентификаторами в списке.

Во время транскрибирования применяются дополнительные ограничения к повторам, чтобы компилятор знал, как правильно их раскрыть:

  1. Метапеременная должна появляться в точно таком же количестве, виде и порядке вложенности повторов в транскрайбере, как и в сопоставителе. Так, для сопоставителя $( $i:ident ),*, транскрайберы => { $i }, => { $( $( $i)* )* } и => { $( $i )+ } являются незаконными, но => { $( $i );* } является корректным и заменяет список идентификаторов, разделенных запятыми, на список, разделенный точками с запятой.
  2. Каждый повтор в транскрайбере должен содержать по крайней мере одну метапеременную, чтобы решить, сколько раз его раскрывать. Если несколько метапеременных появляются в одном и том же повторении, они должны быть связаны с одинаковым количеством фрагментов. Например, ( $( $i:ident ),* ; $( $j:ident ),* ) => (( $( ($i,$j) ),* )) должен связывать одинаковое количество фрагментов $i и $j. Это означает, что вызов макроса с (a, b, c; d, e, f) является законным и раскрывается в ((a,d), (b,e), (c,f)), но (a, b, c; d, e) является незаконным, поскольку не имеет того же количества. Это требование применяется к каждому уровню вложенных повторов.

Области видимости, экспорт и импорт

По историческим причинам область видимости макросов по примеру работает не совсем так, как у элементов. Макросы имеют две формы области видимости: текстовая область видимости и область видимости на основе путей. Текстовая область видимости основана на порядке появления вещей в исходных файлах или даже across нескольких файлах и является областью видимости по умолчанию. Она подробно объясняется ниже. Область видимости на основе путей работает точно так же, как и область видимости элементов. Область видимости, экспорт и импорт макросов в значительной степени контролируются атрибутами.

Когда макрос вызывается с помощью неквалифицированного идентификатора (не часть многосоставного пути), он сначала ищется в текстовой области видимости. Если это не дает результатов, то он ищется в области видимости на основе путей. Если имя макроса квалифицировано путем, то он ищется только в области видимости на основе путей.

use lazy_static::lazy_static; // Импорт на основе пути.

macro_rules! lazy_static { // Текстовое определение.
    (lazy) => {};
}

lazy_static!{lazy} // Текстовый поиск сначала находит наш макрос.
self::lazy_static!{} // Поиск на основе пути игнорирует наш макрос, находит импортированный.

Текстовая область видимости

Текстовая область видимости в значительной степени основана на порядке появления вещей в исходных файлах и работает аналогично области видимости локальных переменных, объявленных с помощью let, за исключением того, что она также применяется на уровне модуля. Когда macro_rules! используется для определения макроса, макрос входит в область видимости после определения (обратите внимание, что он все еще может использоваться рекурсивно, поскольку имена ищутся с места вызова), до тех пор, пока его окружающая область видимости, обычно модуль, не закрыта. Это может входить в дочерние модули и даже охватывать несколько файлов:

//// src/lib.rs
mod has_macro {
    // m!{} // Ошибка: m не в области видимости.

    macro_rules! m {
        () => {};
    }
    m!{} // OK: появляется после объявления m.

    mod uses_macro;
}

// m!{} // Ошибка: m не в области видимости.

//// src/has_macro/uses_macro.rs

m!{} // OK: появляется после объявления m в src/lib.rs

Не является ошибкой определять макрос несколько раз; самое последнее объявление будет затенять предыдущее, если оно не вышло из области видимости.

#![allow(unused)]
fn main() {
macro_rules! m {
    (1) => {};
}

m!(1);

mod inner {
    m!(1);

    macro_rules! m {
        (2) => {};
    }
    // m!(1); // Ошибка: никакое правило не соответствует '1'
    m!(2);

    macro_rules! m {
        (3) => {};
    }
    m!(3);
}

m!(1);
}

Макросы также могут быть объявлены и использованы локально внутри функций и работают аналогично:

#![allow(unused)]
fn main() {
fn foo() {
    // m!(); // Ошибка: m не в области видимости.
    macro_rules! m {
        () => {};
    }
    m!();
}

// m!(); // Ошибка: m не в области видимости.
}

Атрибут macro_use

attributes macro_use имеет две цели: он может использоваться на модулях для расширения области видимости макросов, определенных внутри них, и он может использоваться на extern crate для импорта макросов из другого крейта в macro_use прелюдию.

Example

#![allow(unused)]
fn main() {
#[macro_use]
mod inner {
    macro_rules! m {
        () => {};
    }
}
m!();
}
#[macro_use]
extern crate log;

При использовании на модулях атрибут macro_use использует синтаксис MetaWord.

При использовании на extern crate он использует синтаксисы MetaWord и MetaListIdents. Для получения дополнительной информации о том, как эти синтаксисы могут использоваться, см. macro.decl.scope.macro_use.prelude.

Атрибут macro_use может применяться к модулям или extern crate.

Note

rustc игнорирует использование в других позициях, но выдает предупреждения. Это может стать ошибкой в будущем.

Атрибут macro_use не может использоваться на extern crate self.

Атрибут macro_use может использоваться любое количество раз на форме.

Может быть указано несколько экземпляров macro_use в синтаксисе MetaListIdents. Будет импортировано объединение всех указанных макросов.

Note

На модулях rustc выдает предупреждения против любых атрибутов MetaWord macro_use, следующих после первого.

На extern crate rustc выдает предупреждения против любых атрибутов macro_use, которые не оказывают эффекта из-за того, что не импортируют никакие макросы, не уже импортированные другим атрибутом macro_use. Если два или более атрибутов MetaListIdents macro_use импортируют один и тот же макрос, против первого выдается предупреждение. Если присутствуют любые атрибуты MetaWord macro_use, против всех атрибутов MetaListIdents macro_use выдаются предупреждения. Если присутствуют два или более атрибутов MetaWord macro_use, против следующих после первого выдаются предупреждения.

Когда macro_use используется на модуле, область видимости макросов модуля расширяется за пределы лексической области видимости модуля.

Example

#![allow(unused)]
fn main() {
#[macro_use]
mod inner {
    macro_rules! m {
        () => {};
    }
}
m!(); // OK
}

Указание macro_use на объявлении extern crate в корне крейта импортирует экспортированные макросы из этого крейта.

Макросы, импортированные таким образом, импортируются в macro_use прелюдию, а не текстуально, что означает, что они могут быть затенены любым другим именем. Макросы, импортированные с помощью macro_use, могут использоваться до оператора импорта.

Note

rustc в настоящее время предпочитает последний импортированный макрос в случае конфликта. Не полагайтесь на это. Это поведение необычно, поскольку импорты в Rust обычно не зависят от порядка. Это поведение macro_use может измениться в будущем.

Подробнее см. Rust issue #148025.

При использовании синтаксиса MetaWord импортируются все экспортированные макросы. При использовании синтаксиса MetaListIdents импортируются только указанные макросы.

Example

#[macro_use(lazy_static)] // Или `#[macro_use]` для импорта всех макросов.
extern crate lazy_static;

lazy_static!{}
// self::lazy_static!{} // ОШИБКА: lazy_static не определен в `self`.

Макросы для импорта с помощью macro_use должны быть экспортированы с помощью macro_export.

Атрибут macro_export

Атрибут macro_export экспортирует макрос из крейта и делает его доступным в корне крейта для разрешения на основе путей.

Example

#![allow(unused)]
fn main() {
self::m!();
//  ^^^^ OK: Поиск на основе пути находит `m` в текущем модуле.
m!(); // Как выше.

mod inner {
    super::m!();
    crate::m!();
}

mod mac {
    #[macro_export]
    macro_rules! m {
        () => {};
    }
}
}

Атрибут macro_export использует синтаксисы MetaWord и MetaListIdents. С синтаксисом MetaListIdents он принимает единственное значение local_inner_macros.

Атрибут macro_export может применяться к определениям macro_rules.

Note

rustc игнорирует использование в других позициях, но выдает предупреждения. Это может стать ошибкой в будущем.

Только первое использование macro_export на макросе имеет эффект.

Note

rustc выдает предупреждения против любого использования после первого.

По умолчанию макросы имеют только текстовую область видимости и не могут быть разрешены по пути. Когда используется атрибут macro_export, макрос становится доступным в корне крейта, и на него можно ссылаться по его пути.

Example

Без macro_export макросы имеют только текстовую область видимости, поэтому разрешение на основе пути макроса завершается неудачей.

macro_rules! m {
    () => {};
}
self::m!(); // ОШИБКА
crate::m!(); // ОШИБКА
fn main() {}

С macro_export разрешение на основе пути работает.

#[macro_export]
macro_rules! m {
    () => {};
}
self::m!(); // OK
crate::m!(); // OK
fn main() {}

Атрибут macro_export вызывает экспорт макроса из корня крейта, так что на него можно ссылаться в других крейтах по пути.

Example

Учитывая следующее в крейте log:

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! warn {
    ($message:expr) => { eprintln!("WARN: {}", $message) };
}
}

Из другого крейта вы можете ссылаться на макрос по пути:

fn main() {
    log::warn!("example warning");
}

macro_export позволяет использование macro_use на extern crate для импорта макроса в macro_use прелюдию.

Example

Учитывая следующее в крейте log:

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! warn {
    ($message:expr) => { eprintln!("WARN: {}", $message) };
}
}

Использование macro_use в зависимом крейте позволяет использовать макрос из прелюдии:

#[macro_use]
extern crate log;

pub mod util {
    pub fn do_thing() {
        // Разрешается через макрос прелюдии.
        warn!("example warning");
    }
}

Добавление local_inner_macros к атрибуту macro_export приводит к тому, что все вызовы макросов с одним сегментом в определении макроса имеют неявный префикс $crate::.

Note

Это предназначено в основном как инструмент для миграции кода, написанного до того, как $crate был добавлен в язык, для работы с импортом макросов на основе путей в Rust 2018. Его использование в новом коде не рекомендуется.

Example

#![allow(unused)]
fn main() {
#[macro_export(local_inner_macros)]
macro_rules! helped {
    () => { helper!() } // Автоматически преобразуется в $crate::helper!().
}

#[macro_export]
macro_rules! helper {
    () => { () }
}
}

Гигиена

Макросы по примеру имеют гигиену смешанного сайта. Это означает, что метки циклов, метки блоков и локальные переменные ищутся на сайте определения макроса, в то время как другие символы ищутся на сайте вызова макроса. Например:

#![allow(unused)]
fn main() {
let x = 1;
fn func() {
    unreachable!("this is never called")
}

macro_rules! check {
    () => {
        assert_eq!(x, 1); // Использует `x` с сайта определения.
        func();           // Использует `func` с сайта вызова.
    };
}

{
    let x = 2;
    fn func() { /* не паникует */ }
    check!();
}
}

Метки и локальные переменные, определенные в раскрытии макроса, не разделяются между вызовами, поэтому этот код не компилируется:

#![allow(unused)]
fn main() {
macro_rules! m {
    (define) => {
        let x = 1;
    };
    (refer) => {
        dbg!(x);
    };
}

m!(define);
m!(refer);
}

Особым случаем является метапеременная $crate. Она ссылается на крейт, определяющий макрос, и может использоваться в начале пути для поиска элементов или макросов, которые не находятся в области видимости на сайте вызова.

//// Определения в крейте `helper_macro`.
#[macro_export]
macro_rules! helped {
    // () => { helper!() } // Это может привести к ошибке из-за того, что 'helper' не в области видимости.
    () => { $crate::helper!() }
}

#[macro_export]
macro_rules! helper {
    () => { () }
}

//// Использование в другом крейте.
// Обратите внимание, что `helper_macro::helper` не импортирован!
use helper_macro::helped;

fn unit() {
    helped!();
}

Обратите внимание, что, поскольку $crate ссылается на текущий крейт, он должен использоваться с полностью квалифицированным путем модуля при ссылке на не-макросные элементы:

#![allow(unused)]
fn main() {
pub mod inner {
    #[macro_export]
    macro_rules! call_foo {
        () => { $crate::inner::foo() };
    }

    pub fn foo() {}
}
}

Кроме того, даже though $crate позволяет макросу ссылаться на элементы внутри его собственного крейта при раскрытии, его использование не влияет на видимость. На элемент или макрос все еще должна быть видима с сайта вызова. В следующем примере любая попытка вызвать call_foo!() извне его крейта завершится неудачей, потому что foo() не является публичным.

#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! call_foo {
    () => { $crate::foo() };
}

fn foo() {}
}

Note

До Rust 1.30, $crate и local_inner_macros не поддерживались. Они были добавлены вместе с импортом макросов на основе путей, чтобы гарантировать, что вспомогательные макросы не нужно было вручную импортировать пользователям крейта, экспортирующего макросы. Крейты, написанные для более ранних версий Rust, которые используют вспомогательные макросы, должны быть изменены для использования $crate или local_inner_macros, чтобы хорошо работать с импортом на основе путей.

Ограничения неоднозначности множества следования

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

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

В качестве примера, сопоставитель макроса, такой как $i:expr [ , ], теоретически мог бы быть принят в Rust сегодня, поскольку [,] не может быть частью допустимого выражения, и поэтому разбор всегда был бы однозначным. Однако, поскольку [ может начинать trailing выражения, [ не является символом, который можно безопасно исключить как идущий после выражения. Если бы [,] был принят в более поздней версии Rust, этот сопоставитель стал бы неоднозначным или неправильно разбирался, ломая рабочий код. Сопоставители, такие как $i:expr, или $i:expr;, были бы законными, однако, потому что , и ; являются законными разделителями выражений. Конкретные правила:

  • expr и stmt могут следовать только за одним из: =>, , или ;.
  • pat_param может следовать только за одним из: =>, ,, =, |, if или in.
  • pat может следовать только за одним из: =>, ,, =, if или in.
  • path и ty могут следовать только за одним из: =>, ,, =, |, ;, :, >, >>, [, {, as, where или макропеременной со спецификатором фрагмента block.
  • vis может следовать только за одним из: ,, идентификатором, отличным от не-сырого priv, любым токеном, который может начинать тип, или метапеременной со спецификатором фрагмента ident, ty или path.
  • Все остальные спецификаторы фрагментов не имеют ограничений.

2021 Edition differences

До редакции 2021, pat также может следовать за |.

Когда involved повторы, то правила применяются к каждому возможному количеству раскрытий, принимая во внимание разделители. Это означает:

  • Если повтор включает разделитель, этот разделитель должен быть способен следовать за содержимым повтора.
  • Если повтор может повторяться несколько раз (* или +), то содержимое должно быть способно следовать за самим собой.
  • Содержимое повтора должно быть способно следовать за тем, что идет до, и то, что идет после, должно быть способно следовать за содержимым повтора.
  • Если повтор может сопоставляться ноль раз (* или ?), то то, что идет после, должно быть способно следовать за тем, что идет до.

Для более подробной информации см. формальную спецификацию.