Bevy 反射基本原理

本文依据官方反射的示例代码,简要介绍 Bevy 反射系统(0.17.0-dev)的基本实现原理。

Bevy 正在快速迭代中,反射系统的实现与 API 可能发生变动。

Reflect 特质

参考官方示例代码

Bevy 反射系统提供了 Reflect derive 宏为自定义类型生成代码,使其可以转换为 dyn Reflect 对象进行操作。

1. 动态获取类型名

Bevy 定义了一个 DynamicTypePath Trait ,它仅提供获取类型名的函数:

pub trait DynamicTypePath {
    fn reflect_type_path(&self) -> &str;    // 如 crate::my_mod::MyStruct<u32>
    fn reflect_short_type_path(&self) -> &str;  // 如 MyStruct<u32>
    fn reflect_type_ident(&self) -> Option<&str>;   // 如 MyStruct
    fn reflect_crate_name(&self) -> Option<&str>;   // 如 crate
    fn reflect_module_path(&self) -> Option<&str>;  // 如 my_mode
}

#[derive(Reflect)] 宏会为类型自动实现此 Trait 。

这是 Bevy 反射系统的基础 Trait 之一,其他反射特质都要求实现此特质,因此各种类型可以调用这些函数获取类型名:

// ptr: Box<dyn Reflect>
println!("{}", ptr.reflect_type_path());

2. 类型描述信息

仅有类型名是不够的,我们需要在运行时(以字符串的形式)获取类型的详细定义,Bevy 为此定义了一个 DynamicTyped Trait

pub trait DynamicTyped {
    fn reflect_type_info(&self) -> &'static TypeInfo;
}

此 Trait 也由 #[derive(Reflect)] 宏自动实现,它返回一个 TypeInfo 枚举,每种枚举值都是一个结构体,代表不同情况的完整类型信息:

StructInfo TupleStructInfo TupleInfo
ListInfo ArrayInfo MapInfo
SetInfo EnumInfo OpaqueInfo

StructInfo 为例,它包含以下内容:

  1. 类型名,类型ID (std::any::TypeId)
  2. 泛型信息,自定义属性
  3. 字段名列表,字段信息列表

OpaqueInfo 是特殊类型,表示内部详情不可见,因此只提供 类型ID、路径名、泛型信息。

注意这些结构体是描述类本身的,因此不涉及具体的字段值。

3. 类型操控 Trait

除了获取类型信息,我们还希望能够通过 dyn Reflect 操控数据,比如通过字符串或整数索引访问内部字段。Bevy 为此定义了一系列 Trait

Struct TupleStruct Tuple
List Array Map
Set Enum PartialReflect

这里的九种类型和 TypeInfo 中的恰好对应。PartialReflect 是个特殊的类型、我们会单独介绍,它不提供内部字段的访问,因此与 OpaqueInfo 类型对应。

其他八种特质都要求先实现 PartialReflect ,然后各自定义了一些不同的方法访问内部字段。

Struct 为例,可以通过字符串形式的字段名获取内部字段的引用,但需要显式指定类型,因为内部字段类型依然不确定。

ReflectRef::Struct(value) => {
    _ = value.field("x"); // 返回 Option<&dyn PartialReflect>
    // GetFiled 特征,在 Struct 的基础上提供显式转换的功能
    println!("{}", value.get_field::<usize>("x").unwrap());
}

这些 Trait 也是通过 #[derive(Reflect)] 宏自动实现的。

4. PartialReflect

PartialReflect 特质如同字面意思“部分反射”,它要实现 DynamicTypePath ,本身不提供访问内部字段的功能,但给出了一组适用于所有反射类型的操作函数,重点如下:

  • reflect_ref 函数:尝试转换成上面提到的类型操控 Trait 的对象,返回一个枚举表示九种特质
  • apply 函数:将另一个 dyn PartialReflect 对象逐字段赋值给自身,不要求类型相同,无视多余字段
  • try_as_reflect 函数:尝试转换成 &dyn Reflect 以获得更多功能
  • to_dynamic 函数:根据自身内容生成一个动态类型的 dyn PartialReflect

参考官方示例代码

由于 apply 的存在,很多功能都是以 PartialReflect 类型为基础的。

dyn PartialReflect 类型还提供了向下转换,但实际是先转换成 dyn Reflect 在调用它的向下转换。

5. Reflect

Reflect Trait 要求实现 PartialReflectDynamicTypedAny ,这里的重点是 Any 约束,因此可以向上转换成 &dyn Any ,还可以调用 Any 的向下转换变回原始类型。

实际上它也只提供了向上转换到 &dyn Any 和尝试向下转换回原类型这两种功能。 向下转换实际调用 Any 的向下转换,因此要求对象本身的 TypeId 和转换的目标类型一致。

ReflectRef::Struct(value) => {
    println!("{}", value.get_field::<usize>("x").unwrap());
}

比如这段代码,实际是使用 Structfield 方法获取了 &dyn PartialReflect ,然后转换回 &dyn Reflect,再转换回目标类型 &usize

6. 动态类型

参考官方示例代码

特质对象 &dyn ... 实际是一个指向原对象的指针,并绑定了一个虚表。 由于原对象的类型是固定的,虽然你可以尝试访问和修改特定字段,却无法自由地增加、减少或重命名字段。

Bevy 为此提供了一些动态类型,可以自由增减字段:

DynamicStruct DynamicTupleStruct DynamicTuple
DynamicList DynamicArray DynamicMap
DynamicSet DynamicEnum DynamicOpaque

DynamicStruct 为例,它包含如下字段:

  1. TypeInfo
  2. 字段名列表,字段值列表( Vec<Box<dyn PartialReflect>>
  3. 字段名->字段索引的映射(类似HashMap<&str, usize>

它们的类型操作特质(比如 Struct)的函数实现不再是访问实际字段,而是指向了这些容器中的元素。

这样的好处是你可以自由的增加想要的字段和删除不需要的字段,但它们是独立的类型,具有固定的 TypeId,无法通过 dyn Reflect 向下转换成其他类型,即使内部数据能够匹配。

回顾之前的内容,我们提到 PartialReflectapply 方法能够将另一个 dyn PartialReflect 逐字段赋值给自身。它注重内容而非 TypeId,因此你可以先创建一个自定义类型的对象,然后使用 apply 方法用动态类型对象的内容填充自定义对象。

这在序列化与反序列化中非常有用,比如你可以读取 GLFT 数据生成动态类型对象,然后使用它填充自定义类型(比如 Mesh、Scene)。

注册器与 TypeData

通过上面的内容,我们已经能够通过 &dyn Reflect 操控各种不同的对象,在运行时通过输入字符串访问特定字段。

注册器主要用于将字符串形式的类型名与具体的类型信息一一对应,以便运行时查询。 而 TypeData 则是解决函数调用的问题,下面就会提到。

参考官方示例代码

1. TypeRegistry

Bevy 的反射库提供了一个 TypeRegistry,它用于存储类型信息,也就是 TypeInfo

它的内容如下:

pub struct TypeRegistry {
    registrations: TypeIdMap<TypeRegistration>,
    short_path_to_id: HashMap<&'static str, TypeId>,
    type_path_to_id: HashMap<&'static str, TypeId>,
    ambiguous_names: HashSet<&'static str>,
}

它存储了类型名到 TypeId 的映射和 TypeIdTypeRegistration的映射 ,后者的定义如下:

pub struct TypeRegistration {
    data: TypeIdMap<Box<dyn TypeData>>,
    type_info: &'static TypeInfo,
}

这里的 type_info 就是自身的类型名定义信息。 奇怪的是 TypeIdMap<Box<dyn TypeData>> ,用其他类型的 TypeId 映射到了 TypeData ,什么是 TypeData

2. TypeData

思考这样一个问题,你有类型 T 并通过宏为其实现了反射功能,现在可以通过 &dyn Reflect 操作它。 仅字段修改是不够的,你还为 T 实现了一个 Print 特质、内涵 print 函数,可是怎么通过 &dyn Reflect 调用它?

即使底层类型就是 T 也没法通过 &dyn Reflect 对象调用 print 方法,一种可行的思路是将 &dyn Reflect 转换回 &T 再转换成 &dyn Print ,而 TypeData 做的就是这件事。

你只需要为 Print 特质加上 #[reflect_trait] 宏(或者为 T 类型加上 #[reflect(Print)] 宏),Bevy 会为你自动生成一个 ReflectPrint 类型,它内部只有几个函数指针:

struct ReflectPrint {
    get_func: fn(&dyn Reflect) -> Option<&dyn Print>,
    get_mut_func: fn(&mut dyn Reflect) -> Option<&mut dyn Print>,
    get_boxed_func: fn(Box<dyn Reflect>) -> Result<Box<dyn Print>, Box<dyn Reflect>>,
}

宏会为此类型实现 TypeDataFrom<T> 特质,后者仅提供一个 from_type() 函数用于填充这三个函数指针。

这些函数指针的内容并不复杂,调用 &dyn Reflect 的向下转换函数转换成 &T ,然后在转换回 &dyn Print

使用方式请参考官方示例代码

3. AppTypeRegistry

AppTypeRegistry 是反射系统为 ECS 添加的一个全局资源,内部是一个 Arc<RwLock<TypeRegistry>>,你可以线程安全地获取内部类型注册器并使用。

特殊的一点是,使用 #[derive(Reflect)] 标记的类会自动注册到此资源中,这里使用了一些“黑魔法”。

自动注册-参考官方示例文档

反射函数

除了简单的类型反射,Bevy 还实现了一套高级的函数动态反射系统,这需要启用 reflect_functions 特性。

参考官方示例代码

// TODO

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇