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

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

Процедурные макросы позволяют создавать расширения синтаксиса как выполнение функции. Процедурные макросы бывают трех видов:

Процедурные макросы позволяют запускать код во время компиляции, который работает с синтаксисом Rust, как потребляя, так и производя синтаксис Rust. Можно представить процедурные макросы как функции из AST в другое AST.

Процедурные макросы должны быть определены в корне крейта с типом крейта proc-macro. Макросы не могут использоваться из крейта, где они определены, и могут использоваться только при импорте в другом крейте.

Note

При использовании Cargo крейты процедурных макросов определяются с помощью ключа proc-macro в манифесте:

[lib]
proc-macro = true

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

Процедурные макросы выполняются во время компиляции и thus имеют те же ресурсы, что и компилятор. Например, стандартный ввод, ошибка и вывод те же, к которым имеет доступ компилятор. Аналогично, доступ к файлам тот же. Из-за этого процедурные макросы имеют те же проблемы безопасности, что и сборочные скрипты Cargo.

Процедурные макросы имеют два способа сообщения об ошибках. Первый - паника. Второй - испускать вызов макроса compile_error.

Крейт proc_macro

Крейты процедурных макросов почти всегда будут линковаться с предоставляемым компилятором крейтом proc_macro. Крейт proc_macro предоставляет типы, необходимые для написания процедурных макросов, и средства для облегчения этого.

Этот крейт в основном содержит тип TokenStream. Процедурные макросы работают с потоками токенов вместо узлов AST, что является гораздо более стабильным интерфейсом со временем как для компилятора, так и для процедурных макросов. Поток токенов примерно эквивалентен Vec<TokenTree>, где TokenTree можно roughly рассматривать как лексический токен. Например, foo - это токен Ident, . - токен Punct, а 1.2 - токен Literal. Тип TokenStream, в отличие от Vec<TokenTree>, дешев для клонирования.

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

Гигиена процедурных макросов

Процедурные макросы негигиеничны. Это означает, что они ведут себя так, как если бы выходной поток токенов был просто записан inline в код рядом с ним. Это означает, что он подвержен влиянию внешних элементов и также влияет на внешние импорты.

Авторам макросов нужно быть осторожными, чтобы обеспечить работу их макросов в как можно большем количестве контекстов с учетом этого ограничения. Это часто включает использование абсолютных путей к элементам в библиотеках (например, ::std::option::Option вместо Option) или обеспечение того, чтобы сгенерированные функции имели имена, которые вряд ли будут конфликтовать с другими функциями (например, __internal_foo вместо foo).

Атрибут proc_macro

Атрибут proc_macro определяет функциональноподобный процедурный макрос.

Example

Это определение макроса игнорирует его ввод и испускает функцию answer в свою область видимости.

#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    "fn answer() -> u32 { 42 }".parse().unwrap()
}

Мы можем использовать его в бинарном крейте для вывода “42” в стандартный вывод.

extern crate proc_macro_examples;
use proc_macro_examples::make_answer;

make_answer!();

fn main() {
    println!("{}", answer());
}

Атрибут proc_macro использует синтаксис MetaWord.

Атрибут proc_macro может применяться только к pub функции типа fn(TokenStream) -> TokenStream, где TokenStream происходит из proc_macro крейта. Он должен иметь “Rust” ABI. Другие квалификаторы функций не разрешены. Он должен находиться в корне крейта.

Атрибут proc_macro может быть указан только один раз для функции.

Атрибут proc_macro публично определяет макрос в пространстве имен макросов в корне крейта с тем же именем, что и функция.

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

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

Атрибут proc_macro_derive

Применение атрибута proc_macro_derive к функции определяет макрос наследования (derive), который может быть вызван атрибутом derive. Эти макросы получают поток токенов определения структуры struct, перечисления enum или объединения union и могут испускать новые элементы после него. Они также могут объявлять и использовать вспомогательные атрибуты макросов наследования.

Example

Этот макрос наследования игнорирует его ввод и добавляет токены, определяющие функцию.

#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro_derive(AnswerFn)]
pub fn derive_answer_fn(_item: TokenStream) -> TokenStream {
    "fn answer() -> u32 { 42 }".parse().unwrap()
}

Чтобы использовать его, мы можем написать:

extern crate proc_macro_examples;
use proc_macro_examples::AnswerFn;

#[derive(AnswerFn)]
struct Struct;

fn main() {
    assert_eq!(42, answer());
}

Синтаксис для атрибута proc_macro_derive:

Syntax
ProcMacroDeriveAttribute
    proc_macro_derive ( DeriveMacroName ( , DeriveMacroAttributes )? ,? )

DeriveMacroNameIDENTIFIER

DeriveMacroAttributes
    attributes ( ( IDENTIFIER ( , IDENTIFIER )* ,? )? )

Имя макроса наследования задается DeriveMacroName. Опциональный аргумент attributes описан в macro.proc.derive.attributes.

Атрибут proc_macro_derive может применяться только к pub функции с Rust ABI, определенной в корне крейта, с типом fn(TokenStream) -> TokenStream, где TokenStream происходит из proc_macro крейта. Функция может быть const и может использовать extern для явного указания Rust ABI, но она не может использовать любые другие квалификаторы (например, она не может быть async или unsafe).

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

Атрибут proc_macro_derive публично определяет макрос наследования в пространстве имен макросов в корне крейта.

Входной TokenStream - это поток токенов элемента, к которому применен атрибут derive. Выходной TokenStream должен быть (возможно пустым) набором элементов. Эти элементы добавляются после входного элемента в том же модуле или блоке.

Вспомогательные атрибуты макросов наследования

Макросы наследования могут объявлять вспомогательные атрибуты макросов наследования для использования в области видимости элемента, к которому применен макрос наследования. Эти атрибуты являются инертными. Хотя их цель - быть использованными макросом, который их объявил, они могут быть видны любому макросу.

Вспомогательный атрибут для макроса наследования объявляется путем добавления его идентификатора в список attributes в атрибуте proc_macro_derive.

Example

Это объявляет вспомогательный атрибут, а затем игнорирует его.

#![crate_type="proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro_derive(WithHelperAttr, attributes(helper))]
pub fn derive_with_helper_attr(_item: TokenStream) -> TokenStream {
    TokenStream::new()
}

Чтобы использовать его, мы можем написать:

#[derive(WithHelperAttr)]
struct Struct {
    #[helper] field: (),
}

Атрибут proc_macro_attribute

Атрибут proc_macro_attribute определяет макрос атрибута, который может использоваться как внешний атрибут.

Example

Этот макрос атрибута принимает входной поток и испускает его как есть, фактически являясь атрибутом-заглушкой.

#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn return_as_is(_attr: TokenStream, item: TokenStream) -> TokenStream {
    item
}

Example

Это показывает, в выводе компилятора, строковое представление TokenStream, которые видят макросы атрибутов.

// my-macro/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
    println!("attr: \"{attr}\"");
    println!("item: \"{item}\"");
    item
}
// src/lib.rs
extern crate my_macro;

use my_macro::show_streams;

// Пример: Базовая функция.
#[show_streams]
fn invoke1() {}
// вывод: attr: ""
// вывод: item: "fn invoke1() {}"

// Пример: Атрибут с вводом.
#[show_streams(bar)]
fn invoke2() {}
// вывод: attr: "bar"
// вывод: item: "fn invoke2() {}"

// Пример: Несколько токенов во вводе.
#[show_streams(multiple => tokens)]
fn invoke3() {}
// вывод: attr: "multiple => tokens"
// вывод: item: "fn invoke3() {}"

// Пример: Разделители во вводе.
#[show_streams { delimiters }]
fn invoke4() {}
// вывод: attr: "delimiters"
// вывод: item: "fn invoke4() {}"

Атрибут proc_macro_attribute использует синтаксис MetaWord.

Атрибут proc_macro_attribute может применяться только к pub функции типа fn(TokenStream, TokenStream) -> TokenStream, где TokenStream происходит из proc_macro крейта. Он должен иметь “Rust” ABI. Другие квалификаторы функций не разрешены. Он должен находиться в корне крейта.

Атрибут proc_macro_attribute может быть указан только один раз для функции.

Атрибут proc_macro_attribute определяет атрибут в пространстве имен макросов в корне крейта с тем же именем, что и функция.

Макросы атрибутов могут использоваться только на:

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

Второй TokenStream - это остальная часть элемента, включая другие атрибуты на элементе.

Элемент, к которому применен атрибут, заменяется нулем или более элементами в возвращенном TokenStream.

Токены декларативных макросов и процедурных макросов

Декларативные макросы macro_rules и процедурные макросы используют похожие, но разные определения для токенов (или, точнее, TokenTree).

Деревья токенов в macro_rules (соответствующие сопоставителям tt) определяются как:

  • Разделенные группы ((...), {...}, и т.д.)
  • Все операторы, поддерживаемые языком, как односимвольные, так и многосимвольные (+, +=).
    • Обратите внимание, что этот набор не включает одиночную кавычку '.
  • Литералы ("string", 1, и т.д.)
    • Обратите внимание, что отрицание (например, -1) никогда не является частью таких литеральных токенов, а является отдельным токеном оператора.
  • Идентификаторы, включая ключевые слова (ident, r#ident, fn)
  • Времена жизни ('ident)
  • Подстановки метапеременных в macro_rules (например, $my_expr в macro_rules! mac { ($my_expr: expr) => { $my_expr } } после раскрытия mac, который будет рассматриваться как одно дерево токенов независимо от переданного выражения)

Деревья токенов в процедурных макросах определяются как:

  • Разделенные группы ((...), {...}, и т.д.)
  • Все знаки пунктуации, используемые в операторах, поддерживаемых языком (+, но не +=), а также одиночный символ кавычки ' (обычно используемый в временах жизни, см. ниже поведение разделения и склеивания времен жизни)
  • Литералы ("string", 1, и т.д.)
    • Отрицание (например, -1) поддерживается как часть целочисленных и литералов с плавающей точкой.
  • Идентификаторы, включая ключевые слова (ident, r#ident, fn)

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

При передаче в процедурный макрос:

  • Все многосимвольные операторы разбиваются на отдельные символы.
  • Времена жизни разбиваются на символ ' и идентификатор.
  • Ключевое слово-метапеременная $crate передается как одиночный идентификатор.
  • Все другие подстановки метапеременных представляются как их underlying потоки токенов.
    • Такие потоки токенов могут быть обернуты в разделенные группы (Group) с неявными разделителями (Delimiter::None), когда это необходимо для сохранения приоритетов разбора.
    • Подстановки tt и ident никогда не оборачиваются в такие группы и всегда представляются как их underlying деревья токенов.

При испускании из процедурного макроса:

  • Символы пунктуации склеиваются в многосимвольные операторы, когда применимо.
  • Одиночные кавычки ', соединенные с идентификаторами, склеиваются во времена жизни.
  • Отрицательные литералы преобразуются в два токена ( - и литерал) возможно обернутых в разделенную группу (Group) с неявными разделителями (Delimiter::None), когда это необходимо для сохранения приоритетов разбора.

Обратите внимание, что ни декларативные, ни процедурные макросы не поддерживают токены документирующих комментариев (например, /// Doc), поэтому они всегда преобразуются в потоки токенов, представляющие их эквивалентные атрибуты #[doc = r"str"], когда передаются в макросы.