Делаем макросы в Rust

от автора

Привет, Хабр!

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

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

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

В этой статье мы как раз и рассмотрим то, как их пишут на Rust.

Начнем с декларативных!

Декларативные макросы

Итак, декларативные макросы в Раст позволяют создавать код, похожий на выражение match в Rust, где значение сравнивается с шаблонами и выполняется код, связанный с соответствующим шаблоном. Это происходит во время компиляции. Для определения макроса используется конструкция macro_rules!. Например, макрос vec! позволяет создавать новый вектор с указанными значениями:

Пример определения макроса vec!:

#[macro_export] macro_rules! vec {     ( $( $x:expr ),* ) => {         {             let mut temp_vec = Vec::new();             $(                 temp_vec.push($x);             )*             temp_vec         }     }; }

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

Рассмотрим макрос power!, который может вычислять квадрат или куб числа:

macro_rules! power {     ($value:expr, squared) => { $value.pow(2_i32) };     ($value:expr, cubed) => { $value.pow(3_i32) }; }

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

Макросы могут принимать переменное количество входных данных. Например, вызов vec![2] или vec![1, 2, 3] использует операторы повторения подобно Regex. Чтобы добавить n чисел, можно использовать следующую конструкцию:

macro_rules! adder {     ($($right:expr),+) => {{     let mut total: i32 = 0;     $(          total += $right;     )+     total }};

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

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

macro_rules! no_trailing {     ($($e:expr),*) => {} };  macro_rules! with_trailing {     ($($e:expr,)*) => {} };  macro_rules! either {     ($($e:expr),* $(,)*) => {} };

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

macro_rules! etwas {     ($value:expr, squared) => {{          let x: u32 = $value;         x.pow(2)     }} };

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

macro_rules! operations {     (add $($addend:expr),+; mult $($multiplier:expr),+) => {{         let mut sum = 0;         $(             sum += $addend;          )*           let mut product = 1;          $(               product *= $multiplier;           )*            println!("Sum: {} | Product: {}", sum, product);     }}  };

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

Процедурные макросы в Rust должны быть определены в отдельных крейтах с типом proc-macro в Cargo.toml файле. Процедурные макросы делятся на три основных типа: функциональные, пользовательские derive и атрибутивные​​​​​​.

Для работы с процедурными макросами нужно будет создать отдельный крейт с типом proc-macro в Cargo.toml файле:

[lib] proc-macro = true

И добавить зависимости syn и quote для парсинга входящих TokenStream и генерации выходного кода.

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

Функциональные макросы в Rust позволяют создавать расширения языка, которые могут принимать код Rust в качестве входных данных и генерировать код Rust в качестве выходных данных. Они похожи на функции в том смысле, что вызываются с использованием оператора ! и выглядят как вызовы функций. Пример простого функционального макроса:

extern crate proc_macro; use proc_macro::TokenStream;  #[proc_macro] pub fn my_fn_like_proc_macro(input: TokenStream) -> TokenStream {     // логика обработки входного TokenStream     // и генерации нового TokenStream.     input }

Макрос my_fn_like_proc_macro принимает TokenStream в качестве входных данных (который представляет собой код, переданный в макрос) и возвращает TokenStream в качестве выходных данных (который является кодом Rust, сгенерированным макросом).

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

use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, Ident};  #[proc_macro] pub fn var_name(input: TokenStream) -> TokenStream {     let input_parsed = parse_macro_input!(input as Ident);          let name = input_parsed.to_string();          let expanded = quote! {         {             let my_var_name = stringify!(#input_parsed);             println!("Переменная: {}, значение: {}", #name, my_var_name);         }     };      TokenStream::from(expanded) }

Юзаем крейты syn для парсинга входных данных макроса в AST и quote для генерации кода Rust на основе этого AST. Макрос var_name принимает имя переменной и генерирует код, который выводит имя этой переменной и её значение.

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

let my_variable = 42; var_name!(my_variable);

Это вызовет макрос var_name, который сгенерирует код для печати имени и значения переменной my_variable.

Пользовательские derive макросы

Пользовательские derive макросы в Rust позволяют автоматически реализовывать определенные трейты для структур или перечислений.

Создадим простой derive макрос, который будет реализовывать трейт Description для структуры или перечисления, предоставляя им метод describe(), возвращающий строковое представление:

// в crate для процедурных макросов, в lib.rs  use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput};  #[proc_macro_derive(Describe)] pub fn derive_describe(input: TokenStream) -> TokenStream {     let input = parse_macro_input!(input as DeriveInput);          let name = input.ident;     let gen = quote! {         impl Description for #name {             fn describe() -> String {                 format!("This is a {}", stringify!(#name))             }         }     };      gen.into() }

Юзаем крейты syn для парсинга входящего TokenStream в структуру DeriveInput, которая предоставляет информацию о типе, к которому применяется макрос. Используем quote! для генерации кода, который реализует трейт Description.

Используем макрос:

// в основном crate  #[derive(Describe)] struct MyStruct;  trait Description {     fn describe() -> String; }  fn main() {     println!("{}", MyStruct::describe()); }

После добавления макроса #[derive(Describe)] к MyStruct, можно метод describe(), который был автоматически реализован для MyStruct благодаря процедурному макросу.

Атрибутивные макросы

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

Создадим атрибутивный макрос log_function, который будет добавлять логирование при входе и выходе из функции:

extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, ItemFn};  #[proc_macro_attribute] pub fn log_function(_attr: TokenStream, item: TokenStream) -> TokenStream {     let input_fn = parse_macro_input!(item as ItemFn);     let fn_name = &input_fn.sig.ident;     let fn_body = &input_fn.block;          let result = quote! {         fn #fn_name() {             println!("Entering {}", stringify!(#fn_name));             #fn_body             println!("Exiting {}", stringify!(#fn_name));         }     };      result.into() }

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

use my_proc_macros::log_function;  #[log_function] fn my_function() {     println!("Function body execution"); }

После добавления атрибута #[log_function] к функции my_function, при ее вызове в консоли будут отображаться соответствующие сообщения о входе и выходе.


Про самые популярные ЯП и практические инструменты мои коллеги из OTUS рассказывают в рамках практических онлайн-курсов. По ссылке вы можете ознакомиться с полным каталогом курсов, а также зарегистрироваться на бесплатные вебинары.


ссылка на оригинал статьи https://habr.com/ru/articles/803303/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *