从"runtime类型模块是否可以依赖runtime类型模块?"看UnrealBuildTool的基本概念及流程
一、问题
在配置一个模块的时候,我们通常会指定一个模块的依赖。那么一个runtime类型的module能在依赖中包含Editor类型的module吗?
二、哪个Target.cs文件被使用
当在编译环境中选择一个构建目标时,当选择不同的“解决方案配置”时,构建时传递给UnrealBuildTool的“-Target”选项也各不相同,而这个-Target选项直接决定了构建时使用项目文件夹下使用哪个.Target.cs文件。
该文件最关键的是定义了该目标使用的构建类型,这些构建类型的枚举在Engine\Source\Programs\UnrealBuildTool\Configuration\TargetRules.cs文件中定义,从文件中还可以看到,可以在Target.cs文件中定义链接类型。而且还可以看到,在默认情况下,如果构建目标是Editor类型,则各个模块默认是以模块形式链接,如果是生成game则是以单一二进制形式使用(参看LinkType属性get属性中的 ((Type == global::UnrealBuildTool.TargetType.Editor) ? TargetLinkType.Modular : TargetLinkType.Monolithic)语句)。
namespace UnrealBuildTool
{
/// <summary>
/// The type of target
/// </summary>
[Serializable]
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,
}
/// <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,
}
……
/// <summary>
/// The type of target.
/// </summary>
public global::UnrealBuildTool.TargetType Type = global::UnrealBuildTool.TargetType.Game;
……
/// <summary>
/// Specifies how to link modules in this target (monolithic or modular). This is currently protected for backwards compatibility. Call the GetLinkType() accessor
/// until support for the deprecated ShouldCompileMonolithic() override has been removed.
/// </summary>
public TargetLinkType LinkType
{
get
{
return (LinkTypePrivate != TargetLinkType.Default) ? LinkTypePrivate : ((Type == global::UnrealBuildTool.TargetType.Editor) ? TargetLinkType.Modular : TargetLinkType.Monolithic);
}
set
{
LinkTypePrivate = value;
}
}
/// <summary>
/// Backing storage for the LinkType property.
/// </summary>
[RequiresUniqueBuildEnvironment]
[CommandLine("-Monolithic", Value ="Monolithic")]
[CommandLine("-Modular", Value ="Modular")]
TargetLinkType LinkTypePrivate = TargetLinkType.Default;
三、哪些模块会被构建
在模块的声明中会声明一个模块属于哪种类型,在构建某种类型target的时候,会判断该模块类型和target模块是否兼容,这个兼容性判断在Engine\Source\Programs\UnrealBuildTool\System\ModuleDescriptor.cs文件中的ModuleDescriptor类的IsCompiledInConfiguration函数完成。可以看到:runtime模块可以被Editor/Game/Server等所有非Program类型Target使用,Editor模块只能在Editor类型Target使用,Developer模式只能被Editor和Program使用(任何游戏逻辑无法使用)等规则。
/// <summary>
/// Class containing information about a code module
/// </summary>
[DebuggerDisplay("Name={Name}")]
public class ModuleDescriptor
{
/// <summary>
/// Name of this module
/// </summary>
public readonly string Name;
/// <summary>
/// Usage type of module
/// </summary>
public ModuleHostType Type;
……
public bool IsCompiledInConfiguration(UnrealTargetPlatform Platform, UnrealTargetConfiguration Configuration, string TargetName, TargetType TargetType, bool bBuildDeveloperTools, bool bBuildRequiresCookedData)
{
……
// Check the module is compatible with this target.
switch (Type)
{
case ModuleHostType.Runtime:
case ModuleHostType.RuntimeNoCommandlet:
return TargetType != TargetType.Program;
case ModuleHostType.RuntimeAndProgram:
return true;
case ModuleHostType.CookedOnly:
return bBuildRequiresCookedData;
case ModuleHostType.UncookedOnly:
return !bBuildRequiresCookedData;
case ModuleHostType.Developer:
return TargetType == TargetType.Editor || TargetType == TargetType.Program;
case ModuleHostType.DeveloperTool:
return bBuildDeveloperTools;
case ModuleHostType.Editor:
case ModuleHostType.EditorNoCommandlet:
return TargetType == TargetType.Editor;
case ModuleHostType.EditorAndProgram:
return TargetType == TargetType.Editor || TargetType == TargetType.Program;
case ModuleHostType.Program:
return TargetType == TargetType.Program;
case ModuleHostType.ServerOnly:
return TargetType != TargetType.Program && TargetType != TargetType.Client;
case ModuleHostType.ClientOnly:
case ModuleHostType.ClientOnlyNoCommandlet:
return TargetType != TargetType.Program && TargetType != TargetType.Server;
}
return false;
}
四、项目定义的cs文件如何运行时执行
可以看到,UnrealBuildTool会在执行的时候动态加载项目定义的Target.cs和Build.cs文件,这些文件都有框架层的基类(Target、Build分别对应TargetRules和ModuleRules基类)。这就涉及到这些csharp源文件是如何被UnrealBuildTool运行时加载的(类比C++语言,就是如何在运行时动态执行一个cpp文件的内容)。通过分析整个流程来看,整个应该是使用了csharp内置的运行时编译和构建功能。
在Engine\Source\Programs\UnrealBuildTool\System\RulesAssembly.cs文件中RulesAssembly类的构造函数可以看到,在构造函数中通过DynamicCompilation.CompileAndLoadAssembly==>>DynamicCompilation.CompileAssembly完成csharp源文件的编译。
private static Assembly CompileAssembly(FileReference OutputAssemblyPath, HashSet<FileReference> SourceFileNames, List<string> ReferencedAssembies, List<string> PreprocessorDefines = null, bool TreatWarningsAsErrors = false)
{
……
// Compile the code
CompilerResults CompileResults;
try
{
Dictionary<string, string> ProviderOptions = new Dictionary<string, string>() { { "CompilerVersion", "v4.0" } };
CSharpCodeProvider Compiler = new CSharpCodeProvider(ProviderOptions);
CompileResults = Compiler.CompileAssemblyFromFile(CompileParams, SourceFileNames.Select(x => x.FullName).ToArray());
}
catch (Exception Ex)
{
throw new BuildException(Ex, "Failed to launch compiler to compile assembly from source files:\n {0}\n(Exception: {1})", String.Join("\n ", SourceFileNames), Ex.ToString());
}
……
}
编译完成之后使用反射找到constructor函数并调用Engine\Source\Programs\UnrealBuildTool\System\RulesAssembly.cs文件中的CreateModuleRules函数完成运行时构造函数的查找和执行:
public ModuleRules CreateModuleRules(string ModuleName, ReadOnlyTargetRules Target, string ReferenceChain)
{
……
// 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 });
……
}
五、模块定义中的Private、Public文件的作用和意义
在一个模块的Build.cs文件中,通常会声明一些模块特有的属性,主要包括PublicIncludePaths、PrivateIncludePaths、PublicDependencyModuleNames、PrivateDependencyModuleNames。这里的两个IncludePaths主要是构建本模块时需要包含的头文件路径,Public和Private的区别在于,如果当前模块A被另一个B依赖的时候,编译B的时候需要把A的Public包含目录添加到自己的包含路径中(主要是Class和Public文件),Module的意义相同,不过它们是在连接时使用。
在Engine\Source\Programs\UnrealBuildTool\Configuration\UEBuildModule.cs文件中
/// <summary>
/// Sets up the environment for compiling this module.
/// </summary>
protected virtual void SetupPrivateCompileEnvironment(
HashSet<DirectoryReference> IncludePaths,
HashSet<DirectoryReference> SystemIncludePaths,
List<string> Definitions,
List<UEBuildFramework> AdditionalFrameworks,
List<FileItem> AdditionalPrerequisites,
bool bWithLegacyPublicIncludePaths
)
{
if (!Rules.bTreatAsEngineModule)
{
Definitions.Add("DEPRECATED_FORGAME=DEPRECATED");
Definitions.Add("UE_DEPRECATED_FORGAME=UE_DEPRECATED");
}
// Add this module's private include paths and definitions.
IncludePaths.UnionWith(PrivateIncludePaths);
// Find all the modules that are part of the public compile environment for this module.
Dictionary<UEBuildModule, bool> ModuleToIncludePathsOnlyFlag = new Dictionary<UEBuildModule, bool>();
FindModulesInPrivateCompileEnvironment(ModuleToIncludePathsOnlyFlag);
// Now set up the compile environment for the modules in the original order that we encountered them
foreach (UEBuildModule Module in ModuleToIncludePathsOnlyFlag.Keys)
{
Module.AddModuleToCompileEnvironment(Binary, IncludePaths, SystemIncludePaths, Definitions, AdditionalFrameworks, AdditionalPrerequisites, bWithLegacyPublicIncludePaths);
}
}
……
/// <summary>
/// Sets up the environment for linking this module.
/// </summary>
public virtual void SetupPrivateLinkEnvironment(
UEBuildBinary SourceBinary,
LinkEnvironment LinkEnvironment,
List<UEBuildBinary> BinaryDependencies,
HashSet<UEBuildModule> VisitedModules,
DirectoryReference ExeDir
)
{
// Add the private rpaths
LinkEnvironment.RuntimeLibraryPaths.AddRange(ExpandPathVariables(Rules.PrivateRuntimeLibraryPaths, SourceBinary.OutputDir, ExeDir));
// Allow the module's public dependencies to add library paths and additional libraries to the link environment.
SetupPublicLinkEnvironment(SourceBinary, LinkEnvironment.Libraries, LinkEnvironment.SystemLibraryPaths, LinkEnvironment.SystemLibraries, LinkEnvironment.RuntimeLibraryPaths, LinkEnvironment.Frameworks, LinkEnvironment.WeakFrameworks,
LinkEnvironment.AdditionalFrameworks, LinkEnvironment.AdditionalBundleResources, LinkEnvironment.DelayLoadDLLs, BinaryDependencies, VisitedModules, ExeDir);
// Also allow the module's public and private dependencies to modify the link environment.
List<UEBuildModule> AllDependencyModules = new List<UEBuildModule>();
AllDependencyModules.AddRange(PrivateDependencyModules);
AllDependencyModules.AddRange(PublicDependencyModules);
foreach (UEBuildModule DependencyModule in AllDependencyModules)
{
DependencyModule.SetupPublicLinkEnvironment(SourceBinary, LinkEnvironment.Libraries, LinkEnvironment.SystemLibraryPaths, LinkEnvironment.SystemLibraries, LinkEnvironment.RuntimeLibraryPaths, LinkEnvironment.Frameworks, LinkEnvironment.WeakFrameworks,
LinkEnvironment.AdditionalFrameworks, LinkEnvironment.AdditionalBundleResources, LinkEnvironment.DelayLoadDLLs, BinaryDependencies, VisitedModules, ExeDir);
}
// Add all the additional properties
LinkEnvironment.AdditionalProperties.AddRange(Rules.AdditionalPropertiesForReceipt.Inner);
// this is a link-time property that needs to be accumulated (if any modules contributing to this module is ignoring, all are ignoring)
LinkEnvironment.bIgnoreUnresolvedSymbols |= Rules.bIgnoreUnresolvedSymbols;
}
六、回到最初的问题
一个runtime的module是否可以依赖editor类型的模块?这个依赖于target类型:如果是构建target类型是Editor就可以,如果是game/server/client则不行。(该结论未测试)