在一塊兒!在一塊兒!c++
前文中咱們闡述了類型系統構建的第一個階段:生成。UHT分析源碼的宏標記並生成了包含程序元信息的代碼,繼而編譯進程序,在程序啓動的時候,開始啓動類型系統的後續構建階段。而本文咱們將介紹類型信息的收集階段。c#
另外一種經常使用的C++經常使用的設計模式:Static Auto Register。典型的,當你想要在程序啓動後往一個容器裏註冊一些對象,或者簿記一些信息的時候,一種直接的方式是在程序啓動後手動的一個個調用註冊函數:設計模式
#include "ClassA.h" #include "ClassB.h" int main() { ClassFactory::Get().Register<ClassA>(); ClassFactory::Get().Register<ClassB>(); [...] }
這種方式的缺點是你必須手動的一個include以後再手動的一個個註冊,當要繼續添加註冊的項時,只能再手動的依次序在該文件里加上一條條目,可維護性較差。
因此根據C++ static對象會在main函數以前初始化的特性,能夠設計出一種static自動註冊模式,新增長註冊條目的時候,只要Include進相應的類.h.cpp文件,就能夠自動在程序啓動main函數前自動執行一些操做。簡化的代碼大概以下:安全
//StaticAutoRegister.h template<typename TClass> struct StaticAutoRegister { StaticAutoRegister() { Register(TClass::StaticClass()); } }; //MyClass.h class MyClass { //[...] }; //MyClass.cpp #include "StaticAutoRegister.h" const static StaticAutoRegister<MyClass> AutoRegister;
這樣,在程序啓動的時候就會執行Register(MyClass),把由於新添加類而產生的改變行爲限制在了新文件自己,對於一些順序無關的註冊行爲這種模式尤其合適。利用這個static初始化特性,也有不少個變種,好比你能夠把StaticAutoRegister聲明進MyClass的一個靜態成員變量也能夠。不過注意的是,這種模式只能在獨立的地址空間纔能有效,若是該文件被靜態連接且沒有被引用到的話則極可能會繞過static的初始化。不過UE由於都是dll動態連接,且沒有出現靜態lib再引用Lib,而後又不引用文件的狀況出現,因此避免了該問題。或者你也能夠找個地方強制的去include一下來觸發static初始化。微信
而UE裏一樣是採用這種模式:數據結構
template <typename TClass> struct TClassCompiledInDefer : public FFieldCompiledInInfo { TClassCompiledInDefer(const TCHAR* InName, SIZE_T InClassSize, uint32 InCrc) : FFieldCompiledInInfo(InClassSize, InCrc) { UClassCompiledInDefer(this, InName, InClassSize, InCrc); } virtual UClass* Register() const override { return TClass::StaticClass(); } }; static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof(TClass), TClassCrc);
或者多線程
struct FCompiledInDefer { FCompiledInDefer(class UClass *(*InRegister)(), class UClass *(*InStaticClass)(), 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, bDynamic, DynamicPathName, InInitSearchableValues); } }; static FCompiledInDefer Z_CompiledInDefer_UClass_UMyClass(Z_Construct_UClass_UMyClass, &UMyClass::StaticClass, TEXT("UMyClass"), false, nullptr, nullptr, nullptr);
都是對該模式的應用,把static變量聲明再用宏包裝一層,就能夠實現一個簡單的自動註冊流程了。ide
在上文裏,咱們詳細介紹了Class、Struct、Enum、Interface的代碼生成的信息,顯然的,生成的就是爲了拿過來用的。可是在用以前,咱們就還得辛苦一番,把散亂分佈在各個.h.cpp文件裏的元數據都收集到咱們想要的數據結構裏保存,以便下一個階段的使用。函數
這裏回顧一下,爲了讓新建立的類不修改既有的代碼,因此咱們選擇了去中心化的爲每一個新的類生成它本身的cpp生成文件——上文裏已經分別介紹每一個cpp文件的內容。可是這樣咱們就接着迎來了一個新問題:這些cpp文件裏的元數據散亂在各個模塊dll裏,咱們須要用一種方法從新歸攏這些數據,這就是咱們在一開頭就提到的C++ Static自動註冊模式了。經過這種模式,每一個cpp文件裏的static對象在程序一開始的時候就會所有有機會去作一些事情,包括信息的收集工做。學習
UE4裏也是如此,在程序啓動的時候,UE利用了Static自動註冊模式把全部類的信息都一一登記一遍。而緊接着另外一個就是順序問題了,這麼多類,誰先誰後,互相如果有依賴該怎麼解決。衆所周知,UE是以Module來組織引擎結構的(關於Module的細節會在之後章節敘述),一個個Module能夠經過腳本配置來選擇性的編譯加載。在遊戲引擎衆多的模塊中,玩家本身的Game模塊是處於比較高級的層次的,都是依賴於引擎其餘更基礎底層的模塊,而這些模塊中,最最底層的就是Core模塊(C++的基礎庫),接着就是CoreUObject,正是實現Object類型系統的模塊!所以在類型系統註冊的過程當中,不止要註冊玩家的Game模塊,同時也要註冊CoreUObject自己的一些支持類。
不少人可能會擔憂這麼多模塊的靜態初始化的順序正確性如何保證,在c++標準裏,不一樣編譯單元的全局靜態變量的初始化順序並無明確規定,所以實現上徹底由編譯器本身決定。該問題最好的解決方法是儘量的避免這種狀況,在設計上就讓各個變量不互相引用依賴,同時也採用一些二次檢測的方式避免重複註冊,或者觸發一個強制引用來保證前置對象已經被初始化完成。目前在MSVC平臺上是先註冊玩家的Game模塊,接着是CoreUObject,接着再其餘,不過這其實無所謂的,只要保證不依賴順序而結果正確,順序就並不重要了。
在講完了收集的必要性和順序問題的解決以後,咱們再來分別的看各個類別的結構的信息的收集。依然是按照上文生成的順序,從Class(Interface同理)開始,而後是Enum,接着Struct。接着請讀者朋友們對照着上文的生成代碼來理解。
對照着上文裏的Hello.generated.cpp展開,咱們注意到裏面有:
static TClassCompiledInDefer<UMyClass> AutoInitializeUMyClass(TEXT("UMyClass"), sizeof(UMyClass), 899540749); //…… static FCompiledInDefer Z_CompiledInDefer_UClass_UMyClass(Z_Construct_UClass_UMyClass, &UMyClass::StaticClass, TEXT("UMyClass"), false, nullptr, nullptr, nullptr);
再一次找到其定義:
//Specialized version of the deferred class registration structure. template <typename TClass> struct TClassCompiledInDefer : public FFieldCompiledInInfo { TClassCompiledInDefer(const TCHAR* InName, SIZE_T InClassSize, uint32 InCrc) : FFieldCompiledInInfo(InClassSize, InCrc) { UClassCompiledInDefer(this, InName, InClassSize, InCrc); //收集信息 } virtual UClass* Register() const override { return TClass::StaticClass(); } }; //Stashes the singleton function that builds a compiled in class. Later, this is executed. struct FCompiledInDefer { FCompiledInDefer(class UClass *(*InRegister)(), class UClass *(*InStaticClass)(), 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, bDynamic, DynamicPathName, InInitSearchableValues);//收集信息 } };
能夠見到前者調用了UClassCompiledInDefer來收集類名字,類大小,CRC信息,並把本身的指針保存進來以便後續調用Register方法。而UObjectCompiledInDefer(如今暫時不考慮動態類)最重要的收集的信息就是第一個用於構造UClass*對象的函數指針回調。
再往下咱們會發現這兩者其實都只是在一個靜態Array裏添加信息記錄:
void UClassCompiledInDefer(FFieldCompiledInInfo* ClassInfo, const TCHAR* Name, SIZE_T ClassSize, uint32 Crc) { //... // We will either create a new class or update the static class pointer of the existing one GetDeferredClassRegistration().Add(ClassInfo); //static TArray<FFieldCompiledInInfo*> DeferredClassRegistration; } void UObjectCompiledInDefer(UClass *(*InRegister)(), UClass *(*InStaticClass)(), const TCHAR* Name, bool bDynamic, const TCHAR* DynamicPathName, void (*InInitSearchableValues)(TMap<FName, FName>&)) { //... GetDeferredCompiledInRegistration().Add(InRegister); //static TArray<class UClass *(*)()> DeferredCompiledInRegistration; }
而在整個引擎裏會觸發此Class的信息收集的有UCLASS、UINTERFACE、IMPLEMENT_INTRINSIC_CLASS、IMPLEMENT_CORE_INTRINSIC_CLASS,其中UCLASS和UINTERFACE咱們上文已經見識過了,而IMPLEMENT_INTRINSIC_CLASS是用於在代碼中包裝UModel,IMPLEMENT_CORE_INTRINSIC_CLASS是用於包裝UField、UClass等引擎內建的類,後二者內部也都調用了IMPLEMENT_CLASS來實現功能。
流程圖以下:
思考:爲什麼須要TClassCompiledInDefer和FCompiledInDefer兩個靜態初始化來登記?
咱們也觀察到了這兩者是一一對應的,問題是爲什麼須要兩個靜態對象來分別收集,爲什麼不合二爲一?關鍵在於咱們首先要明白它們兩者的不一樣之處,前者的目的主要是爲後續提供一個TClass::StaticClass的Register方法(其會觸發GetPrivateStaticClassBody的調用,進而建立出UClass對象),然後者的目的是在其UClass身上繼續調用構造函數,初始化屬性和函數等一些註冊操做。咱們能夠簡單理解爲就像是C++中new對象的兩個步驟,首先分配內存,繼而在該內存上構造對象。咱們在後續的註冊章節裏還會繼續討論到這個問題。
思考:爲什麼須要延遲註冊而不是直接在static回調裏執行?
不少人可能會問,爲何static回調裏都是先把信息註冊進array結構裏,並無什麼其餘操做,爲什麼不直接把後續的操做直接在回調裏調用了,這樣結構反而簡單些。是這樣沒錯,可是同時咱們也考慮到一個問題,UE4裏大概1500多個類,若是都在static初始化階段進行1500多個類的收集註冊操做,那麼main函數必須得等好一下子才能開始執行。表現上就是用戶雙擊了程序,沒反應,過了好一下子,窗口才打開。所以static初始化回調裏儘可能少的作事情,就是爲了儘快的加快程序啓動的速度。等窗口顯示出來了,array結構裏數據已經有了,咱們就能夠施展手腳,多線程也好,延遲也好,均可以大大改善程序運行的體驗。
依舊是上文裏的對照代碼,UENUM會生成:
static FCompiledInDeferEnum Z_CompiledInDeferEnum_UEnum_EMyEnum(EMyEnum_StaticEnum, TEXT("/Script/Hello"), TEXT("EMyEnum"), false, nullptr, nullptr); //其定義: struct FCompiledInDeferEnum { FCompiledInDeferEnum(class UEnum *(*InRegister)(), const TCHAR* PackageName, const TCHAR* Name, bool bDynamic, const TCHAR* DynamicPackageName, const TCHAR* DynamicPathName) { if (bDynamic) { GetConvertedDynamicPackageNameToTypeName().Add(FName(DynamicPackageName), FName(Name)); } UObjectCompiledInDeferEnum(InRegister, PackageName, DynamicPathName, bDynamic); // static TArray<FPendingEnumRegistrant> DeferredCompiledInRegistration; } };
在static階段會向內存註冊一個構造UEnum的函數指針用於回調:
注意到這裏並不須要像UClassCompiledInDefer同樣先生成一個UClass,由於UEnum並非一個Class,並無Class那麼多功能集合,因此就比較簡單一些。
對於Struct,咱們先來看上篇裏生成的代碼:
static FCompiledInDeferStruct Z_CompiledInDeferStruct_UScriptStruct_FMyStruct(FMyStruct::StaticStruct, TEXT("/Script/Hello"), TEXT("MyStruct"), false, nullptr, nullptr); //延遲註冊 static struct FScriptStruct_Hello_StaticRegisterNativesFMyStruct { FScriptStruct_Hello_StaticRegisterNativesFMyStruct() { UScriptStruct::DeferCppStructOps(FName(TEXT("MyStruct")),new UScriptStruct::TCppStructOps<FMyStruct>); } } ScriptStruct_Hello_StaticRegisterNativesFMyStruct; //static註冊
一樣是兩個static對象,前者FCompiledInDeferStruct繼續向array結構裏登記函數指針,後者有點特殊,在一個結構名和對象的Map映射裏登記「Struct相應的C++操做類」(後續解釋)。
struct FCompiledInDeferStruct { FCompiledInDeferStruct(class UScriptStruct *(*InRegister)(), const TCHAR* PackageName, const TCHAR* Name, bool bDynamic, const TCHAR* DynamicPackageName, const TCHAR* DynamicPathName) { if (bDynamic) { GetConvertedDynamicPackageNameToTypeName().Add(FName(DynamicPackageName), FName(Name)); } UObjectCompiledInDeferStruct(InRegister, PackageName, DynamicPathName, bDynamic);// static TArray<FPendingStructRegistrant> DeferredCompiledInRegistration; } }; void UScriptStruct::DeferCppStructOps(FName Target, ICppStructOps* InCppStructOps) { TMap<FName,UScriptStruct::ICppStructOps*>& DeferredStructOps = GetDeferredCppStructOps(); if (UScriptStruct::ICppStructOps* ExistingOps = DeferredStructOps.FindRef(Target)) { #if WITH_HOT_RELOAD if (!GIsHotReload) // in hot reload, we will just leak these...they may be in use. #endif { check(ExistingOps != InCppStructOps); // if it was equal, then we would be re-adding a now stale pointer to the map delete ExistingOps; } } DeferredStructOps.Add(Target,InCppStructOps); }
另外的,蒐羅引擎裏的代碼,咱們還會發現對於UE4裏內建的結構,好比說Vector,其IMPLEMENT_STRUCT(Vector)也會相應的觸發DeferCppStructOps的調用。
這裏的Struct也和Enum同理,由於並非一個Class,因此並不須要比較繁瑣的兩步構造,憑着FPendingStructRegistrant就能夠後續一步構造出UScriptStruct對象;對於內建的類型(如Vector),因其徹底不是「Script」的類型,因此就不須要UScriptStruct的構建,那麼其如何像BP暴露,咱們後續再詳細介紹。
還有一點注意的是UStruct類型會配套一個ICppStructOps接口對象來管理C++struct對象的構造和析構工做,其用意就在於若是對於一塊已經擦除了類型的內存數據,咱們怎麼能在其上正確的構造結構對象數據或者析構。這個時候,若是咱們可以獲得一個統一的ICppStructOps指針指向類型安全的TCppStructOps<CPPSTRUCT>對象,就可以經過接口函數動態、多態、類型安全的執行構造和析構工做。
在介紹完了Class、Enum、Struct以後,咱們還遺忘了一些引擎內建的函數的信息收集。咱們在前文中並無介紹到這一點是由於UE已經提供了咱們一個BlueprintFunctionLibrary的類來註冊全局函數。而一些引擎內部定義出來的函數,也是散亂分佈在各處,也是須要收集起來的。
主要有這兩類:
IMPLEMENT_CAST_FUNCTION( UObject, CST_ObjectToBool, execObjectToBool ); IMPLEMENT_CAST_FUNCTION( UObject, CST_InterfaceToBool, execInterfaceToBool ); IMPLEMENT_CAST_FUNCTION( UObject, CST_ObjectToInterface, execObjectToInterface );
IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction); IMPLEMENT_VM_FUNCTION( EX_True, execTrue ); //……
而繼而查其定義:
#define IMPLEMENT_FUNCTION(cls,func) \ static FNativeFunctionRegistrar cls##func##Registar(cls::StaticClass(),#func,(Native)&cls::func); #define IMPLEMENT_CAST_FUNCTION(cls, CastIndex, func) \ IMPLEMENT_FUNCTION(cls, func); \ static uint8 cls##func##CastTemp = GRegisterCast( CastIndex, (Native)&cls::func ); #define IMPLEMENT_VM_FUNCTION(BytecodeIndex, func) \ IMPLEMENT_FUNCTION(UObject, func) \ static uint8 UObject##func##BytecodeTemp = GRegisterNative( BytecodeIndex, (Native)&UObject::func ); /* A struct that maps a string name to a native function */ struct FNativeFunctionRegistrar { FNativeFunctionRegistrar(class UClass* Class, const ANSICHAR* InName, Native InPointer) { RegisterFunction(Class, InName, InPointer); } static COREUOBJECT_API void RegisterFunction(class UClass* Class, const ANSICHAR* InName, Native InPointer); // overload for types generated from blueprints, which can have unicode names: static COREUOBJECT_API void RegisterFunction(class UClass* Class, const WIDECHAR* InName, Native InPointer); };
也能夠發現有3個static對象收集到這些函數的信息並登記到相應的結構中去,流程圖爲:
其中FNativeFunctionRegistrar用於向UClass裏添加Native函數(區別於藍圖裏定義的函數),另外一個方面,在UClass的RegisterNativeFunc相關函數裏,也會把相應的Class內定義的函數添加到UClass內部的函數表裏去。
若是讀者朋友們本身剖析源碼,還會有一個疑惑,做爲Object系統的根類,它是怎麼在最開始的時候觸發相應UClass的生成呢?答案在最開始的IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction)調用上,其內部會緊接着觸發UObject::StaticClass()的調用,做爲最開始的調用,檢測到UClass並未生成,因而接着會轉發到GetPrivateStaticClassBody中去生成一個UClass*。
因篇幅有限,本文緊接着上文,討論了代碼生成的信息是如何一步步收集到內存裏的數據結構裏去的,UE4利用了C++的static對象初始化模式,在程序最初啓動的時候,main以前,就收集到了全部的類型元數據、函數指針回調、名字、CRC等信息。到目前,思路仍是很清晰的,爲每個類代碼生成本身的cpp文件(不需中心化的修改既有代碼),進而在其生成的每一個cpp文件裏用static模式蒐羅一遍信息以便後續的使用。這也算是C++本身實現類型系統流行套路之一吧。
在下一個階段——註冊,咱們將討論UE4接下來是如何消費利用這些信息的。
UE4.15.1
知乎專欄:InsideUE4
UE4深刻學習QQ羣:456247757(非新手入門羣,請先學習完官方文檔和視頻教程)
微信公衆號:aboutue,關於UE的一切新聞資訊、技巧問答、文章發佈,歡迎關注。
我的原創,未經受權,謝絕轉載!