In-depth UE - UObject (5) type system information collection

introduction

In the previous article, we explained the first stage of type system construction: generation. UHT analyzes the macro tags of the source code and generates code containing program meta-information, which is then compiled into the program. When the program starts, the subsequent construction phase of the type system begins. In this article we will introduce the collection stage of type information.

C++ Static automatic registration mode

Another commonly used design pattern in C++: Static Auto Register. Typically, when you want to register some objects in a container or record some information after the program starts, a direct way is to manually call the registration functions one by one after the program starts:

#include "ClassA.h"
#include "ClassB.h"
int main()
{
    ClassFactory::Get().Register<ClassA>();
    ClassFactory::Get().Register<ClassB>();
    [...]
}

The disadvantage of this method is that you must manually include one and then manually register one by one. When you want to continue to add registered items, you can only manually add an entry to the file in sequence, which makes maintainability poor.
Therefore, according to the characteristic that C++ static objects will be initialized before the main function, a static automatic registration mode can be designed. When adding a new registration entry, as long as the corresponding class .h.cpp file is included, main can be automatically started in the program. Automatically perform some operations before the function. The simplified code is roughly as follows:

//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;

In this way, Register(MyClass) will be executed when the program starts, limiting the changes caused by newly added classes to the new file itself. This mode is particularly suitable for some sequence-independent registration behaviors. There are many variations using this static initialization feature. For example, you can declare StaticAutoRegister into a static member variable of MyClass. However, please note that this mode can only be effective in an independent address space. If the file is statically linked and not referenced, static initialization is likely to be bypassed. However, UE avoids this problem because it is all dll dynamic linking, and there is no situation where the static lib then references the lib, and then does not reference the file. Or you can also find a place to force include to trigger static initialization.

UE Static automatic registration mode

This mode is also used in 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); 

or

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);

They are all applications of this pattern. By wrapping the static variable declaration with a macro, a simple automatic registration process can be implemented.

collect

In the above, we introduced in detail the code generation information of Class, Struct, Enum, and Interface. Obviously, the generated code is for use. But before using it, we have to work hard to collect the metadata scattered in various .h.cpp files into the data structure we want and save it for use in the next stage.

To review here, in order to allow newly created classes to not modify the existing code, we chose to decentrally generate its own cpp generation file for each new class - each cpp file has been introduced separately above. Content. But then we face a new problem: the metadata in these cpp files is scattered in various module dlls. We need to use a method to regroup these data. This is the C++ Static automatic we mentioned at the beginning. It’s registration mode. Through this mode, the static objects in each cpp file will all have the opportunity to do something at the beginning of the program, including information collection.

The same is true in UE4. When the program starts, UE uses the Static automatic registration mode to register all class information one by one. And then there is another issue of order. With so many categories, who comes first and who comes last, and how to solve the problem if there is dependence on each other. As we all know, UE organizes the engine structure by modules (the details of modules will be described in later chapters). Each module can be selectively compiled and loaded through script configuration. Among the many modules of the game engine, the player's own Game module is at a relatively advanced level and relies on other more basic modules of the engine. Among these modules, the lowest level is the Core module (the basic library of C++). ), followed by CoreUObject, which is the module that implements the Object type system! Therefore, during the type system registration process, not only the player's Game module must be registered, but also some support classes of CoreUObject itself must be registered.

Many people may be worried about how to ensure the correctness of the order of static initialization of so many modules. In the C++ standard, the order of initialization of global static variables in different compilation units is not clearly defined, so the implementation is completely determined by the compiler itself. The best solution to this problem is to avoid this situation as much as possible. In the design, the variables do not reference and depend on each other. At the same time, some secondary detection methods are used to avoid repeated registration, or a forced reference is triggered to ensure the prefix. The object has been initialized. Currently, on the MSVC platform, the player's Game module is registered first, followed by CoreUObject, and then others. However, this does not matter. As long as the result is correct without relying on the order, the order is not important.

Static collection

After talking about the necessity of collection and the solution to the sequence problem, let's look at the collection of structural information of each category separately. Still following the order of generation above, starting with Class (similar to Interface), then Enum, and then Struct. Then please readers and friends to understand by referring to the generated code above.

Class collection

Expanding Hello.generated.cpp above, we noticed that it contains:

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);

Once again find its definition:

//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);//收集信息
	}
};

It can be seen that the former calls UClassCompiledInDefer to collect the class name, class size, and CRC information, and saves its own pointer for subsequent calls to the Register method. The most important information collected by UObjectCompiledInDefer (not considering dynamic classes for now) is the first function pointer callback used to construct a UClass* object.

Further down we will find that both of them actually just add information records to a static 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;
}

The information collection that will trigger this Class in the entire engine is UCLASS, UINTERFACE, IMPLEMENT_INTRINSIC_CLASS, and IMPLEMENT_CORE_INTRINSIC_CLASS. We have already seen UCLASS and UINTERFACE above, and IMPLEMENT_INTRINSIC_CLASS is used to wrap UModel in the code, and IMPLEMENT_CORE_INTRINSIC_CLASS is used to wrap it. UField, UClass and other engine built-in classes, the latter two also call IMPLEMENT_CLASS internally to implement their functions.
The flow chart is as follows:

Thinking: Why do we need two static initializations, TClassCompiledInDefer and FCompiledInDefer, for registration?
We have also observed that there is a one-to-one correspondence between the two. The question is why two static objects are needed to collect separately, why not combine them into one? The key is that we must first understand the difference between them. The purpose of the former is mainly to provide a Register method of TClass::StaticClass for subsequent use (which will trigger the call of GetPrivateStaticClassBody, thereby creating a UClass* object), while the purpose of the latter It continues to call the constructor, initialize properties and functions and other registration operations on its UClass*. We can simply understand it as the two steps of new object in C++, first allocate memory, and then construct the object on the memory. We will continue to discuss this issue in subsequent registration chapters.

Thinking: Why do you need to delay registration instead of executing it directly in the static callback?
Many people may ask why the static callback first registers the information into the array structure without any other operations. Why not just call the subsequent operations directly in the callback, so that the structure is simpler. This is true, but at the same time we also considered a problem. There are about 1,500 classes in UE4. If more than 1,500 classes are collected and registered during the static initialization phase, then the main function must wait for a while before it can start executing. The performance is that the user double-clicked the program, but there was no response. It took a while before the window opened. Therefore, doing as little as possible in the static initialization callback is to speed up the program startup as quickly as possible. When the window is displayed and the data in the array structure is already there, we can use our tricks, whether it is multi-threading or delay, to greatly improve the program running experience.

Enum collection

Still using the comparison code above, UENUM will generate:

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;

	}
};

In the static phase, a function pointer constructing UEnum* will be registered in the memory for callback:

Note that there is no need to generate a UClass* like UClassCompiledInDefer because UEnum is not a Class and does not have as many functional sets as Class, so it is simpler.

Collection of Structs

For Struct, let’s first look at the code generated in the previous article:

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注册

They are also two static objects. The former FCompiledInDeferStruct continues to register the function pointer into the array structure, and the latter is a bit special. It registers "the corresponding C++ operation class of Struct" in the Map mapping of a structure name and object (explanation later).

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);
}

In addition, by searching the code in the engine, we will also find that for the built-in structures in UE4, such as Vector, its IMPLEMENT_STRUCT(Vector) will also trigger the call of DeferCppStructOps accordingly.

The Struct here is the same as Enum. Because it is not a Class, there is no need for a complicated two-step construction. With FPendingStructRegistrant, the UScriptStruct* object can be constructed in one step; for built-in types (such as Vector), therefore It is not of the type "Script" at all, so there is no need to build UScriptStruct. So how to expose it to BP will be introduced in detail later.
Another thing to note is that the UStruct type will be equipped with an ICppStructOps interface object to manage the construction and destruction of C++struct objects. Its purpose is to how can we create a piece of memory data that has been erased? Correctly construct structure object data or destroy it. At this time, if we can get a unified ICppStructOps* pointer pointing to a type-safe TCppStructOps<CPPSTRUCT> object, we can perform construction and destruction dynamically, polymorphically, and type-safely through the interface function.

Collection of functions

After introducing Class, Enum, and Struct, we also forgot to collect information about some engine built-in functions. We did not introduce this in the previous article because UE has provided us with a BlueprintFunctionLibrary class to register global functions. Some functions defined within the engine are scattered everywhere and need to be collected.
There are mainly two categories:

  • IMPLEMENT_CAST_FUNCTION, define some Object conversion functions

IMPLEMENT_CAST_FUNCTION( UObject, CST_ObjectToBool, execObjectToBool );
IMPLEMENT_CAST_FUNCTION( UObject, CST_InterfaceToBool, execInterfaceToBool );
IMPLEMENT_CAST_FUNCTION( UObject, CST_ObjectToInterface, execObjectToInterface );
  • IMPLEMENT_VM_FUNCTION, defines some functions used by the blueprint virtual machine

IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction);
IMPLEMENT_VM_FUNCTION( EX_True, execTrue );
//……

Then check its definition:

#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);
};

You can also find that there are three static objects that collect the information of these functions and register them into the corresponding structures. The flow chart is:

Among them, FNativeFunctionRegistrar is used to add Native functions to UClass (different from functions defined in blueprints). On the other hand, in the RegisterNativeFunc related functions of UClass, the functions defined in the corresponding Class will also be added to the function table inside UClass. go.

UObject collection

If readers analyze the source code themselves, they will still have a doubt. As the root class of the Object system, how does it trigger the generation of the corresponding UClass* at the very beginning? The answer lies in the initial IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction) call, which will trigger a call to UObject::StaticClass() internally. As the first call, it is detected that UClass* has not been generated, so it will then be forwarded to GetPrivateStaticClassBody. To generate a UClass*.

Summarize

Due to limited space, this article follows the above and discusses how the code-generated information is collected step by step into the data structure in the memory. UE4 uses the static object initialization mode of C++. When the program is initially started, before main , all type metadata, function pointer callbacks, names, CRC and other information are collected. So far, the idea is still very clear. Generate its own cpp file for each type of code (without centrally modifying the existing code), and then use static mode to collect information in each generated cpp file for subsequent use. use. This can be regarded as one of the popular methods for implementing type systems in C++.
In the next stage - registration, we will discuss how UE4 consumes and utilizes this information.

Guess you like

Origin blog.csdn.net/ttod/article/details/133254433