[syn] How do I iterate on the fields of a struct?

Hello. I would like to be able to generate a function by consuming a struct an using the struct name, and the name of its fields.

#[derive(Debug)]
struct Output {
    data: Vec<usize>,
}

trait MyTrait {
    fn do_something(&self) -> Output
    where Self: Sized;
}                   
                        
#[derive(Debug)]
struct MyStruct {       
    pub foo: usize,     
    pub bar: usize, 
}

I would like to automatically generate an impl of MyTrait for MyStruct, like so:

impl MyTrait for MyStruct {
    fn do_something(&self) -> Output {
        Output {
            data: vec![ // FIXME: automatically generate this part
                make_number("foo", self.foo),
                make_number("bar", self.bar),
            ],
        }   
    }   
}

To do so, I need to create a custom derive macro (in a sub-crate), and add #[derive(MyTrait) on MyStruct.

#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream {
    let ast = syn::parse(input).unwrap();
    impl_my_trait(&ast)
}

fn impl_my_trait(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    
    let data: syn::Data = ast.data;

    // A failed experiment to access to the fields of MyStruct
    // The next line doesn't compiles
    if let syn::DataStruct(data) = data {
        let fields = &data.fields;

        // The following part compiles fine if I hardcode the content of data,
        // and I correctly get an impl of MyTrait for MyStruct
        let gen = quote! {
            impl MyTrait for #name { // I can access name, yeah!
                fn do_something(&self) -> Output {
                    Output {
                        // Pseudo code of what I'm expecting to write
                        // I may need to create the Vec outside of the `quote!` block, idk
                        data: fields.iter().map(|&field| {
                            make_number(stringify!(#field), self.#field)
                        }).collect(),
                    }
                }
            }
        };
        gen.into()
    }
    else {
        compile_error!("The MyTrait derive macro can only be applied to flat structs.");
    }
}

As you can see, I'm nearly there, but I don't understand how I am supposed to access to the fields of a struct using syn.

Some documentation:

Any help would be appreciated. I don't know if I'm struggling because I don't understand the documentation of syn, or if it's because I have a hard time with the syntax of if let Variant(value) = SomeEnum.

EDIT: Remove RuleSystem and use MyTrait instead (I forgot to do it when extracting the code I had from my project.

2 Likes

I think you're on the right track - you definitely do need to iterate over the fields outside of the quote!, though.

The way quote! works, it only ever lets you put in single #name variables. Everything besides those variables is copied verbatim to the source.

Another few things to note:

  • there's no magic which will let you copy complicated things, like a Vec of strings, from the proc-macro crate into the data. If you want to send a list of fields, you need to manually send over each string separately and also manually write the vec![] or [] tokens to stick them in a vec.

  • fields is not a list of strings, but rather a Fields.

    The syn crate is huge, but keeping open docs for everything you're using is super helpful.

  • If you need to build up complicated sources, it's usually useful to use quote!() multiple times and to either use TokenStream::extend() or just quoting with a TokenStream variable to build up bigger token streams.

So, going off of that, Fields is an enum of Named and Unnamed. Named contains a Punctuated, which lets us iterate over the actual fields.

Here's some code, then, to produce the source for making a vec of field names:

let mut fields_vec_innards = quote!();
match fields {
    Fields::Named(named) => for field in named.named.iter() {
        let name = field.ident.unwrap();
        fields_vec_innards.extend(quote!(
            stringify!(#name),
        ));
    }
    _ => unimplemented!()
}
let fields_vec = quote!(vec![#fields_vec_innards]);

I haven't tested this, but it should roughly work.


As a last note, the synstructure crate provides a bunch of really, really nice utilities for doing this sort of thing. I transitioned all of my manual-syn-using derive macros to synstructure, and haven't gone back since. It has some great examples, too.

If you're wanting to do this to learn how to do it, then by all means go for it. If your goal is to just have a working and maintainable derive, I highly recommend synstructure.

4 Likes

That's awesome, thanks at lot!

1 Like

You can also use the syntax #(#fields)sep* to iterate over fields.

let mut fields_vec_innards = quote!();
let fields_vec = match fields {
    Fields::Named(named) => {
        let fields = &named.named;
        quote!{ vec![#(#fields),*] }
    }
    _ => unimplemented!()
};
4 Likes
// This is my typical proc-macro prelude
#![allow(unused_imports)]
extern crate proc_macro;
use ::proc_macro::TokenStream;
use ::proc_macro2::{
    Span,
    TokenStream as TokenStream2,
};
use ::quote::{
    quote,
    quote_spanned,
    ToTokens,
};
use ::syn::{*,
    parse::{Parse, Parser, ParseStream},
    punctuated::Punctuated,
    spanned::Spanned,
    Result,
};

#[proc_macro_derive(MyTrait)] pub
fn rule_system_derive (input: TokenStream)
  -> TokenStream
{
    let ast = parse_macro_input!(input as _);
    TokenStream::from(match impl_my_trait(ast) {
        | Ok(it) => it,
        | Err(err) => err.to_compile_error(),
    })
}

fn impl_my_trait (ast: DeriveInput)
  -> Result<TokenStream2>
{Ok({
    let name = ast.ident;
    let fields = match ast.data {
        | Data::Enum(DataEnum { enum_token: token::Enum { span }, .. })
        | Data::Union(DataUnion { union_token: token::Union { span }, .. })
        => {
            return Err(Error::new(
                span,
                "Expected a `struct`",
            ));
        },

        | Data::Struct(DataStruct { fields: Fields::Named(it), .. })
        => it,
        
        | Data::Struct(_)
        => {
            return Err(Error::new(
                Span::call_site(),
                "Expected a `struct` with named fields",
            ));
        },
    };
    
    let data_expanded_members = fields.named.into_iter().map(|field| {
        let field_name = field.ident.expect("Unreachable");
        let span = field_name.span();
        let field_name_stringified =
            LitStr::new(&field_name.to_string(), span)
        ;
        quote_spanned! { span=>
            make_number(#field_name_stringified, &self.#field_name)
        }
    }); // : impl Iterator<Item = TokenStream2>
    quote! {
        impl RuleSystem for #name {
            fn do_something (self: &'_ Self)
              -> Output
            {
                Output {
                    data: vec![
                        #(#data_expanded_members ,)*
                    ],
                }
            }
        }
    }
})}

After that point, if you intend for your derive to be usable by downstream users, you'll have to worry about people calling #[derive(MyTrait)] when neither Output, nor RuleSystem, nor make-number, nor even vec! are in scope:

quote_spanned! { span=>
-   make_number(#field_name_stringified, &self.#field_name)
+   ::your_crate::path::to::make_number(#field_name_stringified, &self.#field_name)
}
quote! {
-       impl RuleSystem for #name {
+       impl ::your_crate::path::to::RuleSystem for #name {
            fn do_something (self: &'_ Self)
-             -> Output
+             -> ::your_crate::path::to::Output
            {
-               Output {
+               ::your_crate::path::to::Output {
-                   data: vec![
+                   data: ::your_crate::std::vec![
                        #(#data_expanded_members ,)*
                    ],
                }
            }
        }
    }
  • where your_crate refers to the name of your frontend crate (i.e., the one that isn't proc-macro = true), since that's the one containing the definitions for all your items.

  • with a #[doc(hidden)] pub use ::std; at the root of your non procedural macro crate;


You will also have to worry about the struct you apply your derive to having generic parameters (and where clauses), but luckily writing an impl block when that's the case is a breeze thanks to the split_for_impl() method on Generics.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.