How do I process enum/struct/field attributes in a procedural macro?
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
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.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 aboutstate_change
. Thesyn
crate gives you all the needed information about definitions (but not implementations unfortunately (I am talking aboutimpl
here) but this is enough for handling attributes of course). To be more detailed, we needsyn::DeriveInput
,syn::Body
,syn::Variant
,syn::Attribute
and finallysyn::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.