Skip to content

Commit

Permalink
bevy_reflect: Add statically available type info for reflected types (b…
Browse files Browse the repository at this point in the history
…evyengine#4042)

# Objective

> Resolves bevyengine#4504

It can be helpful to have access to type information without requiring an instance of that type. Especially for `Reflect`, a lot of the gathered type information is known at compile-time and should not necessarily require an instance.

## Solution

Created a dedicated `TypeInfo` enum to store static type information. All types that derive `Reflect` now also implement the newly created `Typed` trait:

```rust
pub trait Typed: Reflect {
  fn type_info() -> &'static TypeInfo;
}
```

> Note: This trait was made separate from `Reflect` due to `Sized` restrictions.

If you only have access to a `dyn Reflect`, just call `.get_type_info()` on it. This new trait method on `Reflect` should return the same value as if you had called it statically. 

If all you have is a `TypeId` or type name, you can get the `TypeInfo` directly from the registry using the `TypeRegistry::get_type_info` method (assuming it was registered).

### Usage

Below is an example of working with `TypeInfo`. As you can see, we don't have to generate an instance of `MyTupleStruct` in order to get this information.

```rust
#[derive(Reflect)]
struct MyTupleStruct(usize, i32, MyStruct);

let info = MyTupleStruct::type_info();
if let TypeInfo::TupleStruct(info) = info {
  assert!(info.is::<MyTupleStruct>());
  assert_eq!(std::any::type_name::<MyTupleStruct>(), info.type_name());
  assert!(info.field_at(1).unwrap().is::<i32>());
} else {
  panic!("Expected `TypeInfo::TupleStruct`");
}
```

### Manual Implementations

It's not recommended to manually implement `Typed` yourself, but if you must, you can use the `TypeInfoCell` to automatically create and manage the static `TypeInfo`s for you (which is very helpful for blanket/generic impls):

```rust
use bevy_reflect::{Reflect, TupleStructInfo, TypeInfo, UnnamedField};
use bevy_reflect::utility::TypeInfoCell;

struct Foo<T: Reflect>(T);

impl<T: Reflect> Typed for Foo<T> {
  fn type_info() -> &'static TypeInfo {
    static CELL: TypeInfoCell = TypeInfoCell::generic();
    CELL.get_or_insert::<Self, _>(|| {
      let fields = [UnnamedField::new::<T>()];
      let info = TupleStructInfo::new::<Self>(&fields);
      TypeInfo::TupleStruct(info)
    })
  }
}
```

## Benefits

One major benefit is that this opens the door to other serialization methods. Since we can get all the type info at compile time, we can know how to properly deserialize something like:

```rust
#[derive(Reflect)]
struct MyType {
  foo: usize,
  bar: Vec<String>
}

// RON to be deserialized:
(
  type: "my_crate::MyType", // <- We now know how to deserialize the rest of this object
  value: {
    // "foo" is a value type matching "usize"
    "foo": 123,
    // "bar" is a list type matching "Vec<String>" with item type "String"
    "bar": ["a", "b", "c"]
  }
)
```

Not only is this more compact, but it has better compatibility (we can change the type of `"foo"` to `i32` without having to update our serialized data).

Of course, serialization/deserialization strategies like this may need to be discussed and fully considered before possibly making a change. However, we will be better equipped to do that now that we can access type information right from the registry.

## Discussion

Some items to discuss:

1. Duplication. There's a bit of overlap with the existing traits/structs since they require an instance of the type while the type info structs do not (for example, `Struct::field_at(&self, index: usize)` and `StructInfo::field_at(&self, index: usize)`, though only `StructInfo` is accessible without an instance object). Is this okay, or do we want to handle it in another way?
2. Should `TypeInfo::Dynamic` be removed? Since the dynamic types don't have type information available at runtime, we could consider them `TypeInfo::Value`s (or just even just `TypeInfo::Struct`). The intention with `TypeInfo::Dynamic` was to keep the distinction from these dynamic types and actual structs/values since users might incorrectly believe the methods of the dynamic type's info struct would map to some contained data (which isn't possible statically).
4. General usefulness of this change, including missing/unnecessary parts.
5. Possible changes to the scene format? (One possible issue with changing it like in the example above might be that we'd have to be careful when handling generic or trait object types.)

## Compile Tests

I ran a few tests to compare compile times (as suggested [here](bevyengine#4042 (comment))). I toggled `Reflect` and `FromReflect` derive macros using `cfg_attr` for both this PR (aa5178e) and main (c309acd).

<details>
<summary>See More</summary>

The test project included 250 of the following structs (as well as a few other structs):

```rust
#[derive(Default)]
#[cfg_attr(feature = "reflect", derive(Reflect))]
#[cfg_attr(feature = "from_reflect", derive(FromReflect))]
pub struct Big001 {
    inventory: Inventory,
    foo: usize,
    bar: String,
    baz: ItemDescriptor,
    items: [Item; 20],
    hello: Option<String>,
    world: HashMap<i32, String>,
    okay: (isize, usize, /* wesize */),
    nope: ((String, String), (f32, f32)),
    blah: Cow<'static, str>,
}
```

> I don't know if the compiler can optimize all these duplicate structs away, but I think it's fine either way. We're comparing times, not finding the absolute worst-case time.

I only ran each build 3 times using `cargo build --timings` (thank you @devil-ira), each of which were preceeded by a `cargo clean --package bevy_reflect_compile_test`. 

Here are the times I got:

| Test                             | Test 1 | Test 2 | Test 3 | Average |
| -------------------------------- | ------ | ------ | ------ | ------- |
| Main                             | 1.7s   | 3.1s   | 1.9s   | 2.33s   |
| Main + `Reflect`                 | 8.3s   | 8.6s   | 8.1s   | 8.33s   |
| Main + `Reflect` + `FromReflect` | 11.6s  | 11.8s  | 13.8s  | 12.4s   |
| PR                               | 3.5s   | 1.8s   | 1.9s   | 2.4s    |
| PR + `Reflect`                   | 9.2s   | 8.8s   | 9.3s   | 9.1s    |
| PR + `Reflect` + `FromReflect`   | 12.9s  | 12.3s  | 12.5s  | 12.56s  |

</details>

---

## Future Work

Even though everything could probably be made `const`, we unfortunately can't. This is because `TypeId::of::<T>()` is not yet `const` (see rust-lang/rust#77125). When it does get stabilized, it would probably be worth coming back and making things `const`. 

Co-authored-by: MrGVSV <49806985+MrGVSV@users.noreply.github.com>
  • Loading branch information
2 people authored and james7132 committed Oct 28, 2022
1 parent fcde5d9 commit 78b8e51
Show file tree
Hide file tree
Showing 16 changed files with 1,410 additions and 39 deletions.
1 change: 1 addition & 0 deletions crates/bevy_reflect/Cargo.toml
Expand Up @@ -22,6 +22,7 @@ erased-serde = "0.3"
downcast-rs = "1.2"
parking_lot = "0.11.0"
thiserror = "1.0"
once_cell = "1.11"
serde = "1"
smallvec = { version = "1.6", features = ["serde", "union", "const_generics"], optional = true }
glam = { version = "0.20.0", features = ["serde"], optional = true }
Expand Down
100 changes: 100 additions & 0 deletions crates/bevy_reflect/bevy_reflect_derive/src/impls.rs
Expand Up @@ -32,6 +32,10 @@ pub(crate) fn impl_struct(derive_data: &ReflectDeriveData) -> TokenStream {
.unwrap_or_else(|| Member::Unnamed(Index::from(field.index)))
})
.collect::<Vec<_>>();
let field_types = derive_data
.active_fields()
.map(|field| field.data.ty.clone())
.collect::<Vec<_>>();
let field_count = field_idents.len();
let field_indices = (0..field_count).collect::<Vec<usize>>();

Expand All @@ -49,12 +53,27 @@ pub(crate) fn impl_struct(derive_data: &ReflectDeriveData) -> TokenStream {
});
let debug_fn = derive_data.traits().get_debug_impl();

let typed_impl = impl_typed(
struct_name,
derive_data.generics(),
quote! {
let fields: [#bevy_reflect_path::NamedField; #field_count] = [
#(#bevy_reflect_path::NamedField::new::<#field_types, _>(#field_names),)*
];
let info = #bevy_reflect_path::StructInfo::new::<Self>(&fields);
#bevy_reflect_path::TypeInfo::Struct(info)
},
bevy_reflect_path,
);

let get_type_registration_impl = derive_data.get_type_registration();
let (impl_generics, ty_generics, where_clause) = derive_data.generics().split_for_impl();

TokenStream::from(quote! {
#get_type_registration_impl

#typed_impl

impl #impl_generics #bevy_reflect_path::Struct for #struct_name #ty_generics #where_clause {
fn field(&self, name: &str) -> Option<&dyn #bevy_reflect_path::Reflect> {
match name {
Expand Down Expand Up @@ -114,6 +133,11 @@ pub(crate) fn impl_struct(derive_data: &ReflectDeriveData) -> TokenStream {
std::any::type_name::<Self>()
}

#[inline]
fn get_type_info(&self) -> &'static #bevy_reflect_path::TypeInfo {
<Self as #bevy_reflect_path::Typed>::type_info()
}

#[inline]
fn any(&self) -> &dyn std::any::Any {
self
Expand Down Expand Up @@ -184,6 +208,10 @@ pub(crate) fn impl_tuple_struct(derive_data: &ReflectDeriveData) -> TokenStream
.active_fields()
.map(|field| Member::Unnamed(Index::from(field.index)))
.collect::<Vec<_>>();
let field_types = derive_data
.active_fields()
.map(|field| field.data.ty.clone())
.collect::<Vec<_>>();
let field_count = field_idents.len();
let field_indices = (0..field_count).collect::<Vec<usize>>();

Expand All @@ -201,10 +229,25 @@ pub(crate) fn impl_tuple_struct(derive_data: &ReflectDeriveData) -> TokenStream
});
let debug_fn = derive_data.traits().get_debug_impl();

let typed_impl = impl_typed(
struct_name,
derive_data.generics(),
quote! {
let fields: [#bevy_reflect_path::UnnamedField; #field_count] = [
#(#bevy_reflect_path::UnnamedField::new::<#field_types>(#field_indices),)*
];
let info = #bevy_reflect_path::TupleStructInfo::new::<Self>(&fields);
#bevy_reflect_path::TypeInfo::TupleStruct(info)
},
bevy_reflect_path,
);

let (impl_generics, ty_generics, where_clause) = derive_data.generics().split_for_impl();
TokenStream::from(quote! {
#get_type_registration_impl

#typed_impl

impl #impl_generics #bevy_reflect_path::TupleStruct for #struct_name #ty_generics #where_clause {
fn field(&self, index: usize) -> Option<&dyn #bevy_reflect_path::Reflect> {
match index {
Expand Down Expand Up @@ -243,6 +286,11 @@ pub(crate) fn impl_tuple_struct(derive_data: &ReflectDeriveData) -> TokenStream
std::any::type_name::<Self>()
}

#[inline]
fn get_type_info(&self) -> &'static #bevy_reflect_path::TypeInfo {
<Self as #bevy_reflect_path::Typed>::type_info()
}

#[inline]
fn any(&self) -> &dyn std::any::Any {
self
Expand Down Expand Up @@ -315,17 +363,34 @@ pub(crate) fn impl_value(
let partial_eq_fn = reflect_traits.get_partial_eq_impl(bevy_reflect_path);
let debug_fn = reflect_traits.get_debug_impl();

let typed_impl = impl_typed(
type_name,
generics,
quote! {
let info = #bevy_reflect_path::ValueInfo::new::<Self>();
#bevy_reflect_path::TypeInfo::Value(info)
},
bevy_reflect_path,
);

let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
TokenStream::from(quote! {
#get_type_registration_impl

#typed_impl

// SAFE: any and any_mut both return self
unsafe impl #impl_generics #bevy_reflect_path::Reflect for #type_name #ty_generics #where_clause {
#[inline]
fn type_name(&self) -> &str {
std::any::type_name::<Self>()
}

#[inline]
fn get_type_info(&self) -> &'static #bevy_reflect_path::TypeInfo {
<Self as #bevy_reflect_path::Typed>::type_info()
}

#[inline]
fn any(&self) -> &dyn std::any::Any {
self
Expand Down Expand Up @@ -385,3 +450,38 @@ pub(crate) fn impl_value(
}
})
}

fn impl_typed(
type_name: &Ident,
generics: &Generics,
generator: proc_macro2::TokenStream,
bevy_reflect_path: &Path,
) -> proc_macro2::TokenStream {
let is_generic = !generics.params.is_empty();

let static_generator = if is_generic {
quote! {
static CELL: #bevy_reflect_path::utility::GenericTypeInfoCell = #bevy_reflect_path::utility::GenericTypeInfoCell::new();
CELL.get_or_insert::<Self, _>(|| {
#generator
})
}
} else {
quote! {
static CELL: #bevy_reflect_path::utility::NonGenericTypeInfoCell = #bevy_reflect_path::utility::NonGenericTypeInfoCell::new();
CELL.get_or_set(|| {
#generator
})
}
};

let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

quote! {
impl #impl_generics #bevy_reflect_path::Typed for #type_name #ty_generics #where_clause {
fn type_info() -> &'static #bevy_reflect_path::TypeInfo {
#static_generator
}
}
}
}
86 changes: 84 additions & 2 deletions crates/bevy_reflect/src/array.rs
@@ -1,6 +1,9 @@
use crate::{serde::Serializable, Reflect, ReflectMut, ReflectRef};
use crate::{
serde::Serializable, utility::NonGenericTypeInfoCell, DynamicInfo, Reflect, ReflectMut,
ReflectRef, TypeInfo, Typed,
};
use std::{
any::Any,
any::{Any, TypeId},
fmt::Debug,
hash::{Hash, Hasher},
};
Expand Down Expand Up @@ -37,6 +40,73 @@ pub trait Array: Reflect {
}
}

/// A container for compile-time array info.
#[derive(Clone, Debug)]
pub struct ArrayInfo {
type_name: &'static str,
type_id: TypeId,
item_type_name: &'static str,
item_type_id: TypeId,
capacity: usize,
}

impl ArrayInfo {
/// Create a new [`ArrayInfo`].
///
/// # Arguments
///
/// * `capacity`: The maximum capacity of the underlying array.
///
pub fn new<TArray: Array, TItem: Reflect>(capacity: usize) -> Self {
Self {
type_name: std::any::type_name::<TArray>(),
type_id: TypeId::of::<TArray>(),
item_type_name: std::any::type_name::<TItem>(),
item_type_id: TypeId::of::<TItem>(),
capacity,
}
}

/// The compile-time capacity of the array.
pub fn capacity(&self) -> usize {
self.capacity
}

/// The [type name] of the array.
///
/// [type name]: std::any::type_name
pub fn type_name(&self) -> &'static str {
self.type_name
}

/// The [`TypeId`] of the array.
pub fn type_id(&self) -> TypeId {
self.type_id
}

/// Check if the given type matches the array type.
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}

/// The [type name] of the array item.
///
/// [type name]: std::any::type_name
pub fn item_type_name(&self) -> &'static str {
self.item_type_name
}

/// The [`TypeId`] of the array item.
pub fn item_type_id(&self) -> TypeId {
self.item_type_id
}

/// Check if the given type matches the array item type.
pub fn item_is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.item_type_id
}
}

/// A fixed-size list of reflected values.
///
/// This differs from [`DynamicList`] in that the size of the [`DynamicArray`]
Expand Down Expand Up @@ -89,6 +159,11 @@ unsafe impl Reflect for DynamicArray {
self.name.as_str()
}

#[inline]
fn get_type_info(&self) -> &'static TypeInfo {
<Self as Typed>::type_info()
}

#[inline]
fn any(&self) -> &dyn Any {
self
Expand Down Expand Up @@ -185,6 +260,13 @@ impl Array for DynamicArray {
}
}

impl Typed for DynamicArray {
fn type_info() -> &'static TypeInfo {
static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new();
CELL.get_or_set(|| TypeInfo::Dynamic(DynamicInfo::new::<Self>()))
}
}

/// An iterator over an [`Array`].
pub struct ArrayIter<'a> {
pub(crate) array: &'a dyn Array,
Expand Down
84 changes: 84 additions & 0 deletions crates/bevy_reflect/src/fields.rs
@@ -0,0 +1,84 @@
use crate::Reflect;
use std::any::{Any, TypeId};
use std::borrow::Cow;

/// The named field of a reflected struct.
#[derive(Clone, Debug)]
pub struct NamedField {
name: Cow<'static, str>,
type_name: &'static str,
type_id: TypeId,
}

impl NamedField {
/// Create a new [`NamedField`].
pub fn new<T: Reflect, TName: Into<Cow<'static, str>>>(name: TName) -> Self {
Self {
name: name.into(),
type_name: std::any::type_name::<T>(),
type_id: TypeId::of::<T>(),
}
}

/// The name of the field.
pub fn name(&self) -> &Cow<'static, str> {
&self.name
}

/// The [type name] of the field.
///
/// [type name]: std::any::type_name
pub fn type_name(&self) -> &'static str {
self.type_name
}

/// The [`TypeId`] of the field.
pub fn type_id(&self) -> TypeId {
self.type_id
}

/// Check if the given type matches the field type.
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}
}

/// The unnamed field of a reflected tuple or tuple struct.
#[derive(Clone, Debug)]
pub struct UnnamedField {
index: usize,
type_name: &'static str,
type_id: TypeId,
}

impl UnnamedField {
pub fn new<T: Reflect>(index: usize) -> Self {
Self {
index,
type_name: std::any::type_name::<T>(),
type_id: TypeId::of::<T>(),
}
}

/// Returns the index of the field.
pub fn index(&self) -> usize {
self.index
}

/// The [type name] of the field.
///
/// [type name]: std::any::type_name
pub fn type_name(&self) -> &'static str {
self.type_name
}

/// The [`TypeId`] of the field.
pub fn type_id(&self) -> TypeId {
self.type_id
}

/// Check if the given type matches the field type.
pub fn is<T: Any>(&self) -> bool {
TypeId::of::<T>() == self.type_id
}
}

0 comments on commit 78b8e51

Please sign in to comment.