上一篇文章中講解了UE4中對類(UCLASS)的反射支持,這篇文章我們還是以實例的形式來講解虛幻4對結構體(USTRUCT)以及枚舉(UENUM)的支持以及UE4是如何把這些信息收集起來的的。

結構體

首先讓我們看一下測試結構體反射支持的代碼,我們用USTRUCT聲明了一個結構體,告訴虛幻4 要對這個類型支持反射類型,我們向其中添加了一個float類型的值來測試程序。

#pragma once
#include "ReflectionStructTest.generated.h"

USTRUCT(Blueprintable)
struct FReflectionTest
{
GENERATED_USTRUCT_BODY()

UPROPERTY(BlueprintReadWrite)
float ReflectionValue;
};

生成的.generated.h文件

點擊編譯後,我們得到了.generated.h文件,這段代碼就是GENERATED_USTRUCT_BODY()宏展開後對應的內容,代碼比較簡單,如下所示:

// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
/*===========================================================================
C++ class header boilerplate exported from UnrealHeaderTool.
This is automatically generated by the tools.
DO NOT modify this manually! Edit the corresponding .h files instead!
===========================================================================*/

#include "ObjectBase.h"

PRAGMA_DISABLE_DEPRECATION_WARNINGS
#ifdef REFLECTIONSTUDY_ReflectionStructTest_generated_h
#error "ReflectionStructTest.generated.h already included, missing #pragma once in ReflectionStructTest.h"
#endif
#define REFLECTIONSTUDY_ReflectionStructTest_generated_h

#define ReflectionStudy_Source_ReflectionStudy_ReflectionStructTest_h_9_GENERATED_BODY
friend REFLECTIONSTUDY_API class UScriptStruct* Z_Construct_UScriptStruct_FReflectionTest();
REFLECTIONSTUDY_API static class UScriptStruct* StaticStruct();

#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID ReflectionStudy_Source_ReflectionStudy_ReflectionStructTest_h

PRAGMA_ENABLE_DEPRECATION_WARNINGS

它主要做了以下三件事情:

  • friend REFLECTIONSTUDY_API class UScriptStruct* Z_Construct_UScriptStruct_FReflectionTest() 定義了一個友元函數,用於創建這個結構體的反射對象UScriptStruct
  • REFLECTIONSTUDY_API static class UScriptStruct* StaticStruct(); 定義了一個成員函數StaticStruct(),這樣我們可以通過類來獲取它的反射結構體。
  • #define CURRENT_FILE_ID ReflectionStudy_Source_ReflectionStudy_ReflectionStructTest_h 重新定義了CURRENT_FILE_ID 詳細信息請參照開頭提到的內容有對GENERATED_USTRUCT_BODY()的講解

.generated.cpp中相關的內容

UScriptStruct* Z_Construct_UScriptStruct_FReflectionTest()
{
UPackage* Outer = Z_Construct_UPackage__Script_ReflectionStudy();
extern uint32 Get_Z_Construct_UScriptStruct_FReflectionTest_CRC();
static UScriptStruct* ReturnStruct = FindExistingStructIfHotReloadOrDynamic(Outer, TEXT("ReflectionTest"), sizeof(FReflectionTest), Get_Z_Construct_UScriptStruct_FReflectionTest_CRC(), false);
if (!ReturnStruct)
{
ReturnStruct = new(EC_InternalUseOnlyConstructor, Outer, TEXT("ReflectionTest"), RF_Public|RF_Transient|RF_MarkAsNative) UScriptStruct(FObjectInitializer(), NULL, new UScriptStruct::TCppStructOps<FReflectionTest>, EStructFlags(0x00000001));
UProperty* NewProp_ReflectionValue = new(EC_InternalUseOnlyConstructor, ReturnStruct, TEXT("ReflectionValue"), RF_Public|RF_Transient|RF_MarkAsNative) UFloatProperty(CPP_PROPERTY_BASE(ReflectionValue, FReflectionTest), 0x0010000000000004);
ReturnStruct->StaticLink();
#if WITH_METADATA
UMetaData* MetaData = ReturnStruct->GetOutermost()->GetMetaData();
MetaData->SetValue(ReturnStruct, TEXT("BlueprintType"), TEXT("true"));
MetaData->SetValue(ReturnStruct, TEXT("IsBlueprintBase"), TEXT("true"));
MetaData->SetValue(ReturnStruct, TEXT("ModuleRelativePath"), TEXT("ReflectionStructTest.h"));
MetaData->SetValue(NewProp_ReflectionValue, TEXT("Category"), TEXT("ReflectionTest"));
MetaData->SetValue(NewProp_ReflectionValue, TEXT("ModuleRelativePath"), TEXT("ReflectionStructTest.h"));
if
}
return ReturnStruct;
}
uint32 Get_Z_Construct_UScriptStruct_FReflectionTest_CRC() { return 486791486U; }

從上面的代碼中我們可以得知三點:

  • 創建UScriptStruct並添加到當前工程特定的package中
  • 創建我們上文中添加的ReflectionValue屬性
  • 添加元數據,供編輯器使用,比如我們上面在USTRUCT中指定的BlueprintType

class UScriptStruct* FReflectionTest::StaticStruct()
{
extern REFLECTIONSTUDY_API class UPackage* Z_Construct_UPackage__Script_ReflectionStudy();
static class UScriptStruct* Singleton = NULL;
if (!Singleton)
{
extern REFLECTIONSTUDY_API class UScriptStruct* Z_Construct_UScriptStruct_FReflectionTest();
extern REFLECTIONSTUDY_API uint32 Get_Z_Construct_UScriptStruct_FReflectionTest_CRC();
Singleton = GetStaticStruct(Z_Construct_UScriptStruct_FReflectionTest, Z_Construct_UPackage__Script_ReflectionStudy(), TEXT("ReflectionTest"), sizeof(FReflectionTest), Get_Z_Construct_UScriptStruct_FReflectionTest_CRC());
}
return Singleton;
}
static FCompiledInDeferStruct Z_CompiledInDeferStruct_UScriptStruct_FReflectionTest(FReflectionTest::StaticStruct, TEXT("/Script/ReflectionStudy"), TEXT("ReflectionTest"), false, nullptr, nullptr);

上面的代碼主要做了兩件事情:

  • StaticStruct()判斷Singleton是否為空,如果為空,那麼就調用GetStaticStruct(),面GetStaticStruct()就是調用了Z_Construct_UScriptStruct_FReflectionTest()函數。

class UScriptStruct *GetStaticStruct(class UScriptStruct *(*InRegister)(), UObject* StructOuter, const TCHAR* StructName, SIZE_T Size, uint32 Crc)
{
return (*InRegister)();
}

  • 定義了一個靜態全局變數,用於註冊到一個列表中,在引擎初始化的時候調用StaticStruct()方法。

static struct FScriptStruct_ReflectionStudy_StaticRegisterNativesFReflectionTest
{
FScriptStruct_ReflectionStudy_StaticRegisterNativesFReflectionTest()
{
UScriptStruct::DeferCppStructOps(FName(TEXT("ReflectionTest")),new UScriptStruct::TCppStructOps<FReflectionTest>);
}
} ScriptStruct_ReflectionStudy_StaticRegisterNativesFReflectionTest;

  • 定義一個靜態變數用於存儲一個CppStructOps(主要用來 動態獲取結構體的構造和析造函數) ,用於在程序中使用。

枚舉

接下來我們來看下枚舉的實現方式,測試代碼如下所示:

UENUM(BlueprintType)
enum class EReflectionTest : uint8
{
E0,
E1
};

生成的.generated.h文件

編譯代碼,我們在.generated.h文件中會得到如下代碼,它僅僅定義了一個FOREACH_ENUM_EREFLECTIONTEST的宏。

#define FOREACH_ENUM_EREFLECTIONTEST(op)
op(EReflectionTest::E0)
op(EReflectionTest::E1)

.generated.cpp中相關代碼

UEnum* Z_Construct_UEnum_ReflectionStudy_EReflectionTest()
{
UPackage* Outer=Z_Construct_UPackage__Script_ReflectionStudy();
extern uint32 Get_Z_Construct_UEnum_ReflectionStudy_EReflectionTest_CRC();
static UEnum* ReturnEnum = FindExistingEnumIfHotReloadOrDynamic(Outer, TEXT("EReflectionTest"), 0, Get_Z_Construct_UEnum_ReflectionStudy_EReflectionTest_CRC(), false);
if (!ReturnEnum)
{
ReturnEnum = new(EC_InternalUseOnlyConstructor, Outer, TEXT("EReflectionTest"), RF_Public|RF_Transient|RF_MarkAsNative) UEnum(FObjectInitializer());
TArray<TPair<FName, uint8>> EnumNames;
EnumNames.Add(TPairInitializer<FName, uint8>(FName(TEXT("EReflectionTest::E0")), 0));
EnumNames.Add(TPairInitializer<FName, uint8>(FName(TEXT("EReflectionTest::E1")), 1));
EnumNames.Add(TPairInitializer<FName, uint8>(FName(TEXT("EReflectionTest::EReflectionTest_MAX")), 2));
ReturnEnum->SetEnums(EnumNames, UEnum::ECppForm::EnumClass);
ReturnEnum->CppType = TEXT("EReflectionTest");
#if WITH_METADATA
UMetaData* MetaData = ReturnEnum->GetOutermost()->GetMetaData();
MetaData->SetValue(ReturnEnum, TEXT("BlueprintType"), TEXT("true"));
MetaData->SetValue(ReturnEnum, TEXT("ModuleRelativePath"), TEXT("ReflectionStructTest.h"));
#endif
}
return ReturnEnum;
}
uint32 Get_Z_Construct_UEnum_ReflectionStudy_EReflectionTest_CRC() { return 1111016117U; }

上面代碼的主要作用就是:

  • 查看反射的UEnum有沒有生成,如果沒有生成,那麼會new一個UEnum並且將我們定義的E0,E1,兩個枚舉添加進來,而且默認都會添加一個 『枚舉名+_Max』的一個枚舉值
  • 註冊編輯器需要的元數據,比如我們在UENUM()中添加的BlueprintType

static class UEnum* EReflectionTest_StaticEnum()
{
extern REFLECTIONSTUDY_API class UPackage* Z_Construct_UPackage__Script_ReflectionStudy();
static class UEnum* Singleton = NULL;
if (!Singleton)
{
extern REFLECTIONSTUDY_API class UEnum* Z_Construct_UEnum_ReflectionStudy_EReflectionTest();
Singleton = GetStaticEnum(Z_Construct_UEnum_ReflectionStudy_EReflectionTest, Z_Construct_UPackage__Script_ReflectionStudy(), TEXT("EReflectionTest"));
}
return Singleton;
}
static FCompiledInDeferEnum Z_CompiledInDeferEnum_UEnum_EReflectionTest(EReflectionTest_StaticEnum, TEXT("/Script/ReflectionStudy"), TEXT("EReflectionTest"), false, nullptr, nullptr);

這個代碼跟上面結構的比較相似,也是主要做了兩件事情:

  • EReflectionTest_StaticEnum()判斷Singleton是不是為空,如果為空那麼通過GetStaticEnum來創建或返回UEnum對象,具體可參考GetStaticEnum實現。
  • 向一個列表中註冊EReflectionTest_StaticEnum()函數,用於在引擎啟動的時候調用。

信息註冊

虛幻引擎使用一系列靜態變數來註冊需要生成反射信息的函數,這個我們前面的文章已經比較詳細的講過。至於用生成的C++的代碼所帶來的好處,我前面翻譯的文章中也講過。下面我把它貼到這裡。

用生成的C++代碼來存儲反射數據的一個最大好處就是,它可以保證跟二進位做到同步。你永遠也不會載入陳舊或者過時的反射數據,因為它是跟引擎的其它代碼同時編譯的,並且它會在程序啟動的時候使用C++表達式來計算成員偏移等,而不是通過針對特定平台/編譯器/優化的組合中進行逆向工程。UHT作為一個單獨的不使用任何生成頭文件的程序來構建,因此它也避免了雞生蛋、蛋生雞的問題,這個在虛幻3的腳本編譯器中一直被詬病。

UCLASS

對於類的反射支持,虛幻4分為兩步來做的。

  1. IMPLEMENT_CLASS() 這個宏用於在程序啟動時註冊這個類,包括生成UClass類以及註冊C++原生函數等操作。
  2. static FCompiledInDefer 創建一個靜態變數,用於向DeferredCompiledInRegistration這個靜態數組中添加註冊函數,來初始化默認的反射屬性。包括函數、成員變數、元數據等。

USTRUCT

對於結構體的支持,虛幻4引擎也是分為兩步來做的。

  1. static FCompiledInDeferStruct 存儲一個用於構建結構體的一個單例函數,用於在程序啟動的時候調用,讀者可以自行查看代碼就知道這個過程了。
  2. 還會創建一個靜態對象,這個對象在構造函數中會調用UScriptStruct::DeferCppStructOps,它用來向這個DeferredCppStructOps map 中註冊一個動態管理結構體構造、析構的一個類。

UENUM

枚舉比較簡單,只有一步。

  1. static FCompiledInDeferEnum 創建一個靜態變數,用於在程序啟動時存儲一個創建枚舉反射對象的一個單例函數。

啟動過程分析

上面我們講解了這個註冊信息的過程,而它們的執行是伴隨著當前模塊的載入而執行的,我們都知道靜態變數的初始化是先於Main函數執行的。下面我們簡單畫了一下虛幻編輯器的啟動流程,這樣我們就可以準確地看到整個註冊反射信息的過程了。

可以看到void ProcessNewlyLoadedUObjects()這個函數就是我們主要關注的函數,我們前面講到的註冊的信息,包括類、結構體以及枚舉類型的反射信息都會在這裡進行註冊,它的代碼如下所示:

void ProcessNewlyLoadedUObjects()
{
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("ProcessNewlyLoadedUObjects"), STAT_ProcessNewlyLoadedUObjects, STATGROUP_ObjectVerbose);

#if WITH_HOT_RELOAD
UClassGenerateCDODuplicatesForHotReload();
#endif
UClassRegisterAllCompiledInClasses();

while( AnyNewlyLoadedUObjects() )
{
UObjectProcessRegistrants();
UObjectLoadAllCompiledInStructs();
UObjectLoadAllCompiledInDefaultProperties();
}
#if WITH_HOT_RELOAD
UClassReplaceHotReloadClasses();
#endif
}

下面我們對上面代碼做一個簡單的解釋,讀者也可以自行翻閱代碼來仔細查看它是怎麼實現的。

  1. 代碼中WITH_HOT_RELOAD這個宏是用來處理C++代碼熱載入使用的。
  2. UClassRegisterAllCompiledInClasses()用來註冊所有要載入的類,這裡面的所有類就是通過前面IMPLEMENT_CLASS()宏添加進來的。
  3. UObjectProcessRegistrants()用於處理自動註冊的對象,並把它們添加到ObjectArray中去,用於後期的檢索。
  4. UObjectLoadAllCompiledInStructs()用於註冊結構體和枚舉的反射信息。數組裡面的數組是通過FCompiledInDeferStruct和FCompiledInDeferEnum創建的靜態對象註冊進去的。
  5. UObjectLoadAllCompiledInDefaultProperties()用於註冊類的反射信息並且創建一個默認對象(CDO)。

總結

到此為此,這個系列的兩篇文章通過代表示例的方式向讀者展示了虛幻中反射系統的實現方式,原理其實很簡單,網上也有一些如何用C++支持反射類的方法,虛幻實現的是其中一種,因為有了UHT的幫助,所以好多臟活、累活都不用我們來做了,比如最笨的方法是我們自己實現一些宏用來註冊各種反射信息,但這樣效率還是比較低。而有的實現方式會從編譯器產生的調試信息入手,比如vs生成的pdb文件,或者clang生成的文件等,這樣做也可以,但是它有一個比較大的問題就是跨平台的支持不是特別友好。所以虛幻4中這一套UHT工具總體上來說還是很不錯的,如果後面有時間,希望給大家帶來對UHT工具的分析。


推薦閱讀:
相关文章