在一起!在一起!

引言

前文中大家演说了体系系统构建的率先个级次:生成。UHT分析源码的宏标记并生成了富含程序元信息的代码,继而编译进度序,在先后启动的时候,伊始起步项目系统的三番五次创设阶段。而本文大家将介绍类型新闻的采集阶段。

C++ Static 自动注册形式

另一种常用的C++常用的设计方式:Static Auto
Register。典型的,当您想要在先后启动后往一个器皿里登记一些对象,或者簿记一些新闻的时候,一种直接的法子是在先后启动后手动的一个个调用注册函数:

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

那种措施的弱点是你必须手动的一个include之后再手动的一个个注册,当要持续丰裕注册的项时,只好再手动的相继序在该公文里丰裕一条条目,可维护性较差。
因而据悉C++
static对象会在main函数从前先导化的特色,能够陈设出一种static自动注册形式,新增加注册条目的时候,只要Include进相应的类.h.cpp文件,就可以活动在先后启动main函数前自行执行一些操作。简化的代码大约如下:

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

那般,在先后启动的时候就会执行Register(MyClass),把因为新添加类而暴发的改观行为限制在了新文件本身,对于部分挨家挨户非亲非故的注册行为那种格局尤其适宜。利用那一个static早先化特性,也有过多少个变种,比如你可以把StaticAutoRegister注解进MyClass的一个静态成员变量也得以。可是注意的是,那种情势只好在单身的地方空间才能一蹴而就,假使该公文被静态链接且没有被引用到的话则很可能会绕过static的开始化。不过UE因为都是dll动态链接,且尚未出现静态lib再引用Lib,然后又不引用文件的场所出现,所以幸免了该难点。或者你也足以找个地点强制的去include一下来触发static伊始化。

UE Static 自动注册格局

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

或者

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

都是对该方式的行使,把static变量表明再用宏包装一层,就足以兑现一个简单的自动注册流程了。

收集

在上文里,大家详细介绍了Class、Struct、Enum、Interface的代码生成的新闻,明显的,生成的就是为了拿过来用的。不过在用之前,大家就还得费力一番,把散乱分布在各样.h.cpp文件里的元数据都采访到我们想要的数据结构里保存,以便下一个等级的接纳。

此间回看一下,为了让新创立的类不改动既有的代码,所以大家选拔了去中央化的为种种新的类生成它自己的cpp生成文件——上文里早已分头介绍每个cpp文件的内容。但是这么大家就随之迎来了一个新题材:这么些cpp文件里的元数据错乱在各类模块dll里,大家要求用一种艺术重新合并那几个多少,那就是我们在一起来就事关的C++
Static自动注册方式了。通过那种方式,每个cpp文件里的static对象在先后一发轫的时候就会所有有机遇去做一些业务,包蕴音讯的征集工作。

UE4里也是这么,在先后启动的时候,UE利用了Static自动注册方式把所有类的新闻都一一登记几次。而随后另一个就是逐一难题了,这么多类,何人先哪个人后,相互若是有依靠该怎么解决。众所周知,UE是以Module来公司引擎结构的(关于Module的细节会在随后章节叙述),一个个Module可以通过脚本配置来选用性的编译加载。在游玩引擎众多的模块中,玩家自己的Game模块是处于相比较高档的层系的,都是凭借于引擎其他更基础底层的模块,而这一个模块中,最最尾部的就是Core模块(C++的基础库),接着就是CoreUObject,正是贯彻Object类型系统的模块!由此在档次系统注册的过程中,不止要登记玩家的Game模块,同时也要登记CoreUObject本身的有些支撑类。

有的是人唯恐会担心那样多模块的静态先河化的一一正确性怎么着保管,在c++标准里,分裂编译单元的大局静态变量的发轫化顺序并不曾明确规定,因而已毕上完全由编译器自己主宰。该难题最好的解决办法是拼命三郎的防止那种景况,在设计上就让各样变量不相互引用看重,同时也采用部分二次检测的不二法门防止再一次登记,或者触发一个威胁引用来保险前置对象已经被开首化达成。近期在MSVC平台上是先登记玩家的Game模块,接着是CoreUObject,接着再其余,可是这实际上无所谓的,只要有限支撑不正视顺序而结果正确,顺序就并不重大了。

Static的收集

在讲完了搜集的要求性和一一难点的化解未来,大家再来分其余看种种品类的协会的音讯的征集。依旧是依照上文生成的逐一,从Class(Interface同理)伊始,然后是Enum,接着Struct。接着请读者朋友们比较着上文的变迁代码来理解。

Class的收集

相比较着上文里的Hello.generated.cpp展开,大家注意到内部有:

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

再次找到其定义:

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

可以见见前者调用了UClassCompiledInDefer来收集类名字,类大小,CRC新闻,并把自己的指针保存进来以便后续调用Register方法。而UObjectCompiledInDefer(现在临时不考虑动态类)最要害的收集的音讯就是率先个用于社团UClass*对象的函数指针回调。

再往下大家会发觉那四头其实都只是在一个静态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;
}

而在所有引擎里会触发此Class的音讯搜集的有UCLASS、UINTERFACE、IMPLEMENT_INTRINSIC_CLASS、IMPLEMENT_CORE_INTRINSIC_CLASS,其中UCLASS和UINTERFACE我们上文已经见识过了,而IMPLEMENT_INTRINSIC_CLASS是用于在代码中包装UModel,IMPLEMENT_CORE_INTRINSIC_CLASS是用以包装UField、UClass等引擎内建的类,后双方内部也都调用了IMPLEMENT_CLASS来兑现效益。
流程图如下:
图片 1

沉凝:为啥须要TClassCompiledInDefer和FCompiledInDefer四个静态先河化来注册?
俺们也着眼到了那两者是各样对应的,难点是干什么要求几个静态对象来分别收载,为什么不合二为一?关键在于大家第一要精通它们二者的不一样之处,前者的目标关键是为继承提供一个TClass::StaticClass的Register方法(其会触发GetPrivateStaticClassBody的调用,进而制造出UClass目的),而后人的目标是在其UClass随身继续调用构造函数,开首化属性和函数等局部注册操作。大家可以不难精晓为就好像C++中new对象的五个步骤,首先分配内存,继而在该内存上构造对象。大家在后续的注册章节里还会一连研讨到那几个难点。

寻思:为啥必要延期注册而不是一向在static回调里进行?
有的是人唯恐会问,为何static回调里都是先把音信注册进array结构里,并从未什么样其余操作,为啥不直接把后续的操作直接在回调里调用了,那样结构反而不难些。是这般没错,不过还要大家也设想到一个题材,UE4里几乎1500几个类,借使都在static初阶化阶段展开1500七个类的搜集注册操作,那么main函数必须得等好一阵子才能开端推行。表现上就是用户双击了程序,没反应,过了好一阵子,窗口才打开。因而static伊始化回调里尽量少的做事情,就是为着赶紧的加快程序启动的快慢。等窗口体现出来了,array结构里多少已经有了,大家就足以施展手脚,三十二线程也好,延迟也好,都得以大大革新程序运行的体会。

Enum的收集

一如既往是上文里的比较代码,UENUM会生成:

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;

    }
};

在static阶段会向内存注册一个协会UEnum的函数指针用于回调:
图片 2
留意到那边并不需求像UClassCompiledInDefer一样先生成一个UClass
,因为UEnum并不是一个Class,并不曾Class那么多效益汇集,所以就相比较简单一些。

Struct的收集

对此Struct,我们先来看上篇里转变的代码:

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

一律是七个static对象,前者FCompiledInDeferStruct继续向array结构里登记函数指针,后者有点分外,在一个结构名和目的的Map映射里登记“Struct相应的C++操作类”(后续解释)。

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

别的的,搜罗引擎里的代码,大家还会意识对于UE4里内建的布局,比如说Vector,其IMPLEMENT_STRUCT(Vector)也会相应的触发DeferCppStructOps的调用。
图片 3
那边的Struct也和Enum同理,因为并不是一个Class,所以并不要求比较繁琐的两步构造,凭着FPendingStructRegistrant就可以继承一步构造出UScriptStruct对象;对于内建的档次(如Vector),因其完全不是“Script”的序列,所以就不必要UScriptStruct的创设,那么其怎么样像BP暴光,大家三番五次再详尽介绍。
还有一些注意的是UStruct类型会配套一个ICppStructOps接口对象来治本C++struct对象的结构和析构工作,其意图就在于一旦对于联合曾经擦除了品种的内存数据,我们怎么能在其上正确的社团结构对象数据仍旧析构。那些时候,假使大家可以拿走一个统一的ICppStructOps
指南针指向项目安全的TCppStructOps<CPPSTRUCT>对象,就可见通过接口函数动态、多态、类型安全的实施协会和析构工作。

Function的收集

在介绍完了Class、Enum、Struct之后,大家还忘记了一部分发动机内建的函数的新闻搜集。大家在前文中并从未介绍到那或多或少是因为UE已经提供了大家一个BlueprintFunctionLibrary的类来注册全局函数。而一些引擎内部定义出来的函数,也是乱套分布在四处,也是急需收集起来的。
着重有那两类:

  • IMPLEMENT_CAST_FUNCTION,定义一些Object的转换函数

    IMPLEMENT_CAST_FUNCTION( UObject, CST_ObjectToBool, execObjectToBool );
    IMPLEMENT_CAST_FUNCTION( UObject, CST_InterfaceToBool, execInterfaceToBool );
    IMPLEMENT_CAST_FUNCTION( UObject, CST_ObjectToInterface, execObjectToInterface );
    
  • IMPLEMENT_VM_FUNCTION,定义一些蓝图虚拟机使用的函数

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

    而随之查其定义:

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

    };

也足以发现有3个static对象收集到那么些函数的音信并注册到对应的构造中去,流程图为:
图片 4
里面FNativeFunctionRegistrar用于向UClass里添加Native函数(分歧于蓝图里定义的函数),另一个方面,在UClass的RegisterNativeFunc相关函数里,也会把相应的Class内定义的函数添加到UClass内部的函数表里去。

UObject的收集

若果读者对象们自己分析源码,还会有一个迷惑,作为Object系统的根类,它是怎么在最开始的时候接触相应UClass的变型呢?答案在最开端的IMPLEMENT_VM_FUNCTION(EX_CallMath,
execCallMathFunction)调用上,其里面会随着触发UObject::StaticClass()的调用,作为最开始的调用,检测到UClass
没有变化,于是接着会转化到GetPrivateStaticClassBody中去生成一个UClass*。
图片 5

总结

因篇幅有限,本文紧接着上文,钻探了代码生成的消息是怎么一步步收集到内存里的数据结构里去的,UE4利用了C++的static对象初始化情势,在程序最初启动的时候,main以前,就采访到了具备的门类元数据、函数指针回调、名字、CRC等音信。到眼前,思路照旧很清晰的,为每一个类代码生成自己的cpp文件(不须求旨化的改动既有代码),进而在其变化的各类cpp文件里用static格局招致一回音讯以便后续的应用。那也终究C++自己已毕项目系统流行套路之一吧。
在下一个等级——注册,大家将商量UE4接下去是什么样消费应用那些音讯的。

引用

UE4.15.1


博客园专栏:InsideUE4
UE4深远学习QQ群:456247757(非新手入门群,请先读书完官方文档和摄像教程)
微信公众号:aboutue,关于UE的全部音讯资讯、技巧问答、小说发表,欢迎关切。
私家原创,未经授权,谢绝转发!

网站地图xml地图