Как писать макросы rust

Обновлено: 04.07.2024

Rust предоставляет мощную систему макросов, которая позволяет использовать метапрограммирование. Как вы могли видеть в предыдущих главах, макросы выглядят как функции, но их имя заканчивается восклицательным знаком ( ! ). Вместо вызова функции, макросы расширяются в исходный код, который впоследствии компилируется с остальной частью программы. Однако, в отличие от макросов на C и других языках, макросы Rust расширяются в абстрактные синтаксические деревья, а не в подстановку строк, поэтому Вы не получаете неожиданных ошибок приоритета операций.

Макросы создаются с помощью макроса macro_rules!

Так почему же макросы полезны?

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

Предметно-ориентированные языки. Макросы позволяют определить специальный синтаксис для конкретной цели. (Подробнее об этом позже)

Вариативные интерфейсы. Иногда вы хотите объявить интерфейс, принимающий переменное число аргументов. Например, println! , принимающий такое же число аргументов, сколько объявлено в строке с форматом. (Подробнее об этом позже)

Cargo уже взрослый

Cистема сборки компилятора и стандартной библиотеки Rust была переписана на сам Rust с использованием Cargo — стандартного пакетного менеджера и системы сборки, принятой в экосистеме Rust.

С этого момента Cargo является системой сборки по умолчанию. Это был долгий процесс, но он наконец-то принес свои плоды. Авторы утверждают, что новая система сборки используется с декабря прошлого года в master ветке репозитория и пока все идет хорошо.

Теперь файл с названием build.rs , лежащий на одном уровне с Cargo.toml будет интерпретироваться как билд скрипт.

Уже даже завели уже вмержили pull request на удаление всех makefile; интеграция запланирована на релиз 1.17.

Все это готовит почву к прямому использованию пакетов из crates.io для сборки компилятора, как и в любом другом проекте. А еще это неплохая демонстрация возможностей Cargo.

Поддержка IDE

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

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

Это означает, что любой редактор или IDE, которые поддерживают LSP автоматически получают поддержку всех LSP-совместимых языков.

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

Создаем макрос FromUrlQuery

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

Итак, приступим. Для начала объявим наш трейт по разбору URL query. Данный трейт позволит нам получать из произвольной строки структуру данных, для которой этот трейт реализован. Выглядеть он будет так:

Чтобы иметь возможность автоматически реализовывать этот трейт, нам потребуется процедурный макрос. Данный макрос относится к типу макросов derive макросов и должен объявляться следующим образом:

Обычный джентльменский набор писателя процедурных макросов — это библиотеки syn , quote . Первый крейт является парсером Rust синтаксиса, он содержит в себе все типы токенов, встречающиеся в синтаксическом дереве.
Во втором крейте содержится очень важный макрос quote! , который по сути является шаблонизатором и позволяет просто писать некоторый шаблонный Rust код, который будет потом преобразован в выходной набор токенов.
Помимо этих двух обязательных зависимостей есть еще много вспомогательных крейтов, одним из самых полезных, на мой взгяд, является darling . Этот крейт содержит набор процедурных макросов и трейтов для облегчения типовых задач по разбору структур или атрибутов макросов (то есть, входных данных для нашего процедурного макроса).

Под спойлером находится страшный код, писать который мы **не будем**. Просто посмотрите и проникнетесь экзистенциальным ужасом:

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

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

Искомая структура, для которой мы выводим FromUrlQuery , не должна быть пустой, она не должна быть кортежем и для простоты эксперимента она не будет содержать шаблонных параметров и лайфтаймов. То есть, это всегда что-то такого вида:

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

Первый макрос — это FromField , который выводит десериализацию интересующих нас свойств поля структуры из синтаксического дерева:

Хочу отметить, что при этом если бы мы хотели знать, публичное это поле или нет, то могли бы изменить сигнатуру на следующую:

Второй интересующий нас макрос — это FromDeriveInput , который выводит десериализацию уже для целой структуры или перечисления:

И все, на этом наш парсер готов.

Можно приступать к написанию кодогенератора.

Чтобы не перегружать статью сверх меры, мы просто будем делегировать десериализацию URL query в serde . При этом мы спрячем serde максимально глубоко, чтобы он не просочился в обязательные зависимости. Мы будем создавать точную копию нашей структуры и выводить для нее Deserialize , а для реального парсинга запросов будем использовать крейт serde_urlencoded . Но чтобы пользователям не приходилось самим добавлять serde в зависимости, мы в основном крейте сделаем реэкспорты.

А теперь посмотрим, как же будет в реальности выглядеть процесс кодогенерации FromUrlQuery :

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

Вот тут нам уже не поможет FromDeriveInput , нет в darling 'е готового набора инструментов для разбора трейтов, так что придется нам немного повозиться с AST. Но не так черт страшен, как его малюют, поэтому давайте приступим:

Код объявления атрибутных макросов несколько отличается и выглядит вот так:

Есть еще один крайне важный нюанс, над которым мне в свое время пришлось поломать голову: если мы просто решим, что раз уж нам дали целиком синтаксическое дерево трейта со всеми внутренними атрибутами (например, интересующий нас http_api_endpoint ), то мы будем лишь частично правы. Да, они будут видны при разборе TokenStream , но при этом же компилятор будет нам выдавать ошибку "cannot find attribute http_api_endpoint in this scope", что несколько сбивает с толку. Ошибка эта возникает потому, что компилятор не знает о существовании такого атрибута, поэтому не знает, как с ним работать. О том, что этот атрибут нужен для наших внутренних нужд, компилятор не знает, поэтому и реагирует соответственно.

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

Пишем API на Rust с помощью процедурных макросов

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

Тем не менее, многие вполне обоснованно побаиваются пользоваться этим инструментом, в основном из-за того, что разбор синтаксического дерева и атрибутов макроса зачастую превращается в "закат солнца вручную", так как задачу приходится решать на очень низком уровне.

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

Собираем обработчики воедино

Помимо комбинаторов and , которое объединяет фильтры в цепочку, существует еще комбинатор
or , которое позволяет выбирать из двух фильтров подходящий по ситуации, фактически, таким
образом мы организовываем роутинг запросов, причем правила могут быть очень сложными.
Давайте просто взглянем на пример из документации:

И вот теперь мы можем приступить к генерации тела искомой функции serve_ping_interface . Для начала реализуем генерацию соответствующих warp фильтров для соответствующих методов трейта, где service это объект, реализующий бизнес-логику.

А теперь с помощью комбинатора or собираем все фильтры воедино.

Макросы в Rust

Вернемся к нашим баранам.

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

  • Выделение логически законченных частей кода для многократного использования
  • Выделение несамостоятельных фрагментов кода, ничего не значащих вне своего контекста

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

К второму можно отнести макросы, инклуды и прочий препроцессинг. В языке Rust для этого предусмотрено три механизма:

  • Обычные макросы
  • Процедурные макросы
  • Плагины компилятора

Обычные макросы (в документации macro by example) используются, когда хочется избежать повторения однообразного кода, но выделять его в функцию нерационально, либо невозможно. Макросы vec! или println! являются примерами таких макросов. Задаются декларативным образом. Работают по принципу сопоставления и подстановки по образцу. Реализация основана на базе работы 1986-го года, из которой они получили свое полное название.

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

На данный момент компилятором предусмотрено только использование процедурных макросов для поддержки пользовательских атрибутов derive . В будущем количество сценариев будет расширяться.

Плагины компилятора являются самым мощным, но сложным и нестабильным (в смысле API) средством, которое доступно только в ночных сборках компилятора. В документации приведен пример плагина поддержки римских цифр в качестве числовых литералов.

Пример макроса

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

Макро-вставки возможны практически во всех местах иерархии модуля:

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

Например, в стандартной бибилотеке Rust макросы используются для компактного объявления реализации типажа PartialEq для всевозможных сочетаний срезов, массивов и векторов:

Мы же рассмотрим более показательный пример. А именно, реализацию макроса vec! , который выполняет роль конструктора для Vec :

Шаблон сопоставления напоминает регулярные выражения с возможными квантификаторами * и + . Кроме метапеременных через двоеточие указываются еще предполагаемые типы (designator). Например, тип expr соответствует выражению, ident — любому идентификатору, а ty — идентификатору типа. Подробнее про синтаксис макросов написано в руководстве по макросам и в документации, а в porting guide можно найти актуальный разбор макроса vec! с описанием каждой ветви.

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

Чистота и порядок

Макрос в Rust должен быть написан так, чтобы генерировать лексически корректный код. Это означает, что не всякий набор символов может быть валидным макросом. Это позволяет избежать многих проблем, связанных с использованием препроцессора в C/C++.

В безобидном с виду фрагменте кода мы вместо одного элемента вытащили два, вычислили не тот результат, какой ожидали, а последней строкой еще и спровоцировали неопределенное поведение. Три серьезных ошибки на две строки кода — это как-то многовато.

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

Корень зла лежит в том, что препроцессор C/C++ орудует на уровне текста, а компилятору приходится разбирать уже испорченную препроцессором программу.

Напротив, макросы в Rust разбираются и применяются самим компилятором и работают на уровне синтаксического дерева программы. Поэтому описанные выше проблемы не могут возникнуть в принципе.

  • не затеняют переменные
  • не нарушают порядка разбора условий
  • не дают скрытых побочных эффектов
  • не приводят к неопределенному поведению

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

Зато в пределах макроса можно заводить переменные, которые гарантировано не пересекутся с переменными выше по коду. Например, описанный выше макрос vec! можно переписать с использованием промежуточной переменной. Для простоты рассмотрим только основную ветвь:

Таким образом, код

после подстановки макроса будет преобразован в

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

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

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

С точки зрения пользователя использование будет выглядеть так:

Для начала заглянем в исходный код библиотеки, к счастью он не такой большой:

А теперь разберем его по косточкам и попытаемся понять, что он делает.

Во первы́х строках библиотеки задается тип специальный единицы трансляции proc-macro , который говорит, что это будет не абы-что, а плагин к компилятору. Затем подключаются необходимые библиотеки proc_macro и syn со всем инструментарием. Первая задает основные типы, вторая — предоставляет средства парсинга Rust кода в абстрактное синтаксическое дерево (AST). В свою очередь, библиотека quote предоставляет очень важный макрос quote! который мы увидим в действии чуть позже.

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

Далее следует собственно функция, выступающая в роли точки входа в процедурный макрос:

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

Тело функции весьма нехитрое. Сначала мы преобразуем входной набор токенов в строку, а затем разбираем строку как абстрактное синтаксическое дерево. Самое интересное происходит внутри вызова функции new_for_struct() , который принимает AST на вход, а отдает процитированные токены (об этом позже). Наконец, полученные токены преобразуются обратно в строку (не спрашивайте меня, почему так), парсятся в TokenStream и отдаются уже в качестве результата работы макроса компилятору.

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

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

Итак, на вход нам могут подать:

Понятное дело, что синтаксические деревья у всех трех вариантов будут различными. И это нужно учитывать при генерировании метода new() . Собственно, все что делает new_for_struct() , — это смотрит на переданное AST дерево, определяет, с каким вариантом она имеет дело данный момент и генерирует нужную подстановку. А если ей на вход передали незнамо что — она начинает паниковать.

Давайте посмотрим на код, генерирующий подстановку для обычной структуры. Здесь код дробить уже неудобно, поэтому я вставлю комментарии прямо в текст:

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

Если все еще не понятно «как оно работает», взгляните на результат применения процедурного макроса к описанной выше структуре Normal .

Сама структура еще раз:

Результат применения процедурного макроса:

Внезапно, все становится на свои места. Оказывается, мы только что собственноручно сгенерировали impl блок для структуры, добавили в него ассоциированную функцию-конструктор new() с документацией (!), двумя параметрами x и y соответствующих типов и с реализацией, которая возвращает нашу структуру, последовательно инициализируя ее поля значениями из своих параметров.

Поскольку Rust может понять из контекста, чему соответствуют x и y до и после двоеточия, все компилируется успешно.

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

Быстрее! Выше! Сильнее!

Компилятор стал быстрее. А недавно еще и объявили о том, что система инкрементальной компиляции перешла в фазу бета-тестирования. На моих проектах время компиляции после незначительных изменений уменьшилось с

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

Алгоритм slice::sort() был переписан и стал намного, намного, намного быстрее. Теперь это гибридная сортировка, реализованная под влиянием Timsort. Раньше использовалась обычная сортировка слиянием.

В C++ мы можем определить перекрывающую специализацию шаблона для некоторого типа, но пока не можем наложить ограничения на то, какие типы вообще могут использоваться для специализации этого шаблона. Работы в этом направлении ведутся, но пока все очень сложно.

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

В частности, в релизе 1.15 была добавлена специализированная реализация метода extend() для Vec<T> , где T: Copy , которая использует простое линейное копирование регионов памяти, что привело к значительному ускорению.

Помимо этого были ускорены реализации методов chars().count() , chars().last() , и char_indices().last() .

Разбираем методы интерфейсного трейта

Для начала напишем код, который будет разбирать отдельный метод трейта с интерфейсом, который в общем случае будет выглядеть примерно так:

И объявим набор атрибутов для метода, которые мы можем указывать:

Парсить мы будем сигнатуру функции, которая имеет тип syn::Signature , и в этом случае полностью положиться на помощь darling'а мы уже не сможем: большую часть разбора синтаксического дерева придется писать самим, но вот атрибуты методов легко можно получить с помощью знакомого нам уже FromMeta .
А чтобы среди атрибутов метода отыскать нужный нам http_api_endpoint мы напишем небольшую вспомогательную функцию. Мы будем преобразовывать тип нашего атрибута в syn::NestedMeta для того, чтобы была возможность обрабатывать вложенные метаданные вида (foo = "bar", boo(first, second)) .

Теперь можно переходить к разбору сигнатуры. Как я уже упоминал выше, нам нужно рассмотреть
два варианта — с дополнительным аргументом и без оного:

Заключение

Никто не мешает при помощи макросов пойти дальше и выводить еще и openapi или swagger
спецификацию для типажей-интерфейсов. Но мне кажется, в этом случае лучше пойти другим путем и по спецификации написать генератор Rust кода, это даст больший простор для маневров.
Если писать этот генератор в виде build зависимости, то можно воспользоваться библиотеками
syn и quote , таким образом, написание генератора будет очень комфортным и простым. Впрочем, это уже вдаль идущие размышления :)

Полностью рабочий код, примеры которого приводились в данной статье можно найти по этой
ссылке.

Черная магия метапрограммирования: как работают макросы в Rust 1.15


В прошлой статье мы познакомились с одной из самых интересных возможностей языка Rust — процедурными макросами.

Как и обещал, сегодня я расскажу о том, как писать такие макросы самостоятельно и в чем их принципиальное отличие от печально известных макросов препроцессора в C/C++.

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

Что можно почитать?

Язык Rust развивается очень интенсивно. Издатели, натурально, не успевают и не берутся выпускать книги, поскольку они устаревают еще до того, как на страницах высохнет краска.

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

Людям, знакомым с C++, может быть интересна книга, а точнее porting guide, старающаяся подать материал в сравнении с C++ и делающая акцент на различиях языков и на том, какие проблемы Rust решает лучше.

Если вас интересует история развития языка и взгляд с той стороны баррикад, крайне рекомендую блоги Aaron Turon и Niko Matsakis. Ребята пишут очень живым языком и рассказывают о текущих проблемах языка и о том, как предполагается их решать. Зачастую из этих блогов узнаешь куда больше актуальной информации, чем из других источников.

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

Разбираем интерфейсный трейт целиком

Теперь можно приступить к разбору трейта с интерфейсом в целом. Интерфейсный трейт всегда состоит исключительно из методов, разбор которых мы описали выше, а также дополнительных атрибутов.
Таким образом, мы можем разобрать его без особых сложностей:

Переходим к кодогенерации

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

Поэтому, прежде чем приступать к кодогенерации, необходимо разобраться с тем, как устроен warp и каким образом к нему подключаются обработчики запросов. Все в warp'е крутится вокруг концепции, которая называется Filter . Фильтры можно комбинировать в цепочки при помощи комбинаторов and , map , and_then , где каждый наложенный фильтр конкретизирует то, как будет обрабатываться запрос.

Например, если мы хотим просто написать обработчик запросов, который на GET запрос будет просто возвращать некоторый JSON, то мы просто пишем что-то в таком стиле:

Для случая с GET запросами с параметрами мы лишь немного изменим обертку, которую мы написали выше, добавив еще один фильтр в цепочку:

Обработчики остальных двух типов запросов пишутся схожим образом.

Новое в Rust 1.15

С момента выпуска 1.14 прошло около 6 недель. За это время в новый релиз успели войти 1443 патча (неслабо, правда?) исправляющие баги и добавляющие новые возможности. А буквально на днях появился и хотфикс 1.15.1, с небольшими, но важными исправлениями.

За подробностями можно обратиться к странице анонса или к детальному описанию изменений (changelog). Здесь же мы сконцентрируемся на наиболее заметных изменениях.

Предисловие

Прежде всего давайте определим задачу, которую мы будем решать с помощью макросов: мы попробуем определить некоторый абстрактный RPC API в виде трейта, который потом реализует как серверная часть, так и клиентская; а процедурные макросы, в свою очередь, помогут обойтись нам без кучи шаблонного кода. Несмотря на то, что реализовывать мы будем несколько абстрактный API, задача на самом деле довольно жизненная, и, помимо прочего, идеально подходит для демонстрации возможностей процедурных макросов.

Сам API у нас будет выполнен по очень простому принципу: есть 4 типа запросов:

  • GET запросы без параметров, например: /ping .
  • GET запросы с параметрами, параметры к которым будут передаваться в виде URL query, например: /status?name=foo&count=15 .
  • POST запросы без параметров.
  • POST запросы с параметрами, которые передаются в виде JSON объектов.

Во всех случаях сервер будет отвечать валидным JSON объектом.

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

В идеале хочется получить нечто подобное:

Для начала напомню, что процедурными макросами в Rust'е называются специальные плагины к компилятору, которые получают на вход некоторое синтаксическое дерево, с которым производят некоторые манипуляции, а затем возвращают модифицированное дерево для последующей компиляции.
В рамках этой статьи мы будем рассматривать два вида таких макросов: derive-макросы, позволяющие автоматически реализовать трейт для какой-то структуры (многим они уже знакомы по serde ), и атрибутные макросы, которые можно использовать для большего спектра задач.

Итоги

Далее мы применим все, что мы обсуждали в книге и сделаем ещё один проект!

The Rust Programming Language

Мы использовали макросы, такие как println! на протяжении всей этой книги, но мы не изучили полностью, что такое макрос и как он работает. Термин макрос относится к семейству возможностей в Rust. Это декларативные (declarative) макросы с помощью macro_rules! и три вида процедурных (procedural) макросов:

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

Разница между макросами и функциями

По сути, макросы являются способом написания кода, который записывает другой код, что известно как мета программирование. В приложении C мы обсуждаем атрибут derive , который генерирует за вас реализацию различных типажей. Вы также использовали макросы println! и vec! в книге. Все эти макросы раскрываются для генерации большего количества кода, чем исходный код написанный вами вручную.

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

Сигнатура функции должна объявлять некоторое количество и тип этих параметров имеющихся у функции. Макросы, с другой стороны, могут принимать переменное число параметров: мы можем вызвать println!("hello") с одним аргументом или println!("hello <>", name) с двумя аргументами. Также макросы раскрываются до того как компилятор интерпретирует смысл кода, поэтому макрос может, например, реализовать типаж заданного типа. Функция этого не может, потому что она вызывается во время выполнения и типаж должен быть реализован во время компиляции.

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

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

Декларативные макросы с macro_rules! для общего мета программирования

Наиболее широко используемой формой макросов в Rust являются декларативные макросы. Они также иногда упоминаются как "макросы на примере", " macro_rules! макрос" или просто "макросы". По своей сути декларативные макросы позволяют писать нечто похожее на выражение match в Rust. Как обсуждалось в главе 6, match выражения являются управляющими структурами, которые принимают некоторое выражение, результат значения выражения сопоставляют с шаблонами, а затем запускают код для сопоставляемой ветки. Макросы также сравнивают значение с шаблонами, которые связаны с конкретным кодом: в этой ситуации значение является литералом исходного кода Rust, переданным в макрос. Шаблоны сравниваются со структурами этого исходного кода и при совпадении код, связанный с каждым шаблоном, заменяет код переданный макросу. Все это происходит во время компиляции.

Для определения макроса используется конструкция macro_rules! . Давайте рассмотрим, как использовать macro_rules! глядя на то, как объявлен макрос vec! . В главе 8 рассказано, как можно использовать макрос vec! для создания нового вектора с определёнными значениями. Например, следующий макрос создаёт новый вектор, содержащий три целых числа:

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

В листинге 19-28 приведено несколько упрощённое определение макроса vec! .

Листинг 19-28: Упрощённая версия определения макроса vec!

Примечание: фактическое определение макроса vec! в стандартной библиотеке включает сначала код для предварительного выделения правильного объёма памяти. Этот код является оптимизацией, которую мы здесь не включаем, чтобы сделать пример проще.

Затем мы начинаем объявление макроса с помощью macro_rules! и имени макроса, который объявляется без восклицательного знака. Название, в данном случае vec , после которого следуют фигурные скобки, указывающие тело определения макроса.

Структура в теле макроса vec! похожа на структуру match выражения. Здесь у нас есть одна ветвь с шаблоном ( $( $x:expr ),* ) , затем следует ветвь => и блок кода, связанный с этим шаблоном. Если шаблон сопоставлен успешно, то соответствующий блок кода будет сгенерирован. Учитывая, что данный код является единственным шаблоном в этом макросе, существует только один действительный способ сопоставления, любой другой шаблон приведёт к ошибке. Более сложные макросы будут иметь более чем одна ветвь.

Допустимый синтаксис шаблона в определениях макросов отличается от синтаксиса шаблона рассмотренного в главе 18, потому что шаблоны макроса сопоставляются со структурами кода Rust, а не со значениями. Давайте пройдёмся по тому, какие части шаблона в листинге 19-28 что означают; полный синтаксис макроса см. в ссылке.

Во-первых, набор скобок охватывает весь шаблон. Далее идёт знак доллара ( $ ), затем следует набор скобок, который захватывает значения, соответствующие шаблону в скобках для использования в коде замены. Внутри $() находится $x:expr , который соответствует любому выражению Rust и даёт выражению имя $x .

Запятая, следующая за $() указывает на то, что буквенный символ-разделитель запятой может дополнительно появиться после кода, который соответствует коду в $() . Звёздочка * указывает, что шаблон соответствует ноль или больше раз тому, что предшествует * .

Когда вызывается этот макрос с помощью vec![1, 2, 3]; шаблон $x соответствует три раза всем трём выражениям 1 , 2 и 3 .

Теперь давайте посмотрим на шаблон в теле кода, связанного с этой ветвью: temp_vec.push() внутри $()* генерируется для каждой части, которая соответствует символу $() в шаблоне ноль или более раз в зависимости от того, сколько раз шаблон сопоставлен. Символ $x заменяется на каждое совпадающее выражение. Когда мы вызываем этот макрос с vec![1, 2, 3]; , сгенерированный код, заменяющий этот вызов макроса будет следующим:

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

Есть несколько странных краевых случаев у макроса macro_rules! . В будущем у Rust будет второй вид декларативного макроса, который будет работать аналогичным образом, но поправит некоторые из этих краевых случаев. После этого обновления macro_rules! будет фактически устаревшим. Имея это в виду, а также тот факт, что большинство Rust программистов будут использовать макросы больше, чем сами писать макросы, мы далее не будем обсуждать macro_rules! . Чтобы узнать больше о том, как писать макросы, обратитесь к электронной документации или другим ресурсам, таким как The Little Book of Rust Macros”.

Процедурные макросы для генерации кода из атрибутов

Вторая форма макросов - это процедурные макросы (procedural macros), которые действуют как функции (и являются типом процедуры). Процедурные макросы принимают некоторый код в качестве входных данных, работают над этим кодом и создают некоторый код в качестве вывода, а не выполняют сопоставления с шаблонами и замену кода другим кодом, как это делают декларативные макросы.

Все три вида процедурных макросов (пользовательские выводимые, похожие на атрибуты и похожие на функции) все работают аналогично.

При создании процедурных макросов объявления должны находиться в собственном крейте специального типа. Это из-за сложных технических причин, которые мы надеемся будут устранены в будущем. Использование процедурных макросов выглядит как код в листинге 19-29, где some_attribute является заполнителем для использования специального макроса.

Листинг 19-29: Пример использования процедурного макроса

Функция, которая определяет процедурный макрос, принимает TokenStream в качестве входных данных и создаёт TokenStream в качестве вывода. Тип TokenStream объявлен крейтом proc_macro , включённым в Rust и представляет собой последовательность токенов. Это ядро макроса: исходный код над которым работает макрос, является входным TokenStream , а код создаваемый макросом является выходным TokenStream . К функции имеет также прикреплённый атрибут, определяющий какой тип процедурного макроса мы создаём. Можно иметь несколько видов процедурных макросов в одном и том же крейте.

Давайте посмотрим на различные виды процедурных макросов. Начнём с пользовательского, выводимого (derive) макроса и затем объясним небольшие различия, делающие другие формы отличающимися.

Как написать пользовательский derive макрос

Листинг 19-30: Код, который сможет писать пользователь нашего крейта при использовании нашего процедурного макроса

Этот код напечатает Hello, Macro! My name is Pancakes! , когда мы закончим. Первый шаг - создать новый, библиотечный крейт так:

Далее, мы определим типаж HelloMacro и ассоциированную с ним функцию:

У нас есть типаж и его функция. На этом этапе пользователь крейта может реализовать типаж для достижения желаемой функциональности, так:

Тем не менее, ему придётся написать блок реализации для каждого типа, который он хотел использовать вместе с hello_macro ; а мы хотим избавить их от необходимости делать эту работу.

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

Следующим шагом является определение процедурного макроса. На момент написания этой статьи процедурные макросы должны быть в собственном крейте. Со временем это ограничение может быть отменено. Соглашение о структурировании крейтов и макросов является следующим: для крейта с именем foo , его пользовательский, крейт с выводимым процедурным макросом называется foo_derive . Давайте начнём с создания нового крейта с именем hello_macro_derive внутри проекта hello_macro :

Наши два крейта тесно связаны, поэтому мы создаём процедурный макрос-крейт в каталоге крейта hello_macro . Если мы изменим определение типажа в hello_macro , то нам придётся также изменить реализацию процедурного макроса в hello_macro_derive . Два крейта нужно будет опубликованы отдельно и программисты, использующие эти крейты, должны будут добавить их как зависимости, а затем добавить их в область видимости. Мы могли вместо этого сделать так, что крейт hello_macro использует hello_macro_derive как зависимость и реэкспортирует код процедурного макроса. Однако то, как мы структурировали проект, делает возможным программистам использовать hello_macro даже если они не хотят derive функциональность.

Нам нужно объявить крейт hello_macro_derive как процедурный макрос-крейт. Также понадобятся функционал из крейтов syn и quote , как вы увидите через мгновение, поэтому нам нужно добавить их как зависимости. Добавьте следующее в файл Cargo.toml для hello_macro_derive :

Чтобы начать определение процедурного макроса, поместите код листинга 19-31 в ваш файл src/lib.rs крейта hello_macro_derive . Обратите внимание, что этот код не скомпилируется пока мы не добавим определение для функции impl_hello_macro .

Листинг 19-31: Код, который потребуется в большинстве процедурных макро крейтов для обработки Rust кода

Обратите внимание, что мы разделили код на функцию hello_macro_derive , которая отвечает за синтаксический анализ TokenStream и функцию impl_hello_macro , которая отвечает за преобразование синтаксического дерева: это делает написание процедурного макроса удобнее. Код во внешней функции ( hello_macro_derive в данном случае) будет одинаковым для почти любого процедурного макрос крейта, который вы видите или создаёте. Код, который вы указываете в теле внутренней функции (в данном случае impl_hello_macro ) будет отличаться в зависимости от цели вашего процедурного макроса.

Мы представили три новых крейта: proc_macro syn и quote . Макрос proc_macro поставляется с Rust, поэтому нам не нужно было добавлять его в зависимости внутри Cargo.toml. Макрос proc_macro - это API компилятора, который позволяет нам читать и манипулировать Rust кодом из нашего кода.

Крейт syn разбирает Rust код из строки в структуру данных над которой мы может выполнять операции. Крейт quote превращает структуры данных syn обратно в код Rust. Эти крейты упрощают разбор любого вида Rust кода, который мы хотели бы обрабатывать: написание полного синтаксического анализатора для кода Rust не является простой задачей.

Функция hello_macro_derive сначала преобразует input из TokenStream в структуру данных, которую мы можем затем интерпретировать и над которой выполнять операции. Здесь крейт syn вступает в игру. Функция parse в syn принимает TokenStream и возвращает структуру DeriveInput , представляющую разобранный код Rust. Листинг 19-32 показывает соответствующие части структуры DeriveInput , которые мы получаем при разборе строки struct Pancakes; :

Листинг 19-32: Экземпляр DeriveInput получаемый, когда разбирается код имеющий атрибут макроса из Листинга 19-30

Поля этой структуры показывают, что код Rust, который мы разобрали, является блок структуры с ident (идентификатором, означающим имя) для Pancakes . Есть больше полей в этой структуре для описания всех видов кода Rust; проверьте документацию syn о структуре DeriveInput для получения дополнительной информации.

Вскоре мы определим функцию impl_hello_macro , в которой построим новый, дополнительный код Rust. Но прежде чем мы это сделаем, обратите внимание, что выводом для нашего выводимого (derive) макроса также является TokenStream . Возвращаемый TokenStream добавляется в код, написанный пользователями макроса, поэтому, когда они соберут свой крейт, они получат дополнительную функциональность, которую мы предоставляем в изменённом TokenStream .

Теперь, когда у нас есть код для преобразования аннотированного Rust кода из TokenStream в экземпляр DeriveInput , давайте сгенерируем код реализующий типаж HelloMacro у аннотированного типа, как показано в листинге 19-33.

Листинг 19-33. Реализация типажа HelloMacro с использованием проанализированного кода Rust.

Мы получаем экземпляр структуры Ident содержащий имя (идентификатор) аннотированного типа с использованием ast.ident . Структура в листинге 19-32 показывает, что когда мы запускаем функцию impl_hello_macro для кода из листинга 19-30, то получаемый ident будет иметь поле ident со значением "Pancakes" . Таким образом, переменная name в листинге 19-33 будет содержать экземпляр структуры Ident , что при печати выдаст строку "Pancakes" , что является именем структуры в листинге 19-30.

Макрос quote! позволяет определить код Rust, который мы хотим вернуть. Компилятор ожидает что-то отличное от прямого результата выполнения макроса quote! , поэтому нужно преобразовать его в TokenStream . Мы делаем это путём вызова метода into , который использует промежуточное представление и возвращает значение требуемого типа TokenStream .

На этом этапе команда cargo build должна завершиться успешно для обоих hello_macro и hello_macro_derive . Давайте подключим эти крейты к коду в листинге 19-30, чтобы увидеть процедурный макрос в действии! Создайте новый бинарный проект в каталоге ваших проектов с использованием команды cargo new pancakes . Нам нужно добавить hello_macro и hello_macro_derive в качестве зависимостей для крейта pancakes в файл Cargo.toml. Если вы публикуете свои версии hello_macro и hello_macro_derive на сайт crates.io, они будут обычными зависимостями; если нет, вы можете указать их как path зависимости следующим образом:

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

подобные атрибутам макросы

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

Здесь есть два параметра типа TokenStream . Первый для содержимого атрибута: часть GET, "/" . Второй это тело элемента, к которому прикреплён атрибут: в данном случае fn index() <> и остальная часть тела функции.

Кроме того, атрибутные макросы работают так же как и пользовательские выводимые макросы: вы создаёте крейт с типом proc-macro и реализуете функцию, которая генерирует код, который хотите!

Функционально подобные макросы

Функционально подобные макросы выглядят подобно вызову функций. Они аналогично макросам macro_rules! и являются более гибкими, чем функции; например, они могут принимать неизвестное количество аргументов. Тем не менее, макросы macro_rules! можно объявлять только с использованием синтаксиса подобного сопоставлению, который мы обсуждали ранее в разделе "Декларативные макросы macro_rules! для общего мета программирования". Функционально подобные макросы принимают параметр TokenStream и их определение манипулирует этим TokenStream , используя код Rust, как это делают два других типа процедурных макроса. Примером подобного функционально подобного макроса является макрос sql!/code6>, который можно вызвать так:

Этот макрос будет разбирать SQL оператор внутри него и проверять, что он синтаксически правильный, что является гораздо более сложной обработкой, чем то что может сделать макрос macro_rules! . Макрос sql! мог бы быть определён так:

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

Новые архитектуры

У раста появилась поддержка уровня Tier 3 для архитектур i686-unknown-openbsd , MSP430 , и ARMv5TE . Недавно стало известно, что в релизе LLVM 4.0 появляется поддержка архитектуры микроконтроллеров AVR. Разработчики Rust в курсе этого и уже готовятся почти все сделали для интеграции новой версии LLVM и новой архитектуры.

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

Заключение

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

Надеюсь, что статья оказалась вам полезной. А если после ее прочтения вы еще и захотели поиграться с языком Rust, я буду считать свою задачу выполненной полностью :)

Читайте также: