Search code examples
rustrust-macros

Rust macro that counts and generates repetitive struct fields


I want to write a macro that generates varying structs from an integer argument. For example, make_struct!(3) might generate something like this:

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

What's the best way to transform that "3" literal into a number that I can use to generate code? Should I be using macro_rules! or a proc-macro?


Solution

  • You need a procedural attribute macro and quite a bit of pipework. An example implementation is on Github; bear in mind that it is pretty rough around the edges, but works pretty nicely to start with.

    The aim is to have the following:

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

    transpile to:

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

    To do this, first, we're going to establish a couple of things. We're going to need a struct to easily store and retrieve our arguments:

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

    The rest is pipework:

    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()
            })
        }
    }
    

    This defines syn::Parse on our struct and allows us to use syn::parse_macro_input!() to easily parse our arguments.

    #[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()
    }
    

    The behavior of the modifier creates a new TokenStream and adds our fields first. This is extremely important; assume that the struct provided is struct Foo { bar: u8 }; appending last would cause a parse error due to a missing ,. Prepending allows us to not have to care about this, since a trailing comma in a struct is not a parse error.

    Once we have this TokenStream, we successively extend() it with the generated tokens from quote::quote!(); this allows us to not have to build the token fragments ourselves. One gotcha is that the field name needs to be converted to an Ident (it gets quoted otherwise, which isn't something we want).

    We then return this modified TokenStream as a TokenTree::Group to signify that this is indeed a block delimited by brackets.

    In doing so, we also solved a few problems:

    • Since structs without named members (pub struct Foo(u32) for example) never actually have an opening bracket, this macro is a no-op for this
    • It will no-op any item that isn't a struct
    • It will also no-op structs without a member