Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I process enum/struct/field attributes in a procedural macro?

Tags:

rust

Serde supports applying custom attributes that are used with #[derive(Serialize)]:

#[derive(Serialize)]
struct Resource {
    // Always serialized.
    name: String,

    // Never serialized.
    #[serde(skip_serializing)]
    hash: String,

    // Use a method to decide whether the field should be skipped.
    #[serde(skip_serializing_if = "Map::is_empty")]
    metadata: Map<String, String>,
}

I understand how to implement a procedural macro (Serialize in this example) but what should I do to implement #[serde(skip_serializing)]? I was unable to find this information anywhere. The docs don't even mention this. I have tried to look at the serde-derive source code but it is very complicated for me.

like image 968
Victor Polevoy Avatar asked Feb 27 '17 11:02

Victor Polevoy


2 Answers

  1. First you must register all of your attributes in the same place you register your procedural macro. Let's say we want to add two attributes (we still don't talk what will they belong to: structs or fields or both of them):

     #[proc_macro_derive(FiniteStateMachine, attributes(state_transitions, state_change))]
     pub fn fxsm(input: TokenStream) -> TokenStream {
         // ...
     }
    

    After that you may already compile your user code with the following:

     #[derive(Copy, Clone, Debug, FiniteStateMachine)]
     #[state_change(GameEvent, change_condition)] // optional
     enum GameState {
         #[state_transitions(NeedServer, Ready)]
         Prepare { players: u8 },
         #[state_transitions(Prepare, Ready)]
         NeedServer,
         #[state_transitions(Prepare)]
         Ready,
     }
    

    Without that compiler will give a error with message like:

    state_change does not belong to any known attribute.

    These attributes are optional and all we have done is allow them to be to specified. When you derive your procedural macro you may check for everything you want (including attributes existence) and panic! on some condition with meaningful message which will be told by the compiler.

  2. Now we will talk about handling the attribute! Let's forget about state_transitions attribute because it's handling will not vary too much from handling struct/enum attributes (actually it is only a little bit more code) and talk about state_change. The syn crate gives you all the needed information about definitions (but not implementations unfortunately (I am talking about impl here) but this is enough for handling attributes of course). To be more detailed, we need syn::DeriveInput, syn::Body, syn::Variant, syn::Attribute and finally syn::MetaItem.

To handle the attribute of a field you need to go through all these structures from one to another. When you reach Vec<syn:: Attribute> - this is what you want, a list of all attributes of a field. Here our state_transitions can be found. When you find it, you may want to get its content and this can be done by using matching syn::MetaItem enum. Just read the docs :) Here is a simple example code which panics when we find state_change attribute on some field plus it checks does our target entity derive Copy or Clone or neither of them:

    #[proc_macro_derive(FiniteStateMachine, attributes(state_transitions, state_change))]
    pub fn fxsm(input: TokenStream) -> TokenStream {
        // Construct a string representation of the type definition
        let s = input.to_string();

        // Parse the string representation
        let ast = syn::parse_derive_input(&s).unwrap();

        // Build the impl
        let gen = impl_fsm(&ast);

        // Return the generated impl
        gen.parse().unwrap()
    }

    fn impl_fsm(ast: &syn::DeriveInput) -> Tokens {
        const STATE_CHANGE_ATTR_NAME: &'static str = "state_change";

        if let syn::Body::Enum(ref variants) = ast.body {

            // Looks for state_change attriute (our attribute)
            if let Some(ref a) = ast.attrs.iter().find(|a| a.name() == STATE_CHANGE_ATTR_NAME) {
                if let syn::MetaItem::List(_, ref nested) = a.value {
                    panic!("Found our attribute with contents: {:?}", nested);
                }
            }

            // Looks for derive impls (not our attribute)
            if let Some(ref a) = ast.attrs.iter().find(|a| a.name() == "derive") {
                if let syn::MetaItem::List(_, ref nested) = a.value {
                    if derives(nested, "Copy") {
                        return gen_for_copyable(&ast.ident, &variants, &ast.generics);
                    } else if derives(nested, "Clone") {
                        return gen_for_clonable(&ast.ident, &variants, &ast.generics);
                    } else {
                        panic!("Unable to produce Finite State Machine code on a enum which does not drive Copy nor Clone traits.");
                    }
                } else {
                    panic!("Unable to produce Finite State Machine code on a enum which does not drive Copy nor Clone traits.");
                }
            } else {
                panic!("How have you been able to call me without derive!?!?");
            }
        } else {
            panic!("Finite State Machine must be derived on a enum.");
        }
    }

    fn derives(nested: &[syn::NestedMetaItem], trait_name: &str) -> bool {
        nested.iter().find(|n| {
            if let syn::NestedMetaItem::MetaItem(ref mt) = **n {
                if let syn::MetaItem::Word(ref id) = *mt {
                    return id == trait_name;
                }
                return false
            }
            false
        }).is_some()
    }

You may be interested in reading serde_codegen_internals, serde_derive, serenity's #[command] attr, another small project of mine - unique-type-id, fxsm-derive. The last link is actually my own project to explain to myself how to use procedural macros in Rust.


After some Rust 1.15 and updating the syn crate, it is no longer possible to check derives of a enums/structs, however, everything else works okay.

like image 114
Victor Polevoy Avatar answered Nov 13 '22 23:11

Victor Polevoy


You implement attributes on fields as part of the derive macro for the struct (you can only implement derive macros for structs and enums).

Serde does this by checking every field for an attribute within the structures provided by syn and changing the code generation accordingly.

You can find the relevant code here: https://github.com/serde-rs/serde/blob/master/serde_derive/src/internals/attr.rs

like image 28
oli_obk Avatar answered Nov 14 '22 00:11

oli_obk