Pineapple
Pineapple

Reputation: 53

How do you generate a struct dynamically at compile time in Rust?

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?

Upvotes: 4

Views: 1831

Answers (2)

Pineapple
Pineapple

Reputation: 53

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!();

Upvotes: 0

rodrigo
rodrigo

Reputation: 98526

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.

Upvotes: 1

Related Questions