Repository: Ralith/hecs
Pull request #215 (2021-11-29 13:22:32)

Add `DynamicBundleClone` derive macro
Note: `DynamicClone::new` has to be visibile outside the crate in order to be used in the macro.

Fixes issue #214 

ten3roberts: I hope I am of help, and if there are any other features that you'd want implemented or have planned I can look into them.
ten3roberts: There is some code duplication, specifically  `make_structs_fields`. Should I create a crate only common module for reuse?

Patch:
diff --git a/macros/src/bundle.rs b/macros/src/bundle.rs
index a2e01b1..604da04 100644
--- a/macros/src/bundle.rs
+++ b/macros/src/bundle.rs
@@ -1,10 +1,11 @@
 use std::borrow::Cow;
 
-use proc_macro2::Span;
 use proc_macro2::TokenStream as TokenStream2;
 use quote::quote;
 use syn::{DeriveInput, Error, Result};
 
+use crate::common::{member_as_idents, struct_fields};
+
 pub fn derive(input: DeriveInput) -> Result<TokenStream2> {
     let ident = input.ident;
     let data = match input.data {
@@ -163,40 +164,3 @@ fn add_additional_bounds_to_generic_params(mut generics: syn::Generics) -> syn::
     });
     generics
 }
-
-fn struct_fields(fields: &syn::Fields) -> (Vec<&syn::Type>, Vec<syn::Member>) {
-    match fields {
-        syn::Fields::Named(ref fields) => fields
-            .named
-            .iter()
-            .map(|f| (&f.ty, syn::Member::Named(f.ident.clone().unwrap())))
-            .unzip(),
-        syn::Fields::Unnamed(ref fields) => fields
-            .unnamed
-            .iter()
-            .enumerate()
-            .map(|(i, f)| {
-                (
-                    &f.ty,
-                    syn::Member::Unnamed(syn::Index {
-                        index: i as u32,
-                        span: Span::call_site(),
-                    }),
-                )
-            })
-            .unzip(),
-        syn::Fields::Unit => (Vec::new(), Vec::new()),
-    }
-}
-
-fn member_as_idents(members: &[syn::Member]) -> Vec<Cow<'_, syn::Ident>> {
-    members
-        .iter()
-        .map(|member| match member {
-            syn::Member::Named(ident) => Cow::Borrowed(ident),
-            &syn::Member::Unnamed(syn::Index { index, span }) => {
-                Cow::Owned(syn::Ident::new(&format!("tuple_field_{}", index), span))
-            }
-        })
-        .collect()
-}
diff --git a/macros/src/bundle_clone.rs b/macros/src/bundle_clone.rs
new file mode 100644
index 0000000..56dfc53
--- /dev/null
+++ b/macros/src/bundle_clone.rs
@@ -0,0 +1,64 @@
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::{DeriveInput, Error, Result};
+
+use crate::common::struct_fields;
+
+pub fn derive(input: DeriveInput) -> Result<TokenStream2> {
+    let ident = input.ident;
+    let data = match input.data {
+        syn::Data::Struct(s) => s,
+        _ => {
+            return Err(Error::new_spanned(
+                ident,
+                "derive(DynamicBundleClone) does not support enums or unions",
+            ))
+        }
+    };
+    let (tys, field_members) = struct_fields(&data.fields);
+    let generics = add_additional_bounds_to_generic_params(input.generics);
+
+    let dyn_bundle_code = gen_dynamic_bundle_impl(&ident, &generics, &field_members, &tys);
+    Ok(dyn_bundle_code)
+}
+
+fn gen_dynamic_bundle_impl(
+    ident: &syn::Ident,
+    generics: &syn::Generics,
+    field_members: &[syn::Member],
+    tys: &[&syn::Type],
+) -> TokenStream2 {
+    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
+    quote! {
+        unsafe impl #impl_generics ::hecs::DynamicBundleClone for #ident #ty_generics #where_clause {
+            #[allow(clippy::forget_copy)]
+            unsafe fn put_with_clone(mut self, mut f: impl ::std::ops::FnMut(*mut u8, ::hecs::TypeInfo, DynamicClone)) {
+                #(
+                    f(
+                        (&mut self.#field_members as *mut #tys).cast::<u8>(),
+                        ::hecs::TypeInfo::of::<#tys>(),
+                        ::hecs::DynamicClone::new::<#tys>()
+                    );
+                    ::std::mem::forget(self.#field_members);
+                )*
+            }
+        }
+    }
+}
+
+fn make_component_trait_bound() -> syn::TraitBound {
+    syn::TraitBound {
+        paren_token: None,
+        modifier: syn::TraitBoundModifier::None,
+        lifetimes: None,
+        path: syn::parse_quote!(::std::clone::Clone),
+    }
+}
+
+fn add_additional_bounds_to_generic_params(mut generics: syn::Generics) -> syn::Generics {
+    for tp in generics.type_params_mut() {
+        tp.bounds
+            .push(syn::TypeParamBound::Trait(make_component_trait_bound()))
+    }
+    generics
+}
diff --git a/macros/src/common.rs b/macros/src/common.rs
new file mode 100644
index 0000000..327656c
--- /dev/null
+++ b/macros/src/common.rs
@@ -0,0 +1,40 @@
+use std::borrow::Cow;
+
+use proc_macro2::Span;
+
+pub fn struct_fields(fields: &syn::Fields) -> (Vec<&syn::Type>, Vec<syn::Member>) {
+    match fields {
+        syn::Fields::Named(ref fields) => fields
+            .named
+            .iter()
+            .map(|f| (&f.ty, syn::Member::Named(f.ident.clone().unwrap())))
+            .unzip(),
+        syn::Fields::Unnamed(ref fields) => fields
+            .unnamed
+            .iter()
+            .enumerate()
+            .map(|(i, f)| {
+                (
+                    &f.ty,
+                    syn::Member::Unnamed(syn::Index {
+                        index: i as u32,
+                        span: Span::call_site(),
+                    }),
+                )
+            })
+            .unzip(),
+        syn::Fields::Unit => (Vec::new(), Vec::new()),
+    }
+}
+
+pub fn member_as_idents(members: &[syn::Member]) -> Vec<Cow<'_, syn::Ident>> {
+    members
+        .iter()
+        .map(|member| match member {
+            syn::Member::Named(ident) => Cow::Borrowed(ident),
+            &syn::Member::Unnamed(syn::Index { index, span }) => {
+                Cow::Owned(syn::Ident::new(&format!("tuple_field_{}", index), span))
+            }
+        })
+        .collect()
+}
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 8a121a1..ccbf053 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -8,8 +8,11 @@
 extern crate proc_macro;
 
 mod bundle;
+mod bundle_clone;
 mod query;
 
+pub(crate) mod common;
+
 use proc_macro::TokenStream;
 use syn::{parse_macro_input, DeriveInput};
 
@@ -40,6 +43,25 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
     .into()
 }
 
+/// Implement `DynamicBundleClone` for a struct.
+///
+/// This is an extension macro for bundles which allow them to be cloned, and
+/// subsequently used in `EntityBuilderClone::add_bundle`.
+///
+/// Requires that all fields of the struct implement [`Clone`].
+///
+/// The trait Bundle must also be implemented to be able to be used in
+/// entity builder.
+#[proc_macro_derive(DynamicBundleClone)]
+pub fn derive_dynamic_bundle_clone(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    match bundle_clone::derive(input) {
+        Ok(ts) => ts,
+        Err(e) => e.to_compile_error(),
+    }
+    .into()
+}
+
 /// Implement `Query` for a struct
 ///
 /// Queries structs can be passed to the type parameter of `World::query`. They must have exactly
diff --git a/src/bundle.rs b/src/bundle.rs
index 9eb5158..eabb935 100644
--- a/src/bundle.rs
+++ b/src/bundle.rs
@@ -14,6 +14,7 @@ use crate::archetype::TypeInfo;
 use crate::Component;
 
 /// A dynamically typed collection of components
+#[allow(clippy::missing_safety_doc)]
 pub unsafe trait DynamicBundle {
     /// Returns a `TypeId` uniquely identifying the set of components, if known
     #[doc(hidden)]
@@ -36,6 +37,7 @@ pub unsafe trait DynamicBundle {
 }
 
 /// A statically typed collection of components
+#[allow(clippy::missing_safety_doc)]
 pub unsafe trait Bundle: DynamicBundle {
     #[doc(hidden)]
     fn with_static_ids<T>(f: impl FnOnce(&[TypeId]) -> T) -> T;
@@ -57,6 +59,7 @@ pub unsafe trait Bundle: DynamicBundle {
 }
 
 /// A dynamically typed collection of cloneable components
+#[allow(clippy::missing_safety_doc)]
 pub unsafe trait DynamicBundleClone: DynamicBundle {
     /// Allow a callback to move all components out of the bundle
     ///
@@ -73,7 +76,8 @@ pub struct DynamicClone {
 }
 
 impl DynamicClone {
-    pub(crate) fn new<T: Component + Clone>() -> Self {
+    /// Create a new type ereased cloner for the type T
+    pub fn new<T: Component + Clone>() -> Self {
         Self {
             func: |src, f| unsafe {
                 let mut tmp = (*src.cast::<T>()).clone();
diff --git a/src/column.rs b/src/column.rs
index 9b8b130..b15502a 100644
--- a/src/column.rs
+++ b/src/column.rs
@@ -105,6 +105,17 @@ impl<'a, T: Component> ColumnMut<'a, T> {
 
     /// Access the `T` component of `entity`
     pub fn get(&mut self, entity: Entity) -> Result<&mut T, ComponentError> {
+        unsafe { self.get_unchecked(entity) }
+    }
+
+    /// Access the `T` component of `entity` without unique access to `self`. Allows simultaneous
+    /// access to the components of multiple entities.
+    ///
+    /// # Safety
+    ///
+    /// Must not be invoked while a borrow to any part of the same `T` is live, e.g. due to a prior
+    /// call to `get_unchecked` on the same entity.
+    pub unsafe fn get_unchecked(&self, entity: Entity) -> Result<&mut T, ComponentError> {
         let meta = self
             .entities
             .get(entity.id as usize)
@@ -116,13 +127,11 @@ impl<'a, T: Component> ColumnMut<'a, T> {
             .unwrap();
         let state = self.archetype_state[meta.location.archetype as usize]
             .ok_or_else(MissingComponent::new::<T>)?;
-        unsafe {
-            let target = archetype
-                .get_base::<T>(state)
-                .as_ptr()
-                .add(meta.location.index as usize);
-            Ok(&mut *target)
-        }
+        let target = archetype
+            .get_base::<T>(state)
+            .as_ptr()
+            .add(meta.location.index as usize);
+        Ok(&mut *target)
     }
 }
 
diff --git a/src/entities.rs b/src/entities.rs
index 092a273..1db05f2 100644
--- a/src/entities.rs
+++ b/src/entities.rs
@@ -505,7 +505,10 @@ pub(crate) struct EntityMeta {
 
 impl EntityMeta {
     const EMPTY: EntityMeta = EntityMeta {
-        generation: unsafe { NonZeroU32::new_unchecked(1) },
+        generation: match NonZeroU32::new(1) {
+            Some(x) => x,
+            None => unreachable!(),
+        },
         location: Location {
             archetype: 0,
             index: u32::max_value(), // dummy value, to be filled in
diff --git a/src/lib.rs b/src/lib.rs
index ba014ad..bbc39d1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -73,7 +73,7 @@ mod world;
 
 pub use archetype::{Archetype, ArchetypeColumn};
 pub use batch::{BatchIncomplete, BatchWriter, ColumnBatch, ColumnBatchBuilder, ColumnBatchType};
-pub use bundle::{Bundle, DynamicBundle, MissingComponent};
+pub use bundle::{Bundle, DynamicBundle, DynamicBundleClone, MissingComponent};
 pub use column::{Column, ColumnMut};
 pub use command_buffer::CommandBuffer;
 pub use entities::{Entity, NoSuchEntity};
@@ -92,6 +92,8 @@ pub use world::{
 // Unstable implementation details needed by the macros
 #[doc(hidden)]
 pub use archetype::TypeInfo;
+#[doc(hidden)]
+pub use bundle::DynamicClone;
 #[cfg(feature = "macros")]
 #[doc(hidden)]
 pub use lazy_static;
@@ -99,7 +101,7 @@ pub use lazy_static;
 pub use query::Fetch;
 
 #[cfg(feature = "macros")]
-pub use hecs_macros::{Bundle, Query};
+pub use hecs_macros::{Bundle, DynamicBundleClone, Query};
 
 fn align(x: usize, alignment: usize) -> usize {
     debug_assert!(alignment.is_power_of_two());
diff --git a/src/query.rs b/src/query.rs
index 8852410..5236663 100644
--- a/src/query.rs
+++ b/src/query.rs
@@ -28,6 +28,7 @@ pub trait Query {
 pub type QueryItem<'a, Q> = <<Q as Query>::Fetch as Fetch<'a>>::Item;
 
 /// Streaming iterators over contiguous homogeneous ranges of components
+#[allow(clippy::missing_safety_doc)]
 pub unsafe trait Fetch<'a>: Sized {
     /// Type of value to be fetched
     type Item;


Test patch:
diff --git a/tests/derive/wrong_lifetime.stderr b/tests/derive/wrong_lifetime.stderr
index f516545..f462edb 100644
--- a/tests/derive/wrong_lifetime.stderr
+++ b/tests/derive/wrong_lifetime.stderr
@@ -4,7 +4,7 @@ error[E0495]: cannot infer an appropriate lifetime for lifetime parameter `'a` d
 3 | #[derive(Query)]
   |          ^^^^^
   |
-note: first, the lifetime cannot outlive the lifetime `'a` as defined on the impl at 3:10...
+note: first, the lifetime cannot outlive the lifetime `'a` as defined here...
  --> $DIR/wrong_lifetime.rs:3:10
   |
 3 | #[derive(Query)]
diff --git a/tests/tests.rs b/tests/tests.rs
index f0e1877..394b4e0 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -77,6 +77,43 @@ fn derived_query() {
     );
 }
 
+#[test]
+#[cfg(feature = "macros")]
+fn derived_bundle_clone() {
+    #[derive(Bundle, DynamicBundleClone)]
+    struct Foo<T: Clone + Component> {
+        x: i32,
+        y: bool,
+        z: T,
+    }
+
+    #[derive(PartialEq, Debug, Query)]
+    struct FooQuery<'a> {
+        x: &'a i32,
+        y: &'a bool,
+        z: &'a String,
+    }
+
+    let mut world = World::new();
+    let mut builder = EntityBuilderClone::new();
+    builder.add_bundle(Foo {
+        x: 42,
+        y: false,
+        z: String::from("Foo"),
+    });
+
+    let entity = builder.build();
+    let e = world.spawn(&entity);
+    assert_eq!(
+        world.query_one_mut::<FooQuery>(e).unwrap(),
+        FooQuery {
+            x: &42,
+            y: &false,
+            z: &String::from("Foo"),
+        }
+    );
+}
+
 #[test]
 fn query_single_component() {
     let mut world = World::new();
@@ -244,6 +281,39 @@ fn build_builder_clone() {
     assert_eq!(b.get::<i32>(), Some(&123));
 }
 
+#[test]
+#[cfg(feature = "macros")]
+fn build_dynamic_bundle() {
+    #[derive(Bundle, DynamicBundleClone)]
+    struct Foo {
+        x: i32,
+        y: char,
+    }
+
+    let mut world = World::new();
+    let mut entity = EntityBuilderClone::new();
+    entity.add_bundle(Foo { x: 5, y: 'c' });
+    entity.add_bundle((String::from("Bar"), 6.0_f32));
+    entity.add('a');
+    let entity = entity.build();
+    let e = world.spawn(&entity);
+    let f = world.spawn(&entity);
+    let g = world.spawn(&entity);
+
+    world
+        .insert_one(g, Cow::<'static, str>::from("after"))
+        .unwrap();
+
+    for e in [e, f, g] {
+        assert_eq!(*world.get::<i32>(e).unwrap(), 5);
+        assert_eq!(*world.get::<char>(e).unwrap(), 'a');
+        assert_eq!(*world.get::<String>(e).unwrap(), "Bar");
+        assert_eq!(*world.get::<f32>(e).unwrap(), 6.0);
+    }
+
+    assert_eq!(*world.get::<Cow<'static, str>>(g).unwrap(), "after");
+}
+
 #[test]
 fn access_builder_components() {
     let mut world = World::new();