UE4反射机制的通俗理解【生成第一个UClass】
上一篇我們講到了利用static變量把構造函數指針以及生成的類信息都收集到了全局靜態數組中。這一篇就要講講,收集好了之后,我們是怎么利用這些收集的信息來生成我們的UClass的。
上一篇最后說到了,IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction)會觸發UObject::StaticClass()的調用,這里會生成第一個UClass*。
那么UObject的IMPLEMENT_CLASS是在哪里定義的呢?就在NoexportTypes.h文件中。
這個文件定義了Eunm,Struct和UObject,但是不參與編譯,只是提供給UHT來生成反射信息的相關代碼以便于其他地方去調用。那我們接下來去看下StaticClass具體做了什么。
//類的聲明值
DECLARE_CLASS(UMyClass, UObject, COMPILED_IN_FLAGS(0), CASTCLASS_None, TEXT("/Script/Hello"), NO_API)
//值的傳遞
UClass* UMyClass::GetPrivateStaticClass(const TCHAR* Package)
{static UClass* PrivateStaticClass = NULL; //靜態變量,下回訪問就不用再去查找了if (!PrivateStaticClass){/* this could be handled with templates, but we want it external to avoid code bloat */GetPrivateStaticClassBody(Package, //包名,TEXT("/Script/Hello"),用來把本UClass*構造在該UPackage里(TCHAR*)TEXT("UMyClass") + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0),//類名,+1去掉U、A、F前綴,+11去掉Deprecated_前綴PrivateStaticClass, //輸出引用,所以值會被改變StaticRegisterNativesUMyClass, //注冊類Native函數的指針sizeof(UMyClass), //類大小UMyClass::StaticClassFlags, //類標記,值為CLASS_Intrinsic,表示在C++代碼里定義的類UMyClass::StaticClassCastFlags(), //雖然是調用,但只是簡單返回值CASTCLASS_NoneUMyClass::StaticConfigName(), //配置文件名,用于從config里讀取值(UClass::ClassConstructorType)InternalConstructor<UMyClass>,//構造函數指針,包了一層(UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<UMyClass>,//hotreload的時候使用來構造虛函數表,暫時不管&UMyClass::AddReferencedObjects, //GC使用的添加額外引用對象的靜態函數指針,若沒有定義,則會調用到UObject::AddReferencedObjects,默認函數體為空。&UMyClass::Super::StaticClass, //獲取基類UClass*的函數指針,這里Super是UObject&UMyClass::WithinClass::StaticClass //獲取對象外部類UClass*的函數指針,默認是UObject);}return PrivateStaticClass;
}
里面基本上就是簡單的傳值給GetPrivateStaticClassBody。
- Package名字的傳入是為了在構建UClass*之后,把UClass*對象的OuterPrivate設定為正確的UPackage*對象。在UE里,UObject必須屬于某個UPackage。所以傳入名字是為了后續查找或者創建出前置需要的UPackage對象。“/Script/”開頭表示這是個代碼模塊。
StaticRegisterNativesUMyClass這個函數的名字是用宏拼接的,分別在.generated.h和.gen.cpp里聲明和定義。InternalConstructor<UMyClass>這個模板函數是為了包一下C++的構造函數,因為你沒法直接去獲得C++構造函數的函數指針。在.generated.h里會根據情況生成這兩個宏的調用(GENERATED_UCLASS_BODY接收FObjectInitializer參數,GENERATED_BODY不接收參數),從而在以后的UObject*構造過程中,可以調用到我們自己寫的類的構造函數。- Super指的是類的基類,WithinClass指的是對象的Outer對象的類型。這里要區分開的是類型系統和對象系統之間的差異,Super表示的是類型上的必須依賴于基類先構建好UClass*才能構建構建子類的UClass*;WithinClass表示的是這個UObject*在構建好之后應該限制放在哪種Outer下面,這個Outer所屬于的UClass*我們必須先提前構建好。
這里有幾個疑惑:我們在生成代碼的時候,只生成了一個靜態的構造函數包裝器,名字是__DefaultConstructor,那么這里傳入的實參InternalConstructor,是哪里來的呢。
InternalConstructor 其實這個函數就定義在Class.h中,里面簡單的調用了DefaultConstructor。
而且在以后的構造過程中是怎么調用到我們自己的構造函數的呢?
接下來我們看看GetPrivateStaticClassBody做了什么
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*/)
{ReturnClass = (UClass*)GUObjectAllocator.AllocateUObject(sizeof(UClass), alignof(UClass), true);//分配內存ReturnClass = ::new (ReturnClass)UClass //用placement new在內存上手動調用構造函數(EC_StaticConstructor,Name,InSize,InClassFlags,InClassCastFlags,InConfigName,EObjectFlags(RF_Public | RF_Standalone | RF_Transient | RF_MarkAsNative | RF_MarkAsRootSet),InClassConstructor,InClassVTableHelperCtorCaller,InClassAddReferencedObjects);InitializePrivateStaticClass(InSuperClassFn(),ReturnClass,InWithinClassFn(),PackageName,Name);//初始化UClass*對象RegisterNativeFunc();//注冊Native函數到UClass中去
}
- 分配內存。GUObjectAllocator是全局的內存分配器,分配了一塊內存來存放UClass對象。關于存儲的內容后續再說,這里理解為返回一塊內存就可。也要注意的是,ReturnClass是引用,這里一賦值,就代表外面static的PrivateStaticClass就有值了。所以就算這個GetPrivateStaticClassBody函數還沒返回,但是如果去訪問
UMyClass::StaticClass()也是會立即返回這個值的。 - 調用UClass的構造函數。這里的EC_StaticConstructor只是個標記用來指定調用特定的UClass構造函數重載版本。該構造函數內只是簡單的成員變量賦值,并沒有什么特別的。這么二步構造的原因是UObject的內存都是統一管理的,所以應該由GUObjectAllocator來分配,不能像標準C++那樣直接new出來一個。
InitializePrivateStaticClass調用的時候,InSuperClassFn()和InWithinClassFn()是會先被調用的,所以其會先觸發Super::StaticClass()和WithinClass::StaticClass(),再會堆棧式的加載前置的類型。RegisterNativeFunc()就是上文的StaticRegisterNativesUMyClass,在此刻調用,用來像UClass里添加Native函數。Native函數指的是在C++有函數體實現的函數,而藍圖中的函數和BlueprintImplementableEvent的函數就不是Native函數。
接著往里走,是InitializePrivateStaticClass
COREUOBJECT_API void InitializePrivateStaticClass(class UClass* TClass_Super_StaticClass,class UClass* TClass_PrivateStaticClass,class UClass* TClass_WithinClass_StaticClass,const TCHAR* PackageName,const TCHAR* Name)
{//...if (TClass_Super_StaticClass != TClass_PrivateStaticClass){TClass_PrivateStaticClass->SetSuperStruct(TClass_Super_StaticClass); //設定類之間的SuperStruct}else{TClass_PrivateStaticClass->SetSuperStruct(NULL); //UObject無基類}TClass_PrivateStaticClass->ClassWithin = TClass_WithinClass_StaticClass; //設定Outer類類型//...TClass_PrivateStaticClass->Register(PackageName, Name); //轉到UObjectBase::Register()//...
}
- 設定類型的SuperStruct。SuperStruct是定義在UStruct里的UStruct* SuperStruct,用來指向本類型的基類。
- 設定ClassWithin的值。也就是限制Outer的類型。
- 調用
UObjectBase::Register()。終于對每個UClass*開始了注冊,不枉調用鏈條上的UClassRegisterAllCompiledInClasses的Register之名。
struct FPendingRegistrantInfo
{const TCHAR* Name; //對象名字const TCHAR* PackageName; //所屬包的名字static TMap<UObjectBase*, FPendingRegistrantInfo>& GetMap(){ //用對象指針做Key,這樣才可以通過對象地址獲得其名字信息,這個時候UClass對象本身其實還沒有名字,要等之后的注冊才能設置進去static TMap<UObjectBase*, FPendingRegistrantInfo> PendingRegistrantInfo; return PendingRegistrantInfo;}
};
//...
struct FPendingRegistrant
{UObjectBase* Object; //對象指針,用該值去PendingRegistrants里查找名字。FPendingRegistrant* NextAutoRegister; //鏈表下一個節點
};
static FPendingRegistrant* GFirstPendingRegistrant = NULL; //全局鏈表頭
static FPendingRegistrant* GLastPendingRegistrant = NULL; //全局鏈表尾
//...
void UObjectBase::Register(const TCHAR* PackageName,const TCHAR* InName)
{//添加到全局單件Map里,用對象指針做Key,Value是對象的名字和所屬包的名字。TMap<UObjectBase*, FPendingRegistrantInfo>& PendingRegistrants = FPendingRegistrantInfo::GetMap();PendingRegistrants.Add(this, FPendingRegistrantInfo(InName, PackageName));//添加到全局鏈表里,每個鏈表節點帶著一個本對象指針,簡單的鏈表添加操作。FPendingRegistrant* PendingRegistration = new FPendingRegistrant(this);if(GLastPendingRegistrant){GLastPendingRegistrant->NextAutoRegister = PendingRegistration;}else{check(!GFirstPendingRegistrant);GFirstPendingRegistrant = PendingRegistration;}GLastPendingRegistrant = PendingRegistration;
}
初看之下肯定會疑惑,為何這里并沒有做一些實際的操作。其實是因為UClass的注冊分成了多步,在static初始化的時候(連main都沒進去呢),甚至到后面CoreUObject模塊加載的時候,UObject對象分配索引的機制(GUObjectAllocator和GUObjectArray)還沒有初始化完畢,因此這個時候如果走下一步去創建各種UProperty、UFunction或UPackage是不合適,創建出來了也沒有合適的地方來保存索引。所以,在最開始的時候,只能先簡單的創建出各UClass*對象(簡單到對象的名字都還沒有設定,更何況填充里面的屬性和方法了),先在內存里把這些UClass*對象記錄一下,等后續對象的存儲結構準備好了,就可以把這些UClass*對象再拉出來繼續構造了。先劇透一下,后續的初始化對象存儲機制的函數調用是InitUObject(),繼續構造的操作是在ProcessNewlyLoadedUObjects()里的。這些信息在后面會被消費用到的,莫急。
所以其實StaticClass究竟做了哪些有用的東西呢,創建了一塊內存返回用來放UClass對象,然后把生成的UClass對象指針以及UClass對應的類的名字信息存儲起來。
這里為啥要用一個TMap加一個鏈表呢
- 是快速查找的需要。在后續的別的代碼(獲取CDO等)里也會經常調用到
UObjectForceRegistration(NewClass),因此常常有通過一個對象指針來查找注冊信息的需要,這個時候為了性能就必須要用字典類的數據結構才能做到O(1)的查找。 - 順序注冊的需要。而字典類的數據結構一般來說內部為了hash,數據遍歷取出的順序無法保證和添加的順序一致,而我們又想要遵循添加的順序來注冊(很合理,早添加進來的是早加載的,是更底層的,處在依賴順序的前提位置。我們前面的SuperClass和WithinClass的訪問也表明了這一點),因此就需要另一個順序數據結構來輔助。
- 那為什么是鏈表而不是數組呢?鏈表比數組優勢的地方也只在于可以快速的中間插入。但是UE源碼里也沒有這個方面的體現,所以其實二者都可以。我在源碼里把注冊結構改為如下用數組也依然可以正常工作。要嘛是他們的代碼寫得也挺啰嗦,要嘛是我沒懂其他的深意。不過倒也無傷大雅。
講完了注冊,接著說GetPrivateStaticClassBody的最后一步:RegisterNativeFunc的調用,同樣以MyClass為例:
//...MyClass.gen.cpp
void UMyClass::StaticRegisterNativesUMyClass()
{UClass* Class = UMyClass::StaticClass(); //這里是可以立即返回值的static const FNameNativePtrPair Funcs[] = { //exec開頭的都是在.generated.h里定義的藍圖用的,暫時不管它,理解為可以調用就行了。{ "AddHP", &UMyClass::execAddHP },{ "CallableFunc", &UMyClass::execCallableFunc },{ "NativeFunc", &UMyClass::execNativeFunc },};FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, ARRAY_COUNT(Funcs));
}
//...void FNativeFunctionRegistrar::RegisterFunctions(class UClass* Class, const FNameNativePtrPair* InArray, int32 NumFunctions)
{for (; NumFunctions; ++InArray, --NumFunctions){Class->AddNativeFunction(UTF8_TO_TCHAR(InArray->NameUTF8), InArray->Pointer);}
}
//...
void UClass::AddNativeFunction(const ANSICHAR* InName, FNativeFuncPtr InPointer)
{FName InFName(InName);new(NativeFunctionLookupTable) FNativeFunctionLookup(InFName,InPointer);
}
而NativeFunctionLookupTable是在UClass里的一個成員變量
//藍圖調用的函數指針原型
typedef void (*FNativeFuncPtr)(UObject* Context, FFrame& TheStack, RESULT_DECL);
/** A struct that maps a string name to a native function */
struct FNativeFunctionLookup
{FName Name; //函數名字FNativeFuncPtr Pointer;//函數指針
};
//...
class COREUOBJECT_API UClass : public UStruct
{
public:TArray<FNativeFunctionLookup> NativeFunctionLookupTable;
}
其實StaticRegisterNativesUMyClass就是把generated.h里的生成的exec函數指針保存到了UClass的成員數組中,為什么這么猴急的需要一開始就往UClass里添加Native函數?
以IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction)為例,execCallMathFunction是定義在代碼里的一個函數,它的地址必然需要通過一種方式記錄下來。當然你也可以像UE4CodeGen_Private做的那樣,先用各種Params對象保存起來,然后在后面合適的時候調用提取來添加。只不過這個時候因為UClass對象都已經創建出來了,所以就索性直接存到NativeFunctionLookupTable里面去了,后續要用的時候再用名字去里面查找。稍微提一下,這里不用TMap而用TArray是因為一般來說我們在一個類里寫的函數數量并不會太多,對于元素比較少的情況下,TArray的線性查找也很快,而且還省內存。
UE4CodeGen_Private中的一堆Construct函數嗎,其實就是先生成一堆類型需要的參數,然后返回對應的類型,這些函數我搜了引擎中都沒找到調用的地方。所以這些函數是在什么情況下會被調用呢?
那些非Native的函數怎么辦?
其實就是指的就是BlueprintImplementableEvent的函數,它不需要我們自己定義函數體。而UHT會幫我們生成一個函數體,當我們在C++里調用ImplementableFunc的時候,其實會觸發一次函數查找,如果在藍圖中有定義該名字的函數,則會得到調用。
//...MyClass.h
UFUNCTION(BlueprintImplementableEvent)
void ImplementableFunc(); //C++不實現,藍圖實現//...MyClass.gen.cpp
void UMyClass::ImplementableFunc()
{ProcessEvent(FindFunctionChecked(TEXT("ImplementableFunc"),NULL);
}
需要提前注意的是,不管是Native與否,函數后面都會生成一個UFunction對象,只不過Native函數的UFunction在綁定的時候會去它所屬于的UClass里的NativeFunctionLookupTable通過函數名字查找真正的函數指針,而非Native的UFunction會把函數指針指向UObject::ProcessInternal,用來處理藍圖虛擬機調用的情況。
其實上一篇,我們基本上用到了兩個數組來存儲信息,一個是TClassCompiledInDefer,用來保存了一個Register函數,以及把類信息存儲到數組中。一個是FCompiledInDefer把構造函數指針存儲到數組中,現在我們又多了一個數組,用來存儲生成的UClass對象指針以及類信息。
這里要區分一下上一篇的Register和這一篇的Register函數,是前者調用后者的關系。并且現在存儲信息的三個數組還沒有發生任何的碰撞!
至于第一個前者具體在哪里調用的呢,是在CoreObject里的UClassRegisterAllCompiledInClasses函數調用的!!! 因為static變量把包含第一個register的結構體放到了一個靜態數組中,想起來了嘛!~
總結
以上是生活随笔為你收集整理的UE4反射机制的通俗理解【生成第一个UClass】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: drg是什么意思 drg具体是什么意思
- 下一篇: Java IO: PipedOutput