Procedural Macros, pt. 4: Getters

As we saw earlier a getter is a simple function to access the value of a property on a struct. This could be by copy, reference, slice etc., as our requirements may be.

struct Person {
    name: String,
    age: u32,
}

impl Person {
    fn get_name(&self) -> &str {
        self.name.as_str()
    }

    fn get_age(&self) -> u32 {
        self.age
    }
}

To generate the getter code for each field or property, we need to

  1. Derive an appropriate name for the function. Prefixing get_ to the field name makes a reasonable choice.
  2. Determine the mechanism by which the value is to be returned. While age is returned by copy, the getter for name returns a reference to a string slice. Other types may require different treatment. The defaults that we are going to adhere to can be found in the first part of this series.
  3. Evaluate the right return expression based on the return type determined above.

Type Info

The first problem we are going to tackle is that of type information. Before we can evaluate the return type and getter expression for a property, we need to know its type. Unfortunately, the type information we have with a syn::Field in synstructure::Structure is quite limited - at least for our purposes. This means we have to evaluate the type information ourselves. At the moment, we are only interested in the types String and u32. By default, we can treat all numeric types (u8, u16 etc.) the same, and return them by copy. So, let’s define an enum Type which will indicate the type of our field.

#[derive(Clone)]
enum Type {
    String,
    Numeric,
    Unknown,
}

#[derive(Clone)]
struct TypeInfo {
    ty: Type,
    syn_type: syn::Type,
}

impl TypeInfo {
    fn new(ty: Type, syn_type: syn::Type) -> Self {
        TypeInfo { ty, syn_type }
    }

    fn ty(&self) -> &Type {
        &self.ty
    }

    fn syn_type(&self) -> &syn::Type {
        &self.syn_type
    }
}

impl From<&syn::Type> for TypeInfo {
    fn from(syn_type: &syn::Type) -> TypeInfo {
        match syn_type {
            syn::Type::Path(syn::TypePath {
                qself: None,
                ref path,
            }) => match path {
                syn::Path {
                    leading_colon: None,
                    ref segments,
                } if segments.len() == 1 => {
                    let segment = &segments[0];

                    match segment.arguments {
                        syn::PathArguments::None => match segment.ident.to_string().as_str() {
                            "String" => TypeInfo::new(Type::String, syn_type.clone()),
                            \"i8\" | \"i16\" | \"i32\" | \"i64\" | \"u8\" | \"u16\" | \"u32\" | \"u64\"
                            | \"isize\" | \"usize\" | \"f32\" | \"f64\" => {
                                TypeInfo::new(Type::Numeric, syn_type.clone())
                            }
                            _ => TypeInfo::new(Type::Unknown, syn_type.clone()),
                        },
                        _ => TypeInfo::new(Type::Unknown, syn_type.clone()),
                    }
                }
                _ => TypeInfo::new(Type::Unknown, syn_type.clone()),
            },
            _ => TypeInfo::new(Type::Unknown, syn_type.clone()),
        }
    }
}

In the above match code we are only evaluating the syn::Type against String and Numeric types. To be of any real practical use, we’d have to expand this to a lot more -bool, Option<T> etc.

GetterPropertese

To make compiling the information we need to write our getter, let’s include a few helper structs.

struct GetterReturnType(proc_macro2::TokenStream);
struct GetterExpr(proc_macro2::TokenStream);

struct GetterPropertese {
    getter_return_type: GetterReturnType,
    getter_expr: GetterExpr,
}

impl GetterPropertese {
    fn build(field: &syn::Field) -> GetterPropertese {
        let field_ident = field.ident.as_ref().unwrap();
        let type_info = TypeInfo::from(&field.ty);
        let (getter_return_type, getter_expr) = match type_info.ty() {
            Type::String => (
                GetterReturnType(quote!(&str)),
                GetterExpr(quote!(self.#field_ident.as_str())),
            ),
            Type::Numeric => {
                let syn_type = type_info.syn_type();
                (
                    GetterReturnType(quote!(#syn_type)),
                    GetterExpr(quote!(self.#field_ident)),
                )
            },
            _ => {
                let syn_type = type_info.syn_type();
                (
                    GetterReturnType(quote!(&#syn_type)),
                    GetterExpr(quote!(&self.#field_ident)),
                )
            }
        };

        GetterPropertese {
            getter_return_type,
            getter_expr
        }
    }
}

The above snippet generates the token streams that represent the return type and expression evaluator of our getter function.

Come Together, Over Me

The derive_getter function we have is largely empty. Let’s edit it to put together all the information we have.

fn derive_getter(
    field: &syn::Field,
) -> proc_macro2::TokenStream {
    let getter_propertese = GetterPropertese::build(&field);
    let field_ident = field.ident.as_ref().unwrap().clone();
    let fn_name = format_ident!("get_{}", field_ident);
    let getter_return_type = &getter_propertese.getter_return_type.0;
    let getter_expr = &getter_propertese.getter_expr.0;


    quote! { fn #fn_name(&self) -> #getter_return_type {
        #getter_expr
    }}
}

The code above should be largely self-explanatory.

Finally, let’s modify fn derive_propertese() to iterate over each field in the struct and contain all the generated getters in a impl Person block.

fn derive_propertese(s: synstructure::Structure) -> proc_macro2::TokenStream {
    let span = span_of(&s);
    let mut propertese_token_stream = proc_macro2::TokenStream::new();
    match &s.ast().data {
        syn::Data::Struct(data_struct) => match &data_struct.fields {
            syn::Fields::Named(fields_named) => {
                fields\_named.named.iter\().for\_each\(|field| {
                    let getter = derive_getter(field);
                    propertese_token_stream.extend(getter);
                })
            },
            _ => emit_error!(span, "macro propertese can only be called on structs with named fileds")
        }
        _ => emit_error!(span, "macro propertese can only be called on structs")
    }

    let struct_name = s.ast().ident.clone();

    quote! {
        impl #struct_name {
            #propertese_token_stream
        }
    }
}

We obtain the name of the struct from the AST and use that in a quote! macro invocation that wraps everything up neatly in an implementation block. With this we should be at a point where our getters are generated correctly. Well, there is only one way to verify that. Let’s give it a test.