参考:

  1. https://zhuanlan.zhihu.com/p/662720305
  2. https://zhuanlan.zhihu.com/insideue4
  3. https://blog.csdn.net/qq_29523119/article/details/119420238
  4. https://cloud.tencent.com/developer/article/1606872
  5. https://www.jianshu.com/p/c335c3f6cc04
  6. UE5引擎源码小记 —反射信息注册过程

前言

对于 UE 的使用与深入研究而言,反射始终是一个绕不开的话题,在 之前的文章 中也简单介绍过反射的概念以及 UE 中反射的实现,但当时毕竟刚入坑 UE,很多知识点都只是一知半解而已,本节将以一个更深入的视角来分析 UE 中反射的实现。

反射

首先需要说明的还是反射的概念,反射(Reflection)指的是在程序运行时检查、获取和操作其自身的信息,包括数据类型、方法、属性等。反射允许程序在运行时获取类的结构信息,而不需要在编译时就静态地知道这些信息。

在具有反射支持的编程语言中,开发人员可以使用反射来动态地创建对象、调用方法、访问属性,甚至修改类的结构。这提供了一种更灵活的编程方式,特别是在处理未知类型、动态加载类或进行元编程(metaprogramming)时。

总的来说就是一种在运行时遍历与获取类的成员变量,类的成员函数,可以通过名字运行时访问以及调用类的各种数据的能力。

作为 UE 中的基础技术,它相当有用,增强了众多的系统比如编辑器中的属性面板,对象序列化,网络对象传输以及蓝图脚本和C++之间的通信等。

其实在理解反射时,最困扰博主的还是反射到底与属性面板,对象序列化,网络对象传输等功能有什么关系,查阅了相关资料后有了一个初步的结论:首先,我们在 UE 的 Editor 中点击了一个对象,如何在 detail panel 中显示出该对象的类型与属性等数据呢?这显然是通过反射实现的;其次,在序列化与反序列化一个对象时,为了保证字节流能够完整恢复成有意义的对象,确实需要保证能够在运行时就获得一个对象的类型、类型中的变量、方法等数据;而序列化又是网络传输的基础,因此反射对于网络对象传输也是必要的;至于蓝图与 C++ 的通信,则更是建立在完整的反射机制之上。

C++ 是没有反射机制的,或者说仅有一种不完整的反射机制 —— RTTI(Run-Time Type Information)。在 C++ 中,RTTI 通过 typeid 运算符和 dynamic_cast 运算符实现。typeid 运算符返回一个 std::type_info 对象,表示对象的类型信息。dynamic_cast 运算符用于安全地进行类型转换。与一些其他语言相比,C++ 的 RTTI 功能相对较有限,主要限于类型信息的获取和安全类型转换。

也正因如此,UE 才自己造了一套轮子,通过它来收集,查询和修改C++中的类,结构,函数,成员变量和枚举的信息。

UE 采用工具生成代码的方式来实现 C++ 的反射。原理是利用特殊的宏来对变量做标记,再对 C++ 代码文件进行语法分析,识别出特殊的宏,提取出对应的数据,然后生成代码。在初始化时运行生成的代码,将收集到的数据保存。​

image.png

案例分析

我们从一个简单的案例入手,分析 UE 的反射到底是这么做的。UE版本 5.0.3

UE 不同版本之间的反射生成代码差异还是挺大的,分析时要注意版本情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "MyObject.generated.h"

UCLASS()
class MYPROJECT_API UMyObject : public UObject
{
GENERATED_BODY()

public:
UFUNCTION()
void ffffffunc() {};

UPROPERTY()
int pppppprop;
};

这里故意给成员函数与变量起了一个怪异的名字,这样分析生成的代码时找起来就很方便(

UCLASS()

1
2
3
4
5
#if UE_BUILD_DOCS || defined(__INTELLISENSE__ )
#define UCLASS(...)
#else
#define UCLASS(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_PROLOG)
#endif

UCLASS() 是一个宏,用于标记将要被 UHT 扫描的类,可以发现 UCLASS 这个宏仅仅是将三个字符串拼接起来。CURRENT_FILE_ID 代表一个唯一的文件id,_LINE_为当前行号;至于 _PROLOG,就是字面意思。

最后这个 UCLASS 会被替换成空行,我猜想是 UCLASS 宏内的参数在UHT运行的过程中被替换成 flag 之类的东西,所以这后续编译时候就没用了。

接着我们来看看UHT生成genrated.h文件可以发现这样的一个宏:

1
2
3
4
#define FID_MyProject_Source_MyProject_MyObject_h_9_PROLOG

#undef CURRENT_FILE_ID
#define CURRENT_FILE_ID FID_MyProject_Source_MyProject_MyObject_h

发现确实在最后UCLASS这个宏会被替换成空行。

GENERATED_BODY()

GENERATED_BODY() 也是一个宏,UE 通过 UHT 生成的代码都会通过这个宏从这里插入到类中,查看该宏的定义(位于objectMacros.h):

1
2
3
4
5
6
7
8
// This pair of macros is used to help implement GENERATED_BODY() and GENERATED_USTRUCT_BODY()
#define BODY_MACRO_COMBINE_INNER(A,B,C,D) A##B##C##D
#define BODY_MACRO_COMBINE(A,B,C,D) BODY_MACRO_COMBINE_INNER(A,B,C,D)

// Include a redundant semicolon at the end of the generated code block, so that intellisense parsers can start parsing
// a new declaration if the line number/generated code is out of date.
#define GENERATED_BODY_LEGACY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY_LEGACY);
#define GENERATED_BODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY);

可以看到,这个宏的作用只是把一些名字连到了一起,具体来说,是将 CURRENT_FILE_ID, _, __LINE__, _GENERATED_BODY 四个名字连到了一起,即等同于 文件id_行号_GENERATED_BODY

打开 UE 生成的 MyObject.generated.h,可以看到里面有这样的一个宏:

1
2
3
4
5
6
7
8
9
#define FID_MyProject_Source_MyProject_MyObject_h_12_GENERATED_BODY \
PRAGMA_DISABLE_DEPRECATION_WARNINGS \
public: \
FID_MyProject_Source_MyProject_MyObject_h_12_SPARSE_DATA \
FID_MyProject_Source_MyProject_MyObject_h_12_RPC_WRAPPERS_NO_PURE_DECLS \
FID_MyProject_Source_MyProject_MyObject_h_12_INCLASS_NO_PURE_DECLS \
FID_MyProject_Source_MyProject_MyObject_h_12_ENHANCED_CONSTRUCTORS \
private: \
PRAGMA_ENABLE_DEPRECATION_WARNINGS

FID_MyProject_Source_MyProject_MyObject_h_12_GENERATED_BODY 正是 GENERATED_BODY() 展开后的样子,而 12 正是MyObject.hGENERATED_BODY() 所在的行数。这个宏里面又包含了四个宏的定义,我们依次来看:

1
2
3
4
5
6
7
8
9
#define FID_MyProject_Source_MyProject_MyObject_h_12_SPARSE_DATA
#define FID_MyProject_Source_MyProject_MyObject_h_12_RPC_WRAPPERS \
\
DECLARE_FUNCTION(execffffffunc);


#define FID_MyProject_Source_MyProject_MyObject_h_12_RPC_WRAPPERS_NO_PURE_DECLS \
\
DECLARE_FUNCTION(execffffffunc);

SPARSE_DATA 为稀疏数据的定义,WRAPPERS_NO_PURE_DECLSUFunction 的定义,此处可以看出都只与 ffffffunc 有关,DECLARE_FUNCTION 也是一个宏,下文再做分析。

至于 FID_MyProject_Source_MyProject_MyObject_h_12_INCLASS_NO_PURE_DECLS 这个宏,它比较重要:

1
2
3
4
5
6
7
#define FID_MyProject_Source_MyProject_MyObject_h_12_INCLASS_NO_PURE_DECLS \
private: \
static void StaticRegisterNativesUMyObject(); \
friend struct Z_Construct_UClass_UMyObject_Statics; \
public: \
DECLARE_CLASS(UMyObject, UObject, COMPILED_IN_FLAGS(0), CASTCLASS_None, TEXT("/Script/MyProject"), NO_API) \
DECLARE_SERIALIZER(UMyObject)

我们居然发现了一个友元!要知道,GENERATED_BODY() 可是直接插入在类内的,因此该友元类可以访问类中的所有信息,这个类就是完成反射的类了。可以看出,UE 为每个类生成了一个专门的反射数据收集类,以友元类的方式来访问类信息。

最后一个宏定义了类的一些构造函数,如标准构造函数,会在反射完成后进行类的初始化。此外,这个宏也禁止了移动构造与拷贝构造的调用。

1
2
3
4
5
6
7
8
9
10
11
#define FID_MyProject_Source_MyProject_MyObject_h_12_ENHANCED_CONSTRUCTORS \
/** Standard constructor, called after all reflected properties have been initialized */ \
NO_API UMyObject(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; \
private: \
/** Private move- and copy-constructors, should never be used */ \
NO_API UMyObject(UMyObject&&); \
NO_API UMyObject(const UMyObject&); \
public: \
DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, UMyObject); \
DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(UMyObject); \
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(UMyObject)

Z_Construct_UClass_UMyObject_Statics

上文我们分析出,UE 为每个类生成了一个专门的反射数据收集类,以友元类的方式来访问类信息,现在就来分析一下这个类。

其实是 struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Z_Construct_UClass_UMyObject_Statics
{
static UObject* (*const DependentSingletons[])();
static const FClassFunctionLinkInfo FuncInfo[];
#if WITH_METADATA
static const UECodeGen_Private::FMetaDataPairParam Class_MetaDataParams[];
#endif
#if WITH_METADATA
static const UECodeGen_Private::FMetaDataPairParam NewProp_pppppprop_MetaData[];
#endif
static const UECodeGen_Private::FUnsizedIntPropertyParams NewProp_pppppprop;
static const UECodeGen_Private::FPropertyParamsBase* const PropPointers[];
static const FCppClassTypeInfoStatic StaticCppClassTypeInfo;
static const UECodeGen_Private::FClassParams ClassParams;
};

这个数据类只是负责收集原始类的信息。注意到全都是 static 的变量。会通过 static 提前初始化的特性来直接生成类参数。举例来说,FuncInfo[] 中存放收集到的函数相关信息,PropPointers[] 中存放属性相关信息,其他信息类似。

最后将所有的信息都会存放在 ClassParams 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const UECodeGen_Private::FClassParams Z_Construct_UClass_UMyObject_Statics::ClassParams = {
&UMyObject::StaticClass,
nullptr,
&StaticCppClassTypeInfo,
DependentSingletons,
FuncInfo, // 函数信息
Z_Construct_UClass_UMyObject_Statics::PropPointers, // 属性信息
nullptr,
UE_ARRAY_COUNT(DependentSingletons),
UE_ARRAY_COUNT(FuncInfo),
UE_ARRAY_COUNT(Z_Construct_UClass_UMyObject_Statics::PropPointers),
0,
0x001000A0u,
METADATA_PARAMS(Z_Construct_UClass_UMyObject_Statics::Class_MetaDataParams, UE_ARRAY_COUNT(Z_Construct_UClass_UMyObject_Statics::Class_MetaDataParams))
};

这些类信息,会通过调用 StaticClass 传给反射系统:更进一步地,是在 GetPrivateStaticClass() 完成的这些工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/** Returns a UClass object representing this class at runtime */ \
inline static UClass* StaticClass() \
{ \
return GetPrivateStaticClass(); \
} \

// Implement the GetPrivateStaticClass and the registration info but do not auto register the class.
// This is primarily used by UnrealHeaderTool
#define IMPLEMENT_CLASS_NO_AUTO_REGISTRATION(TClass) \
FClassRegistrationInfo Z_Registration_Info_UClass_##TClass; \
UClass* TClass::GetPrivateStaticClass() \
{ \
if (!Z_Registration_Info_UClass_##TClass.InnerSingleton) \
{ \
/* 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), \
Z_Registration_Info_UClass_##TClass.InnerSingleton, \
StaticRegisterNatives##TClass, \
sizeof(TClass), \
alignof(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 Z_Registration_Info_UClass_##TClass.InnerSingleton; \
}

更进一步分析就涉及到类的注册等内容了,关于注册的内容可能会放到下一节或本节的后面说明。

UFUNCTION()

1
2
3
4
5
6
7
8
9
10
11
12
// ObjectMacros.h

// These macros wrap metadata parsed by the Unreal Header Tool, and are otherwise
// ignored when code containing them is compiled by the C++ compiler
#define UPROPERTY(...)
#define UFUNCTION(...)
#define USTRUCT(...)
#define UMETA(...)
#define UPARAM(...)
#define UENUM(...)
#define UDELEGATE(...)
#define RIGVM_METHOD(...)

可以看出,UFUNCTION()UPROPERTY() 等一些宏的定义完全是空定义,个人猜测这些标识只对 UHT 扫描文件有作用,仅用于标识哪些函数或属性需要反射,在反射代码生成完毕后这些宏就没有作用了。

但我们更好奇的还是一个函数如何被反射的,因此还是来分析 .generated.h.gen.cpp

反射函数调用

首先需要注意的是,UHT 会把 UFUNCTION() 标记的函数先进行一次包装,如 ffffffunc,在 .gen.cpp 中可以看到这样的定义:

1
2
3
4
5
6
7
DEFINE_FUNCTION(UMyObject::execffffffunc)
{
P_FINISH;
P_NATIVE_BEGIN;
P_THIS->ffffffunc();
P_NATIVE_END;
}

这里面的宏都定义在 ObjectMacros.hScriptMacros.h 中,将一些宏替换后,就是这个样子:

1
2
3
4
5
6
7
void UMyObject::execffffffunc( UObject* Context, FFrame& Stack, RESULT_DECL )
{
Stack.Code += !!Stack.Code; /* increment the code ptr unless it is null */
P_NATIVE_BEGIN;
((UMyObject*)(Context))->ffffffunc();
P_NATIVE_END;
}

P_NATIVE_BEGIN;P_NATIVE_END; 应该与一些计时的功能有关系,没太看懂,就不分析了。

Context 其实就是执行 UMyObject 实体,参数类型为 UObject,但是实际上是 UMyObject,这里利用的 UObject 做类型擦除,就和 void* 一样。 Stack 为参数,比较好理解,由于这里是无参的函数,因此生成的代码与 Stack 也没什么关系。

这个函数会由对应的UFunction::Invoke函数调用,完成反射函数的调用。

在对函数进行包装完后,就会将它们都添加到该类的函数列表中,之后再进行注册。

1
2
3
4
5
6
7
8
void UMyObject::StaticRegisterNativesUMyObject()
{
UClass* Class = UMyObject::StaticClass();
static const FNameNativePtrPair Funcs[] = {
{ "ffffffunc", &UMyObject::execffffffunc },
};
FNativeFunctionRegistrar::RegisterFunctions(Class, Funcs, UE_ARRAY_COUNT(Funcs));
}

可以看出函数的信息是以 FNameNativePtrPair 这个结构存储的,具体而言就是一个字符串与一个函数指针的键值对,这样通过名称就可以找到对应的函数。

1
2
3
4
5
6
// This class is deliberately simple (i.e. POD) to keep generated code size down.
struct FNameNativePtrPair
{
const char* NameUTF8;
FNativeFuncPtr Pointer;
};

函数形参收集

继续分析 .gen.cpp 可以看到 UHT 生成了一个结构体,用于函数形参的收集:

1
2
3
4
5
6
7
struct Z_Construct_UFunction_UMyObject_ffffffunc_Statics
{
#if WITH_METADATA
static const UECodeGen_Private::FMetaDataPairParam Function_MetaDataParams[];
#endif
static const UECodeGen_Private::FFunctionParams FuncParams;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const UECodeGen_Private::FFunctionParams 
Z_Construct_UFunction_UMyObject_ffffffunc_Statics::FuncParams = {
(UObject*(*)())Z_Construct_UClass_UMyObject, // (*OuterFunc)()
nullptr, // (*SuperFunc)()
"ffffffunc", // NameUTF8
nullptr, // OwningClassName
nullptr, // DelegateName
0, // StructureSize
nullptr, // PropertyArray
0, // NumProperties
RF_Public|RF_Transient|RF_MarkAsNative, // ObjectFlags
(EFunctionFlags)0x00020401, // FunctionFlags
0, // RPCId
0, // RPCResponseId
METADATA_PARAMS(Z_Construct_UFunction_UMyObject_ffffffunc_Statics::Function_MetaDataParams, // MetaDataArray
UE_ARRAY_COUNT(Z_Construct_UFunction_UMyObject_ffffffunc_Statics::Function_MetaDataParams)) // NumMetaData
};

由于案例中的 ffffffunc 是一个比较简单的函数,因此 FuncParams 中的很多参数都是 nullptr 或 0。

函数生成 & 存储

接着通过 UECodeGen_Private::ConstructUFunction 利用上述得到的 FuncParams 构造一个 UFUNCTION,这里就不进一步分析 ConstructUFunction 了,那样就没完没了了。

1
2
3
4
5
6
7
8
9
UFunction* Z_Construct_UFunction_UMyObject_ffffffunc()
{
static UFunction* ReturnFunction = nullptr;
if (!ReturnFunction)
{
UECodeGen_Private::ConstructUFunction(&ReturnFunction, Z_Construct_UFunction_UMyObject_ffffffunc_Statics::FuncParams);
}
return ReturnFunction;
}

最后就是将函数信息存储到上文提到的 FuncInfo 中,

1
2
3
const FClassFunctionLinkInfo Z_Construct_UClass_UMyObject_Statics::FuncInfo[] = {
{ &Z_Construct_UFunction_UMyObject_ffffffunc, "ffffffunc" }, // 1947785863
};

这里 FClassFunctionLinkInfo 也是一种键值对的结构,包含了一个 CreateFuncPtr,与一个函数名。

1
2
3
4
5
struct FClassFunctionLinkInfo
{
UFunction* (*CreateFuncPtr)();
const char* FuncNameUTF8;
};

最后函数的信息也随着 ClassParams 一起传递到反射系统中。

UPROPERTY()

UPROPERTY() 的收集比 UFUNCTION() 简单一些,毕竟没有调用与形参等内容。

观察 Z_Construct_UClass_UMyObject_Statics 可以发现:首先,UHT 会根据原始的类属性进行包装,生成一个新的属性,如 UMyObject 中原始的属性名为 pppppprop,在这里就变成了 NewProp_pppppprop,并且类型为 UECodeGen_Private::FUnsizedIntPropertyParams

1
2
3
4
5
6
7
struct Z_Construct_UClass_UMyObject_Statics
{
// ...
static const UECodeGen_Private::FUnsizedIntPropertyParams NewProp_pppppprop;
static const UECodeGen_Private::FPropertyParamsBase* const PropPointers[];
// ...
};

查看该属性的类外赋值:

1
2
3
4
5
6
7
8
9
10
11
12
const UECodeGen_Private::FUnsizedIntPropertyParams 
Z_Construct_UClass_UMyObject_Statics::NewProp_pppppprop = {
"pppppprop", // NameUTF8
nullptr, // RepNotifyFuncUTF8
(EPropertyFlags)0x0010000000000000, // PropertyFlags
UECodeGen_Private::EPropertyGenFlags::Int, // Flags
RF_Public|RF_Transient|RF_MarkAsNative, // ObjectFlags
1, // ArrayDim
STRUCT_OFFSET(UMyObject, pppppprop), // Offset
METADATA_PARAMS(Z_Construct_UClass_UMyObject_Statics::NewProp_pppppprop_MetaData, // MetaDataArray
UE_ARRAY_COUNT(Z_Construct_UClass_UMyObject_Statics::NewProp_pppppprop_MetaData)) // NumMetaData
};

可以看出 FUnsizedIntPropertyParams 基本就是将不同类型的属性统一再封装一层,额外携带多种 flags 等状态信息。

1
2
3
const UECodeGen_Private::FPropertyParamsBase* const Z_Construct_UClass_UMyObject_Statics::PropPointers[] = {
(const UECodeGen_Private::FPropertyParamsBase*)&Z_Construct_UClass_UMyObject_Statics::NewProp_pppppprop,
};

最后将每个属性都添加到 PropPointers[] 中,由于案例中只有一个属性,因此这里只有一条数据。

这样就完成了UPROPERTY的收集。最后 PropPointers[] 还是随着 ClassParams 一起传递到反射系统中。

小结

本节我们通过 UHT 生成的代码简要分析了 UE 反射系统的一部分内容,当然,我们只分析了 UCLASSUFUNCTIONUPROPERTY 这些情况,还有 USTRUCTUINTERFACE 等多种宏没有分析。但掌握了 UHT 生成代码的规律,想必这些内容分析起来也不是难事。

此外,由于本节中的案例较为简单,不涉及虚函数、RPC、Replicate 等特殊情况,这些应该会在后续的文章中逐一分析。

下一节将会从 StaticClass 与注册的角度继续分析反射系统的工作流程。