on
Procedural Macros, pt. 3: Code
The synstructure crate provides a good starting point for our derive macro via
decl_derive.
decl_derive!([propertese, attributes(propertese)] => #[proc_macro_error] derive_propertese);
fn derive_propertese(s: synstructure::Structure) -> proc_macro2::TokenStream {
// Macro code to be filled in
}Let’s break down the code snippet above
decl_derive!(...);is a macro in thesynstructurecrate.[propertese, ....] => ...declares the name of the derive macro we are writing. In this case,propertese.[..., attributes(propertese)]allows us to pass in attributes to our derive macro.[...] => #[proc_macro_error] derive_propertesespecifies thatderive_properteseis the function that will be called to generate the macro’s code. This function has to take in asynstructure::Strutctureas an input parameter and return aTokenStream.#[proc_macro_error]helps us to better manage errors generated by our procedural macro.
This essentially allows us to do something like the following, and have our getters automatically generated.
#[derive(propertese)]
struct Person {
name: String,
age: u32
}Let’s take an initial stab at filling in our function derive_propertese.
fn derive_propertese(s: synstructure::Structure) -> proc_macro2::TokenStream {
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);
})
},
_ => panic!("macro propertese can only be called on structs with named fileds")
}
_ => panic!("macro propertese can only be called on structs")
}
#propertese_token_stream
}
fn derive_getter(
field: &syn::Field,
) -> proc_macro2::TokenStream {
proc_macro2::TokenStream::new()
}Break it down again
While it looks a bit, the snippet above can be broken down neatly
let mut propertese_token_stream = proc_macro2::TokenStream::new();Our
derive_propertesefunction is to return aTokenStream. This just declares a mutable one. As we build the token streams for each field’s getter, we will append to it.match &s.ast().data ...synstructure::Structuregives us an AST (Abstract Syntax Tree) to work on. The data property on the AST can refer to a Struct, Enum or Union as the case may be. We are only interested in data that has a type of Struct, particularly, ones with named fields. In other instances, we panic as our macro does not support it.fields_named.named_iter().for_each(...)We iterate over each field and build a getter for it. At a later point, we will add functionality that allows us to skip its generation entirely.
Stay calm. Do not panic
In the above snippet, our code generates a panic anytime it encounters an
error. To handle these better, let’s convert these panics into errors. We do
so with the help of the macro emit_error (from the crate proc_macro_error),
which takes in an error message and a
span for the
error.
fn derive_propertese(s: synstructure::Structure) -> proc_macro2::TokenStream {
let span = span_of(&s);
...
match &s.ast().data {
syn::Data::Struct(data_struct) => match &data_struct.fields {
syn::Fields::Named(fields_named) => {
...
},
_ => 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")
}
quote! {
...
}
}
fn span_of(s: &synstructure::Structure) -> proc_macro2::Span {
match &s.ast().data {
syn::Data::Struct(data_struct) => data_struct.struct_token.span,
syn::Data::Enum(data_enum) => data_enum.enum_token.span,
syn::Data::Union(data_union) => data_union.union_token.span,
}
}We have a good base here. Let’s follow it up with writing the code to generate our getters in the next part to this series.