Skip to content

Commit 3d36ec4

Browse files
re-enable #[derive(TypeUuid)] for generics (#4118)
Support for deriving `TypeUuid` for types with generics was initially added in #2044 but later reverted #2204 because it lead to `MyStruct<A>` and `MyStruct<B>` having the same type uuid. This PR fixes this by generating code like ```rust #[derive(TypeUuid)] #[uuid = "69b09733-a21a-4dab-a444-d472986bd672"] struct Type<T>(T); impl<T: TypeUuid> TypeUuid for Type<T> { const TYPE_UUID: TypeUuid = generate_compound_uuid(Uuid::from_bytes([/* 69b0 uuid */]), T::TYPE_UUID); } ``` where `generate_compound_uuid` will XOR the non-metadata bits of the two UUIDs. Co-authored-by: XBagon <xbagon@outlook.de> Co-authored-by: Jakob Hellermann <hellermann@sipgate.de>
1 parent d5e770d commit 3d36ec4

File tree

3 files changed

+144
-11
lines changed

3 files changed

+144
-11
lines changed

crates/bevy_reflect/bevy_reflect_derive/src/type_uuid.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
extern crate proc_macro;
22

33
use bevy_macro_utils::BevyManifest;
4-
use quote::{quote, ToTokens};
4+
use quote::quote;
55
use syn::*;
66
use uuid::Uuid;
77

88
pub fn type_uuid_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
99
// Construct a representation of Rust code as a syntax tree
1010
// that we can manipulate
11-
let ast: DeriveInput = syn::parse(input).unwrap();
11+
let mut ast: DeriveInput = syn::parse(input).unwrap();
1212
let bevy_reflect_path: Path = BevyManifest::default().get_path("bevy_reflect");
1313

1414
// Build the trait implementation
1515
let name = &ast.ident;
1616

17-
let (impl_generics, type_generics, _) = &ast.generics.split_for_impl();
18-
assert!(
19-
impl_generics.to_token_stream().is_empty() && type_generics.to_token_stream().is_empty(),
20-
"#[derive(TypeUuid)] is not supported for generics.",
21-
);
17+
ast.generics.type_params_mut().for_each(|param| {
18+
param
19+
.bounds
20+
.push(syn::parse_quote!(#bevy_reflect_path::TypeUuid))
21+
});
22+
23+
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
2224

2325
let mut uuid = None;
2426
for attribute in ast.attrs.iter().filter_map(|attr| attr.parse_meta().ok()) {
@@ -56,11 +58,17 @@ pub fn type_uuid_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStre
5658
.map(|byte| format!("{:#X}", byte))
5759
.map(|byte_str| syn::parse_str::<LitInt>(&byte_str).unwrap());
5860

61+
let base = quote! { #bevy_reflect_path::Uuid::from_bytes([#( #bytes ),*]) };
62+
let type_uuid = ast.generics.type_params().fold(base, |acc, param| {
63+
let ident = &param.ident;
64+
quote! {
65+
#bevy_reflect_path::__macro_exports::generate_composite_uuid(#acc, <#ident as #bevy_reflect_path::TypeUuid>::TYPE_UUID)
66+
}
67+
});
68+
5969
let gen = quote! {
60-
impl #bevy_reflect_path::TypeUuid for #name {
61-
const TYPE_UUID: #bevy_reflect_path::Uuid = #bevy_reflect_path::Uuid::from_bytes([
62-
#( #bytes ),*
63-
]);
70+
impl #impl_generics #bevy_reflect_path::TypeUuid for #name #type_generics #where_clause {
71+
const TYPE_UUID: #bevy_reflect_path::Uuid = #type_uuid;
6472
}
6573
};
6674
gen.into()

crates/bevy_reflect/src/lib.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,36 @@ pub use type_uuid::*;
4646
pub use bevy_reflect_derive::*;
4747
pub use erased_serde;
4848

49+
#[doc(hidden)]
50+
pub mod __macro_exports {
51+
use crate::Uuid;
52+
53+
/// Generates a new UUID from the given UUIDs `a` and `b`,
54+
/// where the bytes are generated by a bitwise `a ^ b.rotate_right(1)`.
55+
/// The generated UUID will be a `UUIDv4` (meaning that the bytes should be random, not e.g. derived from the system time).
56+
#[allow(clippy::unusual_byte_groupings)] // unusual byte grouping is meant to signal the relevant bits
57+
pub const fn generate_composite_uuid(a: Uuid, b: Uuid) -> Uuid {
58+
let mut new = [0; 16];
59+
let mut i = 0;
60+
while i < new.len() {
61+
// rotating ensures different uuids for A<B<C>> and B<A<C>> because: A ^ (B ^ C) = B ^ (A ^ C)
62+
// notice that you have to rotate the second parameter: A.rr ^ (B.rr ^ C) = B.rr ^ (A.rr ^ C)
63+
// Solution: A ^ (B ^ C.rr).rr != B ^ (A ^ C.rr).rr
64+
new[i] = a.as_bytes()[i] ^ b.as_bytes()[i].rotate_right(1);
65+
66+
i += 1;
67+
}
68+
69+
// Version: the most significant 4 bits in the 6th byte: 11110000
70+
new[6] = new[6] & 0b0000_1111 | 0b0100_0000; // set version to v4
71+
72+
// Variant: the most significant 3 bits in the 8th byte: 11100000
73+
new[8] = new[8] & 0b000_11111 | 0b100_00000; // set variant to rfc4122
74+
75+
Uuid::from_bytes(new)
76+
}
77+
}
78+
4979
#[cfg(test)]
5080
#[allow(clippy::blacklisted_name, clippy::approx_constant)]
5181
mod tests {

crates/bevy_reflect/src/type_uuid.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,98 @@ where
2828
std::any::type_name::<Self>()
2929
}
3030
}
31+
32+
#[cfg(test)]
33+
mod test {
34+
use super::*;
35+
use crate as bevy_reflect;
36+
use bevy_reflect_derive::TypeUuid;
37+
use std::marker::PhantomData;
38+
39+
#[derive(TypeUuid)]
40+
#[uuid = "af6466c2-a9f4-11eb-bcbc-0242ac130002"]
41+
struct TestDeriveStruct<T>
42+
where
43+
T: Clone,
44+
{
45+
_value: T,
46+
}
47+
48+
fn test_impl_type_uuid(_: &impl TypeUuid) {}
49+
50+
#[test]
51+
fn test_generic_type_uuid_derive() {
52+
#[derive(TypeUuid, Clone)]
53+
#[uuid = "ebb16cc9-4d5a-453c-aa8c-c72bd8ec83a2"]
54+
struct T;
55+
56+
let test_struct = TestDeriveStruct { _value: T };
57+
test_impl_type_uuid(&test_struct);
58+
}
59+
60+
#[test]
61+
fn test_generic_type_unique_uuid() {
62+
#[derive(TypeUuid, Clone)]
63+
#[uuid = "49951b1c-4811-45e7-acc6-3119249fbd8f"]
64+
struct A;
65+
66+
#[derive(TypeUuid, Clone)]
67+
#[uuid = "4882b8f5-5556-4cee-bea6-a2e5991997b7"]
68+
struct B;
69+
70+
let uuid_a = TestDeriveStruct::<A>::TYPE_UUID;
71+
let uuid_b = TestDeriveStruct::<B>::TYPE_UUID;
72+
73+
assert_ne!(uuid_a, uuid_b);
74+
assert_ne!(uuid_a, A::TYPE_UUID);
75+
assert_ne!(uuid_b, B::TYPE_UUID);
76+
}
77+
78+
#[test]
79+
fn test_inverted_generic_type_unique_uuid() {
80+
#[derive(TypeUuid, Clone)]
81+
#[uuid = "49951b1c-4811-45e7-acc6-3119249fbd8f"]
82+
struct Inner;
83+
84+
#[derive(TypeUuid, Clone)]
85+
#[uuid = "23ebc0c3-ef69-4ea0-8c2a-dca1b4e27c0d"]
86+
struct TestDeriveStructA<T>
87+
where
88+
T: Clone,
89+
{
90+
_phantom: PhantomData<T>,
91+
}
92+
93+
#[derive(TypeUuid, Clone)]
94+
#[uuid = "a82f9936-70cb-482a-bd3d-cb99d87de55f"]
95+
struct TestDeriveStructB<T>
96+
where
97+
T: Clone,
98+
{
99+
_phantom: PhantomData<T>,
100+
}
101+
102+
let uuid_ab = TestDeriveStructA::<TestDeriveStructB<Inner>>::TYPE_UUID;
103+
let uuid_ba = TestDeriveStructB::<TestDeriveStructA<Inner>>::TYPE_UUID;
104+
105+
assert_ne!(uuid_ab, uuid_ba);
106+
assert_ne!(uuid_ab, TestDeriveStructA::<Inner>::TYPE_UUID);
107+
assert_ne!(uuid_ba, TestDeriveStructB::<Inner>::TYPE_UUID);
108+
}
109+
110+
#[test]
111+
fn test_generic_type_uuid_same_for_eq_param() {
112+
#[derive(TypeUuid, Clone)]
113+
#[uuid = "49951b1c-4811-45e7-acc6-3119249fbd8f"]
114+
struct A;
115+
116+
#[derive(TypeUuid, Clone)]
117+
#[uuid = "49951b1c-4811-45e7-acc6-3119249fbd8f"]
118+
struct BButSameAsA;
119+
120+
let uuid_a = TestDeriveStruct::<A>::TYPE_UUID;
121+
let uuid_b = TestDeriveStruct::<BButSameAsA>::TYPE_UUID;
122+
123+
assert_eq!(uuid_a, uuid_b);
124+
}
125+
}

0 commit comments

Comments
 (0)