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 Reflection #926

Merged
merged 6 commits into from
Nov 28, 2020
Merged

Bevy Reflection #926

merged 6 commits into from
Nov 28, 2020

Conversation

cart
Copy link
Member

@cart cart commented Nov 25, 2020

Bevy Reflect

This crate replaces the old bevy_property and bevy_type_registry crates with a new, much more capable bevy_reflect crate. It enables you to dynamically interact with Rust types:

  • Derive the Reflect traits
  • Interact with fields using their names (for named structs) or indices (for tuple structs)
  • "Patch" your types with new values
  • Look up nested fields using "path strings"
  • Iterate over struct fields
  • Automatically serialize and deserialize via Serde (without explicit serde impls)
  • Trait "reflection"

Why replace bevy_property?

  • bevy_property used a lot of invented jargon and was built for a very specific use case. bevy_reflect is intended to be a "generic" rust reflection crate
  • bevy_property had a number of limitations in how properties could be accessed (ex: nesting didn't work in some cases)
  • bevy_property didn't account for traits very well and it wasn't very extensible
  • bevy_property used a single trait for all types and overloaded behaviors for things like maps and lists in a way that was confusing at best and in some cases, actively harmful

Features

Derive the Reflect traits

// this will automatically implement the Reflect trait and the Struct trait (because the type is a struct)
#[derive(Reflect)]
struct Foo {
    a: u32,
    b: Bar
    c: Vec<i32>,
    d: Vec<Baz>,
}

// this will automatically implement the Reflect trait and the TupleStruct trait (because the type is a tuple struct)
#[derive(Reflect)]
struct Bar(String);

#[derive(Reflect)]
struct Baz {
    value: f32,
};

// We will use this value to illustrate `bevy_reflect` features
let mut foo = Foo {
    a: 1,
    b: Bar("hello".to_string()),
    c: vec![1, 2]
    d: vec![Baz { value: 3.14 }]
};

Interact with fields using their names

assert_eq!(*foo.get_field::<u32>("a").unwrap(), 1);

*foo.get_field_mut::<u32>("a").unwrap() = 2;

assert_eq!(foo.a, 2);

"Patch" your types with new values

let mut dynamic_struct = DynamicStruct::default();
dynamic_struct.insert("a", 42u32);
dynamic_struct.insert("c", vec![3, 4, 5]);

foo.apply(&dynamic_struct);

assert_eq!(foo.a, 42);
assert_eq!(foo.c, vec![3, 4, 5]);

Look up nested fields using "path strings"

let value = *foo.get_path::<f32>("d[0].value").unwrap();
assert_eq!(value, 3.14);

Iterate over struct fields

for (i, value: &Reflect) in foo.iter_fields().enumerate() {
    let field_name = foo.name_at(i).unwrap();
    if let Ok(value) = value.downcast_ref::<u32>() {
        println!("{} is a u32 with the value: {}", field_name, *value);
    } 
}

Automatically serialize and deserialize via Serde (without explicit serde impls)

let mut registry = TypeRegistry::default();
registry.register::<u32>();
registry.register::<i32>();
registry.register::<f32>();
registry.register::<String>();
registry.register::<Bar>();
registry.register::<Baz>();

let serializer = ReflectSerializer::new(&foo, &registry);
let serialized = ron::ser::to_string_pretty(&serializer, ron::ser::PrettyConfig::default()).unwrap();

let mut deserializer = ron::de::Deserializer::from_str(&serialized).unwrap();
let reflect_deserializer = ReflectDeserializer::new(&registry);
let value = reflect_deserializer.deserialize(&mut deserializer).unwrap();
let dynamic_struct = value.take::<DynamicStruct>().unwrap();

assert!(foo.partial_eq(&dynamic_struct).unwrap());

Trait "reflection"

Call a trait on a given &dyn Reflect reference without knowing the underlying type!

#[derive(Reflect)]
#[reflect(DoThing)]
struct MyType {
    value: String,
}

impl DoThing for MyType {
    fn do_thing(&self) -> String {
        format!("{} World!", self.value)
    }
}

#[reflect_trait]
pub trait DoThing {
    fn do_thing(&self) -> String;
}

// First, lets box our type as a Box<dyn Reflect>
let reflect_value: Box<dyn Reflect> = Box::new(MyType {
    value: "Hello".to_string(),
});

// This means we no longer have direct access to MyType or it methods. We can only call Reflect methods on reflect_value.
// What if we want to call `do_thing` on our type? We could downcast using reflect_value.get::<MyType>(), but what if we
// don't know the type at compile time?

// Normally in rust we would be out of luck at this point. Lets use our new reflection powers to do something cool!
let mut type_registry = TypeRegistry::default()
type_registry.register::<MyType>();

// The #[reflect] attribute we put on our DoThing trait generated a new `ReflectDoThing` struct, which implements TypeData.
// This was added to MyType's TypeRegistration.
let reflect_do_thing = type_registry
    .get_type_data::<ReflectDoThing>(reflect_value.type_id())
    .unwrap();

// We can use this generated type to convert our `&dyn Reflect` reference to an `&dyn DoThing` reference
let my_trait: &dyn DoThing = reflect_do_thing.get(&*reflect_value).unwrap();

// Which means we can now call do_thing(). Magic!
println!("{}", my_trait.do_thing());

// This works because the #[reflect(MyTrait)] we put on MyType informed the Reflect derive to insert a new instance
// of ReflectDoThing into MyType's registration. The instance knows how to cast &dyn Reflect to &dyn MyType, because it
// knows that &dyn Reflect should first be downcasted to &MyType, which can then be safely casted to &dyn MyType

Why make this?

The whole point of Rust is static safety! Why build something that makes it easy to throw it all away?

  • Some problems are inherently dynamic (scripting, some types of serialization / deserialization)
  • Sometimes the dynamic way is easier
  • Sometimes the dynamic way puts less burden on your users to derive a bunch of traits (this was a big motivator for Bevy)
  • Rust has minimal reflection features (Any, type_name, TypeId), but they are very limited.

Future Work

  • I would love to integrate something like the inventory crate to remove the need to manually register types. I actually already integrated it, but there were two problems:
    1. inventory (and the general approach it uses) has a show-stopping rustc bug
    2. inventory (and the general approach it uses) doesn't support wasm
  • It would be nice to add "enum reflection" (with a new Enum trait) to remove the need to treat them as value types
  • Map value insertion
  • More "reflect trait" impls (Default, Debug)
  • Integrate TypeUuid with TypeRegistry (optional)
  • Consider implicitly including the ReflectComponent attribute value in Bevy to reduce the amount of boilerplate required
  • "diff" reflected structs and produce a DynamicStruct

@cart cart added C-Feature A new feature, making something new possible core labels Nov 25, 2020
@cart cart force-pushed the reflection branch 4 times, most recently from 4adfad0 to ba21f0b Compare November 26, 2020 02:58
@memoryruins
Copy link
Contributor

Many of these high-level design changes look really great, and I'm looking forward to experimenting with this for scripting. Even the merging of the two previous crates is a big win for learnability and docs.

crates/bevy_asset/src/assets.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/reflect.rs Show resolved Hide resolved
@cart cart merged commit 72b2fc9 into bevyengine:master Nov 28, 2020
@jbarthelmes
Copy link

"diff" reflected structs and produce a DynamicStruct

Maybe this helps: https://github.com/amethyst/serde-diff

@cart
Copy link
Member Author

cart commented Nov 28, 2020

@jbarthelmes thanks! im relatively familiar with serde-diff. its nice, but the implementation is pretty different from what I'm planning.

@CleanCut
Copy link
Member

#[derive(Reflect)]
struct Foo {
    a: u32,
    b: Bar
    c: Vec<i32>,
    d: Vec<Bar>,
}

@cart is that supposed to be d: Vec<Baz> (not Bar) there near the end?

@cart
Copy link
Member Author

cart commented Nov 28, 2020

@CleanCut Yup good call. I just updated the pr description and ill update the markdown in my next pr.

@mrobakowski
Copy link

@cart are you aware of dtolnay's other crazy distributed aggregation crate linkme? As far as I understand it, it circumvents one of the issues you have with inventory, but it doesn't sound like it would work with dynamic linkage (does inventory?). Wasm support still isn't there, but maybe it'll get there sooner than in inventory dtolnay/linkme#6.

@cart
Copy link
Member Author

cart commented Nov 29, 2020

@mrobakowski yeah I came across it during my investigation and the lack of WASM was a dealbreaker.

@cart
Copy link
Member Author

cart commented Nov 29, 2020

I'll be watching the space. As soon as we have a cross-platform option I'll jump on it 😄

@cart cart mentioned this pull request Dec 1, 2020
@fopsdev fopsdev mentioned this pull request Jan 24, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-Feature A new feature, making something new possible
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants