Макрос в раст что это
Перейти к содержимому

Макрос в раст что это

  • автор:

Макросы

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

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

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

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

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

Определение макросов (Макроопределения)

Вы, возможно, видели макрос vec! , который используется для инициализации вектора с произвольным количеством элементов.

let x: Vecu32> = vec![1, 2, 3]; # assert_eq!(x, [1, 2, 3]); 

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

let x: Vecu32> = < let mut temp_vec = Vec::new(); temp_vec.push(1); temp_vec.push(2); temp_vec.push(3); temp_vec >; # assert_eq!(x, [1, 2, 3]); 

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

actual . Фактическое определение vec! в libcollections отличается от ↩

 представленного здесь по соображениям эффективности и повторного использования. 
macro_rules! vec < ( $( $x:expr ),* ) =>< < let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec > >; > # fn main() < # assert_eq!(vec![1,2,3], [1, 2, 3]); # > 

Ого, тут много нового синтаксиса! Давайте разберем его.

macro_rules! vec

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

Сопоставление (Matching) (Синтаксис вызова макрокоманды)

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

( $( $x:expr ),* ) => < . >; 

Это очень похоже на конструкцию match , но сопоставление происходит на уровне синтаксических деревьев Rust, на этапе компиляции. Точка с запятой не является обязательной для последнего (только здесь) варианта. «Образец» слева от => известен как шаблон совпадений (образец) (обнаружитель совпадений) (matcher). Он имеет свою собственную грамматику в рамках языка.

Образец $x:expr будет соответствовать любому выражению Rust, связывая его дерево синтаксиса с метапеременной $x . Идентификатор expr является спецификатором фрагмента; полные возможности перечислены далее в этой главе. Образец, окруженный $(. ),* , будет соответствовать нулю или более выражениям, разделенным запятыми.

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

macro_rules! foo < (x =>$e:expr) => (println!("mode X: <>", $e)); (y => $e:expr) => (println!("mode Y: <>", $e)); > fn main() < foo!(y =>3); > 
mode Y: 3 
foo!(z => 3); 

мы получим ошибку компиляции

error: no rules expected the token `z` 

Развертывание (Expansion) (Синтаксис преобразования макрокоманды)

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

$( temp_vec.push($x); )* 

Каждое соответствующее выражение $x будет генерировать одиночный оператор push в развернутой форме макроса. Повторение в развернутой форме происходит синхронно с повторением в форме образца (более подробно об этом чуть позже).

Поскольку $x уже объявлен в образце как выражение, мы не повторяем :expr с правой стороны. Кроме того, мы не включаем разделяющую запятую в качестве части оператора повторения. Вместо этого, у нас есть точка с запятой в пределах повторяемого блока.

Еще одна деталь: макрос vec! имеет две пары фигурных скобках правой части. Они часто сочетаются таким образом:

macro_rules! foo < () =>> > 

Внешние скобки являются частью синтаксиса macro_rules! . На самом деле, вы можете использовать () или [] вместо них. Они просто разграничивают правую часть в целом.

Внутренние скобки являются частью расширенного синтаксиса. Помните, что макрос vec! используется в контексте выражения. Мы используем блок, для записи выражения с множественными операторами, в том числе включающее let привязки. Если ваш макрос раскрывается в одно единственное выражение, то дополнительной слой скобок не нужен.

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

Повторение (Repetition) (Многовариантность)

Операции повтора всегда сопутствуют два основных правила:

  1. $(. )* проходит через один «слой» повторений, для всех $name , которые он содержит, в ногу, и
  2. каждое $name должно быть под, по крайней мере, стольким количеством $(. )* , сколько было использовано при сопоставлении. Если оно под большим числом $(. )* , $name будет дублироваться, при необходимости.

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

macro_rules! o_O < ( $( $x:expr; [ $( $y:expr ),* ] );* ) => < &[ $($( $x + $y ),*),* ] >> fn main() < let a: &[i32] = o_O!(10; [1, 2, 3]; 20; [4, 5, 6]); assert_eq!(a, [11, 12, 13, 24, 25, 26]); > 

Это наибольшая синтаксиса совпадений. Эти примеры используют конструкцию $(. )* , которая означает «ноль или более» совпадений. Также вы можете написать $(. )+ , что будет означать «одно или более» совпадений. Обе формы записи включают необязательный разделитель, располагающийся сразу за закрывающей скобкой, который может быть любым символом, за исключением + или * .

Эта система повторений основана на «Macro-by-Example» (PDF ссылка).

Гигиена (Hygiene)

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

#define FIVE_TIMES(x) 5 * x int main()

После развертывания мы получаем 5 * 2 + 3 , но умножение имеет больший приоритет чем сложение. Если вы часто использовали C макросы, вы, наверное, знаете стандартные идиомы для устранения этой проблемы, а также пять или шесть других проблем. В Rust мы можем не беспокоиться об этом.

macro_rules! five_times < ($x:expr) =>(5 * $x); > fn main() < assert_eq!(25, five_times!(2 + 3)); > 

Метапеременная $x обрабатывается как единый узел выражения, и сохраняет свое место в дереве синтаксиса даже после замены.

Другой распространенной проблемой в системе макросов является захват переменной (variable capture). Вот C макрос, использующий GNU C расширение, который эмулирует блоки выражениий в Rust.

#define LOG(msg) ( < \ int state = get_log_state(); \ if (state >0) < \ printf("log(%d): %s\n", state, msg); \ >\ >) 

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

const char *state = "reticulating splines"; LOG(state) 

Он раскрывается в

const char *state = "reticulating splines"; int state = get_log_state(); if (state > 0)

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

Эквивалентный макрос в Rust обладает требуемым поведением.

# fn get_log_state() -> i32 < 3 > macro_rules! log < ($msg:expr) =>let state: i32 = get_log_state(); if state > 0 < println!("log(<>): <>", state, $msg); > >>; > fn main() < let state: &str = "reticulating splines"; log!(state); > 

Это работает, потому что Rust имеет систему макросов с соблюдением гигиены. Раскрытие каждого макроса происходит в отдельном контексте синтаксиса, и каждая переменная обладает меткой контекста синтаксиса, где она была введена. Это как если бы переменная state внутри main была бы окрашена в другой «цвет» в отличае от переменной state внутри макроса, из-за чего они бы не конфликтовали.

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

macro_rules! foo < () =>(let x = 3); > fn main() < foo!(); println!("<>", x); > 

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

macro_rules! foo < ($v:ident) =>(let $v = 3); > fn main() < foo!(x); println!("<>", x); > 

Это справедливо для let привязок и меток loop, но не для элементов. Код, приведенный ниже, компилируется:

macro_rules! foo < () =>(fn x() < >); > fn main()

Рекурсия макросов

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

# #![allow(unused_must_use)] macro_rules! write_html < ($w:expr, ) =>(()); ($w:expr, $e:tt) => (write!($w, "<>", $e)); ($w:expr, $tag:ident [ $($inner:tt)* ] $($rest:tt)*) => << write!($w, "<<>>", stringify!($tag)); write_html!($w, $($inner)*); write!($w, ">", stringify!($tag)); write_html!($w, $($rest)*); >>; > fn main() < # // FIXME(#21826) use std::fmt::Write; let mut out = String::new(); write_html!(&mut out, html[ head[title["Macros guide"]] body[h1["Macros are the best!"]] ]); assert_eq!(out, "Macros guide\ 

Macros are the best!

"
); >

Отладка макросов

Чтобы увидеть результаты расширения макросов, выполните команду rustc —pretty expanded . Вывод представляет собой целый контейнер, так что вы можете подать его обратно в rustc , что иногда выдает лучшие сообщения об ошибках, чем при обычной компиляции. Обратите внимание, что вывод —pretty expanded может иметь разное значение, если несколько переменных, имеющих одно и то же имя (но разные контексты синтаксиса), находятся в той же области видимости. В этом случае —pretty expanded,hygiene расскажет вам о контекстах синтаксиса.

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

  • log_syntax!(. ) будет печатать свои аргументы в стандартный вывод во время компиляции, и «развертываться» в ничто.
  • trace_macros!(true) будет выдавать сообщение компилятора каждый раз, когда макрос развертывается. Используйте trace_macros!(false) в конце развертывания, чтобы выключить его.

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

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

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

  • ноль или больше элементов;
  • ноль или больше методов;
  • выражение;
  • оператор;
  • образец.

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

  • ограничиваться фигурными скобками, т.е. foo! < . >;
  • завершаться точкой с запятой, т.е. foo!(. ); .

Другое следствие разбора перед раскрытием макросов — это то, что вызов макроса должен состоять из допустимых лексем. Более того, скобки всех видов должны быть сбалансированы в месте вызова. Например, foo!([) не является разрешённым кодом. Такое поведение позволяет компилятору понимать где заканчивается вызов макроса.

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

  • последовательность деревьев лексем, окружённую согласованными круглыми, квадратными или фигурными скобками ( () , [] , <> );
  • любую другую одиночную лексему.

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

  • ident : идентификатор. Например: x ; foo .
  • path : квалифицированное имя. Например: T::SpecialA .
  • expr : выражение. Например: 2 + 2 ; if true then < 1 >else < 2 >; f(42) .
  • ty : тип. Например: i32 ; Vec <(char, String)>; &T .
  • pat : образец. Например: Some(t) ; (17, ‘a’) ; _ .
  • stmt : единственный оператор. Например: let x = 3 .
  • block : последовательность операторов, ограниченная фигурными скобками. Например: < log(error, "hi"); return 12; >.
  • item : элемент. Например: fn foo() < >; struct Bar; .
  • meta : «мета-элемент», как в атрибутах. Например: cfg(target_os = «windows») .
  • tt : единственное дерево лексем.

Есть дополнительные правила относительно лексем, следующих за метапеременной:

  • за expr должно быть что-то из этого: => , ; ;
  • за ty и path должно быть что-то из этого: => , : = > as ;
  • за pat должно быть что-то из этого : => , = ;
  • за другими лексемами могут следовать любые символы.

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

И ещё: система макросов никак не обрабатывет неоднозначность разбора. Например, грамматика $($t:ty)* $e:expr всегда будет выдавать ошибку, потому что синтаксическому анализатору пришлось бы выбирать между разбором $t и разбором $e . Можно изменить синтаксис вызова так, чтобы грамматика отличалась в начале. В данном случае можно написать $(T $t:ty)* E $e:exp .

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

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

Компилятор определяет и разворачивает макросы при обходе графа исходного кода контейнера в глубину. При этом определения макросов включаются в граф в порядке их встречи компилятором. Поэтому макрос, определённый на уровне модуля, виден во всём последующем коде модуля, включая тела всех вложенных модулей ( mod ).

Макрос, определённый в теле функции, или где-то ещё не на уровне модуля, виден только внутри этого элемента (например, внутри одной функции).

Если модуль имеет атрибут macro_use , то его макросы также видны в его родительском модуле после элемента mod данного модуля. Если родитель тоже имеет атрибут macro_use , макросы также будут видны в модуле-родителе родителя, после элемента mod родителя. Это распространяется на любое число уровней.

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

#[macro_use(foo, bar)] extern crate baz; 

Если атрибут записан просто как #[macro_use] , будут загружены все макросы. Если атрибута нет, никакие макросы не будут загружены. Загружены могут быть только макросы, объявленные с атрибутом #[macro_export] .

Чтобы загрузить макросы из контейнера без компоновки контейнера в выходной артефакт, можно использовать атрибут #[no_link] .

macro_rules! m1 < () =>(()) > // здесь видны: m1 mod foo < // здесь видны: m1 #[macro_export] macro_rules! m2 < () =>(()) > // здесь видны: m1, m2 > // здесь видны: m1 macro_rules! m3 < () =>(()) > // здесь видны: m1, m3 #[macro_use] mod bar < // здесь видны: m1, m3 macro_rules! m4 < () =>(()) > // здесь видны: m1, m3, m4 > // здесь видны: m1, m3, m4 # fn main()

Когда эта библиотека загружается с помощью #[macro_use] extern crate , виден только макрос m2 .

Переменная $crate

Если макрос используется в нескольких контейнерах, всё становится ещё сложнее. Допустим, mylib определяет

pub fn increment(x: u32) -> u32 < x + 1 > #[macro_export] macro_rules! inc_a < ($x:expr) =>( ::increment($x) ) > #[macro_export] macro_rules! inc_b < ($x:expr) =>( ::mylib::increment($x) ) > # fn main()

inc_a работает только внутри mylib , а inc_b — только снаружи. Более того, inc_b сломается, если пользователь импортирует mylib под другим именем.

В Rust пока нет гигиеничных ссылок на контейнеры, но есть простой способ обойти эту проблему. Особая макро-переменная $crate раскроется в ::foo внутри макроса, импортированного из контейнера foo . А когда макрос определён и используется в одном и том же контейнере, $crate станет пустой. Это означает, что мы можем написать

#[macro_export] macro_rules! inc < ($x:expr) =>( $crate::increment($x) ) > # fn main()

чтобы определить один макрос, который будет работать и внутри, и снаружи библиотеки. Имя функции раскроется или в ::increment , или в ::mylib::increment .

Чтобы эта система работала просто и правильно, #[macro_use] extern crate . может быть написано только в корне вашего контейнера, но не внутри mod . Это обеспечивает, что $crate раскроется в единственный идентификатор.

Во тьме глубин

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

Приведём такой радикальный пример использования данной возможности. С помощью рекурсивных макросов можно реализовать конечный автомат типа Bitwise Cyclic Tag. Стоит заметить, что мы не рекомендуем такой подход, а просто иллюстрируем возможности макросов.

macro_rules! bct < // cmd 0: d . => . (0, $($ps:tt),* ; $_d:tt) => (bct!($($ps),*, 0 ; )); (0, $($ps:tt),* ; $_d:tt, $($ds:tt),*) => (bct!($($ps),*, 0 ; $($ds),*)); // cmd 1p: 1 . => 1 . p (1, $p:tt, $($ps:tt),* ; 1) => (bct!($($ps),*, 1, $p ; 1, $p)); (1, $p:tt, $($ps:tt),* ; 1, $($ds:tt),*) => (bct!($($ps),*, 1, $p ; 1, $($ds),*, $p)); // cmd 1p: 0 . => 0 . (1, $p:tt, $($ps:tt),* ; $($ds:tt),*) => (bct!($($ps),*, 1, $p ; $($ds),*)); // halt on empty data string ( $($ps:tt),* ; ) => (()); > 

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

Распространённые макросы

Вот некоторые распространённые макросы, которые вы увидите в коде на Rust.

panic!

Этот макрос вызывает панику текущего потока. Вы можете указать сообщение, с которым поток завершится:

panic!("о нет!"); 

vec!

Макрос vec! используется по всей книге, поэтому вы наверняка уже видели его. Он упрощает создание Vec :

let v = vec![1, 2, 3, 4, 5]; 

Он также позволяет вам создавать векторы с повторяющимися значениями. Например, вот сто нолей:

let v = vec![0; 100]; 

assert! and assert_eq!

Эти два макроса используются в тестах. assert! принимает логическое значение. assert_eq! принимает два значения и проверяет, что они равны. true засчитывается как успех, а false вызывает панику и проваливает тест. Вот так:

// Работает! assert!(true); assert_eq!(5, 3 + 2); // а это нет :( assert!(5 < 3); assert_eq!(5, 3); 

try!

try! используется для обработки ошибок. Он принимает нечто возвращающее Result и возвращает T если было возвращено Ok ; иначе он делает возврат из функции со значением Err(E) . Вроде такого:

use std::fs::File; fn foo() -> std::io::Result

Такой код читается легче, чем этот:

use std::fs::File; fn foo() -> std::io::Result  < let f = File::create("foo.txt"); let f = match f < Ok(t) =>t, Err(e) => return Err(e), >; Ok(()) > 

unreachable!

Этот макрос применяется, когда вы хотите пометить какой-то код, который никогда не должен исполняться:

if false < unreachable!(); > 

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

let x: Optioni32> = None; match x < Some(_) => unreachable!(), None => println!("Я знаю, что x — это None!"), > 

unimplemented!

Макрос unimplemented! можно использовать, когда вы хотите, чтобы ваш код прошёл проверку типов, но пока не хотите реализовывать его настоящую логику. Один из примеров — это реализация типажа с несколькими требуемыми методами. Возможно, вы хотите разбираться с типажом постепенно — по одному методу за раз. В таком случае, определите остальные методы как unimplemented! , пока не захотите наконец реализовать их.

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

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

results matching " "

No results matching " "

Макросы в Rust. macro_rules

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

Давайте сразу определимся, зачем вы хотите их использовать. Макросы - это про метапрограммирование и, можно даже сказать отчасти про reflective программирование. Я, конечно, не разработчик паттернов и стандартов, но использовать макрос (объявленный macro_rules!) как замену функции, как по мне, плохая идея. Во-первых, потому, что функция принимает переменные конкретного типа, а макрос, который принимает переменную, банально не знает её типа, соответственно, понять смысл операций можно только по названию и по самой сигнатуре макроса. А синтаксис макросов не то чтобы очень очевиден…

Но, надеюсь, благодаря этой статье он станет более понятен для вас.

Что за зверь такой, macro_rules!?

Давайте начнём с самого простого. macro_rules! - наш путь к написанию макросов, встроенный прямо в стандартную библиотеку. Давайте начнём с простого примера - макрос, который делает то же, что и unwrap. Это, конечно, невероятно глупая и плохая идея, но для примера подойдёт.

Обратите внимание, что макрос объявлен до вызова.

macro_rules! uncover < ($var:ident) => < match $var< Some(t) =>t, None => panic!("None value") > > > fn main()< let x = Some(2i32); let unwrapped = uncover!(x); println!("<>", unwrapped); >

Ну-с… давайте разбираться. Что же мы сделали? Перво-наперво, мы объявили макрос uncover:

macro_rules! uncover

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

В каком-то смысле, вы передаёте в макрос не переменную, а код. Все, что знает макрос про $var - это название того, что мы передали в uncover (в нашем случае, х).

Далее идёт код, который вполне себе похож на нативный Rust код, однако есть момент, который бросается в глаза: мы используем переменную со знаком доллара. Итак, что же сделает этот макрос при вызове? Он вставит всю сигнатуру на то место, где вы его вызываете. То есть по сути в рантайме код будет выглядеть не так:

let unwrapped = uncover!(x);
let unwrapped = match x < Some(t) =>t, None => panic!(“None value”) >;

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

let x = 2i32; let unwrapped = uncover!(x);

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

Summary

Итак, зачем же тебе, простой Иван город Тверь, писать макросы? Да не знаю, сам подумай. Может, есть повторяющееся место в коде, под которое идеально подойдёт макрос? Или ты написал нереальную реализацию списка со скоростью работы O(1) и хочешь инициализировать его вот так: list![1,2,3] ? Ну или тебе просто нравится заниматься метапрограммированием? Правда, последнее трудно вяжется с macro_rules! Всё-таки, в языке есть более мощное метапрограммирование, тёмная магия proc_macro, syn, qoute и TokenTree, но о ней как-нибудь в другой раз.

Вот, собственно, и всё. Писать макросы с помощью macro_rules не так-то сложно, главное разобраться в базовых правилах. Может, это сбережёт ваши нервы и/или деньги. Конечно, я не затрагивал в этой статье самое интересное, это наиболее простое из того, что есть. Цель статьи - показать, что макросы - это несложно.

Пишите, если хотите статью про proc_macro и syn, там действительно есть на что посмотреть.

Макросы в игре Rust: что это такое?

Макросы и читы – раковая опухоль во многих играх. Безусловно, всегда есть те игроки, которые рады получить преимущество над другими, даже не самым честным способом. Давайте вспомним игру Grand Theft Auto 5, а именно её Online составляющую. Уж не знаю, как в ней дела обстоят на данный момент, но год-два назад читерство в данной игре было чем-то обыденным. Да, возможно первые два-три года после релиза игры на ПК игроки и удивлялись читерам, пытались их кикнуть из сессии, но уже сейчас очень часто заходя на игровой сервер я вижу надпись по типу «Читеры есть? Накрутите денег плз». Буду честен – я и сам грешен. Два раза я заказывал прокачку игрового персонажа, один раз накручивал игровую валюту и уровень при помощи софта. И знаете что? GTA 5 мне уже надоела, а виной этому послужило то, что у меня и так уже в ней всё есть, мне ничего не надо там делать. Зашёл раз в год, посмотрел обновления, вышел. Вот так сейчас играю я в ГТА. Но вернёмся к игре Rust. Чаще всего в нашей любимой игре можно встретить именно макросников. Читеры тоже встречаются, но не так уж и часто, как показывает практика. А так как макросники встречаются чаще, давайте же узнаем про нашего врага побольше информации.

  • 1 Макросы в игре Rust
  • 2 Преимущества макросов в Расте
  • 3 Как бороться с макросниками
  • 4 Макросы в игре Rust запрещены разработчиками

Макросы в игре Rust

Макросы для оружия в игре Rust — что же это такое? Расскажу об этом очень коротко.

Макрос – софт, который выполняет за вас определённую задачу. В нашем случае он гасит отдачу на определённом оружии.

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

Преимущества макросов в Расте

В чём же преимущество людей с макросами в Rust перед обычными игроками? Да всё элементарно – простой игрок не сможет зажимать с пулемёта на 300 метров, попадая при этом в одну точку. Конечно, есть и такие люди, которые мастера по стрельбе в одну точку, но не каждый же игрок так стреляет. Макросники же сразу после установки софта могут хоть всю карту поливать градом пуль.

Стрельба без макросов в Rust

Стрельба с макросами в Rust

Как бороться с макросниками

Борьба с макросниками – сложное дело, если вы простой игрок. Первым делом при встрече макросника отправьте на него жалобу. У нас по этому поводу есть отдельная статья, где мы описали, как работает система жалоб в Rust.

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

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

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

Макросы в игре Rust запрещены разработчиками

Не так давно разработчики Rust ввели существенные ограничения по использованию мышек Bloody, X7 и прочих моделей, где используются макросы через встроенное ПО. Такие девайсы были заблокированы и зайти на официальные серверы, а также большинство модифицированных проектов нельзя! Это сделано с целью защиты игроков от макросников. Из-за этого появился вопрос — как играть с мышкой блади в раст? Есть обходные легальные пути, которые выигрышны для всех пользователей: и тех, кто обладает такой мышкой, но макросы не использует, и для владельцев серверов (заходить могут все на сервер). У нас есть отдельная статья, где мы описали, как играть в rust с мышкой bloody. Там мы рассказали про обход блокировки мышки bloody rust, а также указали о блокировке блади в раст.

Rust: зачем нужны макросы

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

Где и как их уместно применять? Смотрите под катом.

Почему мы должны опасаться макросов

Макросы являются формой метапрограммирования: они являются кодом, который манипулирует кодом. Метапрограммирование получило плохую репутацию, потому что при их использовании нелегко уберечься от написания плохого кода. Примерами служат #define в C, который легко может взаимодействовать с кодом непредсказуемым образом, или eval в JavaScript, который увеличивают опасность инъекции кода.

О макросах в Rust

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

  • генерирование избыточного/тривиального кода (boilerplate) вместо его ручного написания.
  • расширение языка перед тем, как будет добавлен новый синтаксис, закрытие пробелов в языке.
  • оптимизация производительности — ибо некоторые действия, которые ранее выполнялись во время исполнения, теперь исполняются на стадии компиляции -1-.

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

Причиной того, что имеется два типа макросов является то, что они хорошо подходят для решения разных задач:

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

Во всем остальном результаты их применения схожи: компилятор "стирает" макросы во время компиляции, заменяя их кодом, генерирующимся из макроса, и компилируя его с "обычным", не макро-кодом -3-. Реализация двух типов макросов сильно отличается, но мы не будет здесь глубоко в это вдаваться.

Почему функциональные макросы

Функциональный макрос может быть исполнен почти как функция. Данный вид макросов имеет ! в вызове:

let x = action(); // вызов функции let y = action!(); // вызов макроса

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

Полезные утверждения

Начнем с рассмотрения assert! , который используется для проверки того, что некоторое условие выполняется, вызывая панику (panic), если это не так. Они проверяются во время выполнения, так что же нам дает здесь метапрограммирование? Давайте посмотрим на сообщение, которое печатается, когда assert! завершается неудачно:

fn main() < let mut vec = Vec::new(); // создать пустой массив vec.push(1); // добавить элемент в конец массива assert!(vec.is_empty()) // массив не пуст - assert! завершается неудачно // печатается: // thread 'main' panicked at 'assertion failed: vec.is_empty()', src\main.rs:4 >

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

Типо-безопасная работа с форматом строк

Во многих языках программирования поддерживается задание форматов вывода для строк -4-. Rust не является исключением и также поддерживает задание форматов строк посредством format! . Однако по-прежнему стоит вопрос: почему мы должны использовать метапрограммирование для решения проблемы? Давайте посмотрим на println! (он внутри использует format! для обработки переданной строки) -5-.

fn main() < // просто ввод println!("<>is <> in binary", 2, 10); // печатает: 2 is 10 in binary // вывод аргументов в числовой и двоичной форме println!(" is in binary", 3) // печатает: 3 is 11 in binary >

Есть много причин того, что format! реализован как макрос -6-, я же хочу подчеркнуть то, что он может разделить строку на части во время компиляции, проанализировать ее и проверить, является ли обработка переданных аргументов типо-безопасной. Мы можем изменить наш код и получить ошибку компиляции:

fn main() < println!("<>is <> in binary", 2/*, 10*/); // Ошибка компиляции: ожидались два аргумента, был передан один println!(" is in binary", "3") // Ошибка компиляции: не реализовано представление строк в двоичном виде >

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

Легкое логирование

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

Логирование показывает мощность метапрограммирования в том, как оно использует макросы file! и line! ; Данные макросы дают возможность установить точное место расположения вызова функции логирования в исходном коде. Давайте посмотрим на пример. Так как log является фронтендом, добавим бэкенд, пакет flexi_logger.

#[macro_use] extern crate log; extern crate flexi_logger; use flexi_logger::; fn main() < // Установим `trace` в качестве минимального уровня логирования let log_config = LogSpecification::default(LevelFilter::Trace).build(); Logger::with(log_config) .format(flexi_logger::opt_format) // Specify how we want the logs formatted .start() .unwrap(); // Логирование готово к использованию. Используем его для отладки алгоритма info!("Fired up and ready!"); complex_algorithm() >fn complex_algorithm() < debug!("Running complex algorithm."); for x in 0..3 < let y = x * 2; trace!("Step <>gives result <>", x, y) > >

Эта программа напечатает:

[2018-01-25 14:48:42.416680 +01:00] INFO [src\main.rs:16] Fired up and ready! [2018-01-25 14:48:42.418680 +01:00] DEBUG [src\main.rs:22] Running complex algorithm. [2018-01-25 14:48:42.418680 +01:00] TRACE [src\main.rs:25] Step 0 gives result 0 [2018-01-25 14:48:42.418680 +01:00] TRACE [src\main.rs:25] Step 1 gives result 2 [2018-01-25 14:48:42.418680 +01:00] TRACE [src\main.rs:25] Step 2 gives result 4

Как вы видите, наши логи содержат имена файлов и номера строк.

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

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

Если мы заменим логирующие макросы на функции, то по-прежнему можем вызывать file! и line! :

fn info(input: String) < // Надуманный вариант info! Log::log( logger(), RecordBuilder::new() .args(input) .file(Some(file!())) .line(Some(line!())) .build() ) >

А данный код вывел бы следующее:

[2018-01-25 14:48:42.416680 +01:00] INFO [src\loggers\info.rs:7] Fired up and ready!

Имя файла и номер строки бесполезны, ибо указывают на то, где была вызвана логирующая функция. Другими словами, первый пример работал как раз потому, что мы использовали макросы, которые были заменены генерируемым кодом, помещая file! и line! напрямую в исходный код, предоставляя нам необходимую информацию (имя файла и номер строки теперь в исполняемом файле) -8-.

Почему атрибутные макросы

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

#[test] // 0) >

Запуск cargo test запустит данную функцию. Атрибутивные макросы позволяют вам создавать новые атрибуты, который подобны "родным" атрибутам, но имеют другие эффекты. На текущий момент существует важное ограничение: в компиляторе из ветки stable работают только макросы использующие атрибут derive, в то время как пользовательские атрибуты работают в ночных сборках. Рассмотрим разницу ниже.

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

Получение избыточного кода (boilerplate)

Атрибут derive используется в Rust для генерации реализации типажей. Давайте посмотрим на PartialEq .

#[derive(PartialEq, Eq)] struct Data < content: u8 >fn main() < let data = Data < content: 2 >; assert!(data == Data < content: 2 >) >

Здесь мы создаем структуру, экземпляры которой хотим проверять на равенство ( использовать == ), поэтому мы получаем реализацию PartialEq -9-. Мы могли бы реализовать PartialEq самостоятельно, но наша реализация была бы тривиальной, ибо мы хотим только проверять объекты на равенство:

impl PartialEq for Data < fn eq(&self, other: &Data) ->bool < self.content == other.content >>

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

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

Derive с преимуществами

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

Наиболее выдающимся случаем использования на текущий момент является Rocket — библиотека для написания веб-серверов. Создание REST-endpoint'ов требует добавления атрибута к функции, так что теперь функция содержит всю необходимую информацию для обработки запроса.

#[post("/user", data )] fn new_user(admin: AdminUser, new_user: Form) -> T < //. >

Если вы работали с веб-библиотеками в других языка (например, Flask или Spring), то данный стиль для вас, вероятно, не нов. Я не буду здесь сравнивать эти библиотеки, отмечу лишь, что вы можете писать подобный код и в Rust, пользуясь его преимуществами (высокая производительность получаемого нативного кода и т. д.) -11-.

Недостатки

Макросы не идеальны, рассмотрим их некоторые недостатки:

  • увеличенное время компиляции, так как тратится время на получение кода из макроса и компиляцию данного кода.
  • Макросы могут привести к увеличению размера машинного кода, ибо легко впасть в копипаст при их использовании, при котором маленькая строка может развернуться в большой блок кода. Раньше это было проблемой пакета clap, о котором автор написал хорошую заметку с описанием проблемы и то, как посадил код на диету.
  • отладка становится сложнее, ибо нужно отлаживать сгенерированный код. К счастью, имеются инструменты, которые могут вам помочь. Читаемость и информативность сообщений об ошибке при использовании макросов зависит не от компилятора, а от авторов макроса. Опять же, имеются необходимые инструменты (например, compiler_error! и пакеты подобные syn).
  • перегрузка DSL (немного субъективный пункт). Например, format! принимает строку, написанную на мини-языке, который является не Rust'ом, а DSL. Хотя DSL является мощным инструментом, его использование легко может ввести в затруднение, если разработчик задумает создать свой собственный встроенный язык. Если надумаете писать DSL, помните, что большие возможности подразумевают большую ответственность, и то, что вы можете сделать DSL, не подразумевает необходимости делать это.

Выводы

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

-1-: Не путайте с возможностью const fn .
-2-: Известны как Макросы 1.1.
-3-: Замена макроса сгенерированным кодом называется расширением макроса.
-4-: Например, printf в C, String.Format в C#, форматирование строк в Python.
-5-: format! занимается форматированием строки, которая может использоваться макросами println! и другими.
-6-: varargs использует format! . Данная возможность (varargs) входит в противоречие с решением на запрет перегрузки функций, поэтому использование макроса очень уместно — не нужно добавлять поддержку в ядро языка.
-7-: Scala имеет хорошую реализацию интерполяции строк, которая делает проверки на стадии компиляции. Не знаю, будет ли добавлена интерполяция строк в Rust, хотя мы уже видели подобные примеры: try! развился из макроса во встроенную в язык возможность, так что подобное возможно при целесообразности.
-8-: У Rust есть проблема — паникующие методы (например, unwrap и expect ) выдают бесполезные сообщения об ошибке, потому что не имеют доступа к информации о вызывающем коде.
-9-: PartialEq — типаж, используемый для проверки объектов на равенство, мы также используем Eq для корректности. Документация PartialEq объясняет, почему в Rust имеется подобное деление.
-10-: Проблема может быть решена рефлексией, которая не поддерживается в Rust, ибо противоречит дизайну языка, так как уменьшает производительность времени выполнения, ибо требует соответствующий runtime.
-11-: Sergio Benitez, автор Rocket, сделал связанное с этим хорошее выступление.

  • Open source
  • Программирование
  • Системное программирование
  • Компиляторы
  • Rust

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

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