UE 加载和查找 DLL 模块分析

在 Windows 上,UE 的模块在非 IS_MONOLITHIC(打包成一个单独的可执行文件的 单片模式 (Monolithic)) 模式下,是通过查找 DLL 来加载模块的。可以调用 FModuleManager 下的 LoadModuleWithFailureReason/LoadModuleChecked 等函数,通过传入 Module 的字符串名字来加载。
本篇文章算是 UE4 Modules:Load and Startup 的扩展和补充,与之不同的是,这篇文章的 侧重点 在于 Module 的 DLL 的查找和加载的细节 而不是 引擎启动和加载 Module 的时机和顺序。

首先,还记得 MODULE_NAME_API 这个宏的作用吗? 在 Windows 平台上它的宏定义是__declspec(dllexport),导出到 DLL,意味着 UE 的模块就是一个 DLL。

使用 FModuleManager::LoadModuleWithFailureReason 加载 Module 的基本的调用流程为 (这几个函数均在FModuleManager 下):在 LoadModuleWithFailureReason 中调用 AddModule,在其中又调用FindModulePaths->FindModulePathsInDirectory 来查找模块。

FindModulePaths

FModuleManager::FindModulePaths的逻辑也比较简单,它接收 Module 的名字和 返回 ModuleName-DLLPathTMap

它内部的逻辑就是从 引擎 / 引擎插件 / 项目及插件 的 Binaries 路径依次调用FindModulePathsInDirectory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void FModuleManager::FindModulePaths(const TCHAR* NamePattern, TMap<FName, FString> &OutModulePaths, bool bCanUseCache /*= true*/) const
{
// .... USING CACHE PATH ....

// Search through the engine directory
FindModulePathsInDirectory(FPlatformProcess::GetModulesDirectory(), false, NamePattern, OutModulePaths);

// Search any engine directories
for (int Idx = 0; Idx < EngineBinariesDirectories.Num(); Idx++)
{
FindModulePathsInDirectory(EngineBinariesDirectories[Idx], false, NamePattern, OutModulePaths);
}

// Search any game directories
for (int Idx = 0; Idx < GameBinariesDirectories.Num(); Idx++)
{
FindModulePathsInDirectory(GameBinariesDirectories[Idx], true, NamePattern, OutModulePaths);
}
}

上面代码的上半部分从缓存路径中查找就不解释了,重要的是下面三个路径的查找:

  • FPlatformProcess::GetModulesDirectory()
  • EngineBinariesDirectories
  • GameBinariesDirectories

FPlatformProcess::GetModulesDirectory()为相对于当前引擎的 Binaries:

1
L"../../../Engine/Binaries/Win64"

EngineBinariesDirectoriesGameBinariesDirectories 这两个数组均通过 FModuleManager::AddBinariesDirectory 函数添加进来的。

这个函数有三处调用:

  • FModuleManager::Get
  • FEngineLoop::PreInit(Enterprise Project)
  • FPluginManager::ConfigureEnabledPlugin
  • FPluginManager::MountNewlyCreatedPlugin

EngineBinariesDirectories中存储的是引擎目录中 PluginsBinaries的路径:

1
L"../../../Engine/Plugins/"

GameBinariesDirectories中存储的路径为当前工程以及当前工程目录下的插件的 Binaries 路径,比如Binaries/Win64:

小节一下:UE 加载 Module 时查找的路径依次为

  • 引擎的 Binaries 路径(Engine/Binaries/Win64)
  • 引擎中插件的 Binaries 路径(Engine/Plugins/${PLUGIN_NAME}/Win64)
  • ** 游戏项目的 Binaries 及项目目录下所有插件的 Binaries 路径 (${PROJECT_NAME}/Binaries 以及${PROJECT_NAME}/Plugins/${PLUGIN_NAME}/Binaries)**。

FindModulePathsInDirectory

而且,在 FModuleManager::FindModulePathsInDirectory 这个函数中,每传进来一个路径是通过 FModuleEnumerator::QueryModules 来得到当前路径下的有效 Modules 的。

1
2
3
// Runtime/Core/Private/Modules/ModuleManager.cpp
// FModuleManager::FindModulePathsInDirectory
QueryModulesDelegate.Execute(SearchDirectoryName, bIsGameDirectory, ValidModules);

这个调用的派发事件在 FModuleEnumerator::RegisterWithModuleManager() 中绑定,执行的函数为FModuleEnumerator::QueryModules

1
2
3
4
5
6
7
8
void FModuleEnumerator::QueryModules(const FString& InDirectoryName, bool bIsGameDirectory, TMap<FString, FString>& OutModules) const
{
FModuleManifest Manifest;
if(FModuleManifest::TryRead(FModuleManifest::GetFileName(InDirectoryName, bIsGameDirectory), Manifest) && Manifest.BuildId == BuildId)
{
OutModules = Manifest.ModuleNameToFileName;
}
}

FModuleManifest::GetFileName返回的是传进来的 Binaries 目录下的 *.modules 文件,每一个编译出来的 Module 的 Binaries 路径下都会有这个文件,随便打开一个看一下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Engime/Binaires/Win64/UE4Editor.mosules
{
"BuildId": "3709383",
"Modules":
{
"ActorPickerMode": "UE4Editor-ActorPickerMode.dll",
"AddContentDialog": "UE4Editor-AddContentDialog.dll",
"AdvancedPreviewScene": "UE4Editor-AdvancedPreviewScene.dll",
"Advertising": "UE4Editor-Advertising.dll",
"AIGraph": "UE4Editor-AIGraph.dll",
"AIModule": "UE4Editor-AIModule.dll",
// .....
"WindowsNoEditorTargetPlatform": "UE4Editor-WindowsNoEditorTargetPlatform.dll",
"WindowsPlatformEditor": "UE4Editor-WindowsPlatformEditor.dll",
"WindowsServerTargetPlatform": "UE4Editor-WindowsServerTargetPlatform.dll",
"WindowsTargetPlatform": "UE4Editor-WindowsTargetPlatform.dll",
"WorkspaceMenuStructure": "UE4Editor-WorkspaceMenuStructure.dll",
"WorldBrowser": "UE4Editor-WorldBrowser.dll",
"XAudio2": "UE4Editor-XAudio2.dll",
"XGEController": "UE4Editor-XGEController.dll",
"XmlParser": "UE4Editor-XmlParser.dll",
"XMPP": "UE4Editor-XMPP.dll"
}
}

可以看到,其中的 json 对应了 ModuleName 和其 DLL。

然后调用 FModuleManifest::TryRead 将调用 FModuleManifest::GetFileName 得到的 *.modules 文件解析成一个 FModuleManifest 结构,包含 BuildIdTMap<FString,FString>ModuleName-DLLName 的关联容器。

此时 FModuleManager::FindModulePaths 的任务已经全部完成,通过它得到了指定模块中的所有 Module 和 Module 对应的 DLL 信息,此时工作流转回到 FModuleManager::AddModule 中。

AddModule

后续的 FModuleManager::AddModule 执行就中规中矩了。从得到的 Map 中将所要添加的 Module 的信息提取出来,组成一个 ModuleInfoRef 对象:

1
typedef TSharedRef<FModuleInfo, ESPMode::ThreadSafe> ModuleInfoRef;

并将其添加至 Module 的列表中(FModuleManager::Get().AddModuleToModulesList(InModuleName, ModuleInfo)).

PS:在 FModuleManager::AddModule 的代码中有一个风骚的技巧:

1
2
3
4
ON_SCOPE_EXIT
{
FModuleManager::Get().AddModuleToModulesList(InModuleName, ModuleInfo);
};

这段代码的意思是在退出当前的作用域 (Scope) 时执行 {} 中的逻辑,简单地来说,它定义了一个当前作用域的对象并托管了一个 Lambda,在离开当前作用域的时候通过 C++ 的 RAII 机制来调用托管的 Lambda,但它的具体实现不是这篇文章的主题,有时间再来单独分析。

FindModuleWithFailureReason

FModuleManager::AddModule 执行完毕之后,将指定模块的 DLL 信息,存放到了 FModuleManager::Modules 中,接下来就可以得到这个模块的句柄了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
IModuleInterface* FModuleManager::LoadModuleWithFailureReason(const FName InModuleName, EModuleLoadResult& OutFailureReason, bool bWasReloaded /*=false*/)
{
// ....
IModuleInterface* LoadedModule = nullptr;
OutFailureReason = EModuleLoadResult::Success;

// Update our set of known modules, in case we don't already know about this module
AddModule(InModuleName);

// Grab the module info. This has the file name of the module, as well as other info.
ModuleInfoRef ModuleInfo = Modules.FindChecked(InModuleName);

if (ModuleInfo->Module.IsValid())
{
// Assign the already loaded module into the return value, otherwise the return value gives the impression the module failed load!
LoadedModule = ModuleInfo->Module.Get();
// ....
}
// ...
}

因为所有的模块在 ModuleName.cpp 中都使用了 IMPLEMENT_MODULE 及其衍生宏来注册模块,所以它们都继承了 IModuleInterface,其中有StartupModuleShutdownModule,获取到句柄,启动的时候调用的就是每个模块里定义的逻辑了。

注:模块的 StartupModule 是在 FModuleManager::LoadModuleWithFailureReason 中调用的。不管是 LoadModule 或者 LoadModuleChecked 最终都是调用到 LoadModuleWithFailureReason 进行实际的加载和启动的。

同理,FModuleManager::UnLoadModule中执行了模块的ShutdownModule

UE 设计的这一套 Module 架构还是很方便的,但是也是 UE 的工具链实在是太完善了,自成一套体系,有些想要剥离出来某些功能比较麻烦。