之前写了两篇UE中实现反射的文章分析,介绍了UE的反射基础概念和依赖的一些C++特性,本篇文章开始分析UE反射实现的具体流程。
C++标准中并没有反射的特性,UE使用的反射是基于标记语法和UHT扫描生成辅助代码来实现的一套机制,正如David Wheeler的那句名言一样:“All problems in computer science can be solved by another level of indirection”,UHT做的就是这样的事情,在真正执行编译之前分析标记代码并产生真正的C++代码,收集反射类型的元数据,供运行时之用。
UHT生成的代码内容很多,为了避免文章组织上的混乱,本篇文章主要讲GENERATED_BODY
/UFUNCTION
等反射标记通过UHT之后生成到generated.h
中的真正的C++代码。
UHT生成的代码分别在generated.h
和gen.cpp
中,generated.h
中的代码大多是定义了一些宏,用在所声明的类内通过编译器预处理来添加通用成员,gen.cpp
中的代码则是UHT基于反射标记生成的用来描述类反射信息的具体代码,genrated.h
和gen.cpp
也是为了声明和定义分离。
UE的Feeds中写过一篇关于UE Property System的文章:Unreal Property System(Reflection)
UE与反射相关的UHT宏标记大多定义在下列几个头文件中:
- Runtime/CoreUObject/Public/Object/ObjectMacros.h (UHT标记)
- Runtime/CoreUObject/Public/Object/ScriptMacros.h(大多是
P_*
的宏,可以利用反射从Stack
中获取数据) - Runtime/CoreUObject/Public/UObject/Class.h (反射基类的定义
UField
/UEnum
/UStruct
/UClass
等)
注意:不同的引擎版本,有些代码变更幅度很大,要结合具体的引擎版本做参考,重点是分析方法。
GENERATED_BODY
每一个在UE中继承自UObject
的C++类或者声明的USTRUCT类,在类声明中都会有一个GENERATED_XXXX
的系列宏:
1 | // This pair of macros is used to help implement GENERATED_BODY() and GENERATED_USTRUCT_BODY() |
直接看起来并没什么用!就是拼接了一个字符串而已。但是真相却往往另有玄机,搞清楚它可以顺便厘清在工作中写代码遇到的一系列会造成疑惑的问题,本节来分析一下GENERATED_
宏的作用。
考虑下列类代码:
1 | // NetActor.h |
当我们在编译时,UBT会驱动UHT为我们写的这个类生成NetActor.generated.h
和NetActor.gen.cpp
文件。*.generated.h
与*.gen.cpp
文件存放与下列路径(相对于项目根目录):
1 | Intermediate\Build\Win64\UE4Editor\Inc\{PROJECT_NAME} |
其中在NetActor.generated.h
中的代码,是UHT分析我们写的NetActor.h
生成的代码(都是宏定义,供后面使用)。
在分析generated.h
之前需要先来说一下GENERATED_BODY
与GENERATED_UCLASS_BODY
宏。根据本节开头列出的UE所支持的一系列GENERATED_
宏,单纯从宏展开的角度看,GENERATED_BODY
与GENERATED_UCLASS_BODY
的区别就是:
1 | # GENERATED_BODY最终生成了这样的一串字符: |
注意:这里用{}
括着的是其他的宏组成的,这里只是列出来两个宏的不同形式。
CURRENT_FILE_ID
为项目所在的文件夹的名字_源文件相对路径_h
1 | # e.g |
__LINE__
为这条宏所在的文件的行数,也就是上面代码中备注说的第八行。
那么GENERATED_BODY
与GENERATED_UCLASS_BODY
所拼接的实际字符串就是:
1 | // GENERATED_BODY |
说了这么一大堆,那么就算拼接出来了两个这么长的字符,又是干什么用的呢?
此时,打开我们的NetActor.generated.h
文件,可以看到其中定义了一大堆宏的代码:
1 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. |
看到了嘛!这个生成的generated.h
里定义了上面我们写的那些宏:
1 | CURRENT_FILE_ID |
因为我们的NetActor.h
中包含了NetActo.generated.h
这个头文件,所以在真正进行编译的时候会将GENERATED_BODY
进行宏展开,展开的内容就是NetActor.generated.h
中的宏ReflectionExample_Source_ReflectionExample_NetActor_h_8_GENERATED_BODY
展开之后的代码。
因为我在NetActor.h
中使用的是GENERATED_BODY
,我就先分析这个宏展开之后的真实代码。
其实
GENERATED_BODY
与GENERATED_UCLASS_BODY
的区别在于:GENERATED_BODY
声明并定义了一个接收const FObjectInitializer&
的构造函数,GENERATED_UCLASS_BODY
只声明了该构造函数,需要用户自己提供一个定义。
1 | ANetActor(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); |
GENERATED_BODY
的真实宏名字又包裹了一层其他的宏:
1 |
|
展开之后:
1 | class ANetActor:public AActor |
可以看到其中使用了:
DECLARE_CLASS
:声明定义当前类的几个关键信息:Super
和ThisClass
等typedef
在此处被定义,以及StaticClass
/StaticPackage
/StaticClassCastFlags
和重载的new
也被定义;DECLARE_FUNCTION
为使用UFUNCIONT
标记的函数创建中间函数;DECLARE_SERIALIZER
:重载<<
使可以被FArchive
序列化;DECLARE_VTABLE_PTR_HELPER_CTOR
:声明一个接收FVTableHelper&
参数的构造函数;DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER_DUMMY
:用于HotReload
,唯一调用的地方是在Class.h中的模板函数InternalVTableHelperCtorCaller
;DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL
:定义一个名为__DefaultConstructor
的静态函数,其中是调用placement-new
创建类对象(用于统一的内存分配),引擎中唯一调用的位置是在Class.h的模板函数InternalConstructor
;
因为我们没有在ANetActor
这个类上标记XXXX_API
,所以它不会被导出,UHT生成的类ANetActor
的构造函数中都使用的是NO_API
.
还有,因为UFUNCTION
之类的宏在C++的定义里都是空宏,其实严格来说他们并不能称之为宏,它们只是对UHT的标记,用于通过UHT来解析生成.generated.h
和.gen.cpp
的代码,所以在执行完UHT之后,对于C++和编译器来说它们就是不存在的(在预处理之后就是彻底不存在的了)。
这几个宏(被UHT生成之后就是真正的C++宏了),可以在CoreUObject/Public/UObject/OBjectMacros.h中找到定义。
把上面的宏再全部展开:
1 | class ANetActor:public AActor |
这就是经过UHT之后的我们的ANetActtor
类声明,其中定义了一系列的函数、typedef
以及序列化、new
等等。
还要类似于C#中的Super
其实就是UHT给我们的类添加了一个typedef
的形式,把Super定义成了基类,UE通过这种形式给我们的类添加了通用的访问函数,用于支持UE的对象系统。
GetPrivateStaticClass
在StaticClass
中调用的GetPrivateStaticClass
其实现是在NetActor.gen.cpp
中的,通过IMPLEMENT_CLASS
宏来定义(这个IMPLEMENT_
系列宏也是被定义在Class.h中):
1 | IMPLEMENT_CLASS(ANetActor, 2260007263); |
展开之后为:
1 |
|
GetPrivateStaticClass(定义在Class.cpp)其作用是从当前类的信息构造出一个UClass
对象出来,其是一个单例对象,通过UXXX::StaticClass()
获取到的就是这个对象。
注意:在
GetPrivateStaticClass
中调用GetPrivateStaticClassBody
所传递的参数,就是UHT根据我们类的声明产生的所有元数据的访问方法,UClass
里存储的就是我们定义类的元数据,而且也并非是每一个我们定义的类都生成了一个一个UClass类,而是对每一个类产生一个不同的UClass对象实例。
UFUNCTION
在UE中写代码时,所有需要进行反射的函数必须添加UFUNTION()
标记。
1 | UCLASS() |
UHT通过扫描我们在代码中所有标记了UFUNCTION
的函数,生成出来的名为execFUNC_NAME
中间函数定义(被称作thunk
函数)。它统一了所有的UFUNCTION
函数调用规则(this
/调用参数以及返回值),并且包裹了真正要执行的函数。
之后就可以通过反射来调用该函数:
- 通过
UObject::FindFunction
获得所指定函数的UFunction
对象(如果指定的函数没有添加UFUNCTION
标记,则返回NULL
); - 通过
ProcessEvent
来调用函数,第一个参数是调用函数UFunction
,第二个是参数列表void*
;
1 | { |
注:
UFunction
对象中的ParamSize
的大小是所有成员组成的结构大小,并且具有字节对齐,所以可以将所有参数封装为一个结构,再将其转换为void*
。
例,一个函数接收int32
/bool
/AActor*
三个类型参数,其ParamSize
的大小等同于:
1 | // sizeof(Params) == 16 |
内存对齐相关的内容可以看我之前的一篇文章:结构体成员内存对齐问题
DECLARE_FUNCTION
DECLARE_FUNCTION
的宏定义为:
1 | // This macro is used to declare a thunk function in autogenerated boilerplate code |
通过上面我们手动解析之后的代码可以看到,对于使用UFUNCTION
标记的函数,UHT解析时给我们生成了一个DECLARE_FUNCTION
的宏,其宏定义为:
1 | // This macro is used to declare a thunk function in autogenerated boilerplate code |
在我之前的文章中有提到过,C++的成员函数和非成员函数本质没有区别,只不过C++的成员函数有一个隐式的this
指针参数,这个DECLARE_FUNCTION
处理的思想也一样,可以把成员和非成员函数通过这种形式统一起来,至于Context
自然就是传统C++的那个隐式this指针了,代表着当前调用该成员函数的对象。
注意:在老版本的引擎代码中(4.18.3之前),是没有这个Context参数的,从4.19之后才支持。
Now,将上文NetActor.generated.h
中的DECLARE_FUNCTION(execSetHp)
展开为:
1 | // DECLARE_FUNCTION(execSetHp) |
以及DECLARE_FUNCTION(execGetHp)
:
1 | // DECLARE_FUNCTION(execGetHp) |
这些P_
开头的宏,是封装了从参数Context
以及Stack
中获取真正要执行的函数的参数,它们被定义在Runtime/CoreUObject/Public/Object/ScriptMacros.h中。
RESULT_DECL
宏是被定义在Script.h中的:
1 | // Runtime/CoreUObject/Public/Script.h |
被展开后是:
1 | void*const Z_Param__Result |
它是一个顶层const(Top-level const
),指针值不能修改,指针所指向的值可以修改,用于处理函数的返回值。
Custom Thunk Function
上面写道,当我们对一个函数标记UFUNCTION
的时候,UHT就会自动给生成一个execFunc
的函数,如果不想让UHT生成该函数的Thunk
函数,可以使用CustomThunk
来标记,不生成,自己提供。
1 | UCLASS(BlueprintType,Blueprintable) |
其实就是需要把DECLARE_FUNCTION
自己写一遍来处理ProcessEvent
传递过来的逻辑。比如考虑一下实现这样的逻辑:写一个通用的函数,允许传入任何具有反射的的struct(不管是蓝图的还是C++的),然后将其序列化为json。想要实现这样的功能就需要我们在Thunk
函数中自己来写逻辑。
1 | UFUNCTION(BlueprintCallable,CustomThunk, meta = (CustomStructureParam = "StructPack")) |
meta
中CustomStructureParam
的含义是将参数作为通配符,可以传入任何类型的参数。
UPROPERTY
在类内对属性加了UPROPERTY的标记,不会在generated.h中产生额外的代码,但是它会把它的反射信息代码生成到在gen.cpp
中,关于生成在gen.cpp
中的代码细节本篇文章暂时按下不表,留到下一篇文章中详细介绍。
StaticClass/Struct/Enum
在generated.h中,UHT会为当前文件中声明的反射类型(UObject class/struct/enum)生成对应的Static*<>
模板特化:
1 | template<> REFLECTIONEXAMPLE_API UClass* StaticClass<class ANetActor>(); |
这是我们在运行时通过StaticClass<ANetActor>
这种形式获取UClass/UStruct/UEnum的方法。
这种形式在UE4.21之后才添加,在4.21之前要使用以下这种形式:
1 | UEnum* FoundEnum = FindObject<UEnum>(ANY_PACKAGE, *EnumTypeName, true); |
Static*<>()
的定义是在gen.cpp
中。
End
UE反射的实现以一言蔽之:通过UHT生成反射的元数据,把这些元数据在运行时构造出来对应的UClass/UStruct/UEnum,从而提供了反射的支持。本篇文章主要介绍了UHT生成的generated.h中的代码,其实核心是通过UHT给反射类的声明中添加了一堆通用的成员,依赖这些成员支持了UE的对象系统的管理。
下一篇文章会着重介绍UHT生成的gen.cpp
中的代码,它们是真正记录了反射类的对象信息的,比如反射成员的名字、函数地址,传递参数、数据成员类内偏移等等,通过分析它们可以知道我们通过反射能够得到类的哪些信息。