反射机制

反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取的信息以及动态调用对象的方法的功能称为反射机制

通俗解释:反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

反射在Java和C#等语言中比较常见,概况的说,反射数据描述了类在运行时的内容。这些数据所存储的信息包括类的名称、类中的数据成员、每个数据成员的类型、每个成员位于对象内存映像的偏移(offset),此外,它也包含类的所有成员函数信息。

C++ 本身不支持反射,UE4 在 C++ 基础上搭建了自己的一套反射机制。具体来看,对于一个类(UClass),我们可以获得这个类的所有属性和方法,而对于一个类对象(UObject),我们可以调用它所拥有的方法和属性,前提是这些属性和方法被纳入到UE4的反射系统。于是,UE4 使用反射可以实现序列化、editor 的 details panel、垃圾回收、网络复制、蓝图/C++通信和相互调用等功能。

反射系统是选择加入的,只有主动标记的类型、属性、方法会被反射系统追踪,UnrealHeaderTool(UHT) 会收集这些信息,生成用于支持反射机制的C++代码,然后再编译工程。

UE4 的反射机制

UE4 采用工具生成代码的方式来实现 C++ 的反射。原理是利用特殊的宏来对变量做标记,再对 C++ 代码文件进行语法分析,识别出特殊的宏,提取出对应的数据,然后生成代码。在初始化时运行生成的代码,将收集到的数据保存。​反射系统是可以选择加入的,你需要给暴露给反射系统的类型或属性添加注解,这样 Unreal Header Tool (UHT) 就会在编译工程的时候利用那些信息生成特定的代码。

为此,UE4 定义了一系列的宏,来帮助开发者将自定义的字段和函数添加至反射系统。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一定要声明UCLASS
UCLASS()
class MYGAME_API UMyClass : public UObject
{
GENERATED_BODY()
public:
// 定义一个可反射的函数
UFUNCTION(BluprintCallable)
void MyFunc();
private:
// 定义一个可反射的变量
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = "true"))
int MyIntValue;
}

可以使用的宏定义如下:

反射宏名称 作用
UCLASS 告诉UE这个类是一个反射类。类必须派生自UObject
USTRUCT 可以不用派生自UObject。不支持GC,也不能包含函数
UPROPERTY 定义一个反射的变量
UFUNCTION 定义一个反射的函数
UENUM 告诉UE这是一个反射的枚举类。支持enum, enum class, enum namespace
UINTERFACE 定义一个反射接口类,只能包含函数
UMETA 反射的一些元数据定义,可以通过标签定义一些该变量的属性
UPARAM 定义函数的参数属性。主要就是显示名字和Ref属性
UDELEGATE 告诉UE这是一个可反射的delegate(很少用到)

UHT 与 UBT

首先我们要知道,我们写的 UE4 代码不是标准的 C++ 代码,是基于 UE4 源代码层层改装了很多层的,所以,UHT 将 UE4 代码转化成标准的 C++ 代码,而 UBT 负责调用 UHT 来实现这个转化工作的,转化完以后,UBT 调用标准 C++ 代码的编译器来将 UHT 转化后的标准C++ 代码完全编译成二进制文件,整体上看,UHT 是 UBT 的编译流程的一部分。
———— 什么是UBT?

UE 有一组用于自动执行编译虚幻引擎过程的工具,包括 虚幻编译工具(UnrealBuildTool,UBT) 和 虚幻头工具(UnrealHeaderTool,UHT)(以及其他工具)。实现这一套工具的目的是为了简化多平台编译,方便你配置各个平台的参数和编译选项。

  • UnrealBuildTool(UBT,C#):是一个自定义工具,负责管理通过各种编译配置来编译 UE4 源代码的过程。该工具处理所有复杂的项目编译工作,编译 UE4 的逐个模块并处理依赖等。并将项目与引擎关联起来。

  • UnrealHeaderTool (UHT,C++):自定义编译方法,负责处理引擎反射系统编译所必需的信息,是支持 UObject 系统的自定义解析和代码生成工具。

UHT会扫描文件,如果其中有 #include "file.generated.h",就是告知 UHT 该头文件需要被纳入反射系统之中。然后 UHT 会根据相关的宏标记:UCLASS()UFUNCTION()GENERATED_BODY() 来解析头文件中的定义,最终生成 filename.generated.hfilename.gen.cpp 两个文件,包含了用于实现反射的相关代码。其中主要做的工作是将 C++ 类中的真实数据成员和 UClass(用于存储反射数据的类)中的 UPorperty 等数据类型绑定起来。.generated.h 中生成的函数包含了 XXX_Implementation 之类的补全,也包含了用于蓝图中调用 C++ 函数的转换函数,并通过 GENERATED_BODY() 安插到我们编写的类中。

UBT 和 UHT 两个协同工作来生成运行时反射需要的数据。UBT 属性通过扫描头文件,记录任何至少有一个反射类型的头文件的模块。如果其中任意一个头文件从上一次编译起发生了变化,那么 UHT 就会被调用来利用和更新反射数据。UHT 分析头文件,创建一系列反射数据,并且生成包含反射数据的 C++ 代码,还有各种帮助函数。

用生成的 C++ 代码来存储反射数据的一个最大好处就是,它可以保证跟二进制做到同步。你永远也不会加载陈旧或者过时的反射数据,因为它是跟引擎的其它代码同时编译的,并且它会在程序启动的时候使用 C++ 表达式来计算成员偏移等,而不是通过针对特定平台/编译器/优化的组合中进行逆向工程。UHT 作为一个单独的不使用任何生成头文件的程序来构建,因此它也避免了鸡生蛋、蛋生鸡的问题,这个在虚幻3的脚本编译器中一直被诟病。

总结

反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取的信息以及动态调用对象的方法的功能称为反射机制。由于 C++ 本身不支持反射系统,UE4 在 C++ 的基础上搭建了自己的一套反射机制,UE4 使用反射可以实现序列化、editor 的 details panel、垃圾回收、网络复制、蓝图/C++通信和相互调用等功能

UE4 的大致做法为:使用特殊的宏,如 UCLASS()UFUNCTION() 等对类、函数、变量等进行标记,来表明这些对象需要被纳入反射系统中;接着 UnrealHeaderTool(UHT) 会根据相关的宏标记收集这些信息,生成用于支持反射机制的C++代码,并通过 GENERATED_BODY() 安插到我们编写的类中,然后再编译工程。

相关链接

深入研究 UE4 反射系统实现原理(一)
Unreal Property System (Reflection)