Ue Modules Learning

Some basic concepts about ue modules

Posted by Eddy on Saturday, August 10, 2024

1. 重要概念

1.1. Target

  Target 即编译目标,UE 支持多种编译 target 类型

	public enum TargetType
	{
		/// <summary>
		/// Cooked monolithic game executable (GameName.exe).  Also used for a game-agnostic engine executable (UE4Game.exe or RocketGame.exe)
		/// </summary>
		Game,

		/// <summary>
		/// Uncooked modular editor executable and DLLs (UE4Editor.exe, UE4Editor*.dll, GameName*.dll)
		/// </summary>
		Editor,

		/// <summary>
		/// Cooked monolithic game client executable (GameNameClient.exe, but no server code)
		/// </summary>
		Client,

		/// <summary>
		/// Cooked monolithic game server executable (GameNameServer.exe, but no client code)
		/// </summary>
		Server,

		/// <summary>
		/// Program (standalone program, e.g. ShaderCompileWorker.exe, can be modular or monolithic depending on the program)
		/// </summary>
		Program,
	}

  每一个 xx.target.cs 都可以配置某种 targetType 编译时的依赖关系与构建规则。

1.2. Module

  模块是UE中用于组织代码的一种方式,引擎就是以大量模块的集合形式实现的。每个模块可以包含源文件、资源和第三方库等,它们可以被编译成静态或动态链接库(.lib或.dll文件)。模块定义了一组功能,可以被其他模块所依赖。通过**.build.cs可以定义一个模块,在**.build.cs中声明类,同时设置属性用以控制如何构建。整个工程实际上也可看做一个模块,可以在Source目录下找到控制整个工程构建的.build.cs。各个build.cs通过UBT管理,从而确定了整个工程的编译环境。

1.3 target.cs 与 build.cs 区别

  build.cs 控制模块内的依赖关系,target.cs控制编译可执行文件时包含依赖的模块,层级上应该是 build.cs < target.cs

1.4. UBT

  UBT,即 UnrealBuildTool,编译 UE 工程时实际上就是利用命令行调用了UBT.exe,上面提到的target.cs以及build.cs的扫描与处理都是由 UBT 完成的。可以在 VS 的编译配置中找到点击build时触发的build.bat,其中就对应了带命令行参数启动UBT。

\Build.bat -Target="SherylTestEditor Win64 Development -Project=\"$(SolutionDir)SherylTest.uproject\"" -Target="ShaderCompileWorker Win64 Development -Quiet" -WaitMutex -FromMsBuild
// 通过命令行解析编译参数
// Parse the command line arguments
CommandLineArguments Arguments = new CommandLineArguments(ArgumentsArray);

// Parse the global options
GlobalOptions Options = new GlobalOptions(Arguments);

// 根据命令行参数找到对应的 UBT Mode,执行对应 Mode 的 Excute方法
// 
// 1. eg. 处理模块间依赖关系,生成项目文件
// 调用 GenerateProjectFilesMode 内的方法,搜索项目内所有的 target 和 module,生成项目代码
// 
// 2.eg. 编译工程 // todo: 找到build 入口,如何调用到BuildMode.cs
// 3. 

2. 模块编译

2.1. 含义

  UE 引擎和游戏都是以模块来组织的,模块之间可以相互调用,通过模块化的组织形式可以很好地解耦合。

2.2. 实现方式

2.2.1. 模块的管理

  虚幻构建系统通过*.build.cs*.target.cs声明的内容来构建工程,所有的模块都需要由对应的build.cs来声明,其中,模块的定义需要从ModuleRules继承。

  各个模块的链接方式由TargetLinkType指定,对应的定义如下

	/// <summary>
	/// Specifies how to link all the modules in this target
	/// </summary>
	[Serializable]
	public enum TargetLinkType
	{
		/// <summary>
		/// Use the default link type based on the current target type
		/// </summary>
		Default,

		/// <summary>
		/// Link all modules into a single binary
		/// </summary>
		Monolithic,

		/// <summary>
		/// Link modules into individual dynamic libraries
		/// </summary>
		Modular,
	}

  在*.target.cs中指定不同的TargetLinkType指定动态(Modular)或静态(Monolithic)地加载需要使用的模块。

  通过FModuleManager来统一管理工程里的模块。相关实现的地址为\Engine\Source\Runtime\Core\Private\Modules,Module 的管理与IS_MONOLITHIC宏有关,这个宏由 UBT 定义,用来指示是否有 DLL 参与编译,源码中的注释为Whether we want a monolithic build (no DLLs); must be defined by UBT

  在模块A的外部想要使用模块A可以通过 ModuleManager 的 LoadModule 或者 UnLoadModule方法。LoadModule 方法返回一个 IModuleInterface 指针,也就是说如果自己实现一个模块,那么自定义的类需要继承 IModuleInterface。

	/**
	 * Loads the specified module.
	 *
	 * @param InModuleName The base name of the module file.  Should not include path, extension or platform/configuration info.  This is just the "module name" part of the module file name.  Names should be globally unique.
	 * @return The loaded module, or nullptr if the load operation failed.
	 * @see AbandonModule, IsModuleLoaded, LoadModuleChecked, LoadModulePtr, LoadModuleWithFailureReason, UnloadModule
	 */
	IModuleInterface* LoadModule( const FName InModuleName );

	// LoadModule的对应实现
	IModuleInterface* FModuleManager::LoadModule( const FName InModuleName )
{
	// We allow an already loaded module to be returned in other threads to simplify
	// parallel processing scenarios but they must have been loaded from the main thread beforehand.
	if(!IsInGameThread())
	{
		return GetModule(InModuleName);
	}

	EModuleLoadResult FailureReason;
	// LoadModule 具体实现与 Module 的链接方式有关
	IModuleInterface* Result = LoadModuleWithFailureReason(InModuleName, FailureReason );

	// This should return a valid pointer only if and only if the module is loaded
	checkSlow((Result != nullptr) == IsModuleLoaded(InModuleName));

	return Result;
}

  在LoadModule时,区分 Module 不同链接类型,若为静态链接,则去一个全局的StaticallyLinkedModuleInitializers中寻找对应Module;若为动态链接,则直接加载对应 DLL。   

2.2.2. 模块的实现

  在实现一个对其他模块公开的模块时需要继承 IModuleInterface,并且在所有的 include 头文件声明后,调用 IMPLEMENT_MODULE 宏,宏定义可在路径\Engine\Source\Runtime\Core\Public\Modules\ModuleManager.h中找到。

/**
 * Module implementation boilerplate for regular modules.
 *
 * This macro is used to expose a module's main class to the rest of the engine.
 * You must use this macro in one of your modules C++ modules, in order for the 'InitializeModule'
 * function to be declared in such a way that the engine can find it. Also, this macro will handle
 * the case where a module is statically linked with the engine instead of dynamically loaded.
 *
 * This macro is intended for modules that do NOT contain gameplay code.
 * If your module does contain game classes, use IMPLEMENT_GAME_MODULE instead.
 *
 * Usage:   IMPLEMENT_MODULE(<My Module Class>, <Module name string>)
 *
 * @see IMPLEMENT_GAME_MODULE
 */
#if IS_MONOLITHIC

	// If we're linking monolithically we assume all modules are linked in with the main binary.
	#define IMPLEMENT_MODULE( ModuleImplClass, ModuleName ) \
		/** Global registrant object for this module when linked statically */ \
		static FStaticallyLinkedModuleRegistrant< ModuleImplClass > ModuleRegistrant##ModuleName( TEXT(#ModuleName) ); \
		/* Forced reference to this function is added by the linker to check that each module uses IMPLEMENT_MODULE */ \
		extern "C" void IMPLEMENT_MODULE_##ModuleName() { } \
		PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)

#else

	#define IMPLEMENT_MODULE( ModuleImplClass, ModuleName ) \
		\
		/**/ \
		/* InitializeModule function, called by module manager after this module's DLL has been loaded */ \
		/**/ \
		/* @return	Returns an instance of this module */ \
		/**/ \
		extern "C" DLLEXPORT IModuleInterface* InitializeModule() \
		{ \
			return new ModuleImplClass(); \
		} \
		/* Forced reference to this function is added by the linker to check that each module uses IMPLEMENT_MODULE */ \
		extern "C" void IMPLEMENT_MODULE_##ModuleName() { } \
		PER_MODULE_BOILERPLATE \
		PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)

#endif //IS_MONOLITHIC

  IMPLEMENT_MODULE 这个宏的作用是将模块的主要类暴露给UE4引擎,使得模块可以被引擎识别和加载。

3. 模块的依赖传递

  如果项目没有依赖模块 A ,那么在编译过程中模块 A即使在工程中也不会参与编译。

1、 Dependency 与 IncludePath

  通过向以及PrivateDependencyModuleNamesPublicDependencyModuleNames添加另一个模块B的名称,可以指定一个模块对另外一个模块的依赖,该定义在\Engine\Source\Programs\UnrealBuildTool\Configuration\ModuleRules.cs;与之相似的还有PublicIncludePathModuleNamesPrivateIncludePathModuleNames,这两个列表用于声明某个模块需要依赖的其他模块的头文件,编译时如果符号只定义在其他模块的cpp文件中则会有Undefined symbols。这两组 list 名称区别就在于时对模块(包含了cpp实现)整体的依赖还是对模块头文件(不包含cpp实现)的依赖。

2、 Public 与 Private

  假设有三个模块A\B\C,使用public 或 private list区分第一点在于是本模块A的 public 代码还是 private 代码需要引入对模块 B 的依赖;第二点则是模块 C 依赖本模块 A 时,若使用的是 private 传递的依赖,则无法依赖到模块 B,反之,若用的是 public ,则可以依赖到模块 B。

4. 控制模块内符号的导出

  使用{模块名}_API来修饰一个类,可以控制这个类在编译时导出符号。

  以下是使用_API宏的一些关键点:

  1. 模块化编译:当引擎以模块化方式编译时(即面向桌面平台的DLL文件, IS_MONOLITHIC为False),_API 宏才有效。

  2. 单片模式:与模块化模式相对的是单片模式(Monolithic Mode),它将所有代码放入同一个可执行文件中。这种模式由UnrealBuildTool(UBT)和/或平台及编译配置控制。

  3. 宏的实际效果:根据UBT的编译方式,_API 宏相当于以下某种形式:
      __declspec(dllexport):当以模块模式编译模块代码时。
      __declspec(dllimport):当引入一个模块的公开模块头信息时。

  4. 空:当以单片模式编译时。