UE4中的反射之二:运行阶段
环境
引擎版本: github 4.20
简介
在上一篇文章里主要是针对 UHT 生成的 .generated.h 来分析反射相关的代码展开参与编译的过程。这篇文件要通过 UHT 生成的另外一个文件 .gen.cpp 来分析运行过程中的反射信息的收集使用。使用的例子还是上篇文章中的 AMyActor
类
在上篇文章中,分析反射代码的入口是 GENERATED_BODY
宏。本篇文章也一样有个重点的入口,那就是 UHT 在 .gen.cpp 中生成的静态全局变量。我们都知道在 C++ 中,全局变量的初始化是先于 main
函数进行的,所以就可以在这个全局变量的构造函数中进行反射信息的注册,最终保存到 UClass
的实例中,供其它地方的代码使用。
IMPLEMENT_CLASS()
在 .gen.cpp 的最后,有一行代码 IMPLEMENT_CLASS(AMyActor, 1438139441);
这个宏有两个参数,第一个是类名,第二个是类的 CRC 值(主要是用于热加载的,可以先忽略),这个宏的定义如下
// Register a class at startup time.
#define IMPLEMENT_CLASS(TClass, TClassCrc) \
static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof(TClass), TClassCrc); \
UClass* TClass::GetPrivateStaticClass() \
{ \
static UClass* PrivateStaticClass = NULL; \
if (!PrivateStaticClass) \
{ \
/* this could be handled with templates, but we want it external to avoid code bloat */ \
GetPrivateStaticClassBody( \
StaticPackage(), \
(TCHAR*)TEXT(#TClass) + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0), \
PrivateStaticClass, \
StaticRegisterNatives##TClass, \
sizeof(TClass), \
(EClassFlags)TClass::StaticClassFlags, \
TClass::StaticClassCastFlags(), \
TClass::StaticConfigName(), \
(UClass::ClassConstructorType)InternalConstructor<TClass>, \
(UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>, \
&TClass::AddReferencedObjects, \
&TClass::Super::StaticClass, \
&TClass::WithinClass::StaticClass \
); \
} \
return PrivateStaticClass; \
}
这个宏里有两个重点,第一个是定义了一个静态全局变量 TClassCompiledInDefer<AMyActor> AutoInitializeAMyActor
。第二个是实现了 GetPrivateStaticClass
,这个函数在上一篇中也有提到,DECLARE_CLASS
宏的定义里声明了 GetPrivateStaticClass
的原型,并且有一个 GetClass
静态成员函数,调用了它。这个函数的实现就是在这里定义了。
AMyActor::GetPrivateStaticClass
先来看看这个函数的实现(因为 AutoInitializeAMyActor
这个全局变量最终会调用到这里),这个函数的作用就是构造一个 UClass
对象来保存 AMyActor
这个类的反射信息。这个函数里创建了一个局部静态变量 PrivateStaticClass
,在首次调用时进行初始化,之后的调用都是返回首次调用的初始化结果,因为不管 AMyActor
有多少个实例,它们的反射信息都是一样的。
初始化 PrivateStaticClass
是通过调用 GetPrivateStaticClassBody
进行的,这个函数的原型如下
COREUOBJECT_API void GetPrivateStaticClassBody(
const TCHAR* PackageName,
const TCHAR* Name,
UClass*& ReturnClass,
void(*RegisterNativeFunc)(),
uint32 InSize,
EClassFlags InClassFlags,
EClassCastFlags InClassCastFlags,
const TCHAR* InConfigName,
UClass::ClassConstructorType InClassConstructor,
UClass::ClassVTableHelperCtorCallerType InClassVTableHelperCtorCaller,
UClass::ClassAddReferencedObjectsType InClassAddReferencedObjects,
UClass::StaticClassFunctionType InSuperClassFn,
UClass::StaticClassFunctionType InWithinClassFn,
bool bIsDynamic = false);
几个重要参数说明如下
PackageName
: 我们构造的UClass
对象应该放到哪个包里,调用的是 .generated.h 中DECLARE_CLASS
宏里定义的StaticPackage
函数Name
: 类名,这里就是MyActor
,不含前缀。UE4 里的编码规范规定了类需要一个前缀,比如A
代表Actor
,并且会将准备废弃的类或者变量加上DEPRECATED
前缀或后缀,比如DEPRECATED_AOtherActor
,int32 Value_DEPRECATED
,所以这里的实参需要将类名字符串加 1 去掉类的前缀,如果有废弃的标记,还会再加上DEPRECATED_
这个字符串的长度,也就是 11,最终得到真实得类名ReturnClass
: 这是个输出参数,传的实参就是局部静态变量PrivateStaticClass
RegisterNativeFunc
: 一个回调函数,用来向UClass
实例里注册AMyActor
类里可被蓝图调用的exec
版本函数的信息,在这里传的实参是StaticRegisterNativesAMyActor
,这个函数定义在 UHT 生成的MyActor.gen.cpp
里,内如如下void AMyActor::StaticRegisterNativesAMyActor() { UClass* Class = AMyActor::StaticClass(); static const FNameNativePtrPair Funcs[] = { { "MyActorBlueprintCallable", &AMyActor::execMyActorBlueprintCallable }, { "MyActorBlueprintGetter", &AMyActor::execMyActorBlueprintGetter }, { "MyActorBlueprintNativeEvent", &AMyActor::execMyActorBlueprintNativeEvent }, { "MyActorBlueprintPure", &AMyActor::execMyActorBlueprintPure }, { "MyActorBlueprintSetter", &AMyActor::execMyActorBlueprintSetter }, }; FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, ARRAY_COUNT(Funcs)); }
InSize
: 类大小,这里实参传的是sizeof(AMyActor)
InClassConstructor
: 类的默认构造函数,这里实参传的是一个模板函数,内部调用的就是AMyActor::__DefaultConstructor
,这个__DefaultConstructor
在上篇文章里也提到了,就是TestCall_Source_TestCall_MyActor_h_12_STANDARD_CONSTRUCTORS
或者TestCall_Source_TestCall_MyActor_h_12_ENHANCED_CONSTRUCTORS
这个宏里定义的,内部使用 placement new 调用了真正的构造函数,一个是带参的,一个是不带参的默认构造函数InClassAddReferencedObjects
: 这个是和 GC 相关的,这里不展开说了InSuperClassFn
: 直接父类的StaticClass
函数指针
GetPrivateStaticClassBody
的内部实现很清晰,如下
- 调用
GUObjectAllocator.AllocateUObject
来给UClass
或者UDynamicClass
对象分配内存 - 调用
placement new
在前面分配出来的内存上调用UClass
或者UDynamicClass
的构造函数来初始化实例 - 调用
InitializePrivateStaticClass
就是将这个UClass
实例加入到GUObjectArray
并且设置为RootSet
,防止被 GC。(部分代码没看懂) - 调用
RegisterNativeFunc
,在这里就是StaticRegisterNativesAMyActor
,向构造出来的的UClass
或者UDynamicClass
对象里注册AMyActor
类里可被蓝图调用的exec
版本函数的信息
TClassCompiledInDefer
这个模板类的实现很简单,但是作用很重要,在构造函数里进行类名和类大小的初始化,然后调用了 UClassCompiledInDefer
全局函数,这个函数里大部分代码都是和热加载相关的处理,可以先忽略,最重要的是最后一句
GetDeferredClassRegistration().Add(ClassInfo);
将 TClassCompiledInDefer<AMyActor>
这个全局变量加入到 DeferredClassRegistration
列表里,在 TClassCompiledInDefer
这个模板类里,还有两个虚函数可以重载,其中最重要的是 Register
这个函数,这个函数的实现是这样的
virtual UClass* Register() const override
{
LLM_SCOPE(ELLMTag::UObject);
return TClass::StaticClass();
}
在 Register
里调用了 StaticClass
,所以从这里就可以看出来,引擎会在启动后的某个时机,遍历 DeferredClassRegistration
数组,对每个对象调用 Register
函数,调用对应的 StaticClass
,再调用 GetPrivateStaticClass
,这就是这个函数被第一次调用的时候,然后构造一个 UClass
对象。所以在这个时候,我们得到了所有类对应的反射信息类对象,但是这些反射对象还没有完全填充好所有的反射信息,只填好了可以被蓝图调用的 C++ exec
版本函数信息,其他信息需要接下来的那个全局变量负责填充
FCompiledInDefer Z_CompiledInDefer_UClass_AMyActor
上面分析完了 MyActor.gen.cpp
里的 IMPLEMENT_CLASS
宏,紧接着这行代码下面一行,还定义了另外一个全局变量
static FCompiledInDefer Z_CompiledInDefer_UClass_AMyActor(Z_Construct_UClass_AMyActor, &AMyActor::StaticClass, TEXT("/Script/TestCall"), TEXT("AMyActor"), false, nullptr, nullptr, nullptr);
FCompiledInDefer
的实现如下
struct FCompiledInDefer
{
FCompiledInDefer(class UClass *(*InRegister)(), class UClass *(*InStaticClass)(), const TCHAR* PackageName, const TCHAR* Name, bool bDynamic, const TCHAR* DynamicPackageName = nullptr, const TCHAR* DynamicPathName = nullptr, void (*InInitSearchableValues)(TMap<FName, FName>&) = nullptr)
{
if (bDynamic)
{
GetConvertedDynamicPackageNameToTypeName().Add(FName(DynamicPackageName), FName(Name));
}
UObjectCompiledInDefer(InRegister, InStaticClass, Name, PackageName, bDynamic, DynamicPathName, InInitSearchableValues);
}
};
构造函数比较重要的是前两个参数,第一个是一个回调函数指针,这里实参传的是 Z_Construct_UClass_AMyActor
,第二个是类的 StaticClass
函数指针,这里实参传的是 &AMyActor::StaticClass
,在构造函数里调用了另外一个全局函数 UObjectCompiledInDefer
,这个函数里做的事情如下,如果不是一个动态类的话那很简单,直接将 InRegister
回调函数指针放入 DeferredCompiledInRegistration
数组中,供后面统一调用。如果是个动态类的话,就填充好 FDynamicClassStaticData
结构,然后放入 DynamicClassMap
数组中,在后面统一调用。
回过头来看看 Z_Construct_UClass_AMyActor
这个回调里做的事情,其实就是调用 UE4CodeGen_Private::ConstructUClass
函数将 UHT 自动生成好的 UE4CodeGen_Private::FClassParams
结构体填充到 UClass
对象中,这个结构体中重要的有两个
-
FunctionLinkArray
,这个数组里填的是每一个被UFUNCTION
标记的函数对应的UFunction
创建函数回调和对应的函数名称,在这里传的实参是const FClassFunctionLinkInfo Z_Construct_UClass_AMyActor_Statics::FuncInfo[] = { { &Z_Construct_UFunction_AMyActor_MyActorBlueprintCallable, "MyActorBlueprintCallable" }, // 403128632 { &Z_Construct_UFunction_AMyActor_MyActorBlueprintGetter, "MyActorBlueprintGetter" }, // 1207006867 { &Z_Construct_UFunction_AMyActor_MyActorBlueprintImplementableEvent, "MyActorBlueprintImplementableEvent" }, // 2944526560 { &Z_Construct_UFunction_AMyActor_MyActorBlueprintNativeEvent, "MyActorBlueprintNativeEvent" }, // 140128359 { &Z_Construct_UFunction_AMyActor_MyActorBlueprintPure, "MyActorBlueprintPure" }, // 4186474183 { &Z_Construct_UFunction_AMyActor_MyActorBlueprintSetter, "MyActorBlueprintSetter" }, // 3614515892 };
这些
Z_Construct_UFunction_AMyActor_*
函数被调用后,内部调用UE4CodeGen_Private::ConstructUFunction
来创建一个UFunction
对象并返回 -
PropertyArray
,这个数组里填的是所有的被UPROPERTY
标记的属性,每个属性 UHT 都自动填好了类型,名称,偏移等信息,如下const UE4CodeGen_Private::FUnsizedIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_MyActorProperty = { UE4CodeGen_Private::EPropertyClass::Int, "MyActorProperty", RF_Public|RF_Transient|RF_MarkAsNative, (EPropertyFlags)0x0010000000000000, 1, nullptr, STRUCT_OFFSET(AMyActor, MyActorProperty), METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_MyActorProperty_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_MyActorProperty_MetaData)) };
终上所述,运行时的反射信息收集简单来说就一句话:MyActor.gen.cpp 里定义了两个全局变量,一个负责构造 UClass
对象,另一个负责往这个构造好的对象中填充 AMyActor
类的反射信息
引擎运行时初始化流程
UE4 引擎有个最核心最底层的模块,就是 CoreUObject
,在这个模块的 StartupModule
中,调用了 UClassRegisterAllCompiledInClasses
全局函数,在这个函数里遍历 DeferredClassRegistration
数组调用 Register
函数进行 UClass
的构造,然后清空 DeferredClassRegistration
数组
还有个地方会调用,就是在引擎的 FEngineLoop::PreInit
函数里,在所有的模块都加载完成后,包括自己的游戏模块,依赖的插件等等,调用 ProcessNewlyLoadedUObjects
函数,在这个函数里调用 UClassRegisterAllCompiledInClasses
处理上一次调用到现在加入到 DeferredClassRegistration
中的对象。在 ProcessNewlyLoadedUObjects
还对类和结构体的反射信息进行填充,就是遍历调用第二个全局变量注册的回调函数,并且创建 CDO(Class Default Object) 对象,完整流程图如下