環境

引擎版本: 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.hDECLARE_CLASS 宏里定義的 StaticPackage 函數
  • Name: 類名,這裡就是 MyActor,不含前綴。UE4 里的編碼規範規定了類需要一個前綴,比如 A 代表 Actor,並且會將準備廢棄的類或者變數加上 DEPRECATED 前綴或後綴,比如 DEPRECATED_AOtherActorint32 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) 對象,完整流程圖如下


推薦閱讀:
查看原文 >>
相关文章