Rust 宏计算并生成重复的结构字段

Rob*_*b S 2 rust rust-macros

我想编写一个宏,从整数参数生成不同的结构。例如,make_struct!(3)可能会生成如下内容:

pub struct MyStruct3 {
    field_0: u32,
    field_1: u32,
    field_2: u32
}
Run Code Online (Sandbox Code Playgroud)

将“3”文字转换为可用于生成代码的数字的最佳方法是什么?我应该使用macro_rules!还是proc-macro

Séb*_*uld 6

您需要一个程序属性宏和相当多的管道工作。Github上有一个示例实现;请记住,它的边缘相当粗糙,但一开始就工作得很好。

目标是:

#[derivefields(u32, "field", 3)]
struct MyStruct {
     foo: u32
}
Run Code Online (Sandbox Code Playgroud)

转译为:

struct MyStruct {
   pub field_0: u32,
   pub field_1: u32,
   pub field_2: u32,
   foo: u32
}
Run Code Online (Sandbox Code Playgroud)

为此,首先,我们要确定几件事。我们需要一个来struct轻松存储和检索我们的参数:

struct MacroInput {
    pub field_type: syn::Type,
    pub field_name: String,
    pub field_count: u64
}
Run Code Online (Sandbox Code Playgroud)

剩下的就是管道:

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()
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

这定义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()
}
Run Code Online (Sandbox Code Playgroud)

修改器的行为首先TokenStream创建一个新的并添加我们的字段。这一点极其重要;假设提供的结构是;追加最后一个会由于缺少. 前置使我们不必关心这一点,因为结构中的尾随逗号不是解析错误。struct Foo { bar: u8 },

一旦我们有了这个TokenStream,我们就extend()可以使用生成的令牌来连续它quote::quote!();这使我们不必自己构建令牌片段。一个问题是字段名称需要转换为 an Ident(否则它会被引用,这不是我们想要的)。

然后我们将这个修改后的值返回TokenStream为 aTokenTree::Group来表示这确实是一个由方括号分隔的块。

在此过程中,我们还解决了一些问题:

  • 由于没有命名成员的结构(pub struct Foo(u32)例如)实际上从未有左括号,因此该宏对此无效
  • 它将不对任何不是结构的项目进行任何操作
  • 它也将是没有成员的无操作结构