Макрос Rust, который считает и генерирует повторяющиеся структурные поля - PullRequest
1 голос
/ 05 октября 2019

Я хочу написать макрос, который генерирует различные структуры из целочисленного аргумента. Например, make_struct!(3) может генерировать что-то вроде этого:

pub struct MyStruct3 {
    field_0: u32,
    field_1: u32,
    field_2: u32
}

Какой лучший способ преобразовать этот литерал «3» в число, которое я могу использовать для генерации кода? Должен ли я использовать macro_rules! или proc-macro?

1 Ответ

2 голосов
/ 05 октября 2019

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

Цель состоит в том, чтобы:

#[derivefields(u32, "field", 3)]
struct MyStruct {
     foo: u32
}

перейти к:

struct MyStruct {
   pub field_0: u32,
   pub field_1: u32,
   pub field_2: u32,
   foo: u32
}

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

struct MacroInput {
    pub field_type: syn::Type,
    pub field_name: String,
    pub field_count: u64
}

Остальное - конвейер:

impl Parse for MacroInput {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let field_type = input.parse::<syn::Type>()?;
        let _comma = input.parse::<syn::token::Comma>()?;
        let field_name = input.parse::<syn::LitStr>()?;
        let _comma = input.parse::<syn::token::Comma>()?;
        let count = input.parse::<syn::LitInt>()?;
        Ok(MacroInput {
            field_type: field_type,
            field_name: field_name.value(),
            field_count: count.base10_parse().unwrap()
        })
    }
}

Это определяет syn::Parse в нашем struct и позволяет нам использовать syn::parse_macro_input!() для простого анализа наших аргументов.

#[proc_macro_attribute]
pub fn derivefields(attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = syn::parse_macro_input!(attr as MacroInput);
    let mut found_struct = false; // We actually need a struct
    item.into_iter().map(|r| {
        match &r {
            &proc_macro::TokenTree::Ident(ref ident) if ident.to_string() == "struct" => { // react on keyword "struct" so we don't randomly modify non-structs
                found_struct = true;
                r
            },
            &proc_macro::TokenTree::Group(ref group) if group.delimiter() == proc_macro::Delimiter::Brace && found_struct == true => { // Opening brackets for the struct
                let mut stream = proc_macro::TokenStream::new();
                stream.extend((0..input.field_count).fold(vec![], |mut state:Vec<proc_macro::TokenStream>, i| {
                    let field_name_str = format!("{}_{}", input.field_name, i);
                    let field_name = Ident::new(&field_name_str, Span::call_site());
                    let field_type = input.field_type.clone();
                    state.push(quote!(pub #field_name: #field_type,
                    ).into());
                    state
                }).into_iter());
                stream.extend(group.stream());
                proc_macro::TokenTree::Group(
                    proc_macro::Group::new(
                        proc_macro::Delimiter::Brace,
                        stream
                    )
                )
            }
            _ => r
        }
    }).collect()
}

Поведение модификатора создает новый TokenStream и добавляет наши поля first . Это чрезвычайно важно ;предположим, что указанная структура struct Foo { bar: u8 };добавление last вызовет ошибку разбора из-за отсутствия ,. Предварительное добавление позволяет нам не заботиться об этом, поскольку завершающая запятая в структуре не является ошибкой разбора.

Как только мы получим это TokenStream, мы последовательно extend() будем его генерировать с токенами из quote::quote!();это позволяет нам не создавать фрагменты токенов самостоятельно. Одна ошибка заключается в том, что имя поля необходимо преобразовать в Ident (в противном случае оно указывается в кавычках, а это не то, что нам нужно).

Затем мы возвращаем этот измененный TokenStream как TokenTree::Groupчтобы показать, что это действительно блок, разделенный скобками.

При этом мы также решили несколько проблем:

  • Так как структуры без именованных элементов (например, pub struct Foo(u32))на самом деле никогда не имеет открывающей скобки, этот макрос не предназначен для этого
  • Он не будет запрещать любой элемент, не являющийся структурой
  • Он также не будет содержать структуры безучастник
...