UE4 Build System:Target and Module

Module是构成Unreal的基本元素,每一个Module封装和实现了一组功能,并且可以供其他的Module使用,整个Unreal Engine就是靠各个Module组合驱动的,连我们创建的游戏项目本身,都是一个单独的Module。

那么UE又是怎么创建和构建这这些Module的呢?这是写这篇文章的主要目的,研究一下Unreal的构建系统以及它们(Target和Module)支持的各种属性。

建议在看这篇文章之前先看一下我之前的这篇文章:Build flow of the Unreal Engine4 project,主要内容是大致过一遍UE的构建流程,本篇文章只是UE构建系统中的一环。

对于UE项目比较熟悉的都知道,当使用UE创建一个C++游戏项目时,会在项目路径下创建Source文件夹,默认包含了下列文件:

1
2
3
4
5
6
7
8
9
10
Example\GWorld\Source>tree /a /f
| GWorld.Target.cs
| GWorldEditor.Target.cs
|
\---GWorld
GWorld.Build.cs
GWorld.cpp
GWorld.h
GWorldGameModeBase.cpp
GWorldGameModeBase.h

其中,*.Target.cs*.Build.cs是Unreal构建系统的实际控制者,UBT通过扫描这两个文件来确定整个编译环境,它们也是本篇文章研究的重点。
它们的职责各不相同:

  • *.Target.cs控制的是生成的可执行程序的外部编译环境,就是所谓的Target。比如,生成的是什么Type(Game/Client/Server/Editor/Program),开不开启RTTI(bForceEnableRTTI),CRT使用什么方式链接(bUseStaticCRT) 等等。
  • *.Build.cs控制的是Module编译过程,由它来控制所属Module的对其他Module的依赖、文件包含、链接、宏定义等等相关的操作,*.Build.cs告诉UE的构建系统,它是一个Module,并且编译的时候要做哪些事情。

以一言以蔽之:与外部编译环境相关的都归*.target.cs管,与Module自身相关的都归*.build.cs管。

插个题外话,在GWorld.hGWorld.cpp中定义的是Module真正的执行逻辑,使用IMPLEMENT_MODULE定义。UE中所有的Module都是继承自IModuleInterface,具有以下接口:

1
2
3
4
5
6
7
8
9
10
11
12
class IModuleInterface
{
public:
virtual ~IModuleInterface();
virtual void StartupModule();
virtual void PreUnloadCallback();
virtual void PostLoadCallback();
virtual void ShutdownModule();
virtual bool SupportsDynamicReloading();
virtual bool SupportsAutomaticShutdown();
virtual bool IsGameModule(); const
};

通过IModuleInterface来驱动Module的启动与关闭,不过一般Game Module不使用这个控制游戏流程。
这部分的详细内容可以看我之前的文章:UE4 Modules:Load and Startup

Target

每一个基于Unreal的项目,都有一个Tergat.cs,具有一个继承自TargetRules的类定义;并且默认需要关联着一个同名(非必要,但建议)的Module的定义,否则编译时会有Module未定义错误:

1
UnrealBuildTool : error : Could not find definition for module 'GWorld' (referenced via GWorld.Target.cs)

Target关联的Module的名字可以通过ExtraModuleNames来指定:

1
2
3
4
5
6
7
8
public class GWorldTarget : TargetRules
{
public GWorldTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
ExtraModuleNames.AddRange( new string[] { "GWorld" } );
}
}

上面指定的是GWorld,UBT解析的时候就会去找GWorld这个Module的定义,也就是GWorld.build.cs这个文件中的GWorld类定义,如果没有就会产生上面的Module未定义错误。

注意,与Target关联的Module不仅仅只是一个指定的名字这么简单,所有代码中使用的XXXX_API都是与Module的名字相关的。

如果我进行以下改动:ExtraModuleNames.AddRange( new string[] { "GWorldAAA" } );,那么需要对项目中所有的源文件进行的改动有:

  1. 将原有的GWorld.build.cs文件改名为GWorldAAA.build.cs,并将文件内容的所有GWorld替换为GWorldAAA
  2. 将项目内所有头文件的GWORLD_API改名为GWORLDAAA_API,因为XXX_API的导出符号是依赖于ModuleName的;

实在是个不小的工作量,所以还是建议将ExtraModuleNames中指定的名字与Game Module同名。
通过上面的内容,我们可以知道了Target.cs是如何与Build.cs关联的。那么,其实Game/Server/Client/EditorTarget可以共用同一个Module,将他们的ExtraModuleNames都设置成同一个就可以了(如果你想要针对每个Target类型单独写也可以)。

TargetRules的代码在UnrealBuildTools/Configuration/ModuleRulesReadOnlyTargetRules也定义其中),UE对Target支持属性的描述文档:Targets

但是UE的官方文档里面也只是代码里的注释,有些描述看了之后摸不着头脑,后面我会分析一下TargetRule一些属性的含义,先埋个坑。

Module

Target类似,每一个Unreal的Module,都有一个专属的ModuleName.Build.cs里面定义着专属的ModuleName类,它由ModuleRules继承而来,我们对Module构建时进行的操作就是通过它来控制。

注意:不管是Game Module还是Plugin Module,只要是项目依赖的Module,编译时它们都会接收到当前使用的Target信息。

ModuleRules的代码在UnrealBuildTools/Configuration/ModuleRules;UE对Modules描述的官方文档:Modules,这里也同样只有代码的注释内容,没有实际例子,我就先来分析一些在工程中常见的Build.cs中属性的含义。

*.Build.cs中可以通过它构造接收的ReadOnlyTargetRules Target参数来获取*.Target.cs中的属性信息。

1
2
3
4
5
6
7
8
9
10
11
using UnrealBuildTool;
using System.IO;

public class GWorld : ModuleRules
{
public GWorld(ReadOnlyTargetRules ReadOnlyTargetRules) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
// something
}
}

通过Target对象,可以在*.build.cs中控制对不同的平台(Platform),架构(Architecture),以及其他的选项来对Module进行不同的操作(比如定义不同的宏/包含不同的ThridParty/链接不同的Lib等等)。

ModuleDirectory

  • string ModuleDirectory:项目的源码路径PROJECT_NAME/Source/PROJECT_NAME

EngineDirectory

  • string EngineDirectory:引擎目录Engine/

PublicDelayLoadDLLs

  • List<string> PublicDelayLoadDLLs:延迟加载的DLL列表,通常用于第三方库。

PublicDefinitions

  • List<string> PublicDefinitions:为当前Module添加公开宏定义,等同于传统VS项目在项目设置中添加一个预处理宏。

PublicSystemIncludePaths

  • List<string> PublicSystemIncludePaths:文档介绍是用于添加系统的Include路径,与PublicIncludePaths的区别是会跳过头文件解析检查(但是经我测试,使用这种方式包含的代码依然会检测下列错误(UE_4.20)):
1
error : Expected mpack-platform.h to be first header included.

注意:如果不指定路径,则默认的IncludePath路径是Engine/Source

比如:

1
2
3
4
5
PublicSystemIncludePaths.AddRange(
new string[] {
"TEST_LIB"
}
);

它表示的路径是:

1
D:\UnrealEngine\Epic\UE_4.21\Engine\Source\TEST_LIB

所有可以在*.build.cs中指定的*IncludePaths,默认的路径都是Engine/Source.

RuntimeDependencies

  • list<RuntimeDependency> RuntimeDependencies:Module在运行时依赖的文件(.so/.dll等),打包时将会拷贝到存储目录。

PrivateRuntimeLibraryPaths

  • List<string> PrivateRuntimeLibraryPaths:运行时库的搜索路径。例如.so或者.dll

PublicRuntimeLibraryPaths

  • List<string> PublicRuntimeLibraryPaths:运行时库的搜索路径。例如.so或者.dll

因为动态链接库的查找路径默认只有:

  1. 系统的PATH路径;
  2. 可执行程序的当前目录;

如果我们的动态链接库在其他的位置,运行时就会错误,可以通过PublicRuntimeLibraryPaths或者PrivateRuntimeLibraryPaths来添加。

DynamicallyLoadedModuleNames

  • List<string> DynamicallyLoadedModuleNames:添加需要运行时动态加载的Module,使用FModuleManager::LoadModuleChecked<MODULE_TYPE>(TEXT("MODULE_NAME"))等函数启动。
1
2
// e.g
FModuleManager::LoadModuleChecked< IAIModule >( "AIModule" );

PublicDependencyModuleNames

  • List<string> PublicDependencyModuleNames:添加对执行Module的源文件依赖,自动添加所依赖Module的PublicPrivate源文件包含。

PrivateDependencyModuleNames

  • List<string> PrivateDependencyModuleNames:与PublicDependencyModuleNames不同的是,意味着所依赖的Module中的源文件只可以在Private中使用。

假如现在有一个模块A,还有一个模块B,他们中都是UE的Module/PublicModule/Private的文件结构。

  • 如果B中依赖A,如果使用的是PrivateDependencyModuleNames的方式添加的依赖,则A模块的源文件只可以在B的Private目录下的源文件中使用,在Public目录下的源文件使用时会报No such file or directory的错误。
  • 如果使用的是PublicDependencyModuleNames方式添加的依赖,则A的源文件在B的PublicPrivate中都可用。

除了上述的区别之外,还影响依赖于B模块的模块 ,当一个模块C依赖模块B的时候,只能访问到B模块的PublicDependencyModule中的模块暴露出来的类。
例如,C依赖B,B依赖A;那么,假如C想访问A中的类则有两种方式:

  1. 在C的依赖中添加上A模块
  2. 确保B在PublicDependencyModuleNames依赖中添加的A模块,这样C就可以间接的访问到A。

经过测试发现,其实对于游戏模块(PROJECT_NAME/Source/PROJECT_NAME.target.cs)使用而言,所依赖的模块是使用PublicDependencyModuleNames还是PrivateDependencyModuleNames包含,没什么区别。
使用Private方式依赖的Module中的头文件依然可以在游戏模块的Public中用,这一点与插件等其他模块有所不同(但是这只有在所依赖的模块不是bUseCompiled的基础上的,如果所依赖的模块是bUseCompiled的,则与其他的模块一样,PrivateDependencyModuleNames依赖的模块不可以在Pulibc目录下的源文件使用),这个行为比较奇怪:有时候出错有时又不出错。

注意:在游戏项目中使用依赖其他Module时尽量确定性需求地使用PrivateDependencyModuleNames或者PublicDependencyModuleNames,在组合其他的选项时可能会有一些奇怪的行为。

相关的讨论:

  1. What is the difference between PublicDependencyModuleNames and PrivateDependencyModuleNames
  2. Explanation of Source Code folder structure?

bPreCompile与bUsePreCompiled

1
2
3
4
5
6
7
8
9
/// <summary>
/// Whether this module should be precompiled. Defaults to the bPrecompile flag from the target. Clear this flag to prevent a module being precompiled.
/// </summary>
public bool bPrecompile;

/// <summary>
/// Whether this module should use precompiled data. Always true for modules created from installed assemblies.
/// </summary>
public bool bUsePrecompiled;

这个两个属性需要组合来使用。

考虑下列需求:
如果我们写好的一个模块A希望拿给别人来用,但是又不想把所有代码开放出来,该怎么办?

在传统的C++领域,我应该会说:把代码编译成DLL,然后把头文件和DLL发放给用户就可以啦。
对!其实bPreCompilebUseCompiled就是做的类似的事情。

当我们对模块A进行编译之前,在它的*.build.cs中添加:

1
2
3
4
5
6
7
8
9
public class A : ModuleRules
{
public A(ReadOnlyTargetRules Target) : base(Target)
{
// ...
bPreCompile=true;
// ...
}
}

然后编译模块A。编译完成之后,将模块A的Source/Private删除(删除之前请确保你已经备份),然后删除模块目录下的Intermediate,但是要保留Binaries目录。
最后,打开模块A的A.build.cs,将bPreCompile=true;删掉,然后再添加:

1
2
3
4
5
6
7
8
9
public class A : ModuleRules
{
public A(ReadOnlyTargetRules Target) : base(Target)
{
// ...
bUsePreCompiled=true;
// ...
}
}

此时我们想要实现的目标都已经完成了:不发布实现代码(Private),发布预先编译好的二进制,但是这样无法进行静态链接,如果只是暴露给蓝图使用可以,在其他的Module中使用它的符号会有符号未定义错误。

未完待续,如有谬误请不吝指正。
本文标题:UE4 Build System:Target and Module
文章作者:ZhaLiPeng
发布时间:2019年09月12日 13时14分
本文字数:本文一共有2.6k字
原始链接:https://imzlp.me/posts/16643/
许可协议: CC BY-NC-SA 4.0
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!