前言

本篇为对于大钊老师以及网上资料的对于Uobject 相关的反射底层实现与源码阅读。

https://zhuanlan.zhihu.com/p/22813908?refer=insideue4

https://www.bilibili.com/video/BV1bH4y1k7Wg/?spm_id_from=333.1391.0.0&vd_source=d19e47552f1614194f0d0b0662850083

开篇

https://zhuanlan.zhihu.com/p/24319968

img

藉着UObject提供的元数据、反射生成、GC垃圾回收、序列化、编辑器可见、Class Default Object等,UE可以构建一个Object运行的世界。(后续会有一个大长篇深挖UObject)

其他引擎的对象模型情况

Cocos系列,最早是cocos-iphone扎根于objective-c,几乎是机械翻译了objective-c的内存管理机制,搞出了一个CCObject

Unity 上层脚本C#是基于Mono

其他引擎,用的还是C++提供了的那些,顶多自己再定制一些管理辅助类。

Qt QObject

UE的Object系统无疑是最强大的。实际上UE能实践出这么一套UObject是非常非常了不起的,更何况还有GC和HotReload的黑科技。在大型游戏引擎的领域尝试引入一整套UObject系统,对于整个业界也都是有非常大的启发。

  • 那么引入一个Object的根基类设计到底有什么深远的影响,我们又付出了什么代价?

  • 得到:

  1. 万物可追踪。按照纯面向对象的思想,万物皆是对象。
  2. 通用的属性和接口。Equals、Clone、GetHashCode、ToString、GetName、GetMetaData等等。
  3. 统一的内存分配释放。GC,引用计数
  4. 统一的序列化模型。类protobuf,模板化序列化。
  5. 统计功能。哪种对象分配了最多次,哪种对象分配的时间最长,哪种对象存活的时间最长。
  6. 调试的便利。Object基类下的一个子类对象,你可以把地址转换为一个Object指针,然后就可以一目了然的查看对象属性了。
  7. 为反射提供便利。有GetType接口
  8. UI编辑的便利。和编辑器集成的时候,为了让UI的属性面板控件能编辑各种对象。
  • 代价
  1. 臃肿的Object。Object会堆积大量的函数接口和成员属性
  2. 不必要的内存负担。有些类型对象可能一辈子都用不到,用不到的属性,却还占用着内存,就是浪费。
  3. 多重继承的限制。一般有object基类的编程语言,都是直接限制多重继承,改为多重实现接口,避免了数据被继承多份的问题。
  4. 类型系统的割裂。除非是像java和C#那样,对用户隐藏整个背后系统,否则用户在面对原生C++类型和Object类型时,就不得不去思考划分对象类型。

这些代价我们也得想办法去尽量降低和规避:

  1. 针对太过复杂的Object基类。一个UObject你给我分了三层继承:(UObjectBase->UObjectBaseUtility->UObject)
  2. sizeof(UObject)==56。这个问题已经解决得在可接受范围内了。
  3. 规避多重继承。UE在BP里提供的也是多重继承Interface的方案。在C++层面上,我们只能尽量规避不要多重继承多个UObject子类。
  4. 只能多学习了。C++充当BP的VM。如UCLASS等各种宏的便利,NewObject方便接口,UHT的自动分析生成代码,尽量避免用户直接涉及到UObject的内部细节。

类型系统概述

https://zhuanlan.zhihu.com/p/24445322

类型系统

虽然之上一直用反射的术语来描述我们熟知的那一套运行时得到类型信息的系统,动态创建类对象等,但是其实“反射”只是在“类型系统”之后实现的附加功能。

以后更多用“类型系统”这个更精确的术语来表述object之外的类型信息构建,而用“反射”这个术语来描述运行时得到类型的功能,通过类型信息反过来创建对象,读取修改属性,调用方法的功能行为。反射更多是一种行为能力,更偏向动词。类型系统指的是程序运行空间内构建出来的类型信息树组织,

C# Type

Unity用C#作为脚本语言,UE本身也是用C#作为编译UBT的实现语言。

1
Type type = obj.GetType();  //or typeof(MyClass)

img

本篇不是C#反射教程(关心的自己去找相关教程),但这里还是简单提一下我们需要关注的:

  1. Assembly是程序集的意思,通常指的是一个dll。
  2. Module是程序集内部的子模块划分。
  3. Type就是我们最关心的Class对象了,完整描述了一个对象的类型信息。并且Type之间也可以通过BaseType,DeclaringType之类的属性来互相形成Type关系图。
  4. ConstructorInfo描述了Type中的构造函数,可以通过调用它来调用特定的构造函数。
  5. EventInfo描述了Type中定义的event事件(UE中的delegate大概)
  6. FiedInfo描述了Type中的字段,就是C++的成员变量,得到之后可以动态读取修改值
  7. PropertyInfo描述了Type中的属性,类比C++中的get/set方法组合,得到后可以获取设置属性值。
  8. MethodInfo描述了Type中的方法。获得方法后就可以动态调用了。
  9. ParameterInfo描述了方法中的一个个参数。
  10. Attributes指的是Type之上附加的特性,这个C++里并没有,可以简单理解为类上的定义的元数据信息。

可以看到C#里的Type几乎提供了一切信息数据,简直就像是把编译器编译后的数据都给暴露出来了给你。实际上C#的反射还可以提供其他更高级的功能,比如运行时动态创建出新的类,动态Emit编译代码,不过这些都是后话了。

C++ RTTI

C++中的运行时类型系统,我们一般会说RTTI(Run-Time Type Identification),只提供了两个最基本的操作符:

typeid

这个关键字的主要作用就是用于让用户知道是什么类型,并提供一些基本对比和name方法,作用也顶多只是让用户判断从属于不同的类型,所以其实说起来type_info的应用并不广泛,一般来说也只是把它当作编译器提供的一个唯一类型Id。

1
2
3
4
5
6
7
8
9
10
11
12
13
const std::type_info& info = typeid(MyClass);

class type_info
{
public:
type_info(type_info const&) = delete;
type_info& operator=(type_info const&) = delete;
size_t hash_code() const throw();
bool operator==(type_info const& _Other) const throw();
bool operator!=(type_info const& _Other) const throw();
bool before(type_info const& _Other) const throw();
char const* name() const throw();
};

dynamic_cast

该转换符用于将一个指向派生类的基类指针或引用转换为派生类的指针或引用,使用条件是只能用于含有虚函数的类。转换引用失败会抛出bad_cast异常,转换指针失败会返回null。

1
2
3
Base* base=new Derived();
Derived* p=dynamic_cast<Derived>(base);
if(p){...}else{...}

dynamic_cast内部机制其实也是利用虚函数表里的类型信息来判断一个基类指针是否指向一个派生类对象。其目的更多是用于在运行时判断对象指针是否为特定一个子类的对象。

C++当前实现反射的方案

基本思想是采用手动标记。在程序中用手动的方式注册各个类,方法,数据。大概就像这样:

1
2
3
4
5
6
7
struct Test{
Declare_Struct(Test);
Define_Field(1, int, a)
Define_Field(2, int, b)
Define_Field(3, int, c)
Define_Metadata(3)
};

模板

举一个Github实现比较优雅的C++RTTI反射库做例子:rttr

1
2
3
4
5
6
7
8
9
10
#include <rttr/registration>
using namespace rttr;
struct MyStruct { MyStruct() {}; void func(double) {}; int data; };
RTTR_REGISTRATION
{
registration::class_<MyStruct>("MyStruct")
.constructor<>()
.property("data", &MyStruct::data)
.method("func", &MyStruct::func);
}

说实话,这写得已经非常简洁优雅了。算得上是达到了C++模板应用的巅峰。但是可以看到,仍然需要一个个的手动去定义类并获取方法属性注册。优点是轻量程序内就能直接内嵌,缺点是不适合懒人。

编译器数据分析

还有些人就想到既然C++编译器编译完整个代码,那肯定是有完整类型信息数据的。那能否把它们转换保存起来供程序使用呢?事实上这也是可行的,比如@vczh的GacUI里就分析了VC编译生成后pdb文件,然后抽取出类型定义的信息实现反射。VC确实也提供了IDiaDataSource COM组件用来读取pdb文件的内容。用法可以参考:GacUI Demo:PDB Viewer(分析pdb文件并获取C++类声明的详细内容)。 理论上来说,只要你能获取到跟编译器同级别的类型信息,你基本上就像是全知了。但是缺点是分析编译器的生成数据,太过依赖平台(比如只能VC编译,换了Clang就是另一套方案),分析提取的过程往往也比较麻烦艰深,在正常的编译前需要多加一个编译流程。但优点也是得到的数据最是全面。 这种方案也因为太过麻烦,所以业内用的人不多。

工具生成代码

自然的有些人就又想到,既然宏和模板的方法,太过麻烦。那我能不能写一个工具来自动完成呢?

一个好例子就是Qt里面的反射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <QObject>
class MyClass : public QObject
{
Q_OBJECT
  Q_PROPERTY(int Member1 READ Member1 WRITE setMember1 )
  Q_PROPERTY(int Member2 READ Member2 WRITE setMember2 )
  Q_PROPERTY(QString MEMBER3 READ Member3 WRITE setMember3 )
  public:
   explicit MyClass(QObject *parent = 0);
  signals:
  public slots:
  public:
    Q_INVOKABLE int Member1();
    Q_INVOKABLE int Member2();
    Q_INVOKABLE QString Member3();
    Q_INVOKABLE void setMember1( int mem1 );
    Q_INVOKABLE void setMember2( int mem2 );
    Q_INVOKABLE void setMember3( const QString& mem3 );
    Q_INVOKABLE int func( QString flag );
  private:
    int m_member1;
    int m_member2;
    QString m_member3;
 };

大概过程是Qt利用基于moc(meta object compiler)实现,用一个元对象编译器在程序编译前,分析C++源文件,识别一些特殊的宏Q_OBJECT、Q_PROPERTY、Q_INVOKABLE……然后生成相应的moc文件,之后再一起全部编译链接。

UE里UHT的方案

不用多说,你们也能想到UE当前的方案也是如此,实现在C++源文件中空的宏做标记,然后用UHT分析生成generated.h/.cpp文件,之后再一起编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UCLASS()
class HELLO_API UMyClass : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite, Category = "Test")
float Score;

UFUNCTION(BlueprintCallable, Category = "Test")
void CallableFuncTest();

UFUNCTION(BlueprintNativeEvent, Category = "Test")
void NativeFuncTest();

UFUNCTION(BlueprintImplementableEvent, Category = "Test")
void ImplementableFuncTest();
};

完整的C++的语法分析往往是超级复杂的,所以限制是自己写的分析器只能分析一些简单的C++语法规则和宏标记,如果用户使用比较复杂的语法时候,比如用#if /#endif包裹一些声明,就会让自己的分析器出错了,还好这种情况不多。关于多一次编译的问题,也可以通过自定义编译器的编译脚本UBT来规避。

这和C#的Attribute的语法简直差不多一模一样,所以UE也是吸收了C#语法反射的一些优雅写法,并利用上了C++的宏魔法,当然生成的代码里模板肯定也是少不了的。

类型系统设定和结构

https://zhuanlan.zhihu.com/p/24790386

设定

函数也同样加上宏标记,大概就是类似C#Attribute的语法。在宏的参数可以按照我们自定的语法写上内容。在UE里我们就可以看到这些宏标记:

1
2
3
4
5
6
7
8
9
#define UPROPERTY(...)
#define UFUNCTION(...)
#define USTRUCT(...)
#define UMETA(...)
#define UPARAM(...)
#define UENUM(...)
#define UDELEGATE(...)
#define UCLASS(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_PROLOG)
#define UINTERFACE(...) UCLASS()

为何是生成代码而不是数据文件?

简单来说就是避免了不一致性,否则又得有机制去保证数据文件和代码能匹配上。同时跨平台需求也很难保证结构间的偏移在各个平台编译器优化的不同导致得差异。所以还不如简单生成代码文件一起编译进去得了。

如果标记应该分析哪个文件?

类A生成了A.generated.h和A.generated.cpp.

最方便的方式莫过于直接让A.h include A.generated.h了。那既然每个需要分析的文件最后都会include这么一个*.generated.h,那自然就可以把它本身就当作一种标记了。

UE目前的方案是每个要分析的文件加上该Include并且规定只能当作最后一个include,因为他也担心会有各种宏定义顺序产生的问题。

1
#include "FileName.generated.h"

结构

  • Hello类
1
2
3
4
5
6
7
8
9
10
#include "Hello.generated.h"
UClass()
class Hello
{
public:
UPROPERTY()
int Count;
UFUNCTION()
void Say();
};

img

分类 类型 主要功能/特点 作用 补充
聚合类型(UStruct) UFunction 仅可包含属性作为函数的输入输出参数 用于定义可被蓝图调用的方法签名
UScriptStruct 仅可包含属性;类似C++中的POD结构体;拥有反射、序列化、复制支持 轻量级UObject;不受GC管理,需手动控制内存 这个实例负责持有该结构体所有的反射元数据(比如有哪些属性、函数等)。可以简单理解为:UStruct 是基类,UScriptStruct 是派生类,负责具体结构体类型的反射信息管理
UClass 可包含属性和函数;最常用的类型 用于定义对象类型(如Actor、Component等)
原子类型 UEnum 支持普通枚举和enum class 用于定义枚举类型
基础类型 int、FString等可通过不同的UProperty子类支持 基础类型无需特别声明,可通过UProperty系统进行反射和序列化
其他类型 UInterface 生成的类型数据依然用UClass存储。 UClass里通过保存一个TArray< FImplementedInterface > Interfaces数组
UProperty 为用一个类型定义个字段“type instance;” 单的如UBoolProperty、UStrProperty,复杂的如UMapProperty、UDelegateProperty、UObjectProperty。
UMetaData TMap<FName, FString>的键值对 为编辑器提供分类、友好名字、提示等信息,最终发布的时候不会包含此信息。

用法:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "Hello.generated.h"
UENUM()
namespace ESearchCase
{
enum Type
{
CaseSensitive,
IgnoreCase,
};
}

UENUM(BlueprintType)
enum class EMyEnum : uint8
{
MY_Dance UMETA(DisplayName = "Dance"),
MY_Rain UMETA(DisplayName = "Rain"),
MY_Song UMETA(DisplayName = "Song")
};

USTRUCT()
struct HELLO_API FMyStruct
{
GENERATED_USTRUCT_BODY()
UPROPERTY(BlueprintReadWrite)
float Score;
};

UCLASS()
class HELLO_API UMyClass : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite, Category = "Hello")
float Score;

UFUNCTION(BlueprintCallable, Category = "Hello")
void CallableFuncTest();
UFUNCTION(BlueprintCallable, Category = "Hello")
void OutCallableFuncTest(float& outParam);

UFUNCTION(BlueprintCallable, Category = "Hello")
void RefCallableFuncTest(UPARAM(ref) float& refParam);

UFUNCTION(BlueprintNativeEvent, Category = "Hello")
void NativeFuncTest();

UFUNCTION(BlueprintImplementableEvent, Category = "Hello")
void ImplementableFuncTest();
};

UINTERFACE()
class UMyInterface : public UInterface
{
GENERATED_UINTERFACE_BODY()
};

class IMyInterface
{
GENERATED_IINTERFACE_BODY()

UFUNCTION(BlueprintImplementableEvent)
void BPFunc() const;

virtual void SelfFunc() const {}
};

思考:为什么还需要基类UField?

UField名字顾名思义,就是不管是声明还是定义,都可以看作是类型系统里的一个字段,或者叫领域也行,术语不同,但能理解到一个更抽象统一的意思就行。

思考:为什么UField要继承于UObject?

UObject作用与在UField作用 :

  1. GC 可有可无
  2. 反射 略
  3. 编辑器集成 也可以没有
  4. CDO 不需要
  5. 序列化 必须有,类型数据当然需要保存下来,比如蓝图创建的类型。
  6. Replicate 用处不大
  7. RPC 也无所谓
  8. 自动属性更新 也不需要
  9. 统计 可有可无

类型系统代码生成

本篇代码过长,建议观看原文,这边只是简单复述,后期再做修改。

https://zhuanlan.zhihu.com/p/25098685

同一般程序的构建流程需要经过预处理、编译、汇编、链接一样,UE为了在内存中模拟构建的过程,在概念上也需要以下几个阶段:生成,收集,注册,链接。总体的流程比较繁杂,因此本文首先开始介绍第一阶段,生成。在生成阶段,UHT分析我们的代码,并生成类型系统的相关代码。

Note1:生成的代码和注册的过程会因为HotReload功能的开启与否有些不一样,因此为了最简化流程阐述,我们先关闭HotReload,关闭的方式是在Hello.Build.cs里加上一行:Definitions.Add(“WITH_HOT_RELOAD_CTORS=0”);

Note2:本文开始及后续会简单的介绍一些用到的C++基础知识,但只是点到为止,不做深入探讨。

C++ Static Lazy初始化模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Hello* StaticGetHello()
{
static Hello* obj=nullptr;
if(!obj)
{
obj=...
}
return obj;
}

或者

Hello& StaticGetHello()
{
static Hello obj(...);
return obj;
}

前者非常简单,也没考虑多线程安全,但是在单线程环境下足够用了。用指针的原因是,有一些情况,这些对象的生命周期是由别的地方来管理的,比如UE里的GC,因此这里只static化一个指针。否则的话,还是后者更加简洁和安全。

UHT代码生成

在C++程序中的预处理是用来对源代码进行宏展开,预编译指令处理,注释删除等操作。

同样的,一旦我们采用了宏标记的方法,不管是怎么个标记语法,我们都需要进行简单或复杂的词法分析,提取出有用的信息,然后生成所需要的代码。

在引擎里创建一个空C++项目命名为Hello,然后创建个不继承的MyClass类。编译,UHT就会为我们生成以下4个文件(位于Hello\Intermediate\Build\Win64\Hello\Inc\Hello)

  • HelloClasses.h:目前无用
  • MyClass.generated.h:MyClass的生成头文件
  • Hello.generated.dep.h:Hello.generated.cpp的依赖头文件,也就是顺序包含上述的MyClass.h而已
  • Hello.generated.cpp:该项目的实现编译单元。

UCLASS的生成代码剖析

MyClass.h

基础文件样式

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

#pragma once


//include 一些基础类 与 专门供UHT使用来生成蓝图类型的(现在不用管)
#include "UObject/NoExportTypes.h"

//就是为了引用生成的头文件。
//这里请注意的是,该文件include位置在类声明的前面,
//之后谈到宏处理的时候会用到该信息。
#include "MyClass.generated.h"

UCLASS()
class HELLO_API UMyClass : public UObject
{
GENERATED_BODY()
};

GENERATED_BODY(),该宏是重中之重,其他的UCLASS宏只是提供信息,不参与编译,而GENERATED_BODY正是把声明和元数据定义关联到一起的枢纽。继续查看宏定义:

1
2
3
4
5
6
7
8
9
10
11
#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(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY)

//CURRENT_FILE_ID的定义是在MyClass.generated.h的89行:
//这是UHT通过分析文件得到的信息。
#define CURRENT_FILE_ID Hello_Source_Hello_MyClass_h

//__LINE__标准宏指向了该宏使用时候的的行数,这里是17。
//加了一个__LINE__宏的目的是为了支持在同一个文件内声明多个类,
//比如在MyClass.h里接着再声明UMyClass2,就可以支持生成不同的宏名称。

GENERATED_BODY() ==》Hello_Source_Hello_MyClass_h_11_GENERATED_BODY

如果MyClass类需要UMyClass(const FObjectInitializer& ObjectInitializer)的构造函数自定义实现,则需要用GENERATED_UCLASS_BODY宏来让最终生成的宏指向

Hello_Source_Hello_MyClass_h_11_GENERATED_BODY_LEGACY(MyClass.generated.h的66行),其最终展开的内容会多一个构造函数的内容实现。

MyClass.generated.h

代码详见原文

自下而上分析

展开宏 定义 备注
CURRENT_FILE_ID Hello_Source_Hello_MyClass_h
Hello_Source_Hello_MyClass_h_11_GENERATED_BODY Hello_Source_Hello_MyClass_h_11_PRIVATE_PROPERTY_OFFSET Hello_Source_Hello_MyClass_h_11_RPC_WRAPPERS_NO_PURE_DECLS Hello_Source_Hello_MyClass_h_11_INCLASS_NO_PURE_DECLS Hello_Source_Hello_MyClass_h_11_ENHANCED_CONSTRUCTORS
GENERATED_BODY BODY_MACRO_COMBINE(CURRENT_FILE_ID,,LINE,GENERATED_BODY) Hello_Source_Hello_MyClass_h_11_GENERATED_BODY
Hello_Source_Hello_MyClass_h_11_ENHANCED_CONSTRUCTORS 1. 禁止掉C++11的移动和拷贝构造 2.因 WITH_HOT_RELOAD_CTORS关闭 下面俩个空宏 DECLARE_VTABLE_PTR_HELPER_CTOR(NO_API, UMyClass); DEFINE_VTABLE_PTR_HELPER_CTOR_CALLER(UMyClass); 3.
Hello_Source_Hello_MyClass_h_11_STANDARD_CONSTRUCTORS 和上行只差 : Super(ObjectInitializer) { } 构造函数
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL #define DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(TClass) \ static void __DefaultConstructor(const FObjectInitializer& X) { new((EInternal*)X.GetObj())TClass(X); } 声明定义了一个构造函数包装器
DECLARE_CLASS DECLARE_CLASS(UMyClass, UObject, COMPILED_IN_FLAGS(0), 0, TEXT(“/Script/Hello”), NO_API)

DECLARE_CLASS

参数 说明
TClass 类名
TSuperClass 基类名字
TStaticFlags 类的属性标记,这里是0,表示最默认,不带任何其他属性。读者可以查看EClassFlags枚举来查看其他定义。
TStaticCastFlags 指定了该类可以转换为哪些类,这里为0表示不能转为那些默认的类,读者可以自己查看EClassCastFlags声明来查看具体有哪些默认类转换。
TPackage 类所处于的包名,所有的对象都必须处于一个包中,而每个包都具有一个名字,可以通过该名字来查找。这里是”/Script/Hello”,指定是Script下的Hello,Script可以理解为用户自己的实现,不管是C++还是蓝图,都可以看作是引擎外的一种脚本

Hello.generated.cpp

而整个Hello项目会生成一个Hello.generated.cpp

代码详见原文

大部分简单的都注释说明了,本文件的关键点在于IMPLEMENT_CLASS的分析,和上文.h中的DECLARE_CLASS对应,其声明如下:

对照着定义IMPLEMENT_CLASS(UMyClass, 899540749);

代码详见原文

内容也比较简单,就是把该类的信息传进去给GetPrivateStaticClassBody函数。

最后展开结果

MyClass.h展开

代码详见原文

Hello.generated.cpp展开

代码详见原文

这样.h的声明和.cpp的定义就全都有了。不管定义了多少函数,要记得注册的入口就是那两个static对象在程序启动的时候登记信息,才有了之后的注册。

UENUM的生成代码剖析

1
2
3
4
5
6
7
8
9
10
11
#pragma once
#include "UObject/NoExportTypes.h"
#include "MyEnum.generated.h"

UENUM(BlueprintType)
enum class EMyEnum : uint8
{
MY_Dance UMETA(DisplayName = "Dance"),
MY_Rain UMETA(DisplayName = "Rain"),
MY_Song UMETA(DisplayName = "Song")
};

Z_Construct_UEnum_Hello_EMyEnum的简单封装

ScriptStruct_Hello_StaticRegisterNativesFMyStruct会在程序一启动就调用UScriptStruct::DeferCppStructOps向程序注册该结构的CPP信息(大小,内存对齐等)

USTRUCT的生成代码剖析

1
2
3
4
5
6
7
8
9
10
11
12
#pragma once
#include "UObject/NoExportTypes.h"
#include "MyStruct.generated.h"

USTRUCT(BlueprintType)
struct HELLO_API FMyStruct
{
GENERATED_USTRUCT_BODY()

UPROPERTY(BlueprintReadWrite)
float Score;
};

Z_Construct_UScriptStruct_FMyStruct的简单封装

UINTERFACE的生成代码剖析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma once
#include "UObject/NoExportTypes.h"
#include "MyInterface.generated.h"

UINTERFACE(BlueprintType)
class UMyInterface : public UInterface
{
GENERATED_UINTERFACE_BODY()
};

class IMyInterface
{
GENERATED_IINTERFACE_BODY()
public:
UFUNCTION(BlueprintImplementableEvent)
void BPFunc() const;
};

GENERATED_UINTERFACE_BODY和GENERATED_IINTERFACE_BODY

可替换为GENERATED_BODY 只不过会多一个构造函数

UMyInterface(const FObjectInitializer& ObjectInitializer),

不需要这个构造函数,没差别。

我们的类只是继承于IMyInterface,UMyInerface只是作为一个接口类型的载体

UClass中的字段和函数生成代码剖析

如果UMyClass里多了Property和Function之后又会起什么变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once
#include "UObject/NoExportTypes.h"
#include "MyClass.generated.h"

UCLASS(BlueprintType)
class HELLO_API UMyClass : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite)
float Score;
public:
UFUNCTION(BlueprintCallable, Category = "Hello")
void CallableFunc(); //C++实现,蓝图调用

UFUNCTION(BlueprintNativeEvent, Category = "Hello")
void NativeFunc(); //C++实现默认版本,蓝图可重载实现

UFUNCTION(BlueprintImplementableEvent, Category = "Hello")
void ImplementableFunc(); //C++不实现,蓝图实现
};

蓝图虚拟机调用execCallableFunc

类型系统信息收集

https://zhuanlan.zhihu.com/p/26019216

Static Auto Register

自动注册模式:工厂+注册。

https://blog.csdn.net/WHEgqing/article/details/121461936

  • Register

这个缺点是每一次都要添加一行注册。

1
2
3
4
5
6
#include "ClassA.h"
#include "ClassB.h"
int main(){
ClassFactory::Get().Register<ClassA>();
ClassFactory::Get().Register<ClassB>();[...]
}
  • Auto Register

C++ static对象会在main函数之前初始化的特性。

所以在每次初始化的时候去auto注册,只需要Include进对应配的.h.cpp文件。

把因为新添加类而产生的改变行为限制在了新文件本身,对于一些顺序无关的注册行为这种模式尤为合适

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//StaticAutoRegister.h
template<typename TClass>
struct StaticAutoRegister
{
StaticAutoRegister()
{
Register(TClass::StaticClass());
}
};
//MyClass.h
class MyClass
{
//[...]
};
//MyClass.cpp
#include "StaticAutoRegister.h"
const static StaticAutoRegister<MyClass> AutoRegister;
  • Other

你可以把StaticAutoRegister声明进MyClass的一个静态成员变量也可以。

但是因为UE都是dll动态链接,若没被加载lib的话再引用lib的话会绕过static初始化以至于报错,类似懒汉模式。或者你可以直接强制去include一下来触发static初始化(?)。

UE Static Auto Register

都是对该模式的应用,把static变量声明再用宏包装一层,就可以实现一个简单的自动注册流程了。

收集

  • 需要解决的问题

从上一章收集到的所有元数据要进行保存。

但是这些元数据散落在各自的dll中,我们需要在注册的时候进行收集。

在UE Static Auto Register 中会全部注册一遍,但是先后的依赖问题还需要解决。

  • UE 引擎结构
    • Game
      • 玩家自己的Game模块是处于比较高级的层次的,都是依赖于引擎其他更基础底层的模块。
    • Module
      • UE是以Module来组织引擎结构的,一个个Module可以通过脚本配置来选择性的编译加载。
    • CoreUObject
      • 实现Object类型系统的模块
    • Core
      • C++的基础库,最最底层

因此在类型系统注册的过程中,不止要注册玩家的Game模块,同时也要注册CoreUObject本身的一些支持类。

很多人可能会担心这么多模块的静态初始化的顺序正确性如何保证,在c++标准里,不同编译单元的全局静态变量的初始化顺序并没有明确规定,因此实现上完全由编译器自己决定。该问题最好的解决方法是尽可能的避免这种情况,在设计上就让各个变量不互相引用依赖,同时也采用一些二次检测的方式避免重复注册,或者触发一个强制引用来保证前置对象已经被初始化完成。目前在MSVC平台上是先注册玩家的Game模块,接着是CoreUObject,接着再其他,不过这其实无所谓的,只要保证不依赖顺序而结果正确,顺序就并不重要了。

Static的收集

搜集这边可以后面回头再复习

我们再来分别的看各个类别的结构的信息的收集。从Class(Interface同理)开始,然后是Enum,接着Struct。

Class的收集

Hello.generated.cpp展开

调用了UClassCompiledInDefer来收集类名字,类大小,CRC信息,并把自己的指针保存进来以便后续调用Register方法。

而UObjectCompiledInDefer(现在暂时不考虑动态类)最重要的收集的信息就是第一个用于构造UClass*对象的函数指针回调

二者其实都只是在一个静态Array里添加信息记录

而在整个引擎里会触发此Class的信息收集的有UCLASS、UINTERFACE、IMPLEMENT_INTRINSIC_CLASS、IMPLEMENT_CORE_INTRINSIC_CLASS,其中UCLASS和UINTERFACE我们上文已经见识过了,而IMPLEMENT_INTRINSIC_CLASS是用于在代码中包装UModel,IMPLEMENT_CORE_INTRINSIC_CLASS是用于包装UField、UClass等引擎内建的类,后两者内部也都调用了IMPLEMENT_CLASS来实现功能。

img

思考:为何需要TClassCompiledInDefer和FCompiledInDefer两个静态初始化来登记?

思考:为何需要延迟注册而不是直接在static回调里执行?

Enum的收集

static阶段会向内存注册一个构造UEnum*的函数指针用于回调.

这里并不需要像UClassCompiledInDefer一样先生成一个UClass*,因为UEnum并不是一个Class,并没有Class那么多功能集合

img

Struct的收集

Struct也和Enum同理,因为并不是一个Class,所以并不需要比较繁琐的两步构造,凭着FPendingStructRegistrant就可以后续一步构造出UScriptStruct*对象;

img

Function的收集

IMPLEMENT_CAST_FUNCTION,定义一些Object的转换函数

IMPLEMENT_VM_FUNCTION,定义一些蓝图虚拟机使用的函数

img

UObject的收集

在最开始的IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction)调用上,其内部会紧接着触发UObject::StaticClass()的调用,作为最开始的调用,检测到UClass并未生成,于是接着会转发到GetPrivateStaticClassBody中去生成一个UClass

img

类型系统代码生成重构-UE4CodeGen_Private

背景

在UE4.15版本UE4.17重构了UObjectGlobals.h.cpp

为了解决在生成代码元数据生成的时候会有过度重复的内容。

会导致每个反射文件代码量过大,编译时间过长,Debug降低代码可读性和可调试性。 本篇本质是类型代码生成的补充

UE4CodeGen_Private

UE在4.17的时候,在UObjectGlobals.h.cpp里增加了一个UE4CodeGen_Private的命名空间,里面添加了一些生成函数。

这些函数都是用来构造元数据结构和添加元数据的。第一个参数都是函数指针,第二个是关键params

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//UObjectGlobals.h
namespace UE4CodeGen_Private
{
COREUOBJECT_API void ConstructUFunction(UFunction*& OutFunction, const FFunctionParams& Params);
COREUOBJECT_API void ConstructUEnum(UEnum*& OutEnum, const FEnumParams& Params);
COREUOBJECT_API void ConstructUScriptStruct(UScriptStruct*& OutStruct, const FStructParams& Params);
COREUOBJECT_API void ConstructUPackage(UPackage*& OutPackage, const FPackageParams& Params);
COREUOBJECT_API void ConstructUClass(UClass*& OutClass, const FClassParams& Params);
}

//UObjectGlobals.cpp
namespace UE4CodeGen_Private
{
void ConstructUProperty(UObject* Outer, const FPropertyParamsBase* const*& PropertyArray, int32& NumProperties);
void AddMetaData(UObject* Object, const FMetaDataPairParam* MetaDataArray, int32 NumMetaData);
}

思考:FPropertyParamsBaseWithOffset以及后续为何不继承于FPropertyParamsBase?

思考:为什么生成的代码里大量用了函数指针来返回对象?

思考:生成的代码能否做得更加的清晰高效?

类型系统注册-第一个UClass

https://zhuanlan.zhihu.com/p/57005310

9.17 更新 : 在B站视频所说 UE5 应该是重构了IMPLEMENT_CLASS 为 IMPLEMENT_CLASS_NO_AUTO_RIGISTER,就不会有第一个CLASS 这篇 但是在main函数动态加载的时候

加载Core会使用到这部分函数

在前面已经对于生成代码与信息收集进行分析过后

类型系统的元数据依旧零散分部在全局变量中,需要去统一去调用。

在注册文章中重点是处理程序启动过程中,怎么吧手机出的信息和函数去调用,最后加载到内存中去构造中类型系统的类型树的。

Static初始化

在类型信息收集最后的UObject收集中:

IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction)会触发UObject::StaticClass()的调用,因此作为最开始的调用,会生成第一个UClass*。

1
2
3
4
#define IMPLEMENT_FUNCTION(func) \
static FNativeFunctionRegistrar UObject##func##Registar(UObject::StaticClass(),#func,&UObject::func);
//...
IMPLEMENT_VM_FUNCTION(EX_CallMath, execCallMathFunction);//ScriptCore.cpp里的定义

IMPLEMENT_CAST_FUNCTION会触发execCallMathFunction,在上面可以看出 构造的时候第一个参数就会触发 UObject::StaticClass() 调用 而后去 调用 GetPrivateStaticClass (在IMPLEMENT_CLASS定义)

在与UObject相关的IMPLEMENT_CLASS在NoExportTypes.h文件中

NoExportTypes.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#if CPP

//包含一些头文件来让NoExportTypes.gen.cpp可以编译通过

#endif

#if !CPP//这里面的部分是不参与编译的,所以不会产生定义冲突,但是却可以让UHT分析,因为UHT只是个文本分析器而已。

//枚举的声明,只是加上了宏标记。
//结构的声明,只是加上了宏标记。

//UObject的声明,C++的内容其实不重要,重要的是让UHT分析得到些什么信息
UCLASS(abstract, noexport)
class UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintImplementableEvent, meta=(BlueprintInternalUseOnly = "true"))
void ExecuteUbergraph(int32 EntryPoint);
};
#endif

NoExportTypes.gen.cpp 就和之前生成元数据一样的结构了,会有IMPLEMENT_CLASS(UObject, 1563732853) 的定义

GetPrivateStaticClass

收集GetPrivateStaticClassBody的信息,里面有需要注意的点是:

  1. Package 是构建UClass* 后把该对象的OuterPrivate设定为正确的UPackage*,UObject必须属于某个UPackage。
  2. StaticRegisterNativesUMyClass是宏拼接的在.generated.h和.gen.cpp声明定义
  3. InternalConstructor 是封装C++构造函数(你没办法去直接获得C++构造函数),在.generated.h会调用(GENERATED_UCLASS_BODY接收FObjectInitializer参数,GENERATED_BODY不接收参数)
  4. Super 是类的基类依赖于基类先构建好UClass。 WithinClass 是对象的Outer对象的类型在UObject在构建好之后应该限制放在哪种Outer下

GetPrivateStaticClassBody

  1. 全局分配内存器GUObjectAllocator 分配一部分内存,返回的ReturnClass是引用。所以外面的GetPrivateStaticClass可以提前得到值,直接访问UMyClass::StaticClass()也会返回这个值。
  2. 调用UClass构造函数,EC_StaticConstructor表示调用特定的重载的构造函数版本。因为是统一管理内存所以应该要GUObjectAllocator 来分配。
  3. InitializePrivateStaticClass会调用InSuperClassFn() -> Super::StaticClass() 和 InWithinClassFn() -> WithinClass::StaticClass() 会堆栈式的加载前置的类型。
  4. RegisterNativeFunc() 就是StaticRegisterNativesUMyClass。用来像UClass里添加Native函数(指的是在C++有函数体实现的函数,而蓝图中的函数和BlueprintImplementableEvent的函数就不是Native函数。)

InitializePrivateStaticClass

初始化

  1. 设定SuperStruct,在UStruct里的UStruct* SuperStruct的一个变量。
  2. 设定ClassWithin,限制Outer的类型。
  3. 调用UObjectBase::Register() -> UClassRegisterAllCompiledInClasses注册

UObjectBase::Register

先记录一下信息到一个全局单件Map里和一个全局链表里(LRU结构?)

思考:为何Register只是先记录一下信息?

思考:记录信息为何需要一个TMap加一个链表?

RegisterNativeFunc

GetPrivateStaticClassBody的最后一步:RegisterNativeFunc的调用

简单的往UClass*里添加Native函数的数据NativeFunctionLookupTable(UClass里的一个成员变量)

思考:为什么这么猴急的需要一开始就往UClass里添加Native函数?

思考:那些非Native的函数怎么办?

总结

img

  • SuperStruct = NULL,因为UObject上面没有基类了
  • ClassPrivate = NULL,所属的类型,这个时候还没有设置该值。在以后会设置指向UClass::StaticClass(),因为其本身是一个UClass。
  • OuterPrivate = NULL,属于的Outer,也还没放进任何Package里。在以后会设置指向“/Script/CoreUObject”的UPackage。
  • NamePrivate = “None”,还没有设定任何名字。在以后会设置为“Object”
  • ClassWithin = this,这个倒是已经知道了指向自己,表明一个UObject可以放在任何UObject下。
  • PropertiesSize = sizeof(UObject) = 56,所以一个最简单的UObject的大小是56字节

img

类型系统注册-CoreUObject模块加载

https://zhuanlan.zhihu.com/p/57611137

启动流程

UE整体启动流程 绿色部分涉及CoreUObject

img

  • Static初始化: 前文说的收集过程
  • WinMain: 以Windows为例子 是LaunchWindows.cpp 的程序入口
  • GuardedMain:程序循环 其中Engine开头的函数 简单调用到 GEngineLoop
  • FEngineLoop::PreInit : 涉及UObject启动的最开始(重点关注)

FEngineLoop::PreInit

UE是建立在UObject对象系统上的,其他模块要启动的话,那么就要CoreUObject模块初始化完成。因此在引擎循环的预初始化部分就得开始加载CoreUObject了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int32 FEngineLoop::PreInit(const TCHAR* CmdLine){//...
LoadCoreModules(); //加载CoreUObject模块
//...
//LoadPreInitModules(); //加载一些PreInit的模块,比如Engine,Renderer
//...
AppInit(); //程序初始化
//...
ProcessNewlyLoadedUObjects(); //处理最近加载的对象
//...
//LoadStartupModules(); //自己写的LoadingPhase为PreDefault的模块在这个时候加载
//...
GUObjectArray.CloseDisregardForGC(); //对象池启用,最开始是关闭的
//...
//NotifyRegistrationComplete(); //注册完成事件通知,完成Package加载
}

LoadCoreModules() 会调用FModuleManager::Get().LoadModule(TEXT(“CoreUObject”))

从而触发FCoreUObjectModule::StartupModule()

1
2
3
4
5
6
7
8
9
10
11
12
class FCoreUObjectModule : public FDefaultModuleImpl
{
virtual void StartupModule() override
{
// Register all classes that have been loaded so far. This is required for CVars to work.
UClassRegisterAllCompiledInClasses(); //注册所有编译进来的类,此刻大概有1728多个

void InitUObject();
FCoreDelegates::OnInit.AddStatic(InitUObject); //先注册个回调,后续会在AppInit里被调用
//...
}
}

UClassRegisterAllCompiledInClasses

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void UClassRegisterAllCompiledInClasses()
{
TArray<FFieldCompiledInInfo*>& DeferredClassRegistration = GetDeferredClassRegistration();
for (const FFieldCompiledInInfo* Class : DeferredClassRegistration)
{
//这里的Class其实是TClassCompiledInDefer<TClass>
UClass* RegisteredClass = Class->Register(); //return TClass::StaticClass();
}
DeferredClassRegistration.Empty(); //前面返回的是引用,因此这里可以清空数据。
}
//...
static TArray<FFieldCompiledInInfo*>& GetDeferredClassRegistration() //返回可变引用
{
static TArray<FFieldCompiledInInfo*> DeferredClassRegistration; //单件模式
return DeferredClassRegistration;
}

在上篇有提到这个函数,接下来直接上原文

  1. GetDeferredClassRegistration()里的元素是之前收集文章里讲的静态初始化的时候添加进去的,在XXX.gen.cpp里用static TClassCompiledInDefer这种形式添加。
  2. TClassCompiledInDefer<TClass>::Register()内部只是简单的转调TClass::StaticClass()
  3. TClass::StaticClass()是在XXX.generated.h里的DECLARE_CLASS宏里定义的,内部只是简单的转到GetPrivateStaticClass(TPackage)
  4. GetPrivateStaticClass(TPackage)的函数是实现是在IMPLEMENT_CLASS宏里。其内部会真正调用到GetPrivateStaticClassBody。这个函数的内部会创建出UClass对象并调用Register(),在上篇已经具体讲解过了。
  5. 总结这里的逻辑就是对之前收集到的所有的XXX.gen.cpp里定义的类,都触发一次其UClass的构造,其实也只有UObject比较特殊,会在Static初始化的时候就触发构造。因此这个过程其实是类型系统里每一个类的UClass的创建过程。
  6. 这个函数会被调用多次,在后续的ProcessNewlyLoadedUObjects的里仍然会触发该调用。在FCoreUObjectModule::StartupModule()的这次调用是最先的,这个时候加载编译进来的的类都是引擎启动一开始就链接进来的。

思考:猜猜看最先生成的是哪几个类?

思考:Struct和Enum的注册为何在这一个阶段无体现?

类型系统注册-InitUObject

https://zhuanlan.zhihu.com/p/58244447

这边代码看原文

接上文AppInit

AppInit

这一步最重要的是最后一步用一个多播委托通知了程序初始化事件

InitUObject

主要的是ProcessNewlyLoadedUObjects的注册。

我们需要在每次UE编译Module出的dll中的native类构造类型对象(定义新的UClass*对象),根据C++机制每次动态加载一个dll会触发static变量初始化,这样就会得到我们需要的元数据。

这边ProcessNewlyLoadedUObjects会调用多次

StaticUObjectInit

在UObjectBaseInit()后已经可以NewObject了,代表UObject系统的成功建立。

GObjTransientPkg: 一个全局变量,代表所有没有Outer对象都放在这个包里面

若在NewObject时没有提供Outer 则返回这个临时包,符合UObject对象必须在UPackage里的一贯基本原则

UObjectBaseInit

  1. Init了UObject的内存分配存储系统和对象的Hash系统
    1. GUObjectAllocator.AllocatePermanentObjectPool(SizeOfPermanentObjectPool);//初始化对象分配器
    2. GUObjectArray.AllocateObjectPool(MaxUObjects, MaxObjectsNotConsideredByGC, bPreAllocateUObjectArray);//初始化对象管理数组
  2. 创建了异步加载线程,用来后续Package(uasset)的加载。
  3. GObjInitialized=true,这样在后续就可以用bool UObjectInitialized()来判断对象系统是否可用。
  4. 继续转发到UObjectProcessRegistrants来把注册项一一处理。

UObjectProcessRegistrants

遍历FPendingRegistrant 为节点的链表

由GFirstPendingRegistrant和GLastPendingRegistrant 定义

用UObjectForceRegistration来注册

在每一次注册的时候会调用DequeuePendingAutoRegistrants去提取(因为可能会依赖别的模块的东西)会触发另一个依赖Module的加载,递归新的依赖注册项。

UObjectForceRegistration

UObjectForceRegistration 可能多个地方调用

  1. 对于用 UObjectProcessRegistrants 手动注册
  2. 在UClass::CreateDefaultObject() 中调用 UObjectForceRegistration(ParentClass) 确认基类注册完成
  3. 在 UE4CodeGen_Private::ConstructUClass() 中 调用UObjectForceRegistration(NewClass) 保证对象已经注册。

UObjectBase::DeferredRegister

Deferred是延迟的意思 真正注册的地方DeferredRegister

  1. 延迟注册DeferredRegister 区别与UObjectBase::Register ,指的是在对象系统初始化后的注册(GUObjectAllocator和GUObjectArray) 在Register的时候还不能去NewObject和 加载Package 在初始化之后就可以使用UObject的功能了。这边才可以开始CreatePackage
  2. Register,是指代码中class生成对应的UClass*对象并注册到全局对象数组中。

所以总结起来这里所做的是创建出UClass的Outer指向的Package,并设置ClassPrivate(这里都是UClass对象,所以其实都是UClass::StaticClass())。然后在AddObject里设置NamePrivate。因此这步之后这些一个个UClass对象才有名字,之间的联系才算完整。 但同时也需要注意的是,这些UClass对象里仍然没有UProperty和UFunciton,下一篇来讲解这些的构造生成。

总结

后续再补充

类型系统构造-再次触发-ProcessNewlyLoadedUObjects

https://zhuanlan.zhihu.com/p/58868952

上文是UObject系统的初始化和各UClass*对象的初级构造,本篇为FEngineLoop::PreInit()中AppInit()的下一个重要函数ProcessNewlyLoadedUObjects,注意在模块加载后再次触发。需要清楚:一是它是重复调用多次的,二是它的内部流程是一个完整的流程。

ProcessNewlyLoadedUObjects

  1. UClassRegisterAllCompiledInClasses 前文介绍过,为每个编译Class调用 TClassStaticClass()来构建出UClass*对象
  2. 收集到的全局数据:class,struct,enum
  3. UObjectProcessRegistrants 前文介绍过 为生成的UClass* 注册,生成其Package。 这边调用目的是在后续操作之前必须要相关类型的UClass* 对象已注册完毕
  4. UObjectLoadAllCompiledInStructs() 生成UEnum和UScriptStruct对象。
  5. UObjectLoadAllCompiledInDefaultProperties()对UClass* 继续构造和创建COD
  6. 最后一步判断如果有新UClass*对象生成了,并且现在不在初始化载入阶段(GIsInitialLoad初始=true,只有在后续开启GC后才=false表示初始化载入过程结束了),用AssembleReferenceTokenStreams为UClass创建引用记号流(一种辅助GC分析对象引用的数据结构,挖坑留待以后讲GC的时候讲解。)。所以第一次的FEngineLoop::PreInit()里的ProcessNewlyLoadedUObjects并不会触发AssembleReferenceTokenStreams的调用但也会在后续的GUObjectArray.CloseDisregardForGC()里面调用AssembleReferenceTokenStreams。只有后续模块动态加载后触发的ProcessNewlyLoadedUObjects才会AssembleReferenceTokenStreams。通过这个判断保证了在两种情况下,AssembleReferenceTokenStreams只会被调用一次。

思考:为何ProcessNewlyLoadedUObjects函数里前面的步骤总有一种既视感?

UObjectLoadAllCompiledInStructs

构造UEnum和UScriptStruct

  1. 先创建EnumRegistrant.PackageName后创建StructRegistrant.PackageName,在创建前查找是否存在。
  2. MoveTemp触发TArray中右移引用赋,把原数据迁移到目标数组中。
  3. 先enum再struct的调用其注册函数RegisterFn()。(Z_Construct开头的函数)
  4. 因为需要顺序注册所以总是先enum再struct,基础类型先构造,struct可以包含enum,反之不行。

UObjectLoadAllCompiledInDefaultProperties

UClass*对象构造

  1. 从GetDeferredCompiledInRegistration()的数组中MoveTemp来遍历。
  2. 用Registrant()构造,指向Z_Construct_UClass_UMyClass的函数
  3. 将对象放在,对应的Package的对应的三个数组中
  4. 3个数组依次手动构造CDO对象,顺序是CoreUObject、Engine和其他。
  5. 因为Class中可包含struct和enum所以Class更晚构造。

CloseDisregardForGC

开始GC,之前一直都是在初始化载入阶段。

这个阶段构造的类型UClass*对象和CDO对象,及其属于的UPackage对象,都是属于引擎底层的必要对象。

在游戏大退的时候才会被销毁。会OpenForDisregardForGC=true(GC关闭)

类型系统都构建完了之后,就可以打开GC了,因为后续可能就会NewObject了

总结

  1. 大部分是不言自明的,从左到右是类型信息的收集和消费过程。从上到下是依据代码的执行顺序。
  2. ’2. 红色箭头代表数据的产生添加,蓝色箭头代表数据的消费使用。这二者一起表达了类型信息的数据流向。
  3. 浅蓝色箭头和矩形,代表内存中UClass*以及类型对象的创建和构造。
  4. 信息收集里黄色的3个矩形,代表它们的数据会一直在内存中,用来做查找用,不会被清空。

img

类型系统构造-构造绑定链接

https://zhuanlan.zhihu.com/p/59553490

在上篇介绍了类型注册的最后阶段,为每一个enum、struct、class都进行了一次RegisterFn调用(忘了调用时机的请翻阅前文),而这些RegisterFn其实都指向生成代码里的函数。本篇就来讲解这个里面的类型对象生成和构造。 按照生成顺序,也是调用顺序,一一讲解UEnum和UScriptStruct的生成,以及UClass的继续构造。

UEnum

把MyEnum_StaticEnum注册给了RegisterFn。调用的时候,内部的GetStaticEnum会调用参赛里的Z_Construct_UEnum_Hello_MyEnum。而Z_Construct_UEnum_Hello_MyEnum内部其实比较简单,定义了枚举项参数和枚举参数,最终发给UE4CodeGen_Private::ConstructUEnum调用。

img

img

类型系统-总结

https://zhuanlan.zhihu.com/p/60291730

一个UClass*要经历这么几个阶段:

  1. 内存构造。刚创建出来一块白纸一般的内存,简单的调用了UClass的构造函数。UE里一个对象的构造,构造函数的调用只是个起点而已。
  2. 注册。给自己一个名字,把自己登记在对象系统中。这步是通过DeferredRegister而不是Register完成的。
  3. 对象构造。往对象里填充属性、函数、接口和元数据的信息。这步我们应该记得是在gen.cpp里的那些函数完成的。
  4. 绑定链接。属性函数都有了,把它们整理整理,尽量优化一下存储结构,为以后的使用提供更高性能和便利。
  5. CDO创建。既然类型已经有了,那就万事俱备只差国家包分配一个类默认对象了。每个UClass都有一个CDO(Class Default Object),有了CDO,相当于有了一个存档备份和参照,其他对象就心不慌。
  6. 引用记号流构建。一个Class是怎么样有可能引用其他别的对象的,这棵引用树怎么样构建的高效,也是GC中一个非常重要的话题。有了引用记号流,就可以对一个对象高效的分析它引用了其他多少对象。

UMetaData

只在Editor模式下使用,在所有的类型对象Construct的一步就是AddMetaData。

  1. UMetaData是属于UPackage关联,而不是绑定在某个UField中。
  2. UMetaData在Runtime是被略过去的。
  3. UMetaData是个对象。
  • UMetaData的定义
    • TMap< FWeakObjectPtr, TMap<FName, FString> > ObjectMetaDataMap;
      • 对象关联的键值对。
      • FWeakObjectPtr 弱指针引用UObject对象,这样就不会阻碍GC
      • FName,key只用固定的一些Category,Tooltip这些
      • FString,val爱写啥写啥。

思考:为何不把Map<FName,FString> MetaDataMap直接放进UObject里?

GRegisterCast和GRegisterNative的作用

IMPLEMENT_CAST_FUNCTION收集到GRegisterCast,IMPLEMENT_VM_FUNCTION收集到GRegisterNative。

这些其实就是一些函数用来做对象的转换和蓝图虚拟机的一些基础函数。把虚拟机里运行的指令码和真正的函数指针绑定起来。

Flags

UE利用这些枚举标志来判断对象的状态和特征。 重要的有:

  • EObjectFlags:对象本身的标志。
  • ElnternalObjectFlags:对象存储的标志, GC的时候用来检查可达性。
  • EObjectMarkt:用来额外标记对象特征的标志,用在序列化过程中标识状态
  • EClassFlags:类的标志,定义了一个类的特征。
  • EClassCastFlags:类之间的转换,可以快速的测试一个类是否可以转换成某种类型。
  • EStructFlags:结构的特征标志。
  • EFunctionFlags:函数的特征标志。
  • EPropertyFlags:属性的特征标志。

类型系统-反射实战

https://zhuanlan.zhihu.com/p/61042237

功能类别 核心函数/类 主要作用 常用场景示例
获取类型信息 UYourClass::StaticClass() UObjectInstance->GetClass() 获取一个类或对象实例的 UClass(运行时类信息元数据)1,3,7 获取类的名称、判断继承关系、用于后续的属性和函数遍历。
UStruct::StaticStruct() 获取结构体的 UScriptStruct 元数据1,3 获取结构体的类型信息。
FindObject/(ANY_PACKAGE, TEXT(“Name”), true) 根据名称在任意包中查找 UClass 或 UEnum 反射对象5,7 动态查找已知名称的类或枚举类型。
遍历字段 TFieldIterator(SomeUClass) 遍历一个 UClass 或 UScriptStruct 的所有反射属性(FProperty)1,3,7 动态查看一个类的所有属性名称和类型。
TFieldIterator(SomeUClass) 遍历一个 UClass 的所有反射函数(UFunction)1,3 动态查看一个类的所有函数名称。
查看继承 UStruct::GetSuperStruct() 获取当前 UClass 或 UScriptStruct 的父类/父结构体指针7 遍历类的继承链。
UClass::IsChildOf(OtherUClass) 判断一个类是否是另一个类的子类7 检查类型的继承关系。
GetDerivedClasses(BaseUClass, OutArray, bRecursive) 获取所有派生自某个基类的 UClass 列表7 查找游戏中所有特定类型的类(如所有武器类)。
属性值操作 FProperty::ContainerPtrToValuePtr(ObjectInstance) 获取某个对象实例中特定属性值的内存地址7 为动态获取或设置属性值做准备。
FFloatProperty::GetPropertyValue(ValuePtr) FStrProperty::SetPropertyValue(ValuePtr, NewValue) (通过具体属性类型的类)从内存地址读取或写入属性值7 动态获取或设置特定类型的属性值。
FProperty::ImportText(*InString, ValuePtr, nullptr, nullptr) 将字符串值转换并导入到属性值的内存地址中7 从文本文件(如 CSV)中读取数据并动态设置到对象属性上。
反射调用函数 UClass::FindFunctionByName(“FunctionName”) 根据函数名称查找并获取 UFunction1,3,7 为动态调用函数做准备。
UObject::ProcessEvent(UFunction, Params) 让对象实例执行指定的 UFunction1,3,6,7 动态调用蓝图暴露的函数或事件。
对象与GC GetObjectsOfClass(SomeUClass, OutArray) 获取世界中所有属于特定 UClass 的对象实例7 查找场景中所有特定的 Actor。
NewObject() 动态创建一个支持反射的 UObject 实例3 在运行时生成对象。
  • 基础与依赖:上述多数函数都基于 Unreal Header Tool (UHT) 生成的反射数据。你的 C++ 类必须使用 UCLASS(), UFUNCTION(), UPROPERTY() 等宏并包含 .generated.h 文件,这些函数才能正确工作。
  • 性能注意:运行时反射操作(如遍历字段、FindFunctionByNameProcessEvent)比直接的 C++ 调用开销更大,应避免在性能敏感的循环(如 Tick)中频繁使用。
  • 安全注意:使用 ContainerPtrToValuePtr 和手动构建 ProcessEvent 参数时,务必确保类型和内存操作的正确性,否则可能导致崩溃。

视频流程梳理

https://www.bilibili.com/video/BV1bH4y1k7Wg/?share_source=copy_web&vd_source=42dfbaf08c560ddd9e83bed118ff917d)

反射的使用

参考C# 反射

UHT生成文件

  1. .h 文件
    1. GENERATED_BODY()
      1. 生成另一个宏FID_ProjectCppTest_Source_ProjectCppTest_Public_Myobject_h_15_GENERATED_BODY
      2. FID_ProjectCppTest_Source_ProjectCppTest_Public_MyObject_h_15_ACCESSORS 注册natives 函数 在函数体cpp文件中
      3. DECLARE_CLASS 声明一系列重载运算符与StaticClass()
  2. .cpp文件
    1. 构造函数
      1. Z_Construct_UClass_UMyObject()
      2. Z_Construct_UPackage_Script_ProjectCppTest()
    2. IMPLEMENT_CLASS_NO_AUTO_REGISTRATION(UMyobject)宏 (UE5.2 更新过)
      1. 以前UE4版本调用的是IMPLEMENT_CLASS
      2. 重要定义了 Z_Registration_Info_UClass_MyObject 结构
      3. 以及定义了 GetPrivateStaticClass ()函数 这个函数是执行真正注册行为,在StaticClass()函数中调用
    3. Z_Construct_UClass_UMyObject_Static 宏
      1. 静态结构体,包含了函数UFUNCTION和成员变量UPROPERTY

Static 自动注册

C++ Static 自动注册模式,根据C++ static对象会在main函数之前初始化的特性

最后走到RegisterCompiledInInfo()

利用FClassDeferredRegistry::Get().AddRegistration()

收集到 TDeferredRegistry<> 中

后在main 函数构造的时候去构造UClass 对象 和 CDO

(可能与上文不符合的是这边上文讲了UE4会构造第一个UCALSS ,UE5可能没有)

实现流程

在static静态初始化后 main初始化后的动态初始化流程

UE程序入口 LaunchWindows.cpp

  • WinMain()开始

  • 调用LaunchWindowsStartup()

  • GuardedMain(CmdLine); 游戏循环逻辑

  • EnginePreInit(CmdLine);

  • PreInit(CmdLine);

    • PreInitPreStartupScreen(CmdLine);
      • LoadCoreModules()
        • FModuleManager::Get().LoadModule(TEXT(“CoreUObject”)) != nullptr;
        • 生成CoreMoudule -> class FCoreUobjectModule : public FDefaultModuleImpl
          • 在里面调用StartupModule()
            • UClassRegisterAllCompiledInClasses();
              • for (const FClassDeferredRegistry::FRegistrant& Registrant : Registry.GetRegistrations())
              • 注释: 前面Static 自动注册 添加的静态数据遍历
              • UClass* RegisteredClass = FClassDeferredRegistry: :InnerRegister(Registrant);
              • 注释: 这里的Class其实是TClassCompiledInDefer -> return TClass::StaticClass();
              • 在前面UHT生成文件中DECLARE_CLASS 注册过的函数 最后到达GetPrivateStaticClass ()
              • 注释: GetPrivateStaticClass() 函数体在 .cpp 中 的IMPLEMENT_CLASS_NO_AUTO_REGISTRATION 宏 调用 GetPrivateStaticClassBody()
              • GetPrivateStaticClassBody()
                • 是真的创建对象,为其创建内存空间
                • ReturnClass = (UClass*)GUObjectAllocator.AllocateUObject(sizeof(UClass), alignof(UClass), true);
                • InitializePrivateStaticClass() 初始化操作
                  • 指定父类class
                  • 简单注册到LRU结构中UObjectBase::Register()
                • RegisterNativeFunc();
                  • 函数指针 传入 void UMyObject::StaticRegisterNativesUMyObject()
            • InitUObject();
              • FCoreDelegates::OnInit.AddStatic(); ->添加委托 在AppInit 初始化完成调用
  • AppInit();

    • 完成时候 会广播委托 FCoreDelegates::OnInit.Broadcast(); -> 调用StartupModule()中的 InitUObject()
  • InitUObject() 上文有一整个章节讲这个

    • 添加 ProcessNewlyLoadedUObjects() 委托
    • StaticUObjectInit()
      • UObjectBaseInit()
        • 内存分配器和Hash管理器 AllocatePermanentobjectPool 和 AllocateObjectPool (这俩步完成就可以NewObject了?)
        • UObjectProcessRegistrants();
          • 从前面全局LRU的链表结构中获取TArray
          • 遍历TArray 调用UObjectForceRegistration()
            • 调用 DeferredRegister() -> 创建真正的Package
            • 再取一次链表
            • (因为怕有依赖问题,可能依赖于其他Module从而加载底层Module,也可能会有其他Module已经注册好,数据会变化)
      • 新建默认临时Package -> GobjTransientPkg = Newobject(nullptr, TEXT(“/Engine/Transient”), RF_Transient);
        • 没有指定Package 的话会加入这个默认临时Package (GobjTransientPkg )中
  • ProcessNewlyLoadedUObjects()

    • 俩种情况会调用
      • PreInit()调用 默认加载对象已经默认已经加载的对象
      • 加载完成Module调用 加载Module中的对象
    • 遍历Module中所有U对象
      • 调用UObjectProcessRegistrants()(前面有讲到过)

      • UObjectLoadAllCompiledInStructs(); (对Struct处理)

      • UObjectLoadAllCompiledInDefaultProperties()

        • 分成三个数组 NewClasses NewClassesInCoreUObject NewClassesInEngine

        • 调用 DoPendingOuterRegistrations()

          • 调用OuterRegister()
            • 实际上return Registrant.OuterRegisterFn()
            • 是函数指针,指向的是Z_Construct_UClass_UMyObject(),即为ConstructUClass()
              • ConstructUClass()会完善所有信息 构造所有对象 UCLASS 对象会所有构造完毕
        • 前面构造完毕UCLASS对象会对每一个对象赋值 CDO

          • CDO可以获取对象备份

          • 也可以变成单例

    • AssembleReferenceTokenStream(); 获取引用链 GC会使用这个函数

img

总结

看原文

一个UClass*要经历这么几个阶段。。。