Skip to content

Commit 90bb1ad

Browse files
authored
bevy_reflect: Contextual serialization error messages (#13888)
# Objective Reflection serialization can be difficult to debug. A lot of times a type fails to be serialized and the user is left wondering where that type came from. This is most often encountered with Bevy's scenes. Attempting to serialize all resources in the world will fail because some resources can't be serialized. For example, users will often get complaints about `bevy_utils::Instant` not registering `ReflectSerialize`. Well, `Instant` can't be serialized, so the only other option is to exclude the resource that contains it. But what resource contains it? This is where reflection serialization can get a little tricky (it's `Time<Real>` btw). ## Solution Add the `debug_stack` feature to `bevy_reflect`. When enabled, the reflection serializers and deserializers will keep track of the current type stack. And this stack will be used in error messages to help with debugging. Now, if we unknowingly try to serialize `Time<Real>`, we'll get the following error: ``` type `bevy_utils::Instant` did not register the `ReflectSerialize` type data. For certain types, this may need to be registered manually using `register_type_data` (stack: `bevy_time::time::Time<bevy_time::real::Real>` -> `bevy_time::real::Real` -> `bevy_utils::Instant`) ``` ### Implementation This makes use of `thread_local!` to manage an internal `TypeInfoStack` which holds a stack of `&'static TypeInfo`. We push to the stack before a type is (de)serialized and pop from the stack afterwards. Using a thread-local should be fine since we know two (de)serializers can't be running at the same time (and if they're running on separate threads, then we're still good). The only potential issue would be if a user went through one of the sub-serializers, like `StructSerializer`. However, I don't think many users are going through these types (I don't even know if we necessarily want to keep those public either, but we'll save that for a different PR). Additionally, this is just a debug feature that only affects error messages, so it wouldn't have any drastically negative effect. It would just result in the stack not being cleared properly if there were any errors. Lastly, this is not the most performant implementation since we now fetch the `TypeInfo` an extra time. But I figured that for a debug tool, it wouldn't matter too much. ### Feature This also adds a `debug` feature, which enables the `debug_stack` feature. I added it because I think we may want to potentially add more debug tools in the future, and this gives us a good framework for adding those. Users who want all debug features, present and future, can just set `debug`. If they only want this feature, then they can just use `debug_stack`. I also made the `debug` feature default to help capture the widest audience (i.e. the users who want this feature but don't know they do). However, if we think it's better as a non-default feature, I can change it! And if there's any bikeshedding around the name `debug_stack`, let me know! ## Testing Run the following command: ``` cargo test --package bevy_reflect --features debug_stack ``` --- ## Changelog - Added the `debug` and `debug_stack` features to `bevy_reflect` - Updated the error messages returned by the reflection serializers and deserializers to include more contextual information when the `debug_stack` or `debug` feature is enabled
1 parent a5c4606 commit 90bb1ad

29 files changed

+482
-171
lines changed

crates/bevy_reflect/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ keywords = ["bevy"]
1010
rust-version = "1.76.0"
1111

1212
[features]
13-
default = ["smallvec"]
13+
default = ["smallvec", "debug"]
1414
# When enabled, provides Bevy-related reflection implementations
1515
bevy = ["smallvec", "smol_str"]
1616
glam = ["dep:glam"]
1717
petgraph = ["dep:petgraph"]
1818
smallvec = ["dep:smallvec"]
1919
uuid = ["dep:uuid"]
20+
# Enables features useful for debugging reflection
21+
debug = ["debug_stack"]
22+
# When enabled, keeps track of the current serialization/deserialization context for better error messages
23+
debug_stack = []
2024
# When enabled, allows documentation comments to be accessed via reflection
2125
documentation = ["bevy_reflect_derive/documentation"]
2226
# Enables function reflection

crates/bevy_reflect/src/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@
473473
//!
474474
//! | Default | Dependencies |
475475
//! | :-----: | :---------------------------------------: |
476-
//! | ❌ | [`bevy_math`], [`glam`], [`smallvec`] |
476+
//! | ❌ | [`bevy_math`], [`glam`], [`smallvec`] |
477477
//!
478478
//! This feature makes it so that the appropriate reflection traits are implemented on all the types
479479
//! necessary for the [Bevy] game engine.
@@ -493,6 +493,18 @@
493493
//! This can be useful for generating documentation for scripting language interop or
494494
//! for displaying tooltips in an editor.
495495
//!
496+
//! ## `debug`
497+
//!
498+
//! | Default | Dependencies |
499+
//! | :-----: | :-------------------------------------------: |
500+
//! | ✅ | `debug_stack` |
501+
//!
502+
//! This feature enables useful debug features for reflection.
503+
//!
504+
//! This includes the `debug_stack` feature,
505+
//! which enables capturing the type stack when serializing or deserializing a type
506+
//! and displaying it in error messages.
507+
//!
496508
//! [Reflection]: https://en.wikipedia.org/wiki/Reflective_programming
497509
//! [Bevy]: https://bevyengine.org/
498510
//! [limitations]: #limitations
@@ -562,6 +574,8 @@ pub mod attributes;
562574
mod enums;
563575
pub mod serde;
564576
pub mod std_traits;
577+
#[cfg(feature = "debug_stack")]
578+
mod type_info_stack;
565579
pub mod utility;
566580

567581
/// The reflect prelude.

crates/bevy_reflect/src/serde/de/arrays.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ impl<'a, 'de> Visitor<'de> for ArrayVisitor<'a> {
3535
{
3636
let mut vec = Vec::with_capacity(seq.size_hint().unwrap_or_default());
3737
let registration = try_get_registration(self.array_info.item_ty(), self.registry)?;
38-
while let Some(value) =
39-
seq.next_element_seed(TypedReflectDeserializer::new(registration, self.registry))?
40-
{
38+
while let Some(value) = seq.next_element_seed(TypedReflectDeserializer::new_internal(
39+
registration,
40+
self.registry,
41+
))? {
4142
vec.push(value);
4243
}
4344

crates/bevy_reflect/src/serde/de/deserializer.rs

Lines changed: 115 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use crate::serde::de::arrays::ArrayVisitor;
22
use crate::serde::de::enums::EnumVisitor;
3+
use crate::serde::de::error_utils::make_custom_error;
4+
#[cfg(feature = "debug_stack")]
5+
use crate::serde::de::error_utils::TYPE_INFO_STACK;
36
use crate::serde::de::lists::ListVisitor;
47
use crate::serde::de::maps::MapVisitor;
58
use crate::serde::de::options::OptionVisitor;
@@ -231,6 +234,20 @@ pub struct TypedReflectDeserializer<'a> {
231234

232235
impl<'a> TypedReflectDeserializer<'a> {
233236
pub fn new(registration: &'a TypeRegistration, registry: &'a TypeRegistry) -> Self {
237+
#[cfg(feature = "debug_stack")]
238+
TYPE_INFO_STACK.set(crate::type_info_stack::TypeInfoStack::new());
239+
240+
Self {
241+
registration,
242+
registry,
243+
}
244+
}
245+
246+
/// An internal constructor for creating a deserializer without resetting the type info stack.
247+
pub(super) fn new_internal(
248+
registration: &'a TypeRegistration,
249+
registry: &'a TypeRegistry,
250+
) -> Self {
234251
Self {
235252
registration,
236253
registry,
@@ -245,89 +262,106 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> {
245262
where
246263
D: serde::Deserializer<'de>,
247264
{
248-
let type_path = self.registration.type_info().type_path();
249-
250-
// Handle both Value case and types that have a custom `ReflectDeserialize`
251-
if let Some(deserialize_reflect) = self.registration.data::<ReflectDeserialize>() {
252-
let value = deserialize_reflect.deserialize(deserializer)?;
253-
return Ok(value.into_partial_reflect());
254-
}
265+
let deserialize_internal = || -> Result<Self::Value, D::Error> {
266+
let type_path = self.registration.type_info().type_path();
255267

256-
match self.registration.type_info() {
257-
TypeInfo::Struct(struct_info) => {
258-
let mut dynamic_struct = deserializer.deserialize_struct(
259-
struct_info.type_path_table().ident().unwrap(),
260-
struct_info.field_names(),
261-
StructVisitor::new(struct_info, self.registration, self.registry),
262-
)?;
263-
dynamic_struct.set_represented_type(Some(self.registration.type_info()));
264-
Ok(Box::new(dynamic_struct))
265-
}
266-
TypeInfo::TupleStruct(tuple_struct_info) => {
267-
let mut dynamic_tuple_struct = deserializer.deserialize_tuple_struct(
268-
tuple_struct_info.type_path_table().ident().unwrap(),
269-
tuple_struct_info.field_len(),
270-
TupleStructVisitor::new(tuple_struct_info, self.registration, self.registry),
271-
)?;
272-
dynamic_tuple_struct.set_represented_type(Some(self.registration.type_info()));
273-
Ok(Box::new(dynamic_tuple_struct))
274-
}
275-
TypeInfo::List(list_info) => {
276-
let mut dynamic_list =
277-
deserializer.deserialize_seq(ListVisitor::new(list_info, self.registry))?;
278-
dynamic_list.set_represented_type(Some(self.registration.type_info()));
279-
Ok(Box::new(dynamic_list))
280-
}
281-
TypeInfo::Array(array_info) => {
282-
let mut dynamic_array = deserializer.deserialize_tuple(
283-
array_info.capacity(),
284-
ArrayVisitor::new(array_info, self.registry),
285-
)?;
286-
dynamic_array.set_represented_type(Some(self.registration.type_info()));
287-
Ok(Box::new(dynamic_array))
288-
}
289-
TypeInfo::Map(map_info) => {
290-
let mut dynamic_map =
291-
deserializer.deserialize_map(MapVisitor::new(map_info, self.registry))?;
292-
dynamic_map.set_represented_type(Some(self.registration.type_info()));
293-
Ok(Box::new(dynamic_map))
294-
}
295-
TypeInfo::Set(set_info) => {
296-
let mut dynamic_set =
297-
deserializer.deserialize_seq(SetVisitor::new(set_info, self.registry))?;
298-
dynamic_set.set_represented_type(Some(self.registration.type_info()));
299-
Ok(Box::new(dynamic_set))
300-
}
301-
TypeInfo::Tuple(tuple_info) => {
302-
let mut dynamic_tuple = deserializer.deserialize_tuple(
303-
tuple_info.field_len(),
304-
TupleVisitor::new(tuple_info, self.registration, self.registry),
305-
)?;
306-
dynamic_tuple.set_represented_type(Some(self.registration.type_info()));
307-
Ok(Box::new(dynamic_tuple))
308-
}
309-
TypeInfo::Enum(enum_info) => {
310-
let mut dynamic_enum = if enum_info.type_path_table().module_path()
311-
== Some("core::option")
312-
&& enum_info.type_path_table().ident() == Some("Option")
313-
{
314-
deserializer.deserialize_option(OptionVisitor::new(enum_info, self.registry))?
315-
} else {
316-
deserializer.deserialize_enum(
317-
enum_info.type_path_table().ident().unwrap(),
318-
enum_info.variant_names(),
319-
EnumVisitor::new(enum_info, self.registration, self.registry),
320-
)?
321-
};
322-
dynamic_enum.set_represented_type(Some(self.registration.type_info()));
323-
Ok(Box::new(dynamic_enum))
268+
// Handle both Value case and types that have a custom `ReflectDeserialize`
269+
if let Some(deserialize_reflect) = self.registration.data::<ReflectDeserialize>() {
270+
let value = deserialize_reflect.deserialize(deserializer)?;
271+
return Ok(value.into_partial_reflect());
324272
}
325-
TypeInfo::Value(_) => {
326-
// This case should already be handled
327-
Err(Error::custom(format_args!(
328-
"Type `{type_path}` did not register the `ReflectDeserialize` type data. For certain types, this may need to be registered manually using `register_type_data`",
329-
)))
273+
274+
match self.registration.type_info() {
275+
TypeInfo::Struct(struct_info) => {
276+
let mut dynamic_struct = deserializer.deserialize_struct(
277+
struct_info.type_path_table().ident().unwrap(),
278+
struct_info.field_names(),
279+
StructVisitor::new(struct_info, self.registration, self.registry),
280+
)?;
281+
dynamic_struct.set_represented_type(Some(self.registration.type_info()));
282+
Ok(Box::new(dynamic_struct))
283+
}
284+
TypeInfo::TupleStruct(tuple_struct_info) => {
285+
let mut dynamic_tuple_struct = deserializer.deserialize_tuple_struct(
286+
tuple_struct_info.type_path_table().ident().unwrap(),
287+
tuple_struct_info.field_len(),
288+
TupleStructVisitor::new(
289+
tuple_struct_info,
290+
self.registration,
291+
self.registry,
292+
),
293+
)?;
294+
dynamic_tuple_struct.set_represented_type(Some(self.registration.type_info()));
295+
Ok(Box::new(dynamic_tuple_struct))
296+
}
297+
TypeInfo::List(list_info) => {
298+
let mut dynamic_list =
299+
deserializer.deserialize_seq(ListVisitor::new(list_info, self.registry))?;
300+
dynamic_list.set_represented_type(Some(self.registration.type_info()));
301+
Ok(Box::new(dynamic_list))
302+
}
303+
TypeInfo::Array(array_info) => {
304+
let mut dynamic_array = deserializer.deserialize_tuple(
305+
array_info.capacity(),
306+
ArrayVisitor::new(array_info, self.registry),
307+
)?;
308+
dynamic_array.set_represented_type(Some(self.registration.type_info()));
309+
Ok(Box::new(dynamic_array))
310+
}
311+
TypeInfo::Map(map_info) => {
312+
let mut dynamic_map =
313+
deserializer.deserialize_map(MapVisitor::new(map_info, self.registry))?;
314+
dynamic_map.set_represented_type(Some(self.registration.type_info()));
315+
Ok(Box::new(dynamic_map))
316+
}
317+
TypeInfo::Set(set_info) => {
318+
let mut dynamic_set =
319+
deserializer.deserialize_seq(SetVisitor::new(set_info, self.registry))?;
320+
dynamic_set.set_represented_type(Some(self.registration.type_info()));
321+
Ok(Box::new(dynamic_set))
322+
}
323+
TypeInfo::Tuple(tuple_info) => {
324+
let mut dynamic_tuple = deserializer.deserialize_tuple(
325+
tuple_info.field_len(),
326+
TupleVisitor::new(tuple_info, self.registration, self.registry),
327+
)?;
328+
dynamic_tuple.set_represented_type(Some(self.registration.type_info()));
329+
Ok(Box::new(dynamic_tuple))
330+
}
331+
TypeInfo::Enum(enum_info) => {
332+
let mut dynamic_enum = if enum_info.type_path_table().module_path()
333+
== Some("core::option")
334+
&& enum_info.type_path_table().ident() == Some("Option")
335+
{
336+
deserializer
337+
.deserialize_option(OptionVisitor::new(enum_info, self.registry))?
338+
} else {
339+
deserializer.deserialize_enum(
340+
enum_info.type_path_table().ident().unwrap(),
341+
enum_info.variant_names(),
342+
EnumVisitor::new(enum_info, self.registration, self.registry),
343+
)?
344+
};
345+
dynamic_enum.set_represented_type(Some(self.registration.type_info()));
346+
Ok(Box::new(dynamic_enum))
347+
}
348+
TypeInfo::Value(_) => {
349+
// This case should already be handled
350+
Err(make_custom_error(format_args!(
351+
"type `{type_path}` did not register the `ReflectDeserialize` type data. For certain types, this may need to be registered manually using `register_type_data`",
352+
)))
353+
}
330354
}
331-
}
355+
};
356+
357+
#[cfg(feature = "debug_stack")]
358+
TYPE_INFO_STACK.with_borrow_mut(|stack| stack.push(self.registration.type_info()));
359+
360+
let output = deserialize_internal();
361+
362+
#[cfg(feature = "debug_stack")]
363+
TYPE_INFO_STACK.with_borrow_mut(crate::type_info_stack::TypeInfoStack::pop);
364+
365+
output
332366
}
333367
}

crates/bevy_reflect/src/serde/de/enums.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::serde::de::error_utils::make_custom_error;
12
use crate::serde::de::helpers::ExpectedValues;
23
use crate::serde::de::registration_utils::try_get_registration;
34
use crate::serde::de::struct_utils::{visit_struct, visit_struct_seq};
@@ -67,10 +68,9 @@ impl<'a, 'de> Visitor<'de> for EnumVisitor<'a> {
6768
*TupleLikeInfo::field_at(tuple_info, 0)?.ty(),
6869
self.registry,
6970
)?;
70-
let value = variant.newtype_variant_seed(TypedReflectDeserializer::new(
71-
registration,
72-
self.registry,
73-
))?;
71+
let value = variant.newtype_variant_seed(
72+
TypedReflectDeserializer::new_internal(registration, self.registry),
73+
)?;
7474
let mut dynamic_tuple = DynamicTuple::default();
7575
dynamic_tuple.insert_boxed(value);
7676
dynamic_tuple.into()
@@ -121,7 +121,7 @@ impl<'de> DeserializeSeed<'de> for VariantDeserializer {
121121
E: Error,
122122
{
123123
self.0.variant_at(variant_index as usize).ok_or_else(|| {
124-
Error::custom(format_args!(
124+
make_custom_error(format_args!(
125125
"no variant found at index `{}` on enum `{}`",
126126
variant_index,
127127
self.0.type_path()
@@ -135,7 +135,7 @@ impl<'de> DeserializeSeed<'de> for VariantDeserializer {
135135
{
136136
self.0.variant(variant_name).ok_or_else(|| {
137137
let names = self.0.iter().map(VariantInfo::name);
138-
Error::custom(format_args!(
138+
make_custom_error(format_args!(
139139
"unknown variant `{}`, expected one of {:?}",
140140
variant_name,
141141
ExpectedValues::from_iter(names)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use core::fmt::Display;
2+
use serde::de::Error;
3+
4+
#[cfg(feature = "debug_stack")]
5+
thread_local! {
6+
/// The thread-local [`TypeInfoStack`] used for debugging.
7+
///
8+
/// [`TypeInfoStack`]: crate::type_info_stack::TypeInfoStack
9+
pub(super) static TYPE_INFO_STACK: std::cell::RefCell<crate::type_info_stack::TypeInfoStack> = const { std::cell::RefCell::new(
10+
crate::type_info_stack::TypeInfoStack::new()
11+
) };
12+
}
13+
14+
/// A helper function for generating a custom deserialization error message.
15+
///
16+
/// This function should be preferred over [`Error::custom`] as it will include
17+
/// other useful information, such as the [type info stack].
18+
///
19+
/// [type info stack]: crate::type_info_stack::TypeInfoStack
20+
pub(super) fn make_custom_error<E: Error>(msg: impl Display) -> E {
21+
#[cfg(feature = "debug_stack")]
22+
return TYPE_INFO_STACK
23+
.with_borrow(|stack| E::custom(format_args!("{} (stack: {:?})", msg, stack)));
24+
#[cfg(not(feature = "debug_stack"))]
25+
return E::custom(msg);
26+
}

crates/bevy_reflect/src/serde/de/lists.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ impl<'a, 'de> Visitor<'de> for ListVisitor<'a> {
3535
{
3636
let mut list = DynamicList::default();
3737
let registration = try_get_registration(self.list_info.item_ty(), self.registry)?;
38-
while let Some(value) =
39-
seq.next_element_seed(TypedReflectDeserializer::new(registration, self.registry))?
40-
{
38+
while let Some(value) = seq.next_element_seed(TypedReflectDeserializer::new_internal(
39+
registration,
40+
self.registry,
41+
))? {
4142
list.push_box(value);
4243
}
4344
Ok(list)

crates/bevy_reflect/src/serde/de/maps.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ impl<'a, 'de> Visitor<'de> for MapVisitor<'a> {
3333
let mut dynamic_map = DynamicMap::default();
3434
let key_registration = try_get_registration(self.map_info.key_ty(), self.registry)?;
3535
let value_registration = try_get_registration(self.map_info.value_ty(), self.registry)?;
36-
while let Some(key) = map.next_key_seed(TypedReflectDeserializer::new(
36+
while let Some(key) = map.next_key_seed(TypedReflectDeserializer::new_internal(
3737
key_registration,
3838
self.registry,
3939
))? {
40-
let value = map.next_value_seed(TypedReflectDeserializer::new(
40+
let value = map.next_value_seed(TypedReflectDeserializer::new_internal(
4141
value_registration,
4242
self.registry,
4343
))?;

0 commit comments

Comments
 (0)