首发于UE4 Practice
UE4中的反射之二:运行阶段

UE4中的反射之二:运行阶段

环境

引擎版本: github 4.20

简介

在上一篇文章里主要是针对 UHT 生成的 .generated.h 来分析反射相关的代码展开参与编译的过程。这篇文件要通过 UHT 生成的另外一个文件 .gen.cpp 来分析运行过程中的反射信息的收集使用。使用的例子还是上篇文章中的 AMyActor

在上篇文章中,分析反射代码的入口是 GENERATED_BODY 宏。本篇文章也一样有个重点的入口,那就是 UHT 在 .gen.cpp 中生成的静态全局变量。我们都知道在 C++ 中,全局变量的初始化是先于 main 函数进行的,所以就可以在这个全局变量的构造函数中进行反射信息的注册,最终保存到 UClass 的实例中,供其它地方的代码使用。

IMPLEMENT_CLASS()

.gen.cpp 的最后,有一行代码 IMPLEMENT_CLASS(AMyActor, 1438139441); 这个宏有两个参数,第一个是类名,第二个是类的 CRC 值(主要是用于热加载的,可以先忽略),这个宏的定义如下

// Register a class at startup time.
#define IMPLEMENT_CLASS(TClass, TClassCrc) \
    static TClassCompiledInDefer<TClass> AutoInitialize##TClass(TEXT(#TClass), sizeof(TClass), TClassCrc); \
    UClass* TClass::GetPrivateStaticClass() \
    { \
        static UClass* PrivateStaticClass = NULL; \
        if (!PrivateStaticClass) \
        { \
            /* this could be handled with templates, but we want it external to avoid code bloat */ \
            GetPrivateStaticClassBody( \
            StaticPackage(), \
            (TCHAR*)TEXT(#TClass) + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0), \
            PrivateStaticClass, \
            StaticRegisterNatives##TClass, \
            sizeof(TClass), \
            (EClassFlags)TClass::StaticClassFlags, \
            TClass::StaticClassCastFlags(), \
            TClass::StaticConfigName(), \
            (UClass::ClassConstructorType)InternalConstructor<TClass>, \
            (UClass::ClassVTableHelperCtorCallerType)InternalVTableHelperCtorCaller<TClass>, \
            &TClass::AddReferencedObjects, \
            &TClass::Super::StaticClass, \
            &TClass::WithinClass::StaticClass \
            ); \
        } \
        return PrivateStaticClass; \
    }

这个宏里有两个重点,第一个是定义了一个静态全局变量 TClassCompiledInDefer<AMyActor> AutoInitializeAMyActor。第二个是实现了 GetPrivateStaticClass,这个函数在上一篇中也有提到,DECLARE_CLASS 宏的定义里声明了 GetPrivateStaticClass 的原型,并且有一个 GetClass 静态成员函数,调用了它。这个函数的实现就是在这里定义了。

AMyActor::GetPrivateStaticClass

先来看看这个函数的实现(因为 AutoInitializeAMyActor 这个全局变量最终会调用到这里),这个函数的作用就是构造一个 UClass 对象来保存 AMyActor 这个类的反射信息。这个函数里创建了一个局部静态变量 PrivateStaticClass,在首次调用时进行初始化,之后的调用都是返回首次调用的初始化结果,因为不管 AMyActor 有多少个实例,它们的反射信息都是一样的。

初始化 PrivateStaticClass 是通过调用 GetPrivateStaticClassBody 进行的,这个函数的原型如下

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

几个重要参数说明如下

  • PackageName: 我们构造的 UClass 对象应该放到哪个包里,调用的是 .generated.hDECLARE_CLASS 宏里定义的 StaticPackage 函数
  • Name: 类名,这里就是 MyActor,不含前缀。UE4 里的编码规范规定了类需要一个前缀,比如 A 代表 Actor,并且会将准备废弃的类或者变量加上 DEPRECATED 前缀或后缀,比如 DEPRECATED_AOtherActorint32 Value_DEPRECATED,所以这里的实参需要将类名字符串加 1 去掉类的前缀,如果有废弃的标记,还会再加上 DEPRECATED_ 这个字符串的长度,也就是 11,最终得到真实得类名
  • ReturnClass: 这是个输出参数,传的实参就是局部静态变量 PrivateStaticClass
  • RegisterNativeFunc: 一个回调函数,用来向 UClass 实例里注册 AMyActor 类里可被蓝图调用的 exec 版本函数的信息,在这里传的实参是 StaticRegisterNativesAMyActor,这个函数定义在 UHT 生成的 MyActor.gen.cpp 里,内如如下
void AMyActor::StaticRegisterNativesAMyActor()
{
       UClass* Class = AMyActor::StaticClass();
       static const FNameNativePtrPair Funcs[] = {
           { "MyActorBlueprintCallable", &AMyActor::execMyActorBlueprintCallable },
           { "MyActorBlueprintGetter", &AMyActor::execMyActorBlueprintGetter },
           { "MyActorBlueprintNativeEvent", &AMyActor::execMyActorBlueprintNativeEvent },
           { "MyActorBlueprintPure", &AMyActor::execMyActorBlueprintPure },
           { "MyActorBlueprintSetter", &AMyActor::execMyActorBlueprintSetter },
       };
       FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, ARRAY_COUNT(Funcs));
}
  • InSize: 类大小,这里实参传的是 sizeof(AMyActor)
  • InClassConstructor: 类的默认构造函数,这里实参传的是一个模板函数,内部调用的就是 AMyActor::__DefaultConstructor,这个 __DefaultConstructor 在上篇文章里也提到了,就是 TestCall_Source_TestCall_MyActor_h_12_STANDARD_CONSTRUCTORS 或者 TestCall_Source_TestCall_MyActor_h_12_ENHANCED_CONSTRUCTORS 这个宏里定义的,内部使用 placement new 调用了真正的构造函数,一个是带参的,一个是不带参的默认构造函数
  • InClassAddReferencedObjects: 这个是和 GC 相关的,这里不展开说了
  • InSuperClassFn: 直接父类的 StaticClass 函数指针

GetPrivateStaticClassBody 的内部实现很清晰,如下

  • 调用 GUObjectAllocator.AllocateUObject 来给 UClass 或者 UDynamicClass 对象分配内存
  • 调用 placement new 在前面分配出来的内存上调用 UClass 或者 UDynamicClass 的构造函数来初始化实例
  • 调用 InitializePrivateStaticClass 就是将这个 UClass 实例加入到 GUObjectArray 并且设置为 RootSet,防止被 GC。(部分代码没看懂)
  • 调用 RegisterNativeFunc,在这里就是 StaticRegisterNativesAMyActor,向构造出来的的 UClass 或者 UDynamicClass 对象里注册 AMyActor 类里可被蓝图调用的 exec 版本函数的信息

TClassCompiledInDefer

这个模板类的实现很简单,但是作用很重要,在构造函数里进行类名和类大小的初始化,然后调用了 UClassCompiledInDefer 全局函数,这个函数里大部分代码都是和热加载相关的处理,可以先忽略,最重要的是最后一句

GetDeferredClassRegistration().Add(ClassInfo);

TClassCompiledInDefer<AMyActor> 这个全局变量加入到 DeferredClassRegistration 列表里,在 TClassCompiledInDefer 这个模板类里,还有两个虚函数可以重载,其中最重要的是 Register 这个函数,这个函数的实现是这样的

virtual UClass* Register() const override
{
    LLM_SCOPE(ELLMTag::UObject);
    return TClass::StaticClass();
}

Register 里调用了 StaticClass,所以从这里就可以看出来,引擎会在启动后的某个时机,遍历 DeferredClassRegistration 数组,对每个对象调用 Register 函数,调用对应的 StaticClass,再调用 GetPrivateStaticClass,这就是这个函数被第一次调用的时候,然后构造一个 UClass 对象。所以在这个时候,我们得到了所有类对应的反射信息类对象,但是这些反射对象还没有完全填充好所有的反射信息,只填好了可以被蓝图调用的 C++ exec 版本函数信息,其他信息需要接下来的那个全局变量负责填充

FCompiledInDefer Z_CompiledInDefer_UClass_AMyActor

上面分析完了 MyActor.gen.cpp 里的 IMPLEMENT_CLASS 宏,紧接着这行代码下面一行,还定义了另外一个全局变量

static FCompiledInDefer Z_CompiledInDefer_UClass_AMyActor(Z_Construct_UClass_AMyActor, &AMyActor::StaticClass, TEXT("/Script/TestCall"), TEXT("AMyActor"), false, nullptr, nullptr, nullptr);

FCompiledInDefer 的实现如下

struct FCompiledInDefer
{
    FCompiledInDefer(class UClass *(*InRegister)(), class UClass *(*InStaticClass)(), const TCHAR* PackageName, 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, PackageName, bDynamic, DynamicPathName, InInitSearchableValues);
    }
};

构造函数比较重要的是前两个参数,第一个是一个回调函数指针,这里实参传的是 Z_Construct_UClass_AMyActor,第二个是类的 StaticClass 函数指针,这里实参传的是 &AMyActor::StaticClass,在构造函数里调用了另外一个全局函数 UObjectCompiledInDefer,这个函数里做的事情如下,如果不是一个动态类的话那很简单,直接将 InRegister 回调函数指针放入 DeferredCompiledInRegistration 数组中,供后面统一调用。如果是个动态类的话,就填充好 FDynamicClassStaticData 结构,然后放入 DynamicClassMap 数组中,在后面统一调用。

回过头来看看 Z_Construct_UClass_AMyActor 这个回调里做的事情,其实就是调用 UE4CodeGen_Private::ConstructUClass 函数将 UHT 自动生成好的 UE4CodeGen_Private::FClassParams 结构体填充到 UClass 对象中,这个结构体中重要的有两个

  • FunctionLinkArray,这个数组里填的是每一个被 UFUNCTION 标记的函数对应的 UFunction 创建函数回调和对应的函数名称,在这里传的实参是
const FClassFunctionLinkInfo Z_Construct_UClass_AMyActor_Statics::FuncInfo[] = {
    { &Z_Construct_UFunction_AMyActor_MyActorBlueprintCallable, "MyActorBlueprintCallable" }, // 403128632
    { &Z_Construct_UFunction_AMyActor_MyActorBlueprintGetter, "MyActorBlueprintGetter" }, // 1207006867
    { &Z_Construct_UFunction_AMyActor_MyActorBlueprintImplementableEvent, "MyActorBlueprintImplementableEvent" }, // 2944526560
    { &Z_Construct_UFunction_AMyActor_MyActorBlueprintNativeEvent, "MyActorBlueprintNativeEvent" }, // 140128359
    { &Z_Construct_UFunction_AMyActor_MyActorBlueprintPure, "MyActorBlueprintPure" }, // 4186474183
    { &Z_Construct_UFunction_AMyActor_MyActorBlueprintSetter, "MyActorBlueprintSetter" }, // 3614515892
};

这些 Z_Construct_UFunction_AMyActor_* 函数被调用后,内部调用 UE4CodeGen_Private::ConstructUFunction 来创建一个 UFunction 对象并返回 * PropertyArray,这个数组里填的是所有的被 UPROPERTY 标记的属性,每个属性 UHT 都自动填好了类型,名称,偏移等信息,如下

const UE4CodeGen_Private::FUnsizedIntPropertyParams Z_Construct_UClass_AMyActor_Statics::NewProp_MyActorProperty = { UE4CodeGen_Private::EPropertyClass::Int, "MyActorProperty", RF_Public|RF_Transient|RF_MarkAsNative, (EPropertyFlags)0x0010000000000000, 1, nullptr, STRUCT_OFFSET(AMyActor, MyActorProperty), METADATA_PARAMS(Z_Construct_UClass_AMyActor_Statics::NewProp_MyActorProperty_MetaData, ARRAY_COUNT(Z_Construct_UClass_AMyActor_Statics::NewProp_MyActorProperty_MetaData)) };

终上所述,运行时的反射信息收集简单来说就一句话:MyActor.gen.cpp 里定义了两个全局变量,一个负责构造 UClass 对象,另一个负责往这个构造好的对象中填充 AMyActor 类的反射信息

引擎运行时初始化流程

UE4 引擎有个最核心最底层的模块,就是 CoreUObject,在这个模块的 StartupModule 中,调用了 UClassRegisterAllCompiledInClasses 全局函数,在这个函数里遍历 DeferredClassRegistration 数组调用 Register 函数进行 UClass 的构造,然后清空 DeferredClassRegistration 数组

还有个地方会调用,就是在引擎的 FEngineLoop::PreInit 函数里,在所有的模块都加载完成后,包括自己的游戏模块,依赖的插件等等,调用 ProcessNewlyLoadedUObjects 函数,在这个函数里调用 UClassRegisterAllCompiledInClasses 处理上一次调用到现在加入到 DeferredClassRegistration 中的对象。在 ProcessNewlyLoadedUObjects 还对类和结构体的反射信息进行填充,就是遍历调用第二个全局变量注册的回调函数,并且创建 CDO(Class Default Object) 对象,完整流程图如下


发布于 2018-10-15 20:45