In the previous article, we used static variables to collect constructor pointers and generated class information into a global static array. This article will talk about how we use the collected information to generate our UClass.
At the end of the last article, I talked about implementation_ VM_ Function (ex_callmath, execcallmathfunction) will trigger the call of UObject::StaticClass(), and the first UClass * will be generated here.
Then the implementation of UObject_ Where is class defined? At noexporttypes H file.
This file defines Eunm, Struct and UObject, but does not participate in compilation. It only provides UHT with relevant code to generate reflection information for easy calling elsewhere. Then let's take a look at what StaticClass has done.
//Declared value of class DECLARE_CLASS(UMyClass, UObject, COMPILED_IN_FLAGS(0), CASTCLASS_None, TEXT("/Script/Hello"), NO_API) //Value transfer UClass* UMyClass::GetPrivateStaticClass(const TCHAR* Package) { static UClass* PrivateStaticClass = NULL; //Static variables. You don't have to look for them next time if (!PrivateStaticClass) { /* this could be handled with templates, but we want it external to avoid code bloat */ GetPrivateStaticClassBody( Package, //The package name, TEXT("/Script/Hello"), is used to construct this UClass * in the UPackage (TCHAR*)TEXT("UMyClass") + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0),//Class name, + 1 removes U, A and F prefixes, and + 11 removes Deprecated_ prefix PrivateStaticClass, //Output reference, so the value will be changed StaticRegisterNativesUMyClass, //Register pointers to Native functions of class sizeof(UMyClass), //Class size UMyClass::StaticClassFlags, //Class tag with value CLASS_Intrinsic, which represents the class defined in C + + code UMyClass::StaticClassCastFlags(), //Although it is a call, it is only a simple return value CASTCLASS_None UMyClass::StaticConfigName(), //Configuration file name, which is used to read the value from config (UClass::ClassConstructorType)InternalConstructor<UMyClass>,//Constructor pointer, wrapped one layer (UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<UMyClass>,//hotreload is used to construct the virtual function table, which is ignored for the time being &UMyClass::AddReferencedObjects, //If the static function pointer used by GC to add additional reference objects is not defined, it will call UObject::AddReferencedObjects. The default function body is null. &UMyClass::Super::StaticClass, //Get the function pointer of the base class UClass *, where Super is UObject &UMyClass::WithinClass::StaticClass //Get the function pointer of the object's external class UClass *, which is UObject by default ); } return PrivateStaticClass; }
Basically, it simply passes values to GetPrivateStaticClassBody.
- The Package name is passed in to set the OuterPrivate of the UClass * object as the correct UPackage * object after building UClass *. In UE, UObject must belong to a UPackage. Therefore, the name is passed in to find or create the UPackage object required by the front. The beginning of "/ Script /" indicates that this is a code module.
- The names of the StaticRegisterNativesUMyClass function are spliced with macros, respectively generated.h and Declared and defined in gen.cpp.
- Internalconstructor < umyclass > this template function is to package the C + + constructor, because you can't directly obtain the function pointer of the C + + constructor. Yes generated.h will generate calls to these two macros according to the situation (GENERATED_UCLASS_BODY receives the FObjectInitializer parameter, and GENERATED_BODY does not receive the parameter), so that we can call the constructor of our own class during the construction of UObject * in the future.
- Super refers to the base class of the class, and WithinClass refers to the type of Outer object of the object. What needs to be distinguished here is the difference between the type system and the object system. Super means that the type must rely on the base class to build UClass * before building the subclass UClass *; WithinClass means that after the UObject * is built, it should be limited to which Outer it belongs to. We must build the UClass * of the Outer in advance.
Here are some doubts: when we generate code, we only generate a static constructor wrapper named__ DefaultConstructor, where does the argument InternalConstructor passed in here come from.
InternalConstructor is actually defined in class H, which simply calls the DefaultConstructor.
And how do we call our own constructor in the future construction process?
Next, let's see what GetPrivateStaticClassBody does
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);//Allocate memory ReturnClass = ::new (ReturnClass)UClass //Manually call the constructor in memory with 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);//Initialize UClass * object RegisterNativeFunc();//Register the Native function in UClass }
- Allocate memory. GUObjectAllocator is a global memory allocator that allocates a piece of memory to store UClass objects. As for the stored content, it will be understood here as returning a piece of memory. It should also be noted that ReturnClass is a reference. Once assigned here, it means that the PrivateStaticClass of external static has a value. So even if the GetPrivateStaticClassBody function has not returned, it will return this value immediately if you access UMyClass::StaticClass().
- Call the constructor of UClass. EC here_ Staticconstructor is just a tag that specifies to call a specific overloaded version of the UClass constructor. The constructor is just a simple assignment of member variables, nothing special. The reason for this two-step construction is that UObject's memory is managed uniformly, so it should be allocated by GUObjectAllocator, not directly new like standard C + +.
- When InitializePrivateStaticClass is called, InSuperClassFn() and InWithinClassFn() will be called first, so they will trigger Super::StaticClass() and WithinClass::StaticClass() first, and then load the preceding types in a stack manner.
- RegisterNativeFunc() is the StaticRegisterNativesUMyClass mentioned above. It is called at this moment to add a native function like UClass. Native functions refer to functions implemented in C + +, but the functions in blueprint and blueprintimplementationableevent are not native functions.
Then go inside. It's 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); //Set SuperStruct between classes } else { TClass_PrivateStaticClass->SetSuperStruct(NULL); //UObject has no base class } TClass_PrivateStaticClass->ClassWithin = TClass_WithinClass_StaticClass; //Set Outer class type //... TClass_PrivateStaticClass->Register(PackageName, Name); //Go to UObjectBase::Register() //... }
- Sets the SuperStruct of type. SuperStruct is UStruct* SuperStruct defined in UStruct, which is used to point to the base class of this type.
- Set the value of ClassWithin. That is, limit the type of Outer.
- Call UObjectBase::Register(). Finally, the registration of each UClass * is started, which is worthy of calling the Register name of UClassRegisterAllCompiledInClasses on the chain.
struct FPendingRegistrantInfo { const TCHAR* Name; //Object name const TCHAR* PackageName; //Name of the package to which it belongs static TMap<UObjectBase*, FPendingRegistrantInfo>& GetMap() { //Use the object pointer as the Key, so that the name information can be obtained through the object address. At this time, the UClass object itself actually does not have a name, which can be set only after registration static TMap<UObjectBase*, FPendingRegistrantInfo> PendingRegistrantInfo; return PendingRegistrantInfo; } }; //... struct FPendingRegistrant { UObjectBase* Object; //Object pointer. Use this value to find the name in pendingregistrats. FPendingRegistrant* NextAutoRegister; //Next node in the linked list }; static FPendingRegistrant* GFirstPendingRegistrant = NULL; //Global chain header static FPendingRegistrant* GLastPendingRegistrant = NULL; //Global linked list tail //... void UObjectBase::Register(const TCHAR* PackageName,const TCHAR* InName) { //Add to the global singleton Map, use the object pointer as the Key, and Value is the name of the object and the name of the package. TMap<UObjectBase*, FPendingRegistrantInfo>& PendingRegistrants = FPendingRegistrantInfo::GetMap(); PendingRegistrants.Add(this, FPendingRegistrantInfo(InName, PackageName)); //Added to the global linked list, each linked list node carries a local object pointer, which is a simple linked list addition operation. FPendingRegistrant* PendingRegistration = new FPendingRegistrant(this); if(GLastPendingRegistrant) { GLastPendingRegistrant->NextAutoRegister = PendingRegistration; } else { check(!GFirstPendingRegistrant); GFirstPendingRegistrant = PendingRegistration; } GLastPendingRegistrant = PendingRegistration; }
At first glance, I will wonder why there are no practical operations here. In fact, the registration of UClass is divided into multiple steps, During static initialization (not even main), even when the CoreUObject module is loaded later, the UObject object allocates the index mechanism (GUObjectAllocator and GUObjectArray) have not been initialized yet, so it is not appropriate to go to the next step to create various upproperties, ufunctions or upackages, and there is no suitable place to save the index. Therefore, at the beginning, you can only simply create each UClass * object (it's as simple as the name of the object has not been set, let alone filling in the attributes and methods inside). First record these UClass * objects in memory. When the storage structure of subsequent objects is ready, you can pull these UClass * objects out and continue to construct. First, the following function call to initialize the object storage mechanism is InitUObject(), The operation to continue the construction is in processnewlyloadedobjects(). This information will be consumed later. Don't worry.
So what useful things does StaticClass actually do? It creates a piece of memory and returns it to put the UClass object, and then stores the generated UClass object pointer and the name information of the class corresponding to UClass.
Why use a TMap and a linked list here
- It is the need of fast search. UObjectForceRegistration(NewClass) is often called in other subsequent codes (obtaining CDO, etc.), so it is often necessary to find the registration information through an object pointer. At this time, in order to improve performance, the data structure of dictionary class must be used to find O(1).
- Sequential registration is required. Generally speaking, the data structure of the dictionary class is internally hash, and the order of data traversal and extraction cannot be guaranteed to be consistent with the order of addition, And we want to register according to the order of addition (it is reasonable that those added early are loaded early, which is at the lower level and is in the premise position of dependent order. Our previous access to SuperClass and WithinClass also shows this), so we need another sequential data structure to assist.
- Why is it a linked list instead of an array? The advantage of linked list over array is that it can be inserted quickly. However, this aspect is not reflected in the UE source code, so in fact, both can be. In the source code, I changed the registration structure to the following, and the array can still work normally. Either their code is wordy, or I don't understand other meanings. But it doesn't hurt.
After talking about registration, we will go on to the last step of GetPrivateStaticClassBody: calling RegisterNativeFunc. Take MyClass as an example:
//...MyClass.gen.cpp void UMyClass::StaticRegisterNativesUMyClass() { UClass* Class = UMyClass::StaticClass(); //Here is the value that can be returned immediately static const FNameNativePtrPair Funcs[] = { //The beginning of exec is in generated. The blueprint defined in H is used. Ignore it for the time being. It is understood that it can be called. { "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 is a member variable in UClass
//Function pointer prototype for blueprint call typedef void (*FNativeFuncPtr)(UObject* Context, FFrame& TheStack, RESULT_DECL); /** A struct that maps a string name to a native function */ struct FNativeFunctionLookup { FName Name; //Function name FNativeFuncPtr Pointer;//Function pointer }; //... class COREUOBJECT_API UClass : public UStruct { public: TArray<FNativeFunctionLookup> NativeFunctionLookupTable; }
In fact, StaticRegisterNativesUMyClass is generated The generated exec function pointer in H is saved in the member array of UClass. Why is it so urgent to add the Native function to UClass at the beginning?
With IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction) as an example, execCallMathFunction is a function defined in the code, and its address must be recorded in a way. Of course, you can also like ue4codegen_ As private does, first save it with various Params objects, and then call the extraction to add it later when appropriate. But at this time, because the UClass objects have been created, they are simply stored directly in the NativeFunctionLookupTable. Later, when you need to use it, you can use the name to find it. To mention a little, we use TArray instead of TMap because generally speaking, we don't write too many functions in a class. When there are few elements, TArray's linear search is also fast and saves memory.
UE4CodeGen_ A bunch of Construct functions in private are actually a bunch of parameters required by types, and then return the corresponding types. I searched the engine for these functions and couldn't find the place to call. So when will these functions be called?
What about those non Native functions?
In fact, it refers to the blueprintimplementationableevent function, which does not require us to define the function body ourselves. UHT will help us generate a function body. When we call implementationablefunc in C + +, it will actually trigger a function search. If there is a function with that name defined in the blueprint, it will be called.
//...MyClass.h UFUNCTION(BlueprintImplementableEvent) void ImplementableFunc(); //C + + is not implemented, but the blueprint is implemented //...MyClass.gen.cpp void UMyClass::ImplementableFunc() { ProcessEvent(FindFunctionChecked(TEXT("ImplementableFunc"),NULL); }
It should be noted in advance that whether it is Native or not, a uffunction object will be generated behind the function, but when binding, the uffunction of the Native function will go to the NativeFunctionLookupTable in the UClass to which it belongs to find the real function pointer through the function name, while the non Native uffunction will point the function pointer to UObject::ProcessInternal, Used to handle blueprint virtual machine calls.
In fact, in the last article, we basically used two arrays to store information. One is TClassCompiledInDefer, which is used to save a Register function and store class information in the array. One is FCompiledInDefer, which stores the constructor pointer in the array. Now we have another array to store the generated UClass object pointer and class information.
Here we need to distinguish between the Register function in the previous article and the Register function in this article. The former calls the latter. And now there is no collision between the three arrays storing information!