简介: 这里就会牵扯到 UE 哪些形形色色的代码生成宏定义了,得力于前面已经梳理过 UObject 和 FField 相关的类,这里再次遇到就方便很多了。
语法提示:UBT 和 UHT 已经都是 C# 编写的了,所以在阅读记录里的语法点都是 C# 的,本页基本没有 C++ 相关的
心得: 一直以为 UBT 和 UHT 会完全生成全新的 C++ 代码,经过抽丝剥茧的逻辑梳理,最终发现它只是根据我们标记的宏或标签,自动生成一些增量的代码,例如需要实现 C++ 反射用到的代码,宏的展开代码等。这也算是一个巨大收获,毕竟现在连 ue 的功能菜单都还认不全
相关模块状态和入口
-
UnrealHeaderTool:在UE5中已经用 C# 重新,是一个独立的命令行程序,位于
Engine/Source/Programs/Shared/EpicGames.UHT/EpicGames.UHT.csproj,是UHT的主模块。 -
CoreUObject:提供反射运行时的支持,包含反射元数据的基础类(如UClass,UProperty等),位于
Engine/Source/Runtime/CoreUObject -
UnrealHeaderTool 现在是被编译成一个 dll 文件,由 UBT 进行调用,UBT 则会在不同的地方大概率被以命令行的形式调用例如
- FProcHandle ProcHandle = FPlatformProcess::CreateProc(*ExecutableFileName, *CmdLineParams, bLaunchDetached, bLaunchHidden, bLaunchReallyHidden, NULL, 0, NULL, OutWritePipe, OutReadPipe); 其中 ExecutableFileName 就是已经获取好的执行命令的 UBT dll 的名称
- UnrealBuildTool 模块 也被编译成了一个 dll,UnrealBuildTool.cs 内的入口 main 函数主要负责参数的提取与整理校对,获取正确的 ToolMode具体子类,然后执行 ExecuteAsync 命令
- UnrealBuildTool 模块内的 UnrealHeaderToolMode 主要通过 UnrealHeaderTool 模块内的 UhtSession 来进行 UHT 相关功能的调用
UBT 和 UHT 的关系
- UHT 的执行逻辑基本都是通过 UBT 来调用的
-
UHT 会被 UBT 调用的几种情况
- 编译目标时
- Hot Reload 时
- GenerateProjectFiles 时
- 执行 -Clean 命令式
- 蓝图原生基类更新时
-
UBT 调用 UHT 的具体步骤大致如下
- UBT解析目标描述文件(.target.cs)和模块文件(.Build.cs)来确定需要编译的模块。我们前面预览的 Target 和 Build 文件主要用途就在这里了
- UBT 扫描每个模块的公共头文件,检查是否存在反射宏,例如(GENERATED_BODY)
- 对于需要生成反射代码的模块,UBT 就会通过 UhtSession 类来执行 UHT 的相关功能
- UHT 运行后,解析指定的头文件,生成相应的反射代码文件(.generated.h和.generated.cpp)。
- UBT 继续执行编译步骤,将生成的代码文件包含在模块的编译过程中
- 常见的反射宏,这些宏在前面阅读代码的时候经常可以遇到
// 反射标记触发 UHT
- UCLASS()
- USTRUCT()
- UENUM()
- UFUNCTION()
- UPROPERTY()
UHT 模块目录分类
截止到目前 ,UHT 最新的代码结构被归纳在了以下几个目录内
1. Tokenizer 分词器,关键功能是识别 反射宏、关键字、标识符、字面量、运算符等。将源代码文本 -> Tokens 流
2. Parsers 接受来自 Tokenizer 的 token 流,根据 C++ 和反射宏 的语法规则构建抽象语法树。消费 Tokens 流 -> 理解代码结构 (类、函数、属性、宏) -> 输出结构信息 + 属性字符串
3. Specifiers 限定符、属性处理器,用来处理 反射宏 括号内的(...) 属性字符串 解析属性字符串 ((…)) -> 结构化的属性键值对/标志 -> 应用到对应的结构信息上
4. Types 类型表示,将解析器 Parsers 提取的结构化的信息存储在 C++ 对象表示的内存模型中 解析器 (Parsers) 生成的结构化信息(附加了属性) -> 被存储在具体类型 (FClass, FStruct, FEnum, FProperty, FFunction) 的对象实例中
5. Tables收集、存储、查找并管理所有从源代码中提取出来的反射元数据对象 收集、索引、管理来自 Types 的所有对象 -> 构建全局元数据数据库 -> 解决依赖和进行最终验证
6. Exporters 代码生成器,获取存储在 Tables 数据库中的完整元数据模型,并将其转换为目标输出。最主要的输出是每个包含反射标记的 .h 文件对应的 .generated.cpp 文件 消费 Tables 数据库 -> 生成目标输出代码 (.generated.cpp 等)
7. Utils 辅助工具类
运作流程再梳理
- UBT 在其
Main函数内,处理到对应的ToolMode调用ExecuteAsync,若当前对象的类型是UnrealHeaderToolMode则自然会调用UnrealHeaderToolMode的ExecuteAsync函数 -
UnrealHeaderToolMode 内的
ExecuteAsync函数 会初始化UhtTables 、UhtConfigImpl 、UhtGlobalOptions,然后创建UhtSession对象,并将初始化的几个对象设置给Session对象持有 - 执行
UhtSession对象的Run函数,传入 manifestFile 文件路径,和命令行参数 -
UhtSession对象的Run函数内执行一些了操作,基本就是 UHT 的主要流程了,如下:
//一系列的检查
StepReadManifestFile - 读取 manifest 文件,并通过 JsonSerializer 将内容解析到 UhtManifestFile 内的 Manifest 对象
StepPrepareModules() - 初始化引擎模块系统并加载所有参与处理的模块信息。
StepPrepareHeaders() - 扫描并收集所有需要处理的头文件及其依赖关系。
StepParseHeaders() - 解析头文件内容生成抽象语法树(AST)并记录元数据,这个是一个主要的 Parser 入口,其它的parser 也会在执行过程中通过委托的形式调用
StepPopulateTypeTable() - 创建类型系统表并填充基本类型定义关系。
StepResolveInvalidCheck() - 检测并标记无效符号引用与语法错误。
StepBindSuperAndBases() - 建立类继承树并绑定父类基础关系。
RecursiveStructCheck() - 验证结构体嵌套定义是否符合内存布局约束。
StepResolveBases() - 解析并确认所有基类的合法性和可访问性。
StepResolveProperties() - 完善属性系统元数据并建立反射关系链。
StepResolveFinal() - 执行跨符号的最终绑定和类型校验检查。
StepResolveValidate() - 执行完整性检查确保所有符号完全解析。
StepCollectReferences() - 收集类型引用关系为代码生成做准备。
TopologicalSortHeaderFiles() - 按依赖关系拓扑排序头文件确保正确生成顺序。
StepExport();将解析完成的类型系统元数据转换为目标代码格式(如 C++ 或 蓝图),生成运行时反射所需的 .generated.h/.cpp 文件及其他辅助脚本。
Parse的调用过程
其它几个目录内的类基本都是可以搜到调用位置的,而 Parse 内的类只有
UhtHeaderFileParser 和 UhtPropertyParser 可以找到调用位置。其它的 parser 貌似没有被调用。
- UhtHeaderFileParser 的 static 类型的 Parse 函数是在 UhtSession 的 StepParseHeaders 函数内调用的
- UhtPropertyParser 的 static 类型的 Parse 函数是在 UhtFunctionParser 内的 UPROPERTYKeyword 函数内调用的,这个函数就是被反射调用的一个函数
- UhtSpecifierParser 内的函数在被其他的 Parser 需要的时候调用
其实 Parser 内的大部分类使用了反射进行调用,他们大部分都能和前面浏览过的类有关联,然后他们有个特点就是有几个头部函数是有 UhtKeyword 属性标记的,类也有 UnrealHeaderTool 的属性标记。
- UhtClassParser
- UhtEnumParser
- UhtFunctionParser
- UhtAccessSpecifierKeywords
- UhtInterfaceClassParser
- UhtNativeInterfaceClassParser
- UhtScriptStructParser
那么UHT 是如何使用反射调用这些 parser 的呢,根据之前分析的目录功能,Tables 里的类都是负责收集数据的,其中里面就有一个 UhtKeyword 类,它包含一个 Delegate 委托函数,对应的就是那些被 UhtKeyword 属性标记过的函数
- 委托函数的设置路径
UnrealBuildTool 内的 Main 函数调用 ToolMode 实例(具体类为 UnrealHeaderToolMode)的 ExecuteAsync, 在其开头的 UhtTables 的构造函数内 , CheckForAttributes(Assembly? assembly) -> CheckForAttributes(Type type) -> HandleUnrealHeaderToolAttribute(Type type, UnrealHeaderToolAttribute parserAttribute) -> KeywordTables.OnKeywordAttribute(type, methodInfo, keywordAttribute); -> 创建 UhtKeyword 的时候在构造函数内完成了 委托函数的设置,所以 委托函数被 UhtKeyword 持有
UhtKeywordTables 继承自 UhtLookupTables<UhtKeywordTable> 父类里有一个 字典用来根据table的名字保存的 table 对象,在 OnKeywordAttribute 函数内根据名字获取到 UhtKeywordTable 对象,然后给他添加委托函数
- 委托函数的执行调用路径
UhtSession 这个对象用一个 Tables 实例持有了所有的 table,在其它方法调用的时候有时候会通过 UhtSession 来获取需要的 table
也是从 UhtSession 对象的 Run(string manifestFilePath, CommandLineArguments? arguments = null) 函数开始 -> StepParseHeaders() -> UhtHeaderFileParser.Parse(headerFile);进入 UhtHeaderFileParser 内部的 Parse 函数,headerParser.ParseStatements(); -> ParseStatements((char)0, (char)0, true); -> ParseStatement(_topScope, ref token, logUnhandledKeywords); -> DispatchKeyword(topScope, ref token); 在 DispatchKeyword 函数内 通过指定的 Table 获取 对应的 UhtKeyword 对象,然后调用其 委托函数,完成了 parser 的调用
这里有一个需要注意的点,在 UhtHeaderFileParser 的 Parse 函数的开头执行 headerParser.ParseStatements(); 之前 ,创建了一个 topScope 对象,UhtParsingScope 的构造函数参数需要一个 UhtHeaderFileParser 对象,构造函数将刚构建的 UhtParsingScope 对象使用传入的 UhtHeaderFileParser 对象的 PushScope 函数推入,并设置为 _topScope 对象
BindSuperAndBases 的过程
-
UhtType 作为基类定义了一个虚拟函数 BindSuperAndBase(),具体的绑定有继承它的子类来实现,找到了两个进行了 Override 的子类
- UthClass
- UthScriptStruct
-
调用过程
-
UhtSession的Run函数内调用StepBindSuperAndBases () -
ForEachHeader循环遍历_headerFiles文件并调用UhtHeaderFile对象的BindSuperAndBases()函数
-
StepResolve 过程
整体就是在填充对应的 UhtType 的内部属性,用来为最后做代码生成做准备
Exporter 导出、代码的生成
导出相关的主要逻辑文件都存放在了 Exporters 目录下面
Exporter 的使用方法类似 Parser, 它也是使用属性标签,然后在 UhtSession 里给 Parser 设置代理的函数里,进行代理的设置。
在函数上使用了 UhtExporter 属性的类有
ScriptGeneratorUhtJsonExporterUhtCodeGeneratorUhtStatsExporter
IUhtExportFactory 持有了 Exporter 需要的信息,所以在委托函数的参数里会被传入
UHT 相关的核心模块和类
| 模块 | 关键类/文件 | 重要性 | 核心职责 | 关键接口/方法 |
|---|---|---|---|---|
| EpicGames.UHT (C#) | Utils/UhtSession.cs | ★★★★★ | 全局会话状态管理 |
Run() UBT 内主要调用到的,StepParseHeaders(), StepExport()
|
UhtTokenReader.cs->IUhtTokenReader
|
★★★★★ | 头文件词法/语法解析 | Token 的读取接口,UhtTokenBufferReader 和 UhtTokenReplayReader 继承了这个接口。UhtTokenBufferReader接口内声明的方法很少,但是在同目录下使用 8 个Extensions 文件以扩展的形式给他添加了更多的函数,来应对更多的数据类型 ,且很多都是static 函数 |
|
IUhtTokenPreprocessor |
★★★★★ | token 预处理 |
UhtHeaderFileParser 继承了它 ParsePreprocessorDirective()
|
|
| UhtType.cs | ★★★★★ | 反射类型基类 ,Types 目录下有一批和 UObject 继承结构类似的 Uht 开头的类,他们继承自 UhtObject,UhtObject 继承自 UhtTypes |
BindSuperAndBases() , Resolve(UhtResolvePhase phase)
|
|
| UhtHeaderFile.cs | ★★★★★ | 头文件类型,它在Types目录内,但是没有继承 UhtTypes |
AddReferencedHeader() , AddChild , Resolve , BindSuperAndBases , CollectReferences
|
|
| UhtHeaderFileParser.cs | ★★★★★ | Parser 的主要触发类 |
Parse(UhtHeaderFile headerFile) 输入一个HeadFile 进行解析 |
|
| UhtTables.cs | ★★★★★ | 保存反射元数据的类 | 里面声明了很多 table 类,其中 UhtTables 是外部使用的最大的类 UhtTableNames 定义了一些列标准的table名,是个静态类 |
|
UHTManifest |
★★★★★ | 保存ManifestFile文件解析后的结果,被 UhtManifestFile 对象持有 |
UhtManifestFile 调用 Read 函数后,将结果保存在内部的 UHTManifest 对象上,里面有解析好的一些属性 。这个UHTManifest 在 Shared EpicGames.Core 模块内的 UHTTypes.cs 文件内 |
|
UhtMetaData |
★★★☆☆ | 保存宏定义内的 meta 元数据 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Player", meta=(DisplayName="生命值", ClampMin=0.0)) |
|
UhtExportFactory |
★★★☆☆ | 构造时传入 Exporter 对象,执行 Run 函数进行代码输出 | Run() |
|
| UnrealBuildTool | UnrealBuildTool.cs | ★★★★★ | 构建系统入口 | Main(string[] ArgumentsArray) |
| ToolMode.cs | ★★★★★ | 工具构建模式基类,有很多子类实现 ExecuteAsync 的命令行执行 | ExecuteAsync(CommandLineArguments Arguments, ILogger Logger) |
|
| UnrealHeaderToolMode -> ToolMode | ★★★★★ | UHT 模块工具 | ExecuteAsync(CommandLineArguments Arguments, ILogger Logger) |
阅读记录
UBA (Unreal Build Accelerator) unreal 的分布式编译系统
FileReference? RunFile = null; 这个C#的 FileReference 类后面的问号表示 RunFile可以为空, dotnet8 的新语法规则, 不加? RunFile 不可以赋予空值
PrintUsage UnrealBuildTool 内的函数,它通过获取 GlobalOptions 内的 Fields 然后再根据 Field的attribute 的 信息就可以输出 helper 信息,这个方法很好,在写代码的时候就自动带了 helper 信息~~~
Tokenizer UHT 模块的 Tokenizer 目录下,保存的是 token相关类型,基本就是一个接口IUhtTokenReader,两个实现类UhtTokenBufferReader,UhtTokenReplayReader,剩下的 Extensions 文件都是为 IUhtTokenReader 添加对应类型数据的函数扩展
Token 这里的 token 类似将代码拆成数组,例如 public void test(float a), 就变成了 [public,void,test,(,float,a,)] 这样子,然后它使用类似堆盏的方式,可以取出,这样就可以一步一步的判断下一个是什么。IUhtTokenReader 的 require 函数返回的还是 IUhtTokenReader,这样就可以函数传承一条链。
_ = PropertyTypeTable.Default; 这是一种丢失语法,他会执行一下 Default 的getter,然后将返回值丢弃,就是空跑一下,但是可以触发 getter 内的逻辑。还可以消除 Default never used 的编辑器警告
UhtLookupTableBase Table 数据的基类
-
UhtLookupTable类继承自UhtLookupTableBase用于实现查找 Table 内的具体数据 -
UhtLookupTables用于保存和查找UhtLookupTable,它并继承 UhtLookupTableBase
public UhtExporterTable ExporterTable => Tables!.ExporterTable; !的作用是禁用 编辑器的 null 报警,意思就是可以保证 ExporterTalbe 肯定不会为空,不用报警
Parallel.ForEach() C# 的并行计算功能
Task C# 的并行计算功能