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

14 януари 2020

Административни неща

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

Предоставят ни възможността да пишем синтактични разширения под формата на функции

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

Фази на компилация

За да разберем какво точно правят процедурните макроси, трябва да знаем фазите на компилация

Фази на компилация

Всеки код минава през няколко фази при компилация/интерпретация в зависимост от компилатора

Стандартно първите две са Lexer и Parser

Фази на компилация

Lexer

Целта на Lexer-a е да разбие кода, който е под формата на низ или поток от символи на значещи за езика Token-и
Примери за Token са ключови думи, литерали, оператори, имена на променливи и др.
Крайният резултат е поток от Token-и

Фази на компилация

Parser

Parser-ът приема поток от Token-и и им придава значение като крайният резултат е дърво
Всеки възел от дървото е синтактична конструкция на езика
Примери за такъв възел са if, block, variable, expression и др.

Фази на компилация

Процедурните макроси се вмъкват между лексъра и парсъра и работят с поток от опростени Token-и

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

Особености

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

За да създадем такъв макрос ни трябва нов проект, в чиито манифест да се съдържа

1 2
[lib]
proc-macro = true

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

Видове

Function-like procedural macros

Изглеждат като нормални макроси, но зад тях стои Rust код вместо синтаксиса на macro_rules!
Не могат да се ползват като statement, expression или pattern, но са позволени на всички останали места

Function-like procedural macros

1 2 3 4 5 6 7 8 9 10
/// Macro

extern crate proc_macro;

use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    "fn answer() -> u32 { 42 }".parse().unwrap()
}
1 2 3 4 5 6 7 8 9
/// Usage

use proc_macro_examples::make_answer;

make_answer!();

fn main() {
    println!("{}", answer());
}

Derive macros

Анотират структури или енумерации, като добавят код към модула или блока на анотирания item без да променят item-a

Derive macros

1 2 3 4 5 6 7 8 9 10
/// Macro

extern crate proc_macro;

use proc_macro::TokenStream;

#[proc_macro_derive(AnswerFn)]
pub fn derive_answer_fn(_item: TokenStream) -> TokenStream {
    "fn answer() -> u32 { 42 }".parse().unwrap()
}
1 2 3 4 5 6 7 8 9 10
/// Usage

use proc_macro_examples::AnswerFn;

#[derive(AnswerFn)]
struct Struct;

fn main() {
    assert_eq!(42, answer());
}

Derive macro helper attributes

Може да дефинираме и помощни атрибути, които служат само за ориентация на макроса

Derive macro helper attributes

1 2 3 4 5 6
/// Macro

#[proc_macro_derive(HelperAttr, attributes(helper))]
pub fn derive_helper_attr(_item: TokenStream) -> TokenStream {
    TokenStream::new()
}
1 2 3 4 5 6
/// Usage

#[derive(HelperAttr)]
struct Struct {
    #[helper] field: ()
}

Attribute macros

Дефинират произволен атрибут
За разлика от Derive макросите, заместват кода който анотират
Също така са по-гъвкави от Derive макросите като могат да анотират повече конструкции например функции

Attribute macros

1 2 3 4 5 6 7 8 9
/// Macro

/// Noop with prints
#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
    println!("attr: \"{}\"", attr.to_string());
    println!("item: \"{}\"", item.to_string());
    item
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/// Usage

use my_macro::show_streams;

// Example: Basic function
#[show_streams]
fn invoke1() {} // attr: ""   item: "fn invoke1() { }"

// Example: Attribute with input
#[show_streams(bar)]
fn invoke2() {} // attr: "bar"   item: "fn invoke2() {}"

// Example: Multiple tokens in the input
#[show_streams(multiple => tokens)]
fn invoke3() {} // attr: "multiple => tokens"   item: "fn invoke3() {}"

#[show_streams { delimiters }]
fn invoke4() {} // attr: "delimiters"   item: "fn invoke4() {}"

Обработка на входните данни

TokenStream имплементира IntoIterator, което ни позволява да превърнем потока в итератор

1 2 3 4 5 6 7 8
#[proc_macro]
pub fn exmaple(input: TokenStream) -> TokenStream {
    for token in input.into_iter() {
        println!("{}", token);
    }

    // ...
}

Обработка на входните данни

Може да си направим собствен парсър на итератор от TokenTree, но това обикновено е трудоемка и времеемка задача

Ще видим как може да улесним задачата малко по-късно

Обработка на изходните данни

Дали построяването на изходния поток е по-лесно?

Обработка на изходните данни

Както видяхме в един от предните примери, може да използваме .parse(), тъй като TokenStream имплементира FromStr

1 2 3 4
#[proc_macro]
pub fn exmaple(input: TokenStream) -> TokenStream {
    "fn f() {}".parse().unwrap()
}

Обработка на изходните данни

Комбинирайки .parse() с format!(), може да постигнем гъвкаво конструиране на крайния резултат

1 2 3 4
#[proc_macro]
pub fn exmaple(input: TokenStream) -> TokenStream {
    format!(r#"fn f() {{ println!("{{}}", {}); }}"#, 42).parse().unwrap()
}

Обработка на изходните данни

Недостатъците на подхода с format! са

syn and quote

Освен вградения proc_macro пакет съществуват два, които се използват най-често при работа с процедурни макроси

syn

syn пакетът предоставя парсър, който превръща TokenStream в синтактично дърво AST (Abstract syntax tree)

1 2 3 4 5 6 7 8 9
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast: syn::DeriveInput = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_hello_macro(&ast)
}

AST (Abstract syntax tree)

Нарича се абстрактно дърво, защото не описва всяка подробност от реалния синтаксис, а само структурата на кода
Например скобите не присъстват в дървото, те само насочват парсъра

quote

quote пакетът предоставя начин да превърнем синтактичното дърво обратно в TokenStream, който да върнем на компилатора

1 2 3 4 5 6 7 8 9 10 11 12
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    // Извлича името на структурата която сме анотирали
    let name = &ast.ident;
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}", stringify!(#name));
            }
        }
    };
    gen.into()
}

quote

#var интерполира стойността на променливи в token-и

Ресурси

Въпроси