参考:
https://zhuanlan.zhihu.com/p/662720305
https://zhuanlan.zhihu.com/insideue4
https://blog.csdn.net/qq_29523119/article/details/119420238
https://cloud.tencent.com/developer/article/1606872
https://www.jianshu.com/p/c335c3f6cc04
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++ 代码文件进行语法分析,识别出特殊的宏,提取出对应的数据,然后生成代码。在初始化时运行生成的代码,将收集到的数据保存。
案例分析
我们从一个简单的案例入手,分析 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 #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 #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) #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.h
中 GENERATED_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_DECLS
为 UFunction
的定义,此处可以看出都只与 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 \ \ NO_API UMyObject(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; \ private: \ \ 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 , 0x001000A0 u, 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 \ inline static UClass* StaticClass () \ { \ return GetPrivateStaticClass (); \ } \ #define IMPLEMENT_CLASS_NO_AUTO_REGISTRATION(TClass) \ FClassRegistrationInfo Z_Registration_Info_UClass_##TClass; \ UClass* TClass::GetPrivateStaticClass() \ { \ if (!Z_Registration_Info_UClass_##TClass.InnerSingleton) \ { \ \ 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 #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.h
及 ScriptMacros.h
中,将一些宏替换后,就是这个样子:
1 2 3 4 5 6 7 void UMyObject::execffffffunc ( UObject* Context, FFrame& Stack, RESULT_DECL ) { Stack.Code += !!Stack.Code; 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 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, nullptr , "ffffffunc" , nullptr , nullptr , 0 , nullptr , 0 , RF_Public|RF_Transient|RF_MarkAsNative, (EFunctionFlags)0x00020401 , 0 , 0 , METADATA_PARAMS (Z_Construct_UFunction_UMyObject_ffffffunc_Statics::Function_MetaDataParams, UE_ARRAY_COUNT (Z_Construct_UFunction_UMyObject_ffffffunc_Statics::Function_MetaDataParams)) };
由于案例中的 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" }, };
这里 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" , nullptr , (EPropertyFlags)0x0010000000000000 , UECodeGen_Private::EPropertyGenFlags::Int, RF_Public|RF_Transient|RF_MarkAsNative, 1 , STRUCT_OFFSET (UMyObject, pppppprop), METADATA_PARAMS (Z_Construct_UClass_UMyObject_Statics::NewProp_pppppprop_MetaData, UE_ARRAY_COUNT (Z_Construct_UClass_UMyObject_Statics::NewProp_pppppprop_MetaData)) };
可以看出 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 反射系统的一部分内容,当然,我们只分析了 UCLASS
、UFUNCTION
、UPROPERTY
这些情况,还有 USTRUCT
、UINTERFACE
等多种宏没有分析。但掌握了 UHT 生成代码的规律,想必这些内容分析起来也不是难事。
此外,由于本节中的案例较为简单,不涉及虚函数、RPC、Replicate 等特殊情况,这些应该 会在后续的文章中逐一分析。
下一节将会从 StaticClass
与注册的角度继续分析反射系统的工作流程。