
Привет, Хабр!
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/
Добавить комментарий