Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bevy_reflect: Consistent reflect_hash and reflect_partial_eq #8695

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

MrGVSV
Copy link
Member

@MrGVSV MrGVSV commented May 27, 2023

Objective

Fixes #6601

The issue already describes the problem in detail but here's a quick description of what we want.

A reflected (dyn Reflect) type should share the same reflect_hash and reflect_partial_eq behavior as a proxy of that type.

Additionally, reflect_partial_eq should be symmetric. In other words, a.reflect_partial_eq(b) produces the same result as b.reflect_partial_eq(a).

This means that the following should test should succeed:

let value: Box<dyn TupleStruct> = Box::new(Foo(123));
let dynamic: DynamicTupleStruct = value.clone_dynamic();

// `reflect_hash` values are the same
assert_eq!(value.reflect_hash(), dynamic.reflect_hash());

// `reflect_partial_eq` works and is symmetric
assert_eq!(Some(true), value.reflect_partial_eq(&dynamic));
assert_eq!(Some(true), dynamic.reflect_partial_eq(&value));

Solution

Both reflect_hash and reflect_partial_eq now make use of stored type information in order to perform their computations. Furthermore, they are now consistent and symmetric.

This replaces the old system of registering Hash and PartialEq like #[reflect(Hash, PartialEq)]. Although, this still exists for value types via #[reflect_value(Hash, PartialEq)], since value types base their implementation on the actual traits.

Any field that cannot be hashed or compared, may opt-out of the computation using #[reflect(skip_hash)] and #[reflect(skip_partial_eq)], respectively.

#[derive(Reflect)]
struct Foo {
  a: i32,
  // `f32` doesn't implement `Hash` so its `reflect_hash` will
  // always return `None`, causing our container to return `None`
  // unless we opt it out of the hashing computation.
  #[reflect(skip_hash)]
  b: f32,
}

#[derive(Reflect)]
struct Bar {
  id: u32,
  // We can exclude certain fields from comparison as well.
  #[reflect(skip_partial_eq)]
  _temp: u32,
}

If any field returns None during one of these operations, the entire container will return None.

reflect_partial_eq Behavior

The behavior of reflect_partial_eq is a bit more intuitive now that it is consistent between concrete and proxy types. We essentially only consider types equal if they all share the same non-skipped fields and those fields are equivalent between them.

Example Behavior

Here's an example comparing a type, Foo, to a dynamic type.

Note that we use a dynamic type here to just make demonstrating the existence and nonexistence of fields simpler. The same would be true when comparing against another concrete type.

#[derive(Reflect)]
struct Foo {
  a: u32,
  #[reflect(skip_partial_eq)]
  b: u32,
  c: u32,
}

let foo = Foo {
  a: 1,
  b: 2,
  c: 3,
};

let mut dynamic = DynamicStruct::default();

// 1.
assert_eq!(Some(false), foo.reflect_partial_eq(&dynamic));

// 2.
dynamic.insert("a", 1u32);
assert_eq!(Some(false), foo.reflect_partial_eq(&dynamic));

// 3.
dynamic.insert("c", 3u32);
assert_eq!(Some(true), foo.reflect_partial_eq(&dynamic));

// 4.
dynamic.insert("c", 3u32);
assert_eq!(Some(true), foo.reflect_partial_eq(&dynamic));

// 5.
dynamic.insert("b", 555u32);
assert_eq!(Some(true), foo.reflect_partial_eq(&dynamic));

// 6.
dynamic.insert("d", 4u32);
assert_eq!(Some(false), foo.reflect_partial_eq(&dynamic));

#[derive(Reflect)]
struct Bar {
  #[reflect(skip_partial_eq)]
  d: u32,
}

// 7.
dynamic.set_represented_type(Some(Bar::type_info()));
assert_eq!(Some(true), foo.reflect_partial_eq(&dynamic));

#[derive(Reflect)]
struct Baz {
  b: u32,
}

// 8.
dynamic.set_represented_type(Some(Baz::type_info()));
assert_eq!(Some(false), foo.reflect_partial_eq(&dynamic));

Explanation for the code above:

  1. false: Foo requires fields a and c, but dynamic is missing both of them.
  2. false: Foo requires fields a and c, but dynamic is missing c.
  3. false: Foo and dynamic share the same required fields, but have different values for c.
  4. true: Foo and dynamic share the same required fields and are all equivalent.
  5. true: Despite the b fields not being equivalent, Foo skips it so it is not considered for comparison.
  6. false: Foo does not contain a d field but dynamic does.
  7. true: dynamic has become a proxy of Bar which skips d, making only fields a and c relevant for comparison.
  8. false: dynamic has become a proxy of Baz which requires b even though Foo skips b

***Meta Types

This PR also adds the concept of "metadata" on info types. This is meant to capture various attribute ("meta") data on containers and their fields. For example, the docs attribute has been moved to these types rather than on the info type directly.

These meta types are also where we keep track of skip_hash and skip_partial_eq so they can be utilized within dynamic contexts.

Rationale

There were a few ways to accomplish this: add getters and setters to the meta fields, create separate builders for each meta type, or just make all members pub. I went with that last one as it was the simplest and the others felt like possible overkill with the data we have now. Also, there's no concern over accidental mutations since types always share their type information as a static immutable reference.

I also decided to make the constructor for this type const. This might be controversial, but my thought was that this helps force us to:

  1. Strive to be const-ready. The main reason the info types aren't const is because TypeId and type_name are not const-stable. Stabilization is probably a ways out for them, but it's probably best we don't establish any patterns that would prevent us from easily making the switch to being const.
  2. Store only necessary data. Since we're storing additional metadata, we might be tempted to store everything: type data registrations, custom attributes, etc. And while I think that's worth considering, there's also a concern that it creates a split between stuff stored on the type and stuff stored in the registry. Users shouldn't be able to configure the names of fields or certain other metadata at runtime. However, they should be able to register type data structs on types they don't own (whether they created the type data or not). With such a split, we would be asking users and library authors to check both the type and the registry for type data, which is prone to error.
  3. Simplify the API. At the end of the day, this is a public interface. We should avoid complex, crate/case-specific fields and methods that make this interface more difficult to understand and use. This is another reason I went with just making the fields pub— it indicates that these fields are standalone and no additional care is needed in using them.

With all that being said, I'm definitely open to changing how meta types are constructed and what they're allowed to contain. This was all a very opinionated implementation on my part and I understand it might enforce restrictions prematurely when we could just wait until it's absolutely needed. So feel free to leave a comment telling me I'm wrong so that we can find a pattern the community can agree on.

Notes

Concrete vs Reflected

This change now means that the corresponding concrete impls may give different results than the reflected ones:

  • Reflect::reflect_hash = / ≠ Hash::hash
  • Reflect::reflect_partial_eq = / ≠ PartialEq::eq

This could be an issue when comparing results between the concrete realm and the reflected one. However, this is likely not a major issue for the following reasons:

Hash vs reflect_hash

The general usage for hashing is to interact with maps and sets. Most users are not manually creating a hasher and calculating the u64 of a value. Because of this, they are unlikely to run into the scenario where they would be comparing the results of Hash and reflect_hash.

PartialEq vs reflect_partial_eq

This could be a potential issue for users who rely on PartialEq to establish and maintain contracts guaranteeing that two values are equal. If they then try to use reflect_partial_eq, the result may go against that supposed guarantee.

In most cases, though, the results should be the same. This is because in most cases PartialEq is derived, which means it essentially performs the same operations that reflect_partial_eq does: ensure that all fields are equal recursively.

However, if the user has a custom PartialEq implementation or marks their fields as #[reflect(skip_partial_eq)], then the divergence is more likely.

User Responsibility

In either case, the best course of action would be for the user to decide which solution they want to use and be consistent in using it— or at least not rely on the results being identical across concrete and reflected implementations.

Transitivity

One aspect of PartialEq that reflect_partial_eq does not fully guarantee is transitivity. That is, if A == B and B == C, then A == C.

While concrete types and their proxies should respect transitivity, dynamic types (ones without a represented TypeInfo) do not.

Example
#[derive(Reflect)]
struct Foo {
  #[reflect(skip_partial_eq)]
  a: u32,
  b: u32,
}

#[derive(Reflect)]
struct Bar {
  #[reflect(skip_partial_eq)]
  c: u32,
  b: u32,
}

let foo = Foo { a: 1, b: 2 };
let bar = Bar { c: 1, b: 2 };

// `foo == bar` -> ✅
assert_eq!(Some(true), foo.reflect_partial_eq(&bar));

let mut baz = DynamicStruct::default();
baz.insert("c", 1u32);
baz.insert("b", 2u32);

// `bar == baz`  -> ✅
assert_eq!(Some(true), bar.reflect_partial_eq(&baz));

// `foo == baz` -> ❌ (not transitive)
// Not equal because `foo` doesn't contain `c`
assert_eq!(Some(false), foo.reflect_partial_eq(&baz));

// Make `baz` a proxy of `Bar`
baz.set_represented_type(Some(Bar::type_info()));

// `foo == baz` -> ✅ (transitive)
assert_eq!(Some(true), foo.reflect_partial_eq(&baz));

As you can see, comparison between concrete values and proxy values are transitive. It's when comparing against a dynamic type that transitivity starts to no longer be guaranteed.

Hashing and Equality

The docs for Hash state the following:

When implementing both Hash and Eq, it is important that the following property holds:

k1 == k2 -> hash(k1) == hash(k2)

In other words, if two keys are equal, their hashes must also be equal. HashMap and HashSet both rely on this behavior.

While the idea is that these Reflect methods uphold this property, there is a chance for this to be broken when skipping fields (see example below). This could be problematic for cases where users expect two "equal" values to result in the same hash.

Currently, we just warn against it in the documentation. But there may be other solutions.

Additionally, it's not certain whether or not this truly applies to us since reflect_partial_eq is meant to be used like PartialEq, and PartialEq cannot make these same guarantees as Eq. If anything, we would probably need something like reflect_eq to match Eq behavior.

Example
#[derive(Reflect)]
struct Foo {
    a: u32,
    #[reflect(skip_partial_eq)]
    b: u32,
}

let foo1 = Foo { a: 123, b: 456 };
let foo2 = Foo { a: 123, b: 0 };

// OK!
assert_eq!(Some(true), foo1.reflect_partial_eq(&foo2));
// PANIC!
assert_eq!(foo1.reflect_hash(), foo2.reflect_hash());

In the above case foo1 == foo2 but hash(foo1) != hash(foo2). Again, this property might not be applicable due to the differences between PartialEq and Eq, but it should nevertheless be noted.

Open Questions

  1. Should we make these operations opt-in only? Should we allow entire containers to opt out?
  2. Should we return None during reflect_partial_eq if a field has differing skip configuration (i.e. type A skips field x while type B requires it)? Should we make reflect_partial_eq return a Result to allow users to branch on this versus other causes for failures?
  3. Should we have a convenience attribute that does the same as #[reflect(skip_hash)] and #[reflect(skip_partial_eq)] combined? Such as a #[reflect(skip_equivalence)] attribute.

Future Work

There are some things we could possibly consider adding in a future PR:

  • Add a reflect_eq method to mimic Eq
  • Improve proxy types to be more robust so we can possibly branch and reduce the number of lookups and redundant checks

Changelog

Changed

  • Reflect::reflect_hash and Reflect::reflect_partial_eq are now consistent between concrete types and proxies of those types (proxies are simply dynamic types like DynamicStruct that contain the TypeInfo for a concrete type )
  • Reflect::reflect_hash and Reflect::reflect_partial_eq no longer rely on Hash and PartialEq implementations
    • This also means they no longer need to be registered via #[reflect(Hash)] and #[reflect(PartialEq)] for most types, and doing so will result in a compile error
    • All items that derive Reflect now opt into this behavior by default
  • Accessing a type's docs has moved from its info type (e.g. StructInfo) to its meta type (e.g. StructMeta)

Added

  • Added #[reflect(skip_hash)] and #[reflect(skip_partial_eq)] attributes for controlling the implementations of Reflect::reflect_hash and Reflect::reflect_partial_eq, respectively
  • Added "meta types" which are accessible through a type's info type (e.g. StructInfo):
    • ValueMeta
    • TupleMeta
    • ArrayMeta
    • ListMeta
    • MapMeta
    • TupleStructMeta
    • StructMeta
    • EnumMeta
    • VariantMeta
    • FieldMeta

Migration Guide

Hash and PartialEq registrations

Registering Hash and/or PartialEq now results in a compile error. Such registrations will need to be removed.

// BEFORE
#[derive(Reflect, Hash, PartialEq, Default)]
#[reflect(Hash, PartialEq, Default)]
struct MyType(u32);

// AFTER
#[derive(Reflect, Hash, PartialEq, Default)]
#[reflect(Default)]
struct MyType(u32);

This does not apply to value types created with #[reflect_value]:

#[derive(Reflect, Hash, PartialEq, Default)]
#[reflect_value(Hash, PartialEq, Default)]
struct MyType(u32);

New Reflect::reflect_hash behavior

Reflect::reflect_hash no longer relies on a concrete Hash implementation. All types that derive Reflect come with a built-in implementation of Reflect::reflect_hash that is purely based on reflection.

If a particular field is not hashable, you will need to mark it with #[reflect(skip_hash)]:

// BEFORE
#[derive(Reflect)]
#[reflect(Hash)]
struct MyType {
  id: String,
  _temp: f32,
}

impl Hash for MyType {
  fn hash<H: Hasher>(&self, state: &mut H) {
    self.id.hash(state);
  }
}

// AFTER
#[derive(Reflect)]
struct MyType {
  id: String,
  #[reflect(skip_hash)]
  _temp: f32,
}

New Reflect::reflect_partial_eq behavior

Reflect::reflect_partial_eq no longer relies on a concrete PartialEq implementation. All types that derive Reflect come with a built-in implementation of Reflect::reflect_partial_eq that is purely based on reflection.

If a particular field is not comparable or should not be considered for comparison, you will need to mark it with #[reflect(skip_partial_eq)]:

// BEFORE
#[derive(Reflect)]
#[reflect(PartialEq)]
struct MyType {
  id: String,
  _temp: f32,
}

impl PartialEq for MyType {
  fn eq(&self, other: &Self) -> bool {
    self.id == other.id
  }
}

// AFTER
#[derive(Reflect)]
struct MyType {
  id: String,
  #[reflect(skip_partial_eq)]
  _temp: f32,
}

This also means that some types that might have been considered equal or unequal before may no longer provide the same result. This is especially true for comparison against dynamic types since they now aim to be consistent with their concrete counterparts.

docs access

Those using the documentation feature to access doc strings will now need to get it from a type's meta type rather than the info type directly:

// BEFORE

/// Some documentation...
#[derive(Reflect)]
struct MyType;

let docs = MyType::type_info().docs();
assert_eq!(Some("Some documentation..."), docs);

// AFTER

/// Some documentation...
#[derive(Reflect)]
struct MyType;

let docs = MyType::type_info().meta().docs;
assert_eq!(Some("Some documentation..."), docs);

@MrGVSV MrGVSV added C-Usability A simple quality-of-life change that makes Bevy easier to use A-Reflection Runtime information about types C-Breaking-Change A breaking change to Bevy's public API that needs to be noted in a migration guide labels May 27, 2023
@MrGVSV MrGVSV force-pushed the reflect-consistent-equivalence branch 3 times, most recently from 06e1fb8 to a84dfc9 Compare June 9, 2023 07:41
@MrGVSV MrGVSV force-pushed the reflect-consistent-equivalence branch from a84dfc9 to 6c1c0ff Compare June 22, 2023 07:23
@dmlary
Copy link
Contributor

dmlary commented Jul 4, 2023

One thing I'm getting hung up on here is that we'll no longer use Hash and PartialEq implementations on the concrete types. The follow-on effects of this behavior you've documented are the Concrete vs Reflected Hash mismatch, possible lack of transitivity in PartialEq, and that this may break the Hash/Eq behavior. Another effect is that we'll disregard any custom implementations of these traits for types.

I'm curious why we can't fall back to the concrete type implementation for proxies.

There's one note in #6601 about using the concrete implementations:

To do this, we can add two new TypeData structs: ReflectHash and ReflectPartialEq. Both will simply contain function pointers that can be be used to perform the concrete implementations. This data can then be retrieved from the TypeRegistry. And since getting TypeData returns an Option, the functions stored in these structs no longer need to return Option and Option like they currently do. They can simply return bool and u64.

Looking at the PR (ex: tuple_hash()), it doesn't look like this is being used.

Why did the PR move away from falling back on the concrete implementations? How difficult would it be to use them if implemented?

@MrGVSV
Copy link
Member Author

MrGVSV commented Jul 4, 2023

I'm curious why we can't fall back to the concrete type implementation for proxies.

There's one note in #6601 about using the concrete implementations:

To do this, we can add two new TypeData structs: ReflectHash and ReflectPartialEq. Both will simply contain function pointers that can be be used to perform the concrete implementations. This data can then be retrieved from the TypeRegistry. And since getting TypeData returns an Option, the functions stored in these structs no longer need to return Option and Option like they currently do. They can simply return bool and u64.

Looking at the PR (ex: tuple_hash()), it doesn't look like this is being used.

Why did the PR move away from falling back on the concrete implementations? How difficult would it be to use them if implemented?

This is a great callout! I should have mentioned this in the PR description, but I'll mention it here instead.

The reason we can't rely on concrete implementations, even though I at one time thought we could, is that proxies are not those concrete types. And this becomes even clearer when we look at an example.

This will fail to compile:

#[derive(Reflect)]
struct Foo {
  id: usize,
  data: String
}

impl Hash for Foo {
  fn hash<H: Hasher>(&self, state: &mut H) {
    self.id.hash(state);
  }
}

let foo = Foo { id: 123, data: String::new() };
let proxy: DynamicStruct = foo.clone_dynamic();

let mut hasher = AHasher::default();

Foo::hash(&proxy, &mut hasher); // ERROR

We can't pass DynamicStruct in the &self position for Foo::hash because it isn't Foo. And if these were instead dyn Reflect objects, we'd still run into the same problem (since we'd need to cast them back to the concrete forms internally in order to make use of the function pointers).

Note: Another reason is that you can't store generic function pointers, which would make storing Hash::hash::<H> unviable, unless we required a specific hasher or set of hashers.

So that's why I abandoned that original approach in the issue. Hopefully that helps. Please let me know if you need more clarification or if you have any other questions!

@dmlary
Copy link
Contributor

dmlary commented Jul 8, 2023

So I've dug pretty deeply into this. There is nightly support for creating a fat pointer for something like dyn Hash from a pointer & the vtable (https://rust-lang.github.io/rfcs/2580-ptr-meta.html), but that's only in nightly.

From that RFC, there is a unsafe (probably cursed) method to do it in stable (https://play.rust-lang.org/?version=nightly&mode=debug&edition=2015&gist=bbeecccc025f5a7a0ad06086678e13f3). Using this method it's possible for us to store the vtable for dyn Hash in the type registry.

Admittedly this is unsafe and cursed and there's real support coming in nightly (whenever it gets into stable). I just want to put this here so it's discussed. I'll understand if this is a scary direction to go in.

use std::fmt::Debug;
use std::mem;

#[derive(Debug)]
#[repr(C)]
struct FatPtr {
    data: *mut (),
    meta: *mut (),
}

fn build_fat_ptr<T: ?Sized>(ptr: *mut (), meta: *mut ()) -> &'static T {
    let fat_ptr = FatPtr { data: ptr, meta };
    unsafe { mem::transmute_copy::<FatPtr, &T>(&fat_ptr) }
}

#[derive(Debug)]
struct DerivedDebug;

struct ImplDebug;
impl Debug for ImplDebug {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "ImplDebug (manual impl)")
    }
}

fn main() {
    assert_eq!(mem::size_of::<&dyn Debug>(), mem::size_of::<FatPtr>());

    // example using derived debug
    let m = DerivedDebug;
    let dyn_debug: &dyn Debug = &m;
    let repr = unsafe { mem::transmute_copy::<&dyn Debug, FatPtr>(&dyn_debug) };
    // rebuild the &dyn Debug from `mut *` and the vtable
    let dyn_debug: &dyn Debug = build_fat_ptr(repr.data, repr.meta);
    println!("m {:?}, dyn_debug {:?}", m, dyn_debug);

    // Do the same thing with a manually implemented debug
    let m = ImplDebug;
    let dyn_debug: &dyn Debug = &m;
    let repr = unsafe { mem::transmute_copy::<&dyn Debug, FatPtr>(&dyn_debug) };
    // rebuild the &dyn Debug from `mut *` and the vtable
    let dyn_debug: &dyn Debug = build_fat_ptr(repr.data, repr.meta);
    println!("m {:?}, dyn_debug {:?}", m, dyn_debug);
}

@MrGVSV
Copy link
Member Author

MrGVSV commented Jul 8, 2023

So I've dug pretty deeply into this. There is nightly support for creating a fat pointer for something like dyn Hash from a pointer & the vtable (https://rust-lang.github.io/rfcs/2580-ptr-meta.html), but that's only in nightly.

From that RFC, there is a unsafe (probably cursed) method to do it in stable (https://play.rust-lang.org/?version=nightly&mode=debug&edition=2015&gist=bbeecccc025f5a7a0ad06086678e13f3). Using this method it's possible for us to store the vtable for dyn Hash in the type registry.

Admittedly this is unsafe and cursed and there's real support coming in nightly (whenever it gets into stable). I just want to put this here so it's discussed. I'll understand if this is a scary direction to go in.

Yeah the unsafe stuff is a little out of my comfort zone haha. However, this still would be an issue because I'm pretty sure the vtable means nothing to a type that isn't the type that vtable expects, right?

And that's the bigger issue with trying to store a function pointer to the conrete impl: dynamic types are not guaranteed to have the same layout as their concrete counterparts.

@dmlary
Copy link
Contributor

dmlary commented Jul 8, 2023

I’m also a little uncomfortable with unsafe in rust, which is kind of silly with the number of years I’ve been writing C & assembly.

dynamic types are not guaranteed to have the same layout as their concrete counterparts.

Understood. My initial thought was for those dynamic types that are proxies, we reflect to the concrete type as dyn Hash and use the per-type impl. For those pure dynamic types without concrete types, we use the pure dynamic implementations you have here.

There are performance & caching things to sort out, but it’s a pattern for ensuring reflection leverages user impls.

That said, I still don’t know how comfortable the bevy project is with adding unsafes in cases like this. (edit) And probably more importantly, is this level of complexity really even worth it. Perhaps this can wait until someone encounters a real case where they need the type impl to be executed before doing the complex, unsafe thing.

I can take some time to flesh out an example to make it easier to see how scary things get. Something that shows how we’d go from MyStruct to &dyn Reflect to DynamicStruct to MyStruct as dyn Hash.

@MrGVSV
Copy link
Member Author

MrGVSV commented Jul 8, 2023

Understood. My initial thought was for those dynamic types that are proxies, we reflect to the concrete type as dyn Hash and use the per-type impl. For those pure dynamic types without concrete types, we use the pure dynamic implementations you have here.

Oh are you suggesting we construct the concrete types using FromReflect whenever we need to create a hash and stuff like that? I don't think that'll be much faster tbh since both this PR and using FromReflect need to recurse down the structure. And there's no other way to construct the concrete type from a dynamic one.

There are performance & caching things to sort out, but it’s a pattern for ensuring reflection leverages user impls.

Yeah, that makes sense. Though, I'm wondering how much users really need to rely on these custom impls. The skip_hash and skip_partial_eq attributes should help most cases. For fully custom implementations, we'd need a clever way of allowing users to define a custom reflect_hash function (likely via attribute) while still supporting dynamic proxies. And ideally users wouldn't need to worry about handling all the edge cases.

That said, I still don’t know how comfortable the bevy project is with adding unsafes in cases like this. (edit) And probably more importantly, is this level of complexity really even worth it. Perhaps this can wait until someone encounters a real case where they need the type impl to be executed before doing the complex, unsafe thing.

We should probably avoid as much unsafe as we reasonably can. That being said, I don't think Bevy is uncomfortable adding unsafe code (the entire ECS relies on it). Reflection doesn't have much, but we could consider it if it solves a real problem (#6042, for example, uses unsafe code).

I can take some time to flesh out an example to make it easier to see how scary things get. Something that shows how we’d go from MyStruct to &dyn Reflect to DynamicStruct to MyStruct as dyn Hash.

Sounds good! I think it will also be more helpful (for me anyways) to see it using Reflect since I'm still having trouble understanding how it's supposed to work.

Also, keep in mind that if we did decide to go the unsafe route for Hash, we should probably do the same for PartialEq (and maybe Debug eventually).

@dmlary
Copy link
Contributor

dmlary commented Jul 8, 2023

For now my approach of storing the vtable for dyn Hash for each registered type will not work. I'm unable to find a way to get the vtable for dyn Hash as I can't make it into an object, or a boxed object. I don't know of any other way to discover the vtable address.

Until https://doc.rust-lang.org/std/ptr/trait.Pointee.html#associatedtype.Metadata becomes stable, we'll have to stick with dynamic-only implementations for Hash and PartialEq.

As MrGVSV already explained, it's unlikely the dynamic-only implementations for these traits will cause any problem for users.

@dmlary
Copy link
Contributor

dmlary commented Jul 14, 2023

Update, it is possible for us to save off Hash and PartialEq functions during type registration. It doesn't require anything unsafe either; we literally just save the function pointer for each type, and DefaultHasher, and any other hashers we may care about.

By doing this we are able to use the implementations on the concrete types.

use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

#[derive(Eq, PartialEq)]
struct Location {
    x: u32,
    y: u32,
}

impl Hash for Location {
    fn hash<H>(&self, _state: &mut H)
       where H: Hasher 
    {
        println!("ran hash function")
    }
}

// fake "registration" function that really just returns the function pointer for <T as Hash>::hash
pub fn register<T: std::hash::Hash + 'static, H: std::hash::Hasher + 'static>() -> impl Fn(&T, &mut H) {
    T::hash::<H>
}

pub fn main() {
    let loc = Location { x: 1_u32, y: 1_32 };
    let hash_func = register::<Location, DefaultHasher>();
    let mut hasher = DefaultHasher::new();
    hash_func(&loc, &mut hasher);
    println!("Hash result {:x}", hasher.finish())
}

Playground: https://godbolt.org/z/7bYPn9b9a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Reflection Runtime information about types C-Breaking-Change A breaking change to Bevy's public API that needs to be noted in a migration guide C-Usability A simple quality-of-life change that makes Bevy easier to use
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

bevy_reflect: reflect_hash and reflect_partial_eq inconsistent on Dynamic types
2 participants