在一起!在一起!

引言

前文中我们阐述了型系统构建的第一只级次:生成。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地图