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


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


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


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


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


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


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


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


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


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


Новое в Rust 1.15


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


Поддержка IDE


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


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


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


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


Макросы в Rust


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


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


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

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


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


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

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


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


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


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


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


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


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


  • внутри выражений
  • в trait и impl блоках
  • в телах функций и методов
  • в теле модуля

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


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


Осторожно, мозг!
macro_rules! __impl_slice_eq1 {
    ($Lhs: ty, $Rhs: ty) => {
        __impl_slice_eq1! { $Lhs, $Rhs, Sized }
    };

    ($Lhs: ty, $Rhs: ty, $Bound: ident) => {
        #[stable(feature = "rust1", since = "1.0.0")]
        impl<'a, 'b, A: $Bound, B> PartialEq<$Rhs> for $Lhs where A: PartialEq<B> {
            #[inline]
            fn eq(&self, other: &$Rhs) -> bool { self[..] == other[..] }
            #[inline]
            fn ne(&self, other: &$Rhs) -> bool { self[..] != other[..] }
        }
    }
}

__impl_slice_eq1! { Vec<A>, Vec<B> }
__impl_slice_eq1! { Vec<A>, &'b [B] }
__impl_slice_eq1! { Vec<A>, &'b mut [B] }
__impl_slice_eq1! { Cow<'a, [A]>, &'b [B], Clone }
__impl_slice_eq1! { Cow<'a, [A]>, &'b mut [B], Clone }
__impl_slice_eq1! { Cow<'a, [A]>, Vec<B>, Clone }

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


macro_rules! vec {
    // Задание через значение и длину: vec![0; 32]
    ( $elem:expr; $n:expr ) => ( $crate::vec::from_elem($elem, $n) );

    // Задание перечислением элементов: vec![1, 2, 3]
    ( $($x:expr),* ) => ( <[_]>::into_vec(box [$($x),*]) );

    // Рекурсивная обработка финальной запятой: vec![1, 2, 3, ]
    ( $($x:expr,)* ) => ( vec![$($x),*] )
}

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


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


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


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


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


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


#define SQUARE(a) a*a

int x = SQUARE(my_list.pop_front());
int y = SQUARE(x++);

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


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


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


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


Макросы в Rust:


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

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


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


macro_rules! vec {
    ( $($x:expr),* ) => {
        {
            // Объявляем переменную-аккумулятор
            let mut result = Vec::new();

            // На каждое выражение из $x подставляем свою строку
            $(result.push($x);)*

            // Возвращаем result как результат применения макроса
            result
        }
    };
}

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


let vector = vec![1, 2, 3];

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


let vector = {
    let mut result = Vec::new();

    result.push(1);
    result.push(2);
    result.push(3);

    result
};

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


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


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


В качестве подопытного кролика возьмем реализацию автоматически выводимого конструктора #[derive(new)] из соответствующей библиотеки.


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


#[macro_use]
extern crate derive_new;

#[derive(new)]
struct Bar {
    x: i32,
    y: String,
}

fn main() {
    let _ = Bar::new(42, "Hello".to_owned());
}

То есть, определив атрибут #[derive(new)] мы попросили компилятор самостоятельно вывести… а что именно? Откуда компилятор поймет, какой именно метод мы ожидаем получить? Давайте разбираться.


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


Много буков (75 строк)
#![crate_type = "proc-macro"]

extern crate proc_macro;
extern crate syn;
#[macro_use]
extern crate quote;

use proc_macro::TokenStream;

#[proc_macro_derive(new)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input: String = input.to_string();

    let ast = syn::parse_macro_input(&input).expect("Couldn't parse item");

    let result = new_for_struct(ast);

    result.to_string().parse().expect("couldn't parse string to tokens")
}

fn new_for_struct(ast: syn::MacroInput) -> quote::Tokens {
    let name = &ast.ident;
    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
    let doc_comment = format!("Constructs a new `{}`.", name);

    match ast.body {
        syn::Body::Struct(syn::VariantData::Struct(ref fields)) => {
            let args = fields.iter().map(|f| {
                let f_name = &f.ident;
                let ty = &f.ty;
                quote!(#f_name: #ty)
            });
            let inits = fields.iter().map(|f| {
                let f_name = &f.ident;
                quote!(#f_name: #f_name)
            });

            quote! {
                impl #impl_generics #name #ty_generics #where_clause {
                    #[doc = #doc_comment]
                    pub fn new(#(args),*) -> Self {
                        #name { #(inits),* }
                    }
                }
            }
        },
        syn::Body::Struct(syn::VariantData::Unit) => {
            quote! {
                impl #impl_generics #name #ty_generics #where_clause {
                    #[doc = #doc_comment]
                    pub fn new() -> Self {
                        #name
                    }
                }
            }
        },
        syn::Body::Struct(syn::VariantData::Tuple(ref fields)) => {
            let (args, inits): (Vec<_>, Vec<_>) = fields.iter().enumerate().map(|(i, f)| {
                let f_name = syn::Ident::new(format!("value{}", i));
                let ty = &f.ty;
                (quote!(#f_name: #ty), f_name)
            }).unzip();

            quote! {
                impl #impl_generics #name #ty_generics #where_clause {
                    #[doc = #doc_comment]
                    pub fn new(#(args),*) -> Self {
                        #name(#(inits),*)
                    }
                }
            }
        },
        _ => panic!("#[derive(new)] can only be used with structs"),
    }
}

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


#![crate_type = "proc-macro"]

extern crate proc_macro;
extern crate syn;

#[macro_use]
extern crate quote;

use proc_macro::TokenStream;

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


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


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


#[proc_macro_derive(new)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input: String = input.to_string();
    let ast = syn::parse_macro_input(&input).expect("Couldn't parse item");
    let result = new_for_struct(ast);
    result.to_string().parse().expect("couldn't parse string to tokens")
}

Обратите внимание на атрибут proc_macro_derive(new), который говорит компилятору, что эта функция отвечает за #[derive(new)].


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


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


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


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


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


// Обычная структура
#[derive(new)]
struct Normal {
    x: i32,
    y: String,
}

// Вариант tuple struct
#[derive(new)]
struct Tuple(i32, i32, i32);

// Структура-пустышка
#[derive(new)]
struct Empty;

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


fn new_for_struct(ast: syn::MacroInput) -> quote::Tokens {
    let name = &ast.ident;
    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
    let doc_comment = format!("Constructs a new `{}`.", name);

    match ast.body {
        syn::Body::Struct(syn::VariantData::Struct(ref fields)) => { /* обычная структура */ },
        syn::Body::Struct(syn::VariantData::Unit) => { /* единичный тип */ },
        syn::Body::Struct(syn::VariantData::Tuple(ref fields)) => { /* tuple struct */ }
        _ => panic!("#[derive(new)] can only be used with structs"),
    }
}

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


// Для каждого поля в структуре генерируем список пар имя:тип
// которые позже используем в списке параметров конструктора
let args = fields.iter().map(|f| {
    let f_name = &f.ident;
    let ty = &f.ty;
    quote!(#f_name: #ty)
});

// Генерируем тело конструктора, пары имя:имя
let inits = fields.iter().map(|f| {
    let f_name = &f.ident;
    quote!(#f_name: #f_name)
});

// Наконец, собираем все воедино и цитируем весь блок целиком:
quote! {
    impl #impl_generics #name #ty_generics #where_clause {
        #[doc = #doc_comment]
        pub fn new(#(args),*) -> Self {
            #name { #(inits),* }
        }
    }
}

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


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


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


#[derive(new)]
struct Normal {
    x: i32,
    y: String,
}

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


/// Constructs a new `Normal`.
impl Normal {
    pub fn new(x: i32, y: String) -> Self {
        Normal { x: x, y: y }
    }
}

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


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


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


Заключение


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


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


Материал подготовлен совместно с Дарьей Щетининой.