2

I have the following:

struct Health {
  health: f32,
}

struct Position {
  position: Vec2,
}

struct Collections {
  healths: Vec<Health>,
  positions: Vec<Position>,
}

I would like to generate the Collections struct automatically; I am thinking using a macro?

I thought perhaps I could mark each struct I want to include with a custom attribute and then have a macro which builds the Collections struct.

How could I do this?

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
Pineapple
  • 33
  • 4
  • Seems like you might be interested in the [entity-component-system pattern](https://en.wikipedia.org/wiki/Entity_component_system). There's several crates providing an ECS implementation for Rust. – cdhowie Jul 02 '22 at 19:29
  • @cdhowie Thanks, I am actually implementing my own ECS, that is exactly why I need this. I have been trying to reverse engineer the existing implementations, but they seem to be overly complicated. Often their implementations do not play well with serialization because they have no way of iterating over each component type. – Pineapple Jul 02 '22 at 19:35
  • Unless you have all your structs in one place, this is generally impossible, although there are workarounds. – Chayim Friedman Jul 02 '22 at 22:49
  • Does this answer your question? [A macro registering marked structures into an enum](https://stackoverflow.com/questions/71695837/a-macro-registering-marked-structures-into-an-enum) – Chayim Friedman Jul 02 '22 at 22:50
  • Linking question [Is it possible to define structs at runtime or otherwise achieve a similar effect?](https://stackoverflow.com/questions/47159418/is-it-possible-to-define-structs-at-runtime-or-otherwise-achieve-a-similar-effec) – JamesThomasMoon Apr 23 '23 at 01:37

2 Answers2

1

To be able to do something like custom attributes you need to write a proc_macro, that can do almost anything you need with your code.

For a simpler solution you may try with a normal macro_rules. For that you will need to enclose your type definitions into a macro that does the parsing, and emits back the type definition plus the extra code you need, in your case the Container class.

Something like this:

macro_rules! collectables {
    (
        $(
            #[collection=$fname:ident]
            $(#[$attr:meta])?
            $vis:vis struct $name:ident $def:tt
        )*
    ) => {
        // The struct definitions
        $(
            $(#[$attr])?
            $vis struct $name $def
        )*
        
        // The container
        #[derive(Default, Debug)]
        pub struct Collections {
          $(
            $fname: Vec<$name>,
          )*
        }
    };
}

Now you can use the macro to build your original code (playground):

collectables!{
    #[collection=healths]
    #[derive(Debug)]
    struct Health {
      health: f32,
    }
    
    #[collection=positions]
    #[derive(Debug)]
    struct Position {
      position: (f32, f32),
    }
}

Note that as written the #[collection=xxx] attribute is mandatory and must be the first in every struct definition.

rodrigo
  • 94,151
  • 12
  • 143
  • 190
0

So I managed to solve this problem using a proc_macro. Each struct which is to be included in the final Storage struct is marked with the Component derive attribute. The Storage struct is then built with the storage!() macro.

use lazy_static::lazy_static;
use proc_macro::TokenStream;
use quote::quote;
use std::sync::Mutex;
use syn::{parse_macro_input, parse_str, DeriveInput, ExprType};

lazy_static! {
    static ref COMPONENTS: Mutex<Vec<String>> = Mutex::new(Vec::new());
}

#[proc_macro_derive(Component)]
pub fn component(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput); ‣DeriveInput
    let ident = input.ident; ‣Ident
    COMPONENTS.lock().unwrap().push(ident.to_string());
    let expanded = quote! { ‣TokenStream
        impl component::Component for #ident {}
    };
    TokenStream::from(expanded)
}

#[proc_macro]
pub fn storage(_input: TokenStream) -> TokenStream {
    println!("Building Storage with: {:?}", COMPONENTS.lock().unwrap());
    let mut fields = Vec::new(); ‣Vec<ExprType>
    for type_name in COMPONENTS.lock().unwrap().iter() { ‣&String
        let field = parse_str::<ExprType>( ‣ExprType
            format!("{}s: Vec<{}>", type_name.to_lowercase(), type_name).as_str(),
        ) ‣Result<ExprType, Error>
        .expect("Could not parse component field type");
        fields.push(field);
    }
    let expanded = quote! { ‣TokenStream
        #[derive(Serialize, Deserialize, Debug, Default)]
        struct Storage {
            #(#fields),*
        }
    };
    TokenStream::from(expanded)
}
#[derive(Debug, Serialize, Deserialize, Component)]
struct Health {
    health: f32,
}
#[derive(Debug, Serialize, Deserialize, Component)]
pub struct Age {
    pub age: u64,
}
storage!();
Pineapple
  • 33
  • 4