What do you think about this kind of derive macro with attributes?

This project is purely for experimentation. It's not for production. I just want to get your reviews and suggestions on this code. I'm trying to experiment on procedural derive macros with custom attributes.

Project Structure

.
├── Cargo.toml
├── reflect-derive
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
└── src
    ├── lib.rs
    └── main.rs

Code

Each file path (relative to the project rooot) is at the head of file as comment.

# Cargo.toml

[package]
name = "reflect"
version = "0.1.0"
edition = "2021"

[dependencies]
reflect-derive = { path = "reflect-derive" }
# reflect-derive/Cargo.toml

[package]
name = "reflect-derive"
version = "0.1.0"
edition = "2021"

[dependencies]
proc-macro2 = "1.0.56"
quote = "1.0.26"
syn = "2.0.15"

[lib]
proc-macro = true
// src/main.rs

use reflect::Reflective;

#[derive(Reflective)]
struct Foo {
    a: i32,
    b: u32,
    c: bool,
    #[reflective(hidden)]
    d: String,
}

fn main() {
    let foo = Foo {
        a: 1,
        b: 2,
        c: false,
        d: "Hello".to_string(),
    };
    println!("Name of struct: {}", foo.name());
    println!("Props: {:?}", foo.props());
}
// src/lib.rs

pub use reflect_derive::Reflective;

pub trait Reflective {
    fn name(&self) -> String;
    fn props(&self) -> Vec<String>;
}
// reflect-derive/src/lib.rs

use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote;
use syn::{parse_macro_input, Attribute, Data, DeriveInput, Field};

#[proc_macro_derive(Reflective, attributes(reflective))]
pub fn reflect_derive_fn(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    // get struct identifier
    let struct_ident = input.ident;
    let struct_ident_str = struct_ident.to_string();

    // get fields
    let data = input.data;
    let fields = get_fields(data);

    let field_idents: Vec<&Ident> = fields
        .iter()
        // filter `#[repetitive(hidden)]` fields
        .filter(|f| !is_field_hidden(f))
        // get field identifiers
        .map(|f| f.ident.as_ref().unwrap())
        .collect();

    // stringify field identifiers
    let field_ident_strs: Vec<String> = field_idents.iter().map(|f| f.to_string()).collect();

    let output = quote! {
        impl Reflective for #struct_ident {
            // simple
            fn name(&self) -> String {
                String::from(#struct_ident_str)
            }

            // repetition
            fn props(&self) -> Vec<String> {
                vec![#(String::from(#field_ident_strs)),*]
            }
        }
    };

    TokenStream::from(output)
}

fn get_fields(data: Data) -> Vec<Field> {
    match data {
        Data::Struct(data) => data
            .fields
            .into_iter()
            .filter(|f| f.ident.is_some())
            .collect::<Vec<Field>>(),
        Data::Enum(_) => panic!("Enums are not supported by reflect."),
        Data::Union(_) => panic!("Unions are not supported by reflect."),
    }
}

fn is_field_hidden(field: &Field) -> bool {
    let ref attrs = field.attrs;

    attrs.iter().any(contains_hidden_attr)
}

fn contains_hidden_attr(attr: &Attribute) -> bool {
    if attr.path().is_ident("reflective") {
        let mut contains_hidden = false;
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("hidden") {
                contains_hidden = true;
                Ok(())
            } else {
                Err(meta.error("Unrecognized `repetitive` parameters."))
            }
        })
        .expect("Could not parse `repetitive` attribute macro.");
        contains_hidden
    } else {
        false
    }
}

Explanation

This is kind of a reflection library to get the metadata of something, which is a struct in this case. I wanted to write it for education purposes.

There is a trait named Reflective, and reflect_derive_fn generates the implementation for it.

Reflective::name is quite simple, it just returns a String with struct's name.

Reflect::props returns a Vec<String> of attributes of a struct.

A field in a struct, in this case Foo struct, might have an attribute named #[reflective(hidden)], which hides it from the return of Reflect::props.


The code works as expected. I just wanted to get your suggestions if you have any. The things I don't like with this implementation are:

  • Getting #[reflective(hidden)] seems like too much boilerplate. IS there a third party library that makes it easier?
  • (I will add further if I have any more questions.)

Thanks in advance.

For declarative attribute parsing, you can use deluxe.

There are two things that immediately stick out to me in the code:

  • Since every type and field name is a compile-time constant, you could just return &'static str and &'static [&'static str] instead of String and Vec<String>, avoiding unneeded allocations.
  • Instead of filtering on is_some() then unwrapping, use filter_map().
2 Likes

This seems amazing. Will check it out.

I'm aware of this, just wanted to keep things extra simple.

I have never understood filter_map (or the idea of filtering and mapping at the same time to be precise). Let me check that again.

Thank you. :slight_smile:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.