4
\$\begingroup\$

To learn how custom derive macros work in Rust, I decided to create a simple derive macro to provide getters and setters for a given struct.

It would be used like this:

#[derive(GetSet)]
pub struct Example {
    #[get]
    #[set]
    field1: String,
    #[get]
    field2: i32,
}

However, there are some things that feel wrong to me about it:

  1. The code uses both proc_macro and proc_macro2, is this necessary?
  2. Currently, every getter / setter is in their own impl block. But the quote! macro requires code that is well-formed regarding braces, so I think I cannot create impl #name { first and fill in the methods afterwards. Is there a better way?
  3. The function identifiers are created with format!, is this okay?
  4. Would it be possible to adapt it so one could just write
#[get, set]
field: (),

instead of

#[get]
#[set]
field: (),

Apart from that, what else would you improve or change?

Here is the code:

use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{Data, DeriveInput};

#[proc_macro_derive(GetSet, attributes(get, set))]
pub fn get_set_derive(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap();

    let mut gen = proc_macro2::TokenStream::new();

    let name = &ast.ident;
    let data = &ast.data;

    if let Data::Struct(data_struct) = data {
        let fields = &data_struct.fields;

        for field in fields {
            let field_name = field
                .ident
                .as_ref()
                .expect("struct idents are required for this to work!");
            let field_type = &field.ty;

            for attr in &field.attrs {
                let path = attr.path();

                if path.is_ident("get") {
                    let fn_ident = Ident::new(&format!("get_{field_name}"), Span::call_site());

                    let getter = quote! {
                        impl #name {
                            pub fn #fn_ident(&self) -> &#field_type {
                                &self.#field_name
                            }
                        }
                    };

                    gen.extend(getter);
                }
                if path.is_ident("set") {
                    let fn_ident = Ident::new(&format!("set_{field_name}"), Span::call_site());

                    let setter = quote! {
                        impl #name {
                            pub fn #fn_ident(&mut self, new: #field_type) {
                                self.#field_name = new;
                            }
                        }
                    };

                    gen.extend(setter);
                }
            }
        }
    } else {
        panic!("no support for enums or unions!")
    }

    gen.into()
}
\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

The code uses both proc_macro and proc_macro2, is this necessary?

This is normal. proc_macro is the compiler API stub, only available to proc-macro crates, and proc_macro2 is a plain library that doesn't depend on the compiler proc macro API. It's possible to write a proc-macro crate that doesn't depend on proc_macro2 at all, but then you'd also need to avoid using syn and quote, or any other proc-macro utility libraries — they all depend on proc_macro2, precisely because without it there's no way to manipulate tokens in library code (as opposed to proc-macro crate code).

Currently, every getter / setter is in their own impl block. But the quote! macro requires code that is well-formed regarding braces, so I think I cannot create impl #name { first and fill in the methods afterwards.

You can use an iterator or collection in quote! to assemble the complete structure at once.

let mut functions: Vec<proc_macro2::TokenStream> = Vec::new();

// ... gather functions into vector ...

// this is the entire output of the derive (no `gen` stream)
quote! {
    impl #name {
        #( #functions )*
    }
}

The function identifiers are created with format!, is this okay?

Yes.

Would it be possible to adapt it …

No, the syntax of attributes does not allow #[foo, bar]. You have to pick one name for the attribute, but it can have parameters: #[getset(get, set)].

Apart from that, what else would you improve or change?

It looks pretty good. You asked the right questions and I can't think of more things to change.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.