Skip to content

Commit

Permalink
re-enable #[derive(TypeUuid)] for generics (bevyengine#4118)
Browse files Browse the repository at this point in the history
Support for deriving `TypeUuid` for types with generics was initially added in bevyengine#2044 but later reverted bevyengine#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>
  • Loading branch information
3 people authored and ItsDoot committed Feb 1, 2023
1 parent a58195d commit c621a0b
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 11 deletions.
30 changes: 19 additions & 11 deletions crates/bevy_reflect/bevy_reflect_derive/src/type_uuid.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
extern crate proc_macro;

use bevy_macro_utils::BevyManifest;
use quote::{quote, ToTokens};
use quote::quote;
use syn::*;
use uuid::Uuid;

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

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

let (impl_generics, type_generics, _) = &ast.generics.split_for_impl();
assert!(
impl_generics.to_token_stream().is_empty() && type_generics.to_token_stream().is_empty(),
"#[derive(TypeUuid)] is not supported for generics.",
);
ast.generics.type_params_mut().for_each(|param| {
param
.bounds
.push(syn::parse_quote!(#bevy_reflect_path::TypeUuid))
});

let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();

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

let base = quote! { #bevy_reflect_path::Uuid::from_bytes([#( #bytes ),*]) };
let type_uuid = ast.generics.type_params().fold(base, |acc, param| {
let ident = &param.ident;
quote! {
#bevy_reflect_path::__macro_exports::generate_composite_uuid(#acc, <#ident as #bevy_reflect_path::TypeUuid>::TYPE_UUID)
}
});

let gen = quote! {
impl #bevy_reflect_path::TypeUuid for #name {
const TYPE_UUID: #bevy_reflect_path::Uuid = #bevy_reflect_path::Uuid::from_bytes([
#( #bytes ),*
]);
impl #impl_generics #bevy_reflect_path::TypeUuid for #name #type_generics #where_clause {
const TYPE_UUID: #bevy_reflect_path::Uuid = #type_uuid;
}
};
gen.into()
Expand Down
30 changes: 30 additions & 0 deletions crates/bevy_reflect/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,36 @@ pub use type_uuid::*;
pub use bevy_reflect_derive::*;
pub use erased_serde;

#[doc(hidden)]
pub mod __macro_exports {
use crate::Uuid;

/// Generates a new UUID from the given UUIDs `a` and `b`,
/// where the bytes are generated by a bitwise `a ^ b.rotate_right(1)`.
/// The generated UUID will be a `UUIDv4` (meaning that the bytes should be random, not e.g. derived from the system time).
#[allow(clippy::unusual_byte_groupings)] // unusual byte grouping is meant to signal the relevant bits
pub const fn generate_composite_uuid(a: Uuid, b: Uuid) -> Uuid {
let mut new = [0; 16];
let mut i = 0;
while i < new.len() {
// rotating ensures different uuids for A<B<C>> and B<A<C>> because: A ^ (B ^ C) = B ^ (A ^ C)
// notice that you have to rotate the second parameter: A.rr ^ (B.rr ^ C) = B.rr ^ (A.rr ^ C)
// Solution: A ^ (B ^ C.rr).rr != B ^ (A ^ C.rr).rr
new[i] = a.as_bytes()[i] ^ b.as_bytes()[i].rotate_right(1);

i += 1;
}

// Version: the most significant 4 bits in the 6th byte: 11110000
new[6] = new[6] & 0b0000_1111 | 0b0100_0000; // set version to v4

// Variant: the most significant 3 bits in the 8th byte: 11100000
new[8] = new[8] & 0b000_11111 | 0b100_00000; // set variant to rfc4122

Uuid::from_bytes(new)
}
}

#[cfg(test)]
#[allow(clippy::blacklisted_name, clippy::approx_constant)]
mod tests {
Expand Down
95 changes: 95 additions & 0 deletions crates/bevy_reflect/src/type_uuid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,98 @@ where
std::any::type_name::<Self>()
}
}

#[cfg(test)]
mod test {
use super::*;
use crate as bevy_reflect;
use bevy_reflect_derive::TypeUuid;
use std::marker::PhantomData;

#[derive(TypeUuid)]
#[uuid = "af6466c2-a9f4-11eb-bcbc-0242ac130002"]
struct TestDeriveStruct<T>
where
T: Clone,
{
_value: T,
}

fn test_impl_type_uuid(_: &impl TypeUuid) {}

#[test]
fn test_generic_type_uuid_derive() {
#[derive(TypeUuid, Clone)]
#[uuid = "ebb16cc9-4d5a-453c-aa8c-c72bd8ec83a2"]
struct T;

let test_struct = TestDeriveStruct { _value: T };
test_impl_type_uuid(&test_struct);
}

#[test]
fn test_generic_type_unique_uuid() {
#[derive(TypeUuid, Clone)]
#[uuid = "49951b1c-4811-45e7-acc6-3119249fbd8f"]
struct A;

#[derive(TypeUuid, Clone)]
#[uuid = "4882b8f5-5556-4cee-bea6-a2e5991997b7"]
struct B;

let uuid_a = TestDeriveStruct::<A>::TYPE_UUID;
let uuid_b = TestDeriveStruct::<B>::TYPE_UUID;

assert_ne!(uuid_a, uuid_b);
assert_ne!(uuid_a, A::TYPE_UUID);
assert_ne!(uuid_b, B::TYPE_UUID);
}

#[test]
fn test_inverted_generic_type_unique_uuid() {
#[derive(TypeUuid, Clone)]
#[uuid = "49951b1c-4811-45e7-acc6-3119249fbd8f"]
struct Inner;

#[derive(TypeUuid, Clone)]
#[uuid = "23ebc0c3-ef69-4ea0-8c2a-dca1b4e27c0d"]
struct TestDeriveStructA<T>
where
T: Clone,
{
_phantom: PhantomData<T>,
}

#[derive(TypeUuid, Clone)]
#[uuid = "a82f9936-70cb-482a-bd3d-cb99d87de55f"]
struct TestDeriveStructB<T>
where
T: Clone,
{
_phantom: PhantomData<T>,
}

let uuid_ab = TestDeriveStructA::<TestDeriveStructB<Inner>>::TYPE_UUID;
let uuid_ba = TestDeriveStructB::<TestDeriveStructA<Inner>>::TYPE_UUID;

assert_ne!(uuid_ab, uuid_ba);
assert_ne!(uuid_ab, TestDeriveStructA::<Inner>::TYPE_UUID);
assert_ne!(uuid_ba, TestDeriveStructB::<Inner>::TYPE_UUID);
}

#[test]
fn test_generic_type_uuid_same_for_eq_param() {
#[derive(TypeUuid, Clone)]
#[uuid = "49951b1c-4811-45e7-acc6-3119249fbd8f"]
struct A;

#[derive(TypeUuid, Clone)]
#[uuid = "49951b1c-4811-45e7-acc6-3119249fbd8f"]
struct BButSameAsA;

let uuid_a = TestDeriveStruct::<A>::TYPE_UUID;
let uuid_b = TestDeriveStruct::<BButSameAsA>::TYPE_UUID;

assert_eq!(uuid_a, uuid_b);
}
}

0 comments on commit c621a0b

Please sign in to comment.