UE 项目的编译流程

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
2
3
4
5
6
# Build
Engine\Build\BatchFiles\Build.bat
# ReBuild
Engine\Build\BatchFiles\Rebuild.bat
# Clean
Engine\Build\BatchFiles\Clean.bat

以 Build.bat 为例:

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
@echo off
setlocal enabledelayedexpansion

REM The %~dp0 specifier resolves to the path to the directory where this .bat is located in.
REM We use this so that regardless of where the .bat file was executed from, we can change to
REM directory relative to where we know the .bat is stored.
pushd "%~dp0\..\..\Source"

REM %1 is the game name
REM %2 is the platform name
REM %3 is the configuration name

IF EXIST ..\..\Engine\Binaries\DotNET\UnrealBuildTool.exe (
..\..\Engine\Binaries\DotNET\UnrealBuildTool.exe %* -DEPLOY
popd

REM Ignore exit codes of 2 ("ECompilationResult.UpToDate") from UBT; it's not a failure.
if "!ERRORLEVEL!"=="2" (
EXIT /B 0
)

EXIT /B !ERRORLEVEL!
) ELSE (
ECHO UnrealBuildTool.exe not found in ..\..\Engine\Binaries\DotNET\UnrealBuildTool.exe
popd
EXIT /B 999
)

可以看到 Build.bat 将接收的参数都转发给了UnrealBuildTool.exe:

1
..\..\Engine\Binaries\DotNET\UnrealBuildTool.exe %*

UBT

通过 UnrealBuildTool 构建项目需要传递参数:

  1. %1 is the game name
  2. %2 is the platform name
  3. %3 is the configuration name
  4. %4 is the ProjectPath
1
2
# Example
UnrealBuildTool.exe ThridPerson420 Win64 Development "C:\Users\visionsmile\Documents\Unreal Projects\Examples\ThridPerson420\ThridPerson420.uproject" -WaitMutex -FromMsBuild

然后来看一下 UnrealBuildTools 是怎么处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Engine\Source\Programs\UnrealBuildTools\UnrealBuildTool.cs
private static int Main(string[] Arguments)
{
// make sure we catch any exceptions and return an appropriate error code.
// Some inner code already does this (to ensure the Mutex is released),
// but we need something to cover all outer code as well.
try
{
return GuardedMain(Arguments);
}
catch (Exception Exception)
{
if (Log.IsInitialized())
{
Log.TraceError("UnrealBuildTool Exception: " + Exception.ToString());
}
if (ExtendedErrorCode != 0)
{
return ExtendedErrorCode;
}
return (int)ECompilationResult.OtherCompilationError;
}
}


可以看到传入进来的参数。
在 GuardedMain 中对引擎和传入参数做了一堆检测之后,会调用RunUBT:

RulesAssembly

RunUBT 中,有一个相当重要的函数调用UEBuildTarget.CreateTarget

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
// Engine\Source\Programs\UnrealBuildTools\UnrealBuildTool.cs
internal static ECompilationResult RunUBT(BuildConfiguration BuildConfiguration, string[] Arguments, FileReference ProjectFile, bool bCatchExceptions)
{

// other code...

if (UBTMakefile != null && !bIsGatheringBuild && bIsAssemblingBuild)
{
// If we've loaded a makefile, then we can fill target information from this file!
Targets = UBTMakefile.Targets;
}
else
{
DateTime TargetInitStartTime = DateTime.UtcNow;

ReadOnlyBuildVersion Version = new ReadOnlyBuildVersion(BuildVersion.ReadDefault());

Targets = new List<UEBuildTarget>();
foreach (TargetDescriptor TargetDesc in TargetDescs)
{
UEBuildTarget Target = UEBuildTarget.CreateTarget(TargetDesc, Arguments, bSkipRulesCompile, BuildConfiguration.SingleFileToCompile != null, BuildConfiguration.bUsePrecompiled, Version);
if ((Target == null) && (BuildConfiguration.bCleanProject))
{
continue;
}
Targets.Add(Target);
}

if (UnrealBuildTool.bPrintPerformanceInfo)
{
double TargetInitTime = (DateTime.UtcNow - TargetInitStartTime).TotalSeconds;
Log.TraceInformation("Target init took " + TargetInitTime + "s");
}
}

// other code ...
}

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
  1. BaseDir:项目目录
  2. Plugins:项目依赖的所有插件的 .uplugin 文件的绝对路径
  3. ModuleFiles:是当前 Target 中的所有 Module(Game Module 和所有插件内的 Module)的 .build.cs 文件的绝对路径
  4. TargetFiles:当前 Target 中所有的 target.cs 的文件绝对路径
  5. ModuleFileToPluginInfo:插件的信息 map,Module 的 build.cs 文件与 Module 的基本信息
  6. AssemblyFileName:项目的 BuildRules 的 DLL 文件绝对路径 (该文件位于(Intermediate/Build/BuildRules 下,是调用 UnrealVersionSelector 生成 VS 项目时创建的))
1
2
// e.g
C:\Users\imzlp\Documents\Unreal Projects\GWorld\Intermediate\Build\BuildRules\GWorldModuleRules.dll
  1. 其他参数(不是本篇文章的重点)

RulesAssembly类中定义了两个重要的成员:TargetNameToTargetFileModuleNameToModuleFile,在构造函数中把当前的项目中中所有定义的TargetRulesModuleRules都添加到了里面。

下面是 RulesAssembly 的构造函数:

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
// UnrealBuildTool/System/RulesAssembly.cs

/// <summary>
/// Constructor. Compiles a rules assembly from the given source files.
/// </summary>
/// <param name="BaseDir">The base directory for this assembly</param>
/// <param name="Plugins">All the plugins included in this assembly</param>
/// <param name="ModuleFiles">List of module files to compile</param>
/// <param name="TargetFiles">List of target files to compile</param>
/// <param name="ModuleFileToPluginInfo">Mapping of module file to the plugin that contains it</param>
/// <param name="AssemblyFileName">The output path for the compiled assembly</param>
/// <param name="bContainsEngineModules">Whether this assembly contains engine modules. Used to initialize the default value for ModuleRules.bTreatAsEngineModule.</param>
/// <param name="bUseBackwardsCompatibleDefaults">Whether modules in this assembly should use backwards-compatible defaults.</param>
/// <param name="bReadOnly">Whether the modules and targets in this assembly are installed, and should be created with the bUsePrecompiled flag set</param>
/// <param name="bSkipCompile">Whether to skip compiling this assembly</param>
/// <param name="Parent">The parent rules assembly</param>
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)
{
this.BaseDir = BaseDir;
this.Plugins = Plugins;
this.ModuleFileToPluginInfo = ModuleFileToPluginInfo;
this.bContainsEngineModules = bContainsEngineModules;
this.bUseBackwardsCompatibleDefaults = bUseBackwardsCompatibleDefaults;
this.bReadOnly = bReadOnly;
this.Parent = Parent;

// Find all the source files
List<FileReference> AssemblySourceFiles = new List<FileReference>();
AssemblySourceFiles.AddRange(ModuleFiles);
AssemblySourceFiles.AddRange(TargetFiles);

// Compile the assembly
if (AssemblySourceFiles.Count > 0)
{
List<string> PreprocessorDefines = GetPreprocessorDefinitions();
CompiledAssembly = DynamicCompilation.CompileAndLoadAssembly(AssemblyFileName, AssemblySourceFiles, PreprocessorDefines: PreprocessorDefines, DoNotCompile: bSkipCompile);
}

// Setup the module map
foreach (FileReference ModuleFile in ModuleFiles)
{
string ModuleName = ModuleFile.GetFileNameWithoutAnyExtensions();
if (!ModuleNameToModuleFile.ContainsKey(ModuleName))
{
ModuleNameToModuleFile.Add(ModuleName, ModuleFile);
}
}

// Setup the target map
foreach (FileReference TargetFile in TargetFiles)
{
string TargetName = TargetFile.GetFileNameWithoutAnyExtensions();
if (!TargetNameToTargetFile.ContainsKey(TargetName))
{
TargetNameToTargetFile.Add(TargetName, TargetFile);
}
}

// ignore other code..

}

编译环境:构造 Target 并执行

Target 的主要作用是收集和设定项目的编译信息用于编译真正的可执行程序的设置,类似于在 VS 中的项目设置。

RunUBT 中会对传入的参数(Platform/Configuration 等)做提取,并添加上一系列参数,之后通过调用 UEBuildTarget.CreateTarget,创建一个UBuildTarget 的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// UnrealBuildTool/Configuration/UEBuildTarget.cs
// 执行 `target.cs` 中的逻辑,并构造出一个 URBduileTarget 对象

public static UEBuildTarget CreateTarget(TargetDescriptor Desc, string[] Arguments, bool bSkipRulesCompile, bool bCompilingSingleFile, bool bUsePrecompiled, ReadOnlyBuildVersion Version)
{
DateTime CreateTargetStartTime = DateTime.UtcNow;

RulesAssembly RulesAssembly = RulesCompiler.CreateTargetRulesAssembly(Desc.ProjectFile, Desc.Name, bSkipRulesCompile, bUsePrecompiled, Desc.ForeignPlugin);

FileReference TargetFileName;
TargetRules RulesObject = RulesAssembly.CreateTargetRules(Desc.Name, Desc.Platform, Desc.Configuration, Desc.Architecture, Desc.ProjectFile, Version, Arguments, out TargetFileName);
// ...

}

CreateTarget 中又调用了 RulesAssembly.CreateTargetRules(获取target.cs 文件,以及将最基本的编译环境信息构造出 TargetInfo 并传递给 CreateTargetRulesInstance 用于创建 TargetRules 对象):

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
// UnrealBuildTool/System/RulesAssembly.cs

/// <summary>
/// Creates a target rules object for the specified target name.
/// </summary>
/// <param name="TargetName">Name of the target</param>
/// <param name="Platform">Platform being compiled</param>
/// <param name="Configuration">Configuration being compiled</param>
/// <param name="Architecture">Architecture being built</param>
/// <param name="ProjectFile">Path to the project file for this target</param>
/// <param name="Version">The current build version</param>
/// <param name="Arguments">Command line arguments for this target</param>
/// <param name="TargetFileName">The original source file name of the Target.cs file for this target</param>
/// <returns>The build target rules for the specified target</returns>
public TargetRules CreateTargetRules(string TargetName, UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, string Architecture, FileReference ProjectFile, ReadOnlyBuildVersion Version, string[] Arguments, out FileReference TargetFileName)
{
// ignore conditional check code...

// Return the target file name to the caller
TargetFileName = TargetNameToTargetFile[TargetName];
// Currently, we expect the user's rules object type name to be the same as the module name + 'Target'
string TargetTypeName = TargetName + "Target";
// The build module must define a type named '<TargetName>Target' that derives from our 'TargetRules' type.
// 通过构造 TargetInfo 对象,将基本的编译环境信息传递给 `target.cs` 中定义的构造函数
return CreateTargetRulesInstance(TargetTypeName, new TargetInfo(TargetName, Platform, Configuration, Architecture, ProjectFile, Version), Arguments);
}

获得项目的 *.Target.cs 文件,然后调用 CreateTargetRulesInstance 构造出一个 TargetRules 的对象(target.cs中构造的就是这个类型的对象),并执行 target.cs 中的构造函数中的代码。

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
// UnrealBuildTool/System/RulesAssembly.cs
// 获取定义在 target.cs 中的 TargetRules 对象,并调用其构造函数

/// Construct an instance of the given target rules
/// <param name="TypeName">Type name of the target rules</param>
/// <param name="TargetInfo">Target configuration information to pass to the constructor</param>
/// <param name="Arguments">Command line arguments for this target</param>
/// <returns>Instance of the corresponding TargetRules</returns>
protected TargetRules CreateTargetRulesInstance(string TypeName, TargetInfo TargetInfo, string[] Arguments)
{
// The build module must define a type named '<TargetName>Target' that derives from our 'TargetRules' type.
Type RulesType = CompiledAssembly.GetType(TypeName);
if (RulesType == null)
{
throw new BuildException("Expecting to find a type to be declared in a target rules named '{0}'. This type must derive from the 'TargetRules' type defined by Unreal Build Tool.", TypeName);
}

// Create an instance of the module's rules object, and set some defaults before calling the constructor.
TargetRules Rules = (TargetRules)FormatterServices.GetUninitializedObject(RulesType);
Rules.bUseBackwardsCompatibleDefaults = bUseBackwardsCompatibleDefaults;

// Find the constructor
ConstructorInfo Constructor = RulesType.GetConstructor(new Type[] { typeof(TargetInfo) });
if(Constructor == null)
{
throw new BuildException("No constructor found on {0} which takes an argument of type TargetInfo.", RulesType.Name);
}

// Invoke the regular constructor
try
{
Constructor.Invoke(Rules, new object[] { TargetInfo });
}
catch (Exception Ex)
{
throw new BuildException(Ex, "Unable to instantiate instance of '{0}' object type from compiled assembly '{1}'. Unreal Build Tool creates an instance of your module's 'Rules' object in order to find out about your module's requirements. The CLR exception details may provide more information: {2}", TypeName, Path.GetFileNameWithoutExtension(CompiledAssembly.Location), Ex.ToString());
}

// ignore other code...
}

经过上面的一波操作之后,我们现在已经从 target.cs 中得到了 TargetRules 对象,它代表了当前项目的编译环境,代码列了一堆,看着有点烦人,他们的调用栈如下:

编译目标:Module

Module是 UE 中真正用来执行的一个个小目标文件,编译出 exe 或者 DLL(通过启动模块编译出 exe,非启动模块编译出 DLL,或者静态链接到 exe 中)。

在上面的执行完毕之后,UBT 会开始读取和分析项目中的ModuleRules,并通过它们构造出一个个UEBuildModule,用于后续的编译处理。

RunUBT->UEBuildTarget.Build->PreBuidSetup,可以理解真正的执行逻辑是在PreBuildSetup 中执行的:

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// UEBuildTarget.cs

/// <summary>
/// Setup target before build. This method finds dependencies, sets up global environment etc.
/// </summary>
public void PreBuildSetup(UEToolChain TargetToolChain)
{
// Describe what's being built.
Log.TraceVerbose("Building {0} - {1} - {2} - {3}", AppName, TargetName, Platform, Configuration);

// Setup the target's binaries.
SetupBinaries();

// Setup the target's plugins
SetupPlugins();

// Setup the custom build steps for this target
SetupCustomBuildSteps();

// Add the plugin binaries to the build
foreach (UEBuildPlugin Plugin in BuildPlugins)
{
foreach(UEBuildModuleCPP Module in Plugin.Modules)
{
AddModuleToBinary(Module);
}
}

// Add all of the extra modules, including game modules, that need to be compiled along
// with this app. These modules are always statically linked in monolithic targets, but not necessarily linked to anything in modular targets,
// and may still be required at runtime in order for the application to load and function properly!
AddExtraModules();

// Create all the modules referenced by the existing binaries
foreach(UEBuildBinary Binary in Binaries)
{
Binary.CreateAllDependentModules(FindOrCreateModuleByName);
}

// Bind every referenced C++ module to a binary
for (int Idx = 0; Idx < Binaries.Count; Idx++)
{
List<UEBuildModule> DependencyModules = Binaries[Idx].GetAllDependencyModules(true, true);
foreach (UEBuildModuleCPP DependencyModule in DependencyModules.OfType<UEBuildModuleCPP>())
{
if(DependencyModule.Binary == null)
{
AddModuleToBinary(DependencyModule);
}
}
}

// Add all the modules to the target if necessary.
if(Rules.bBuildAllModules)
{
AddAllValidModulesToTarget();
}

// Add the external and non-C++ referenced modules to the binaries that reference them.
foreach (UEBuildModuleCPP Module in Modules.Values.OfType<UEBuildModuleCPP>())
{
if(Module.Binary != null)
{
foreach (UEBuildModule ReferencedModule in Module.GetUnboundReferences())
{
Module.Binary.AddModule(ReferencedModule);
}
}
}

if (!bCompileMonolithic)
{
if (Platform == UnrealTargetPlatform.Win64 || Platform == UnrealTargetPlatform.Win32)
{
// On Windows create import libraries for all binaries ahead of time, since linking binaries often causes bottlenecks
foreach (UEBuildBinary Binary in Binaries)
{
Binary.SetCreateImportLibrarySeparately(true);
}
}
else
{
// On other platforms markup all the binaries containing modules with circular references
foreach (UEBuildModule Module in Modules.Values.Where(x => x.Binary != null))
{
foreach (string CircularlyReferencedModuleName in Module.Rules.CircularlyReferencedDependentModules)
{
UEBuildModule CircularlyReferencedModule;
if (Modules.TryGetValue(CircularlyReferencedModuleName, out CircularlyReferencedModule) && CircularlyReferencedModule.Binary != null)
{
CircularlyReferencedModule.Binary.SetCreateImportLibrarySeparately(true);
}
}
}
}
}

// On Mac AppBinaries paths for non-console targets need to be adjusted to be inside the app bundle
if (Platform == UnrealTargetPlatform.Mac && !Rules.bIsBuildingConsoleApplication)
{
TargetToolChain.FixBundleBinariesPaths(this, Binaries);
}
}

其中的:

1
2
3
4
5
6
7
8
9
// Setup the target's binaries.
// 读取 LaunchModule, 要编译出来的可执行目标 exe,创建出启动模块的 UEBuildModuleCPP 和 UEBuildBinary(编译 exe)
// UEBuildBinary 只在这里被创建
SetupBinaries();

// Setup the target's plugins
// 构造所有插件的 Module 并创建出编译对象 UEBuildModuleCPP
SetupPlugins();

它们直接或间接地又调用 FindOrCreateCppModuleByName,最终又会调用到CreateModuleRulesAndSetDefaults 来构造出真正的 ModuleRules 对象,并创建出 UEBuildModuleCPP 用于编译的模块:

CreateModuleRulesAndSetDefaults又调用了 RulesAssembly.CreateModuleRules(注意此时执行流已经又回到RulesAssembly 来了)。

RulesAssembly.CreateModuleRules通过上面构造时存起来的 ModuleNameToModuleFile 通过 Module 名拿到 *.build.cs 文件,然后与调用 TargetRules 的构造方法一样调用 ModuleRules 的构造函数,并且将上面构造出来的 TargetRules 传递给ModuleRules

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
/// <summary>
/// Creates an instance of a module rules descriptor object for the specified module name
/// </summary>
/// <param name="ModuleName">Name of the module</param>
/// <param name="Target">Information about the target associated with this module</param>
/// <param name="ReferenceChain">Chain of references leading to this module</param>
/// <returns>Compiled module rule info</returns>
public ModuleRules CreateModuleRules(string ModuleName, ReadOnlyTargetRules Target, string ReferenceChain)
{
// Currently, we expect the user's rules object type name to be the same as the module name
string ModuleTypeName = ModuleName;

// Make sure the module file is known to us
FileReference ModuleFileName;
if (!ModuleNameToModuleFile.TryGetValue(ModuleName, out ModuleFileName))
{
if (Parent == null)
{
throw new BuildException("Could not find definition for module '{0}' (referenced via {1})", ModuleName, ReferenceChain);
}
else
{
return Parent.CreateModuleRules(ModuleName, Target, ReferenceChain);
}
}

// The build module must define a type named 'Rules' that derives from our 'ModuleRules' type.
Type RulesObjectType = GetModuleRulesTypeInternal(ModuleName);
if (RulesObjectType == null)
{
throw new BuildException("Expecting to find a type to be declared in a module rules named '{0}' in {1}. This type must derive from the 'ModuleRules' type defined by Unreal Build Tool.", ModuleTypeName, CompiledAssembly.FullName);
}

// Create an instance of the module's rules object
try
{
// Create an uninitialized ModuleRules object and set some defaults.
ModuleRules RulesObject = (ModuleRules)FormatterServices.GetUninitializedObject(RulesObjectType);
RulesObject.Name = ModuleName;
RulesObject.File = ModuleFileName;
RulesObject.Directory = ModuleFileName.Directory;
ModuleFileToPluginInfo.TryGetValue(RulesObject.File, out RulesObject.Plugin);
RulesObject.bTreatAsEngineModule = bContainsEngineModules;
RulesObject.bUseBackwardsCompatibleDefaults = bUseBackwardsCompatibleDefaults && Target.bUseBackwardsCompatibleDefaults;
RulesObject.bPrecompile = (RulesObject.bTreatAsEngineModule || ModuleName.Equals("UE4Game", StringComparison.OrdinalIgnoreCase)) && Target.bPrecompile;
RulesObject.bUsePrecompiled = bReadOnly;

// Call the constructor
ConstructorInfo Constructor = RulesObjectType.GetConstructor(new Type[] { typeof(ReadOnlyTargetRules) });
if(Constructor == null)
{
throw new BuildException("No valid constructor found for {0}.", ModuleName);
}
Constructor.Invoke(RulesObject, new object[] { Target });

return RulesObject;
}
catch (Exception Ex)
{
Exception MessageEx = (Ex is TargetInvocationException && Ex.InnerException != null)? Ex.InnerException : Ex;
throw new BuildException(Ex, "Unable to instantiate module '{0}': {1}\n(referenced via {2})", ModuleName, MessageEx.ToString(), ReferenceChain);
}
}

我们在所有的 GameMode 与所有 Plugin 中的 build.cs 中的代码在此时执行。

Launch 模块的编译

上面写到了编译 Module,有一个Module 很特殊,那就是 Launch 模块。

任何可执行程序都会有一个执行入口,在 UE 中,每一个 Target 都会编译出一个可执行程序。引擎启动是从 Launch 模块开始的,main函数也是定义在其中的,所以需要将 Launch 模块中的 main 函数编译出一个可执行程序来。

启动模块是在 UBTTargetRules中指定的:

1
2
3
4
5
6
7
8
9
10
11
public string LaunchModuleName
{
get
{
return (LaunchModuleNamePrivate == null && Type != global::UnrealBuildTool.TargetType.Program)? "Launch" : LaunchModuleNamePrivate;
}
set
{
LaunchModuleNamePrivate = value;
}
}

可以在 TargetRules 中使用 LaunchModuleNamePrivate 指定一个启动模块,如果 没有指定 且 Target 类型不为 Program,则使用Launch 模块,否则使用指定的模块。即不管是 Game/Editor/Server/Client 的 Target 启动模块都是 Launch
但是 ,因为LaunchModuleNamePrivateTargetRules的定义中是一个 private 成员,无法在我们继承来的 TargetRules 中赋值,所以目前也没有什么用。

UEBuildTarget.SetupBinaries 中被使用(上面也已经提到过了,UEBuildBinary就是我们要编译出的启动模块的可执行 exe 编译对象,并且只在这里被创建):

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
/// <summary>
/// Sets up the binaries for the target.
/// </summary>
protected void SetupBinaries()
{
// If we're using the new method for specifying binaries, fill in the binary configurations now
if(Rules.LaunchModuleName == null)
{
throw new BuildException("LaunchModuleName must be set for all targets.");
}

// Create the launch module
UEBuildModuleCPP LaunchModule = FindOrCreateCppModuleByName(Rules.LaunchModuleName, TargetRulesFile.GetFileName());

// Create the binary
UEBuildBinary Binary = new UEBuildBinary(
Type: Rules.bShouldCompileAsDLL? UEBuildBinaryType.DynamicLinkLibrary : UEBuildBinaryType.Executable,
OutputFilePaths: OutputPaths,
IntermediateDirectory: (!LaunchModule.RulesFile.IsUnderDirectory(UnrealBuildTool.EngineDirectory) || ShouldCompileMonolithic()) ? ProjectIntermediateDirectory : EngineIntermediateDirectory,
bAllowExports: Rules.bHasExports,
PrimaryModule: LaunchModule,
bUsePrecompiled: LaunchModule.Rules.bUsePrecompiled && OutputPaths[0].IsUnderDirectory(UnrealBuildTool.EngineDirectory)
);
Binaries.Add(Binary);

// Add the launch module to it
LaunchModule.Binary = Binary;
Binary.AddModule(LaunchModule);

// Create an additional console app for the editor
if (Platform == UnrealTargetPlatform.Win64 && Configuration != UnrealTargetConfiguration.Shipping && TargetType == TargetType.Editor)
{
Binary.bBuildAdditionalConsoleApp = true;
}
}

执行环境如下:

可以看到这里的输出文件就是我们编译的项目 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 路径是:

  1. 在 VS 中点击 Build,调用 build.bat
  2. build.bat 中调用 UBT
  3. UBT 执行 target.cs 和所有 Module 的 build.cs 中的逻辑
  4. UBT 调用 UHT(根据 UE 的 宏标记 生成代码)
  5. UHT 生成完毕后,UBT 调用编译器
  6. 预处理
  7. 编译
  8. 链接

这个流程的关键点在:UBT 调用 UHT 生成的顺序是在调用编译器的预处理之前的,这意味着我们无法包裹 UE 的宏 (其实UCLASS/UFUNCTION 之类的不应该叫宏,应该叫 标记),因为 UE 的宏由 UHT 先于编译器预处理了。