《InsideUE4》UObject(六)類型系統代碼生成重構-UE4CodeGen_Private

讀的不如寫的快

引言

在之前的《InsideUE4》UObject(四)類型系統代碼生成和《InsideUE4》UObject(五)類型系統收集章節里,我們介紹了UE4是如何根據我們的代碼和元標記生成反射代碼,並在Main函數調用之前,利用靜態變數的初始化來收集類型的元數據信息。經過了我這麼長時間的拖更……也經過了Epic這麼長時間的版本更替,把UE從4.15.1進化到了4.18.3,自然的,CoreUObject模塊也進行了一些改進。本文就先補上一個關於代碼生成的改進:在UE4.17(20170722)的時候進行的UObjectGlobals.h.cpp重構,優化了代碼生成的內容和組織形式。

舊版本代碼生成

首先來看一下之前的版本的代碼元數據生成:

UEnum的生成:

//測試代碼#pragma once#include "UObject/NoExportTypes.h"#include "MyEnum.generated.h"UENUM(BlueprintType)enum class EMyEnum : uint8{ MY_Dance UMETA(DisplayName = "Dance"), MY_Rain UMETA(DisplayName = "Rain"), MY_Song UMETA(DisplayName = "Song")};//生成代碼節選(Hello.genrated.cpp):ReturnEnum = new(EC_InternalUseOnlyConstructor, Outer, TEXT("EMyEnum"), RF_Public|RF_Transient|RF_MarkAsNative) UEnum(FObjectInitializer());//直接創建該UEnum對象TArray<TPair<FName, uint8>> EnumNames;//設置枚舉里的名字和值EnumNames.Add(TPairInitializer<FName, uint8>(FName(TEXT("EMyEnum::MY_Dance")), 0));EnumNames.Add(TPairInitializer<FName, uint8>(FName(TEXT("EMyEnum::MY_Rain")), 1));EnumNames.Add(TPairInitializer<FName, uint8>(FName(TEXT("EMyEnum::MY_Song")), 2));EnumNames.Add(TPairInitializer<FName, uint8>(FName(TEXT("EMyEnum::MY_MAX")), 3)); //添加一個默認的MAX欄位ReturnEnum->SetEnums(EnumNames, UEnum::ECppForm::EnumClass);ReturnEnum->CppType = TEXT("EMyEnum");#if WITH_METADATA //設置元數據UMetaData* MetaData = ReturnEnum->GetOutermost()->GetMetaData();MetaData->SetValue(ReturnEnum, TEXT("BlueprintType"), TEXT("true"));MetaData->SetValue(ReturnEnum, TEXT("ModuleRelativePath"), TEXT("MyEnum.h"));MetaData->SetValue(ReturnEnum, TEXT("MY_Dance.DisplayName"), TEXT("Dance"));MetaData->SetValue(ReturnEnum, TEXT("MY_Rain.DisplayName"), TEXT("Rain"));MetaData->SetValue(ReturnEnum, TEXT("MY_Song.DisplayName"), TEXT("Song"));#endif

UStruct的生成:

//測試代碼:#pragma once#include "UObject/NoExportTypes.h"#include "MyStruct.generated.h"USTRUCT(BlueprintType)struct HELLO_API FMyStruct{ GENERATED_USTRUCT_BODY() UPROPERTY(BlueprintReadWrite) float Score;};//生成代碼節選(Hello.genrated.cpp):ReturnStruct = new(EC_InternalUseOnlyConstructor, Outer, TEXT("MyStruct"), RF_Public|RF_Transient|RF_MarkAsNative) UScriptStruct(FObjectInitializer(), NULL, new UScriptStruct::TCppStructOps<FMyStruct>, EStructFlags(0x00000201));//直接創建UScriptStruct對象UProperty* NewProp_Score = new(EC_InternalUseOnlyConstructor, ReturnStruct, TEXT("Score"), RF_Public|RF_Transient|RF_MarkAsNative) UFloatProperty(CPP_PROPERTY_BASE(Score, FMyStruct), 0x0010000000000004);//直接關聯相應的Property信息ReturnStruct->StaticLink(); //鏈接#if WITH_METADATA //元數據UMetaData* MetaData = ReturnStruct->GetOutermost()->GetMetaData();MetaData->SetValue(ReturnStruct, TEXT("BlueprintType"), TEXT("true"));MetaData->SetValue(ReturnStruct, TEXT("ModuleRelativePath"), TEXT("MyStruct.h"));MetaData->SetValue(NewProp_Score, TEXT("Category"), TEXT("MyStruct"));MetaData->SetValue(NewProp_Score, TEXT("ModuleRelativePath"), TEXT("MyStruct.h"));#endif

UClass的生成:

//測試代碼:#pragma once#include "UObject/NoExportTypes.h"#include "MyClass.generated.h"UCLASS(BlueprintType)class HELLO_API UMyClass : public UObject{ GENERATED_BODY()public: UPROPERTY(BlueprintReadWrite) float Score;public: UFUNCTION(BlueprintCallable, Category = "Hello") void CallableFunc(); //C++實現,藍圖調用 UFUNCTION(BlueprintNativeEvent, Category = "Hello") void NativeFunc(); //C++實現默認版本,藍圖可重載實現 UFUNCTION(BlueprintImplementableEvent, Category = "Hello") void ImplementableFunc(); //C++不實現,藍圖實現};//生成代碼節選(Hello.genrated.cpp)://添加子欄位OuterClass->LinkChild(Z_Construct_UFunction_UMyClass_CallableFunc());OuterClass->LinkChild(Z_Construct_UFunction_UMyClass_ImplementableFunc());OuterClass->LinkChild(Z_Construct_UFunction_UMyClass_NativeFunc());PRAGMA_DISABLE_DEPRECATION_WARNINGSUProperty* NewProp_Score = new(EC_InternalUseOnlyConstructor, OuterClass, TEXT("Score"), RF_Public|RF_Transient|RF_MarkAsNative) UFloatProperty(CPP_PROPERTY_BASE(Score, UMyClass), 0x0010000000000004);//添加屬性PRAGMA_ENABLE_DEPRECATION_WARNINGS//添加函數名字映射OuterClass->AddFunctionToFunctionMapWithOverriddenName(Z_Construct_UFunction_UMyClass_CallableFunc(), "CallableFunc"); // 774395847OuterClass->AddFunctionToFunctionMapWithOverriddenName(Z_Construct_UFunction_UMyClass_ImplementableFunc(), "ImplementableFunc"); // 615168156OuterClass->AddFunctionToFunctionMapWithOverriddenName(Z_Construct_UFunction_UMyClass_NativeFunc(), "NativeFunc"); // 3085959641OuterClass->StaticLink();#if WITH_METADATA //元數據UMetaData* MetaData = OuterClass->GetOutermost()->GetMetaData();MetaData->SetValue(OuterClass, TEXT("BlueprintType"), TEXT("true"));MetaData->SetValue(OuterClass, TEXT("IncludePath"), TEXT("MyClass.h"));MetaData->SetValue(OuterClass, TEXT("ModuleRelativePath"), TEXT("MyClass.h"));MetaData->SetValue(NewProp_Score, TEXT("Category"), TEXT("MyClass"));MetaData->SetValue(NewProp_Score, TEXT("ModuleRelativePath"), TEXT("MyClass.h"));#endif

可以見到,以往的方式在生成的代碼里有很多的「套路化」的SetValue、Add段落,都是用來添加欄位屬性、函數和元數據的信息。雖然這些代碼也是UHT程序化生成的,不用人手工操作,看起來也只能說是略有瑕疵,但要是從精益求精的角度上來說,缺點有:

  1. 本著DRY(Dont Repeat Yourself)原則,這些模式化的代碼在每一個反射文件里也都會重複N次,增大了代碼的體積。
  2. 代碼文件的膨脹,自然會增加編譯的時間消耗。
  3. 即使是程序生成的代碼,有時也難免要閱讀Debug,大量的模式代碼噪音顯然降低了關鍵代碼的可讀性和課調試性。
  4. UHT的編寫維護,更多的代碼量生成,自然會帶來UHT工具代碼的編寫量增長,增大了編寫維護的成本;代碼越多,Bug越多;UHT要輸出更多的代碼,自然效率會降低,從而導致總編譯時間的消耗增長。

UE4CodeGen_Private

改善方式是顯然易得的,同一件事不要做兩遍。既然到處都是這些膠水代碼,那就把這些代碼封裝成函數;既然到處都散布著這些元數據信息數據,那就把這些數據封裝成結構作為函數的參數。

所以,UE在4.17的時候,在UObjectGlobals.h.cpp里增加了一個UE4CodeGen_Private的命名空間,裡面添加了一些生成函數:

//UObjectGlobals.hnamespace UE4CodeGen_Private{ COREUOBJECT_API void ConstructUFunction(UFunction*& OutFunction, const FFunctionParams& Params); COREUOBJECT_API void ConstructUEnum(UEnum*& OutEnum, const FEnumParams& Params); COREUOBJECT_API void ConstructUScriptStruct(UScriptStruct*& OutStruct, const FStructParams& Params); COREUOBJECT_API void ConstructUPackage(UPackage*& OutPackage, const FPackageParams& Params); COREUOBJECT_API void ConstructUClass(UClass*& OutClass, const FClassParams& Params);}//UObjectGlobals.cppnamespace UE4CodeGen_Private{ void ConstructUProperty(UObject* Outer, const FPropertyParamsBase* const*& PropertyArray, int32& NumProperties); void AddMetaData(UObject* Object, const FMetaDataPairParam* MetaDataArray, int32 NumMetaData);}

函數的名字含義顯而易見,都是用來構造一些元數據結構:UEnum、UFunction、UProperty、UScriptStruct、UClass、UPackage和添加一些元數據(這些結構後續會詳解)。第一個參數都是指針的引用,所以是用來向外構造一個對象用指針返回的;關鍵的是在第二個參數:都是一個個XXXParams參數,用來傳進去信息的。

所以我們繼續查看這些參數信息:

UEnum的Params和生成:

//UObjectGlobals.hnamespace UE4CodeGen_Private{#if WITH_METADATA //只有在編輯器模式下,才保留元數據信息 struct FMetaDataPairParam //元數據對 { //例:MetaData->SetValue(ReturnEnum, TEXT("MY_Song.DisplayName"), TEXT("Song")); const char* NameUTF8; //元數據的鍵值對信息 const char* ValueUTF8; };#endif struct FEnumeratorParam //枚舉項 { const char* NameUTF8; //枚舉項的名字 int64 Value; //枚舉項的值#if WITH_METADATA const FMetaDataPairParam* MetaDataArray; //一個枚舉項依然可以包含多個元數據鍵值對 int32 NumMetaData;#endif }; struct FEnumParams //枚舉參數 { UObject* (*OuterFunc)(); //獲取Outer對象的函數指針回調,用於獲取所屬於的Package EDynamicType DynamicType; //是否動態,一般是非動態的 const char* NameUTF8; //枚舉的名字 EObjectFlags ObjectFlags; //UEnum對象的標誌 FText (*DisplayNameFunc)(int32); //獲取自定義顯示名字的回調,一般是nullptr,就是默認規則生成的名字 uint8 CppForm; /*CppForm指定這個枚舉是怎麼定義的,用來在之後做更細的處理。 enum class ECppForm { Regular, //常規的enum MyEnum{}這樣定義 Namespaced, //MyEnum之外套一層namespace的定義 EnumClass //enum class定義的 }; */ const char* CppTypeUTF8; //C++里的類型名字,一般是等同於NameUTF8的,但有時定義名字和反射的名字可以不一樣 const FEnumeratorParam* EnumeratorParams; //枚舉項數組 int32 NumEnumerators;#if WITH_METADATA const FMetaDataPairParam* MetaDataArray; //元數據數組 int32 NumMetaData;#endif };}//MyEnum.gen.cpp生成代碼:static const UE4CodeGen_Private::FEnumeratorParam Enumerators[] = { //所有的枚舉項 { "MyEnum::MY_Dance", (int64)MyEnum::MY_Dance }, { "MyEnum::MY_Rain", (int64)MyEnum::MY_Rain }, { "MyEnum::MY_Song", (int64)MyEnum::MY_Song }, };#if WITH_METADATAstatic const UE4CodeGen_Private::FMetaDataPairParam Enum_MetaDataParams[] = { //枚舉的元數據 { "BlueprintType", "true" }, { "IsBlueprintBase", "true" }, { "ModuleRelativePath", "MyEnum.h" }, { "MY_Dance.DisplayName", "Dance" }, { "MY_Rain.DisplayName", "Rain" }, { "MY_Song.DisplayName", "Song" },};#endifstatic const UE4CodeGen_Private::FEnumParams EnumParams = { //枚舉的元數據參數信息 (UObject*(*)())Z_Construct_UPackage__Script_Hello, UE4CodeGen_Private::EDynamicType::NotDynamic, "MyEnum", RF_Public|RF_Transient|RF_MarkAsNative, nullptr, (uint8)UEnum::ECppForm::EnumClass, "MyEnum", Enumerators, //枚舉項數組 ARRAY_COUNT(Enumerators), METADATA_PARAMS(Enum_MetaDataParams, ARRAY_COUNT(Enum_MetaDataParams))//枚舉元數據數組};UE4CodeGen_Private::ConstructUEnum(ReturnEnum, EnumParams); //利用枚舉參數構造UEnum*對象到ReturnEnum

先挑最軟的椰子開始捏,枚舉的構造比較簡單,就只是包含枚舉項(字元串-整形),所以只要依次添加進去就可以。元數據對指的就是那些UMETA等宏標記裡面那些的內容,可以在很多地方上使用來添加額外的信息。

UStruct的Params和生成:

因為UStruct里只能包含屬性,所以我們在這裡著重關注屬性信息是怎麼生成的。

//UObjectGlobals.h//PS:為了閱讀方便,與源碼有一定的代碼位置微調,但不影響功能正確性namespace UE4CodeGen_Private{ enum class EPropertyClass //屬性的類型 { Byte, Int8, Int16, Int, Int64, UInt16, UInt32, UInt64, UnsizedInt, UnsizedUInt, Float, Double, Bool, SoftClass, WeakObject, LazyObject, SoftObject, Class, Object, Interface, Name, Str, Array, Map, Set, Struct, Delegate, MulticastDelegate, Text, Enum, }; // This is not a base class but is just a common initial sequence of all of the F*PropertyParams types below. // We dont want to use actual inheritance because we want to construct aggregated compile-time tables of these things. struct FPropertyParamsBase //屬性參數基類 { EPropertyClass Type; //屬性的類型 const char* NameUTF8; //屬性的名字 EObjectFlags ObjectFlags; //屬性生成的UProperty對象標誌,標識這個UProperty對象的特徵,RF_XXX那些宏 uint64 PropertyFlags; //屬性生成的UProperty屬性標誌,標識這個屬性的特徵,CPF_XXX那些宏 int32 ArrayDim; //屬性有可能是個數組,數組的長度,默認是1 const char* RepNotifyFuncUTF8; //屬性的網路複製通知函數名字 }; struct FPropertyParamsBaseWithOffset // : FPropertyParamsBase { EPropertyClass Type; const char* NameUTF8; EObjectFlags ObjectFlags; uint64 PropertyFlags; int32 ArrayDim; const char* RepNotifyFuncUTF8; int32 Offset; //在結構或類中的內存偏移,可以理解為成員變數指針(成員變數指針其實本質上就是從對象內存起始位置的偏移) }; //通用的屬性參數 struct FGenericPropertyParams // : FPropertyParamsBaseWithOffset { EPropertyClass Type; const char* NameUTF8; EObjectFlags ObjectFlags; uint64 PropertyFlags; int32 ArrayDim; const char* RepNotifyFuncUTF8; int32 Offset;#if WITH_METADATA const FMetaDataPairParam* MetaDataArray; int32 NumMetaData;#endif }; //一些普通常用的數值類型就通過這個類型定義別名了 // These property types dont add new any construction parameters to their base property typedef FGenericPropertyParams FInt8PropertyParams; typedef FGenericPropertyParams FInt16PropertyParams; //枚舉類型屬性參數 struct FBytePropertyParams // : FPropertyParamsBaseWithOffset { EPropertyClass Type; const char* NameUTF8; EObjectFlags ObjectFlags; uint64 PropertyFlags; int32 ArrayDim; const char* RepNotifyFuncUTF8; int32 Offset; UEnum* (*EnumFunc)(); //定義的枚舉對象回調#if WITH_METADATA const FMetaDataPairParam* MetaDataArray; int32 NumMetaData;#endif }; //...省略一些定義,可自行去UObjectGlobals.h查看 //對象引用類型屬性參數 struct FObjectPropertyParams // : FPropertyParamsBaseWithOffset { EPropertyClass Type; const char* NameUTF8; EObjectFlags ObjectFlags; uint64 PropertyFlags; int32 ArrayDim; const char* RepNotifyFuncUTF8; int32 Offset; UClass* (*ClassFunc)(); //用於獲取該屬性定義類型的函數指針回調#if WITH_METADATA const FMetaDataPairParam* MetaDataArray; int32 NumMetaData;#endif }; struct FStructParams //結構參數 { UObject* (*OuterFunc)(); //所屬於的Package UScriptStruct* (*SuperFunc)(); //該結構的基類,沒有的話為nullptr void* (*StructOpsFunc)(); // really returns UScriptStruct::ICppStructOps*,結構的構造分配的輔助操作類 const char* NameUTF8; //結構名字 EObjectFlags ObjectFlags; //結構UScriptStruct*的對象特徵 uint32 StructFlags; // EStructFlags該結構的本來特徵 SIZE_T SizeOf; //結構的大小,就是sizeof(FMyStruct),用以後續分配內存時候用 SIZE_T AlignOf;//結構的內存對齊,就是alignof(FMyStruct),用以後續分配內存時候用 const FPropertyParamsBase* const* PropertyArray; //包含的屬性數組 int32 NumProperties; #if WITH_METADATA const FMetaDataPairParam* MetaDataArray; //元數據數組 int32 NumMetaData; #endif };}//MyStruct.gen.cpp生成代碼:#if WITH_METADATAstatic const UE4CodeGen_Private::FMetaDataPairParam Struct_MetaDataParams[] = { //結構的元數據 { "BlueprintType", "true" }, { "ModuleRelativePath", "MyStruct.h" },};#endifauto NewStructOpsLambda = []() -> void* { return (UScriptStruct::ICppStructOps*)new UScriptStruct::TCppStructOps<FMyStruct>(); }; //一個獲取操作類的回調#if WITH_METADATA//屬性的元數據static const UE4CodeGen_Private::FMetaDataPairParam NewProp_Score_MetaData[] = { { "Category", "MyStruct" }, { "ModuleRelativePath", "MyStruct.h" },};#endifstatic const UE4CodeGen_Private::FFloatPropertyParams NewProp_Score = { UE4CodeGen_Private::EPropertyClass::Float, "Score", RF_Public|RF_Transient|RF_MarkAsNative, 0x0010000000000004, 1, nullptr, STRUCT_OFFSET(FMyStruct, Score), METADATA_PARAMS(NewProp_Score_MetaData, ARRAY_COUNT(NewProp_Score_MetaData)) };//Score屬性的信息//屬性的數組static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[] = { (const UE4CodeGen_Private::FPropertyParamsBase*)&NewProp_Score,};//結構的參數信息static const UE4CodeGen_Private::FStructParams ReturnStructParams = { (UObject* (*)())Z_Construct_UPackage__Script_Hello, nullptr, &UE4CodeGen_Private::TNewCppStructOpsWrapper<decltype(NewStructOpsLambda)>::NewCppStructOps, "MyStruct", RF_Public|RF_Transient|RF_MarkAsNative, EStructFlags(0x00000201), sizeof(FMyStruct), alignof(FMyStruct), PropPointers, ARRAY_COUNT(PropPointers), METADATA_PARAMS(Struct_MetaDataParams, ARRAY_COUNT(Struct_MetaDataParams))};UE4CodeGen_Private::ConstructUScriptStruct(ReturnStruct, ReturnStructParams);//構造UScriptStruct*到ReturnStruct里去

代碼比較簡單,上下對照和看看注釋就能大概明白。就是收集一個個屬性的信息整合成數組,然後合併到結構參數里去,最後傳給ConstructUScriptStruct來構造。

思考:FPropertyParamsBaseWithOffset以及後續為何不繼承於FPropertyParamsBase?

我們在FPropertyParamsBase和FPropertyParamsBaseWithOffset等後續的注釋後面以及屬性成員的相似性上來看,很容易就看到這些F*PropertyParams其實是用了繼承語義的,那為何不直接繼承而是費勁的再寫一遍呢?

雖然官方在FPropertyParamsBase上已經寫了注釋,但是有些朋友可能還是依然比較懵懂。其實這裡涉及到一個C++的Aggregate類型的aggregate initialization規則。具體的C++語法規則請自行去補充學習。簡單來說,一個Aggregate是一個數組或者一個沒有用戶聲明構造函數,沒有私有或保護類型的非靜態數據成員,沒有父類和虛函數的類型。 Aggregatel類型就可以用形如 T object = {arg1, arg2, ...} 的初始化列表來初始化。我們在上文中見到的:

static const UE4CodeGen_Private::FFloatPropertyParams NewProp_Score = { UE4CodeGen_Private::EPropertyClass::Float, "Score", RF_Public|RF_Transient|RF_MarkAsNative, 0x0010000000000004, 1, nullptr, STRUCT_OFFSET(FMyStruct, Score), METADATA_PARAMS(NewProp_Score_MetaData, ARRAY_COUNT(NewProp_Score_MetaData)) };

後面的={}就是初始化列表。這麼寫當然是為了簡潔的目的,否則一個個參數的欄位設置過去,那也太麻煩了。

那如果用繼承會怎麼樣呢?我們可以來做個測試:

struct Point2{ float X; float Y;};struct Point3 :public Point2 //這不是個Aggregate類型,因為有父類{ float Z;};struct Point3_Aggregate //這是個Aggregate類型{ float X; float Y; float Z;};const static Point3 pos{ 1.f,2.f,3.f }; // error C2440:initializing: cannot convert from initializer list to Point3const static Point3_Aggregate pos2{ 1.f,2.f,3.f };

因此UE選擇不繼承,寧願每個重複寫一遍欄位聲明,就是為了可以簡單用{}初始化列表來構造對象。但是我們也觀察到,在PropPointers數組裡,也依然把一個個元素都轉為FPropertyParamsBase*。因為根據C/C++的對象內存模型,繼承的時候,基類成員排在派生類成員之前的內存地址上。又因為F*PropertyParams是如此的POD,所以只要保證內存地址和屬性成員順序一致,就可以保證轉為另一個結構指針後依然可以正確的使用。

雖然看起來這麼解釋的通,但還是感覺很麻煩,本來應該用繼承的語義卻偏偏為了初始化列表妥協了。對完美主義者來說還是不能忍,那麼有沒有一種既可以用繼承又可以用初始化列表的解決方案呢?

其實加上構造函數就可以了。不用Aggregate類型,放寬限制,改用POD類型(POD類型就是沒有非靜態類型的non-POD類型 (或者這些類型的數組)和引用類型的數據成員,也沒有用戶定義的賦值操作符和析構函數的類型。)。如:

struct Point2{ float X; float Y; Point2(float x, float y) :X(x), Y(y) {} //構造函數};struct Point3_POD :public Point2{ float Z; Point3_POD(float x, float y, float z) :Point2(x, y), Z(z) {}//構造函數};struct Point3_Aggregate{ float X; float Y; float Z;};const static Point3_POD pos{ 1.f,2.f,3.f }; //works happy ^_^const static Point3_Aggregate pos2{ 1.f,2.f,3.f }; //works happy ^_^

所以只要把F*PropertyParams加上構造函數就可以了。至於為啥UE不這麼做?問Epic的人去,攤手~

UFunction和UClass的Params和生成:

為了測試UClass里的函數輸入輸出參數,所以增加一個AddHP函數。

//測試文件:UCLASS()class HELLO_API UMyClass :public UObject{ GENERATED_BODY()public: UPROPERTY(BlueprintReadWrite) float Score;public: UFUNCTION(BlueprintCallable, Category = "Hello") float AddHP(float HP); UFUNCTION(BlueprintCallable, Category = "Hello") void CallableFunc(); //C++實現,藍圖調用 UFUNCTION(BlueprintNativeEvent, Category = "Hello") void NativeFunc(); //C++實現默認版本,藍圖可重載實現 UFUNCTION(BlueprintImplementableEvent, Category = "Hello") void ImplementableFunc(); //C++不實現,藍圖實現};//Class.h//類里的函數鏈接信息,一個函數名字對應一個UFunction對象struct FClassFunctionLinkInfo { UFunction* (*CreateFuncPtr)(); //獲得UFunction對象的函數指針回調 const char* FuncNameUTF8; //函數的名字};//類在Cpp里的類型信息,用一個結構是為了將來也許還會添加別的欄位struct FCppClassTypeInfoStatic{ bool bIsAbstract; //是否抽象類};//UObjectGlobals.hnamespace UE4CodeGen_Private{ //函數參數 struct FFunctionParams { UObject* (*OuterFunc)(); //所屬於的外部對象,一般是外部的UClass*對象 const char* NameUTF8; //函數的名字 EObjectFlags ObjectFlags; //UFunction對象的特徵 UFunction* (*SuperFunc)(); //UFunction的基類,一般為nullptr EFunctionFlags FunctionFlags; //函數本身的特徵 SIZE_T StructureSize; //函數的參數返回值包結構的大小 const FPropertyParamsBase* const* PropertyArray; //函數的參數和返回值欄位數組 int32 NumProperties; //函數的參數和返回值欄位數組大小 uint16 RPCId; //網路間的RPC Id uint16 RPCResponseId; //網路間的RPC Response Id #if WITH_METADATA const FMetaDataPairParam* MetaDataArray; //元數據數組 int32 NumMetaData; #endif }; //實現的介面參數,篇幅所限,介面的內容可以自行分析 struct FImplementedInterfaceParams { UClass* (*ClassFunc)(); //外部所屬於的UInterface對象 int32 Offset; //在UMyClass里的實現的IMyInterface的虛函數表地址偏移 bool bImplementedByK2; //是否在藍圖中實現 }; //類參數 struct FClassParams { UClass* (*ClassNoRegisterFunc)(); //獲得UClass*對象的函數指針 UObject* (*const *DependencySingletonFuncArray)(); //獲取依賴對象的函數指針數組,一般是需要前提構造的基類,模塊UPackage對象 int32 NumDependencySingletons; uint32 ClassFlags; // EClassFlags,類特徵 const FClassFunctionLinkInfo* FunctionLinkArray; //鏈接的函數數組 int32 NumFunctions; const FPropertyParamsBase* const* PropertyArray; //類里定義的成員變數數組 int32 NumProperties; const char* ClassConfigNameUTF8; //配置文件名字,有些類可以從配置文件從載入數據 const FCppClassTypeInfoStatic* CppClassInfo; //Cpp里定義的信息 const FImplementedInterfaceParams* ImplementedInterfaceArray; //實現的介面信息數組 int32 NumImplementedInterfaces; #if WITH_METADATA //類的元數據 const FMetaDataPairParam* MetaDataArray; int32 NumMetaData; #endif };}//MyClass.gen.cpp//構造AddHp函數的UFunction對象UFunction* Z_Construct_UFunction_UMyClass_AddHP(){ struct MyClass_eventAddHP_Parms //函數的參數和返回值包 { float HP; float ReturnValue; }; static UFunction* ReturnFunction = nullptr; if (!ReturnFunction) { //定義兩個屬性用來傳遞信息 static const UE4CodeGen_Private::FFloatPropertyParams NewProp_ReturnValue = { UE4CodeGen_Private::EPropertyClass::Float, "ReturnValue", RF_Public|RF_Transient|RF_MarkAsNative, 0x0010000000000580, 1, nullptr, STRUCT_OFFSET(MyClass_eventAddHP_Parms, ReturnValue), METADATA_PARAMS(nullptr, 0) }; static const UE4CodeGen_Private::FFloatPropertyParams NewProp_HP = { UE4CodeGen_Private::EPropertyClass::Float, "HP", RF_Public|RF_Transient|RF_MarkAsNative, 0x0010000000000080, 1, nullptr, STRUCT_OFFSET(MyClass_eventAddHP_Parms, HP), METADATA_PARAMS(nullptr, 0) }; static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[] = { (const UE4CodeGen_Private::FPropertyParamsBase*)&NewProp_ReturnValue, (const UE4CodeGen_Private::FPropertyParamsBase*)&NewProp_HP, };#if WITH_METADATA static const UE4CodeGen_Private::FMetaDataPairParam Function_MetaDataParams[] = { { "Category", "Hello" }, { "ModuleRelativePath", "MyClass.h" }, };#endif static const UE4CodeGen_Private::FFunctionParams FuncParams = { (UObject*(*)())Z_Construct_UClass_UMyClass, "AddHP", RF_Public|RF_Transient|RF_MarkAsNative, nullptr, (EFunctionFlags)0x04020401, sizeof(MyClass_eventAddHP_Parms), PropPointers, ARRAY_COUNT(PropPointers), 0, 0, METADATA_PARAMS(Function_MetaDataParams, ARRAY_COUNT(Function_MetaDataParams)) }; UE4CodeGen_Private::ConstructUFunction(ReturnFunction, FuncParams); //構造函數 } return ReturnFunction;}//...構造其他的函數//該類依賴的對象列表,用函數指針來獲取。static UObject* (*const DependentSingletons[])() = { (UObject* (*)())Z_Construct_UClass_UObject, (UObject* (*)())Z_Construct_UPackage__Script_Hello, };//函數鏈接信息static const FClassFunctionLinkInfo FuncInfo[] = { { &Z_Construct_UFunction_UMyClass_CallableFunc, "CallableFunc" }, // 1841300010 { &Z_Construct_UFunction_UMyClass_ImplementableFunc, "ImplementableFunc" }, // 2010696670 { &Z_Construct_UFunction_UMyClass_NativeFunc, "NativeFunc" }, // 2593520329};#if WITH_METADATAstatic const UE4CodeGen_Private::FMetaDataPairParam Class_MetaDataParams[] = { { "IncludePath", "MyClass.h" }, { "ModuleRelativePath", "MyClass.h" },};#endif#if WITH_METADATAstatic const UE4CodeGen_Private::FMetaDataPairParam NewProp_Score_MetaData[] = { { "Category", "MyClass" }, { "ModuleRelativePath", "MyClass.h" },};#endifstatic const UE4CodeGen_Private::FFloatPropertyParams NewProp_Score = { UE4CodeGen_Private::EPropertyClass::Float, "Score", RF_Public|RF_Transient|RF_MarkAsNative, 0x0010000000000004, 1, nullptr, STRUCT_OFFSET(UMyClass, Score), METADATA_PARAMS(NewProp_Score_MetaData, ARRAY_COUNT(NewProp_Score_MetaData)) };static const UE4CodeGen_Private::FPropertyParamsBase* const PropPointers[] = { (const UE4CodeGen_Private::FPropertyParamsBase*)&NewProp_Score,};static const FCppClassTypeInfoStatic StaticCppClassTypeInfo = { TCppClassTypeTraits<UMyClass>::IsAbstract,};static const UE4CodeGen_Private::FClassParams ClassParams = { &UMyClass::StaticClass, DependentSingletons, ARRAY_COUNT(DependentSingletons), 0x00100080u, FuncInfo, ARRAY_COUNT(FuncInfo), PropPointers, ARRAY_COUNT(PropPointers), nullptr, &StaticCppClassTypeInfo, nullptr, 0, METADATA_PARAMS(Class_MetaDataParams, ARRAY_COUNT(Class_MetaDataParams))};UE4CodeGen_Private::ConstructUClass(OuterClass, ClassParams); //構造UClass對象

注意,對於一個函數來說,參數和返回值都可以算是函數內部定義的屬性,只不過其有不同的特徵和用途。類里包含屬性和函數,而函數又包含屬性。屬性的構造和Struct里的規則一樣,就不贅述了。不同的是,因為Class里可以包含Function,所以構造UClass之前必須先構造出所有的UFunction。所以整理下,其實上述的那些構造就是結構套結構,加上一些數組整合出來的信息集合而已。

思考:為什麼生成的代碼里大量用了函數指針來返回對象?

如UClass* (*ClassNoRegisterFunc)()或UFunction* (*CreateFuncPtr)()都用函數指針來獲取定義的UClass*對象和前提依賴的UFunction*對象。為什麼不直接用個UClass*或UFunction*指針呢?

答案很簡單,因為構造順序的不確定。

在一個類型系統中,類型的依賴管理是項很麻煩但又非常重要的事,你必須保證當前類型的所有前置類型都已經定義完畢,才能開始本類型的構造。針對此問題,當然你可以小心翼翼的理清定義順序,確保所有的順序都是由底向上的。可是理想很美好,現實很骨感,這一步驟很難實現,是人都會犯錯,更何況面對UE4當前的1572個UClass、1039個UStruct、588個Enum……你真的相信有人能管理好這些?所以在類型系統里想人工整理好類型的依賴定義順序基本不現實,你幾乎很難在構造本類型的時候,恰好的取得前置類型的對象。

那怎麼辦?也簡單,就如同C++里處理static單件對象的依賴順序一樣,既然處理不了,那就不處理!採用懶惰求值的思想,在需要前置類型的時候,先判斷有沒有構造出來,如果有就立即返回,如果沒有就構造後再返回——一個簡易版的單件模式。因為這個套路是如此的普遍,所以這一些判斷加上構造的邏輯封裝一下就成了一個個函數,為了獲得那些對象,就變成了先獲得那些函數指針了。生成的代碼里都是大概這種套路:

UClass* Z_Construct_UClass_UMyClass() //用以獲取UMyClass所對應的UClass的函數{ static UClass* OuterClass = nullptr; //一個函數局部靜態指針 if (!OuterClass) //反正都是單線程執行,所以不需要線程安全的保護 { //...一些構造代碼 UE4CodeGen_Private::ConstructUClass(OuterClass, ClassParams); } return OuterClass;}

利用此法,就在代碼中形成了一個可自動上溯的前置對象獲取鏈條。任何時候,想得到某一個類的UClass*對象,我們不需要去操心是否已經構造完成,也不需要擔心它的依賴項是否已經全部構造了,因為代碼的機制保證了前置項的按需構造。

UPackage的Params和生成:

對於Hello模塊而言,按照UE4的Module規則,我們需要定義一個Hello UPackage來存放該模塊里定義的類型。

之前的4.15的代碼形式為:

UPackage* Z_Construct_UPackage__Script_Hello(){ static UPackage* ReturnPackage = NULL; if (!ReturnPackage) { ReturnPackage = CastChecked<UPackage>(StaticFindObjectFast(UPackage::StaticClass(), NULL, FName(TEXT("/Script/Hello")), false, false)); ReturnPackage->SetPackageFlags(PKG_CompiledIn | 0x00000000); FGuid Guid; Guid.A = 0x79A097CD; Guid.B = 0xB58D8B48; Guid.C = 0x00000000; Guid.D = 0x00000000; ReturnPackage->SetGuid(Guid); } return ReturnPackage;}

在4.17之後改為:

//UObjectGlobals.hnamespace UE4CodeGen_Private{ //包參數 struct FPackageParams { const char* NameUTF8; //名字 uint32 PackageFlags; // EPackageFlags包的特徵 uint32 BodyCRC; //內容的CRC,CRC的部分後續介紹 uint32 DeclarationsCRC; //聲明部分的CRC UObject* (*const *SingletonFuncArray)(); //依賴的對象列表 int32 NumSingletons;#if WITH_METADATA const FMetaDataPairParam* MetaDataArray;//元數據數組 int32 NumMetaData;#endif };}//Hello.init.gen.cppUPackage* Z_Construct_UPackage__Script_Hello(){ static UPackage* ReturnPackage = nullptr; if (!ReturnPackage) { static const UE4CodeGen_Private::FPackageParams PackageParams = { "/Script/Hello", //包名字 PKG_CompiledIn | 0x00000000, 0xA1EAFF6A, 0x41CF0543, nullptr, 0, METADATA_PARAMS(nullptr, 0) }; UE4CodeGen_Private::ConstructUPackage(ReturnPackage, PackageParams); } return ReturnPackage;}

本塊內容比較簡單,能堅持看到此處的朋友,對上文的代碼應該是一目了然的。有一點需要知道的是,在Hello模塊里的定義的類型數據,都是放在"/Script/Hello"Package里的,所以Hello Package是第一個首先構造出來的,因為它被後續的其他類型都依賴著。

總結

對比了前後兩版本的代碼,我們不難看出重構了之後,生成的代碼更加的緊緻,語法的噪音減少了很多,代碼的信息量密度大大提高了。但要注意,本文關注的類型系統階段是對之前《InsideUE4》UObject(四)類型系統代碼生成的補充,後續依然是接著《InsideUE4》UObject(五)類型系統收集章節的內容進行開始收集,所以前文的那些static靜態收集機制並沒有改變。

至於UE4CodeGen_Private::ConstructXXX構造的具體實現,我們在後續章節講到類型系統的結構組織時候再詳細講解,我保證,那天不會太久遠。當前階段你可以簡單的理解為都是通過一個個參數構造出一個個類型對象。

思考:生成的代碼能否做得更加的清晰高效?

雖然通過此次重構,代碼的可讀性上升了許多。但平心而論,現在的代碼生成依然遠遠算不上優雅。那麼在程序化代碼生成的時候一般有哪些手段可以繼續提升呢?

追其本質,讓代碼變得簡潔的手段其實都是在提升信息密度。把代碼比作文件的話,重構就像壓縮軟體一樣,把代碼的信息量壓縮到無所壓就算是到了極致了。但是當然這中間當然也要權衡平台移植性(否則直接存一個二進位文件好了)、可讀性、編譯效率等問題。提升信息密度的手段就只有一個:同樣的信息不要書寫兩遍。因此帶來的方式就是封裝!而封裝,在代碼生成的時候,我們其實可以用到:

  1. 宏,UE4里其實已經用了一些宏來縮減代碼,比如ARRAYCOUNT、VTABLEOFFSET、IMPLEMENT_CLASS等。但目前的生成代碼里依然有大量的長名字,套路化數組拼接,可以用宏來拼接。過度用宏當然也會降低可讀性可調試性,但恰當的地方使用可以如同開掛一般優化掉巨量的代碼。宏一直是代碼拼接的最強大利器。
  2. 函數,把相似的邏輯封裝成函數可以優化掉大量的操作,只對外提供最簡潔的信息輸入介面。本文介紹的UE4優化方式就是用了函數來優化。我個人的傾向是寧願在核心層多定義一些方便的輔助函數來接收多種輸入,而不是在代碼生成的時候去一個個拼函數的實體,這樣可以大大減小生成代碼的體積。函數的實現上巧用不定參數、數組和循環,可以使你的函數吞吐能力驚人。
  3. 模板,更深層次的挖掘編譯器提供的信息,壓榨每個欄位提供的信息量,利用它,從而自動推導出你所需要的其他信息。比如屬性的類型就可以用模板根據欄位的c++類型自動推導出來,而不需要手動分析注入了。UE4的生成代碼里模板用的不多,是因為模板也有其很大的缺點:編譯慢和難理解。在已經有了UHT分析代碼的基礎上,再用模板推導一遍,好像意義就不是那麼大了,所以編譯消耗還是能省一點是一點吧。至於模板的難理解,一款開源面向大眾的引擎,在技術的選型實現上不應該過分的炫技,因為從業人員的技術水平,初中級的才是占絕大多數。考慮到受眾問題和推廣,有時候還是應該用一些樸實無華的實現比較能廣為接受,同時也能有更大概率爭取到重構維護者。否則,你寫的代碼,是很厲害,但是只有你自己能改得動改得明白,那叫社區里的人怎麼為你貢獻維護升級。
  4. 擴展性,同時建議盡量把類型系統的構造邏輯放到Runtime里去,而不是在生成代碼里(之前UE4就是在生成代碼里直接new出一個個UXXX類型對象。放到Runtime,對外提供函數API介面,這樣的好處是可測試性大大增長,不需要依賴UHT就可以手動構造出想要的類型進行測試。另外對於一些有在運行時動態Emit創建類型的需求來說,脫離UHT,保持自身功能的完備性也是必需的。

上述的討論不限於UE4引擎,只是對於有興趣實現一個類型系統的人來說,在每個階段其實都有很多技術選擇,但設計就是權衡的藝術,清楚了解你的受眾,清晰你的設計目的願景,對可用的各種手段信手拈來,最後才能組合出優雅的設計。

後記

在我們閱讀UE4源碼的過程中,也要時刻認識到UE4的源碼不是完美的。有很多時候,在閱讀一段具體代碼時,我們可能會去使勁猜測這段代碼的用意,琢磨當初是怎麼的設計理念卻不可得。其實,現實的情況是這些代碼往往只是一時的修復,且不是所有代碼的編寫者技術都是那麼高超。他會出錯,另一個他會修復,然後再犯錯修復,周而復始。如同生物的進化一樣,一次次重構優化,最終得到的往往並不是最優解,就像人眼睛的盲點和人的智齒都不是最正確的設計,而是會留下進化的痕迹。但是儘管如此,我們也並不需要感到沮喪,因為接受不完美,接受最後的這個可接受解,適當的懂得妥協和接受缺陷,也許也是一個技術人成熟的標誌之一吧。

有一個有趣的現象,對於大工程量的項目(如UE4)來說,越是底層的模塊越是缺乏推動力去重構,越底層的代碼其改動的阻力也越大。牽一髮而動全身,在一些時候,重構底層模塊其實也是最能產生巨量效益的時候,因為其影響會層層放大到最上層上去。但是代碼畢竟是人在寫,在一個公司里,一個團隊里,形成的開發氛圍往往是只要底層代碼能工作,就不會有人去改,也不會有人敢改。拿UE4的CoreUObject模塊來說,是UE4的對象系統模塊,可以說是最底層核心的模塊了,但是根據我這麼一大段時間的研讀來說,代碼里充斥著各種歷史痕迹和小修小補,一些代碼結構也是讓人無可奈何,but it works,所以這塊代碼從UE3過來,到UE4里,相信有生之年也是會繼續追隨到UE5的。CoreUObject代碼模塊目前能工作,雖然有時也會有點BUG,但是到時小修補就好了,那些代碼的優雅追求和結構的設計,改的好了效益不太明顯;改過之後出了Bug是不是都算你的?所以正是因為這種效益和責任的擔負,導致往往最需要重視的模塊,最得不到升級改造。但歷史的規律也表明了,代碼的小缺陷積累多了,開發者的怨氣積攢足夠了,再適逢一個不動底層不能開工的功能需求的刺激,到時候才能下得了決心大改,或者乾脆另起爐灶重新設計了。說這些,是希望同讀UE4源碼的朋友,在遇到代碼里莫名其妙的設計,抓耳撓腮苦思冥想的時候,可以放寬心態,稍安勿躁,休息一下,我們繼續前行。

作者的話

我拖更,我可恥,請大家不要向我學習!

實在無顏面對自己曾經寫下的目錄……只能化羞憤為動力,知恥而後勇,之後我會盡量把精力分配過來,爭取把腦袋裡的東西都早點掏出來寫出來。但也實在不敢再保證更新周期了,只能說盡我最大努力,不定期更新吧。

但還是稍微解釋一下吧,2017年一整年,我在某機構擔任UE4的技術培訓高級講師,在熟悉了之後擔任整個UE4教學的負責,主導UE4的教學大綱設計和課程研發等內容。因個人的怪趣味使然,想試試從研發的崗位轉到教學崗位上是種什麼體驗,哪成想教學竟然比研發還要佔用人的精力。自己會和教會別人會真不是一個量級的難度,以前一些理所當然的常識性知識和結構,都需要深入淺出的去給初學者講解明白,對知識的全面性、技術深度的洞察力,本質邏輯都是很大的挑戰。可是想到講台下嗷嗷待哺飽含對知識渴望眼神的學生們,從職業和師德上就不敢有一絲怠慢,於是2017年里時常備課到深夜兩三點。不過所幸的是,收穫也很多,除了一幫成材可愛的學生之外,也是讓我在講解一個技術原理給別人的時候功力+0.1。在外出給別的合作公司做技術培訓和技術支持的時候,從另一個角度了解了很多別人的常見關心的問題,切入一些自己以往並不涉及的業務領域,對整個行業的了解+0.1。

打個廣告

2018年,目前也還是在從事UE4技術培訓領域,也仍在繼續接公司的UE4技術培訓業務,可以根據需求定製化課程大綱,有UE4技術培訓需求的公司或個人,歡迎私信諮詢進一步溝通。

上篇:

大釗:《InsideUE4》UObject(五)類型系統信息收集?

zhuanlan.zhihu.com圖標

UE4.18.3


知乎專欄:InsideUE4

UE4深入學習QQ群:456247757(非新手入門群,請先學習完官方文檔和視頻教程,回答正確驗證問題才能進入)

個人原創,未經授權,謝絕轉載!


推薦閱讀:

[CppCon16] Rainbow Six Siege - Quest for Performance
從零開始寫引擎(OPENGL)(四)-後處理實現
[CppCon14] How Ubisoft Montreal develops games formulticore – before and after C++11
[DOD Series][CppCon14] Data-Oriented Design and C++
[翻譯]Metal Gear Solid V – Graphics Study

TAG:遊戲開發 | 遊戲引擎 | 虛幻引擎 |