UE通过UBT来构建项目(不管是VS里的Build也好,Editor里的Compile也好,最终都会调用UBT)。UBT和UHT是UE工具链的基石,内容太多,没办法一次性分析全部,先梳理出一个大致的轮廓,有时间再慢慢补充。
先对UBT和UHT的工作职责有一个大概介绍:
UBT:
- Scans solution directory for modules and plug-ins
- Determines all modules that need to be rebuilt
- Invokes UHT to parse C++ headers
- Creates compiler & linker options from .Build.cs & .Target.cs
- Executes platform specific compilers (VisualStudio, LLVM)
UHT:
- Parses all C++ headers containing UClasses
- Generates glue code for all Unreal classes & functions
- Generated files stored in Intermediates directory
VS
言归正传。首先,从零开始,第一步先创建一个C++项目(BasicCode/ThridPerson任选),并打开VS。
打开VS之后可以看到这样的Solution结构:
在Solution中选中创建的Project点击右键-Properties:
可以看到,NMake-Gerneral下的构建命令(Build Command)
使用的均是Engine\Build\BatchFiles
目录下的bat(在Windows平台):
1 | # Build |
以Build.bat为例:
1 | @echo off |
可以看到Build.bat
将接收的参数都转发给了UnrealBuildTool.exe
:
1 | ..\..\Engine\Binaries\DotNET\UnrealBuildTool.exe %* |
UBT
通过UnrealBuildTool构建项目需要传递参数:
- %1 is the game name
- %2 is the platform name
- %3 is the configuration name
- %4 is the ProjectPath
1 | # Example |
然后来看一下UnrealBuildTools是怎么处理的:
1 | // Engine\Source\Programs\UnrealBuildTools\UnrealBuildTool.cs |
可以看到传入进来的参数。
在GuardedMain中对引擎和传入参数做了一堆检测之后,会调用RunUBT
:
RulesAssembly
在RunUBT
中,有一个相当重要的函数调用UEBuildTarget.CreateTarget
:
1 | // Engine\Source\Programs\UnrealBuildTools\UnrealBuildTool.cs |
UEBuildTarget.CreateTarget
的定义在Configuration/UEBuildTarget.cs
中。它在里面构造了一个RulesAssembly
的对象,它是用来读取和构造项目中的target.cs
和Module的build.cs
的。RulesAssembly
的构造调用栈如下:
RulesAssembly
的构造函数接收了一堆参数:
1 | public RulesAssembly(DirectoryReference BaseDir, IReadOnlyList<PluginInfo> Plugins, List<FileReference> ModuleFiles, List<FileReference> TargetFiles, Dictionary<FileReference, PluginInfo> ModuleFileToPluginInfo, FileReference AssemblyFileName, bool bContainsEngineModules, bool bUseBackwardsCompatibleDefaults, bool bReadOnly, bool bSkipCompile, RulesAssembly Parent |
- BaseDir:项目目录
- Plugins:项目依赖的所有插件的
.uplugin
文件的绝对路径 - ModuleFiles:是当前Target中的所有Module(Game Module和所有插件内的Module)的
.build.cs
文件的绝对路径 - TargetFiles:当前Target中所有的
target.cs
的文件绝对路径 - ModuleFileToPluginInfo:插件的信息map,Module的
build.cs
文件与Module的基本信息 - AssemblyFileName:项目的BuildRules的DLL文件绝对路径(该文件位于(
Intermediate/Build/BuildRules
下,是调用UnrealVersionSelector
生成VS项目时创建的))
1 | // e.g |
- 其他参数(不是本篇文章的重点)
RulesAssembly
类中定义了两个重要的成员:TargetNameToTargetFile
和ModuleNameToModuleFile
,在构造函数中把当前的项目中中所有定义的TargetRules和ModuleRules都添加到了里面。
下面是RulesAssembly
的构造函数:
1 | // UnrealBuildTool/System/RulesAssembly.cs |
编译环境:构造Target并执行
Target
的主要作用是收集和设定项目的编译信息用于编译真正的可执行程序的设置,类似于在VS中的项目设置。
在RunUBT
中会对传入的参数(Platform/Configuration等)做提取,并添加上一系列参数,之后通过调用UEBuildTarget.CreateTarget
,创建一个UBuildTarget
的对象:
1 | // UnrealBuildTool/Configuration/UEBuildTarget.cs |
在CreateTarget
中又调用了RulesAssembly.CreateTargetRules
(获取target.cs
文件,以及将最基本的编译环境信息构造出TargetInfo
并传递给CreateTargetRulesInstance
用于创建TargetRules
对象):
1 | // UnrealBuildTool/System/RulesAssembly.cs |
获得项目的*.Target.cs
文件,然后调用CreateTargetRulesInstance
构造出一个TargetRules
的对象(target.cs
中构造的就是这个类型的对象),并执行target.cs
中的构造函数中的代码。
1 | // UnrealBuildTool/System/RulesAssembly.cs |
经过上面的一波操作之后,我们现在已经从target.cs
中得到了TargetRules
对象,它代表了当前项目的编译环境,代码列了一堆,看着有点烦人,他们的调用栈如下:
编译目标:Module
Module
是UE中真正用来执行的一个个小目标文件,编译出exe或者DLL(通过启动模块编译出exe,非启动模块编译出DLL,或者静态链接到exe中)。
在上面的执行完毕之后,UBT会开始读取和分析项目中的ModuleRules
,并通过它们构造出一个个UEBuildModule
,用于后续的编译处理。
在RunUBT
->UEBuildTarget.Build
->PreBuidSetup
,可以理解真正的执行逻辑是在PreBuildSetup
中执行的:
1 | // UEBuildTarget.cs |
其中的:
1 | // Setup the target's binaries. |
它们直接或间接地又调用FindOrCreateCppModuleByName
,最终又会调用到CreateModuleRulesAndSetDefaults
来构造出真正的ModuleRules
对象,并创建出UEBuildModuleCPP
用于编译的模块:
CreateModuleRulesAndSetDefaults
又调用了RulesAssembly.CreateModuleRules
(注意此时执行流已经又回到RulesAssembly
来了)。
RulesAssembly.CreateModuleRules
通过上面构造时存起来的ModuleNameToModuleFile
通过Module名拿到*.build.cs
文件,然后与调用TargetRules
的构造方法一样调用ModuleRules
的构造函数,并且将上面构造出来的TargetRules
传递给ModuleRules
。
1 | /// <summary> |
我们在所有的GameMode与所有Plugin中的build.cs
中的代码在此时执行。
Launch模块的编译
上面写到了编译Module
,有一个Module
很特殊,那就是Launch
模块。
任何可执行程序都会有一个执行入口,在UE中,每一个Target都会编译出一个可执行程序。引擎启动是从Launch
模块开始的,main
函数也是定义在其中的,所以需要将Launch模块中的main函数编译出一个可执行程序来。
启动模块是在UBT
的TargetRules
中指定的:
1 | public string LaunchModuleName |
可以在TargetRules
中使用LaunchModuleNamePrivate
指定一个启动模块,如果没有指定且Target类型不为Program
,则使用Launch
模块,否则使用指定的模块。即不管是Game/Editor/Server/Client的Target启动模块都是Launch
。
但是,因为LaunchModuleNamePrivate
在TargetRules
的定义中是一个private
成员,无法在我们继承来的TargetRules
中赋值,所以目前也没有什么用。
在UEBuildTarget.SetupBinaries
中被使用(上面也已经提到过了,UEBuildBinary
就是我们要编译出的启动模块的可执行exe编译对象,并且只在这里被创建):
1 | /// <summary> |
执行环境如下:
可以看到这里的输出文件就是我们编译的项目exe了。
UHT
之后会调用UHT来生成代码:
调用的函数为ExecuteHeaderToolIfNecessary
(System/ExternalExecution.cs):
如果上一步通过UHT生成成功,就会执行编译的Action了(ActionGraph.ExecuteActions
in System/ActionGraphs.cs):
继续进入会检测一堆引擎的构建配置(e.g:Engine/Saved/UnrealBuildTool/BuildConfiguration.xml):
我这里保持的是引擎默认的构建配置,则创建了一个ParallelExecutor
(System/ParallelExecutor.cs),然后执行:
将当前的编译任务创建出多个Action,并执行:
开始编译代码:
后记
根据上面的分析,UE的build路径是:
- 在VS中点击Build,调用build.bat
- build.bat中调用UBT
- UBT执行
target.cs
和所有Module的build.cs
中的逻辑 - UBT调用UHT(根据UE的宏标记生成代码)
- UHT生成完毕后,UBT调用编译器
- 预处理
- 编译
- 链接
这个流程的关键点在:UBT调用UHT生成的顺序是在调用编译器的预处理之前的,这意味着我们无法包裹UE的宏(其实UCLASS
/UFUNCTION
之类的不应该叫宏,应该叫标记),因为UE的宏由UHT先于编译器预处理了。