Build flow of the Unreal Engine4 project

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先于编译器预处理了。

未完待续。

微信扫描二维码,关注我的公众号。

本文标题:Build flow of the Unreal Engine4 project
文章作者:查利鹏
发布时间:2019年03月16日 23时09分
更新时间:2019年09月16日 19时01分
本文字数:本文一共有4k字
原始链接:https://imzlp.com/posts/6362/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!