UE4中反射信息收集

信息收集分析,static自动注册

Posted by bbkgl on February 12, 2021

千里共如何

微风吹兰杜

上一篇中已经展开了类型生成文件中的宏,并较为详细地分析了生成代码中的类型注册信息。这里接着上面的三个疑问,继续进行探讨。

类型信息静态自动收集

C++ 静态自动注册

这里说的静态自动注册,指的是通过定义静态全局变量的方式,直接在该变量的构造函数里完成注册。

举个简单的例子,如果想在 main() 函数执行前,自动启动部分功能,使得 main() 函数依赖的部分逻辑能提前准备好,则可以将这部分逻辑写在static变量的构造函数中。

#include <iostream>

class A {
public:
    bool ready;

    A() {
        ready = true;
    }
};

const static A a;

int main() {
    if (a.ready) {
        printf("OK\n");
    } else {
        printf("Not ready\n");
        exit(-1);
    }
    return 0;
}

程序输出:

1613141414389

UE收集类型信息(反射信息)也是采用同样的方式,在 UE4中C++类型生成文件分析 中已经提到两个静态全局变量:

static TClassCompiledInDefer<UHH> AutoInitializeUHH(TEXT("UHH"), sizeof(UHH), 1368286490); 

static FCompiledInDefer Z_CompiledInDefer_UClass_UHH(Z_Construct_UClass_UHH, &UHH::StaticClass, TEXT("/Script/CCReflection"), TEXT("UHH"), false, nullptr, nullptr, nullptr);

就是出于同样的目的,当然生成代码中随处可见这样的使用姿势。

1613142628061

需要说明的是,不应该依赖于不同文件中的静态变量的初始化顺序,因为C++标准中没有规定不同文件中静态变量的初始化顺序,这样一旦出现bug,将极难定位问题,因为不同编译器甚至同种但不同版本的编译器的处理都不一样(菜鸡如我就查过此类问题)。

何处生成UClass 对象

继续探讨在 UE4中C++类型生成文件分析 中的三个疑问:

  1. ` ClassParams 最终用于生成 UClass 对象吗?是的话,什么时候生成,在哪里生成?与调用 StaticClass() 返回 UClass` 对象是什么关系?
  2. 函数指针的收集是如何完成的?
  3. 如何利用两个全局静态变量的构造函数?

现在看来这三个问题很可能是同一问题,继续带着疑问去看静态自动注册的过程。之前已经提到,Z_Construct_UClass_UHH_Statics 结构体的信息会被注册到ClassParams 静态成员中,ClassParams 的处理就决定了信息收集的下一步走向。

ClassParams 静态成员实际会用于在 Z_Construct_UClass_UHH() 函数中生成 UClass 对象。

UClass* Z_Construct_UClass_UHH()
{
	static UClass* OuterClass = nullptr;
	if (!OuterClass)
	{
		UE4CodeGen_Private::ConstructUClass(OuterClass, Z_Construct_UClass_UHH_Statics::ClassParams);
	}
	return OuterClass;
}

不巧的是,StaticClass() -> GetPrivateStaticClass() 也会生成一个 UClass 对象。

UClass* UHH::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("UHH") + 1 + ((StaticClassFlags & CLASS_Deprecated) ? 11 : 0), 
			PrivateStaticClass, 
			...
		); 
	} 
	return PrivateStaticClass; 
}

这里 PrivateStaticClassOuterClass 是同一个吗?

其实在 UE4中C++类型生成文件分析 中已经有埋坑了,生成 ClassParams 静态对象的时候就已经把StaticClass()指针传进去了,分析引擎中 UE4CodeGen_Private::ConstructUClass() 实现,就发现会先调用 StaticClass() 得到一个 UClass 对象,然后再进行值的注册。不过我猜测, StaticClass() 的第一次调用并不是在这,这里只要确定 PrivateStaticClassOuterClass 是同一个就好了。

1613145217043

延迟注册

UE4反射类型信息延迟注册

继续研究 TClassCompiledInDeferFCompiledInDefer

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
	{
        LLM_SCOPE(ELLMTag::UObject);
		return TClass::StaticClass();
	}
	virtual const TCHAR* ClassPackage() const override
	{
		return TClass::StaticPackage();
	}
};

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

进一步查看 UClassCompiledInDefer()UObjectCompiledInDefer() 的实现:

void UClassCompiledInDefer(FFieldCompiledInInfo* ClassInfo, const TCHAR* Name, SIZE_T ClassSize, uint32 Crc)
{
    ...
    GetDeferredClassRegistration().Add(ClassInfo); // 得到static对象,数组
    ...
}

void UObjectCompiledInDefer(UClass *(*InRegister)(), UClass *(*InStaticClass)(), const TCHAR* Name, const TCHAR* PackageName, bool bDynamic, const TCHAR* DynamicPathName, void (*InInitSearchableValues)(TMap<FName, FName>&))
{
    ...
	TArray<UClass *(*)()>& DeferredCompiledInRegistration = GetDeferredCompiledInRegistration();  // 得到static对象
    checkSlow(!DeferredCompiledInRegistration.Contains(InRegister));
    DeferredCompiledInRegistration.Add(InRegister);  // static
	...
}

按照顺序分析,可以总结出TClassCompiledInDeferFCompiledInDefer 静态初始化的大致作用:

  • 前者将自己本身注册到一个数组中,并提供了 Register() 方法返回 UClass 对象(可能是创建 UClass 对象)
  • 后者将前面说到的指向 Z_Construct_UClass_UHH() 函数的指针,添加到了一个数组中,用于在可能已经创建的UClass 对象上,注册之前已经收集好的反射类型信息( ClassParams

以目前收集到的资料以及我对源码的理解,解释下这样做的目的:

  1. 就像之前说到的,Z_Construct_UClass_UHH() 中不一定会创建 UClass 对象,而 FCompiledInDefer 作用就是将 Z_Construct_UClass_UHH() 作为回调注册到数组中,Z_Construct_UClass_UHH() 就负责添加反射信息到 UClass 对象上;而创建 UClass 对象的工作,应该是由 TClassCompiledInDefer 提供的 Register() 完成的;
  2. 延迟注册是因为UE4中要注册的 UClass 对象较多,如果全部要在 main() 函数前注册完成,会导致编辑器实际逻辑不能立即执行,会让用户觉得启动很慢。所以先把注册的回调都传入到数组中,在main() 函数开始执行后,再调用注册回调执行相应的类型信息注册逻辑。

进行一下总结,就可以解答之前的三个疑问了:

  1. ` ClassParams 应该是用于在已经生成的 UClass 对象上注册信息,而不是生成 UClass 对象; UClass 对象由 TClassCompiledInDefer 提供的 Register() 来生成,而 Register()` 会在编辑器的启动控制下延迟调用
  2. 函数指针的收集,由 TClassCompiledInDefer 提供的 Register() 调用 StaticClass() 注册, Register() 调用时机前面已经解释了
  3. 见对 TClassCompiledInDeferFCompiledInDefer 静态初始化的分析

类型信息收集流程梳理

回忆前面的生成文件的分析,可以将整个收集过程整理一下(属性和函数信息的收集)。

  • 属性信息
    • 定义 UE4CodeGen_Private::FUnsizedIntPropertyParams 类型的静态成员
    • 将静态成员添加到 UE4CodeGen_Private::FPropertyParamsBase* 类型的指针的数组中
    • 指针数组作为参数,生成 UE4CodeGen_Private::FClassParams 类型的 ClassParams
    • ClassParams 作为参数,用于给 UClass 收集信息,该收集动作发生于 Z_Construct_UClass_XXX() 函数的调用
    • FCompiledInDefer 类型的 Z_CompiledInDefer_UClass_XXX 静态初始化的时候,将Z_Construct_UClass_XXX() 的函数指针传入,并将该指针添加到 DeferredCompiledInRegistration 数组中
  • 函数信息
    • 函数指针收集
      • 定义统一签名的函数 execXXX() ,并在其实现中调用实际 XXX() 函数
      • 提供 XXX::StaticRegisterNativesXXX() 函数,将函数 execXXX() 的名字指向指针的映射添加到了对应 UClass 对象的 NativeFunctionLookupTable 的数组成员中
      • XXX::StaticRegisterNativesXXX 函数也会通过函数指针,传入 GetPrivateStaticClassBody() 函数,用于创建 XXX 对应的 UClass 对象
      • 在第一次调用 UHH::StaticClass() 的时候会真正执行 ”映射添加到 NativeFunctionLookupTable 的数组成员中“ 的逻辑
      • 这里猜测:第一次调用 UHH::StaticClass() 应该是由引擎启动时,调用 TClassCompiledInDefer 提供的 Register() 完成的
    • 函数签名信息的收集(用于生成 UFunction 对象)
      • 参数和返回值生成各自的 UE4CodeGen_Private::FUnsizedIntPropertyParams 类型的静态成员
      • 函数的参数和返回值的静态成员,通过指针存储到 UE4CodeGen_Private::FPropertyParamsBase* 类型的 PropPointers 静态指针数组中
      • 静态指针数组将用于静态初始化 UE4CodeGen_Private::FFunctionParams 类型的 FuncParams 静态成员
      • FuncParams 静态成员在 Z_Construct_UFunction_XXX_XXXX() 函数中用于生成 UFunction 对象
      • Z_Construct_UFunction_XXX_XXXX() 函数同样以函数指针的形式添加到 FClassFunctionLinkInfo 类型的 FuncInfo 数组中
      • FuncInfo 数组用于生成 UE4CodeGen_Private::FClassParams 类型的 ClassParams
      • ClassParams 作为参数,用于给 UClass 收集信息,该收集动作发生于 Z_Construct_UClass_XXX() 函数的调用
      • FCompiledInDefer 类型的 Z_CompiledInDefer_UClass_UHH 静态初始的时候,将Z_Construct_UClass_XXX() 的函数指针传入,并将该指针添加到 DeferredCompiledInRegistration 数组中

联系延迟注册的内容,可以将类型信息注册( UClass 完整注册)分成两部分

  • 生成类对应的 UClass 对象
  • 收集所有反射信息,在已存在的 UClass 对象完成信息填充

需要注意的是,Native 函数指针收集在生成 UClass 对象时进行,即第一次调用 StaticClass() 函数的时候就执行了。

生成UClass 对象的流程:

1613225008415

UClass 对象上填充信息的流程:

1613225080469

现在明确了 UClass 对象创建的方式,创建的位置;同时也明确了往 UClass 填充信息的方式,填充的位置。而未确定的是执行这两个动作的时间,从之前的分析来看,应该在编辑器启动以后,也就是 mian() 函数开始执行以后。