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

前端 未结 2 1400
不知归路
不知归路 2021-01-04 03:31

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

#[derive(Serialize)]
struct Resource {
    // Always serializ         


        
2条回答
  •  借酒劲吻你
    2021-01-04 04:10

    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 - 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.

提交回复
热议问题