UE4 Modules:Find the DLL and load it

在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的工具链实在是太完善了,自成一套体系,有些想要剥离出来某些功能比较麻烦。

全文完,若有不足之处请评论指正。
本文标题:UE4 Modules:Find the DLL and load it
文章作者:ZhaLiPeng
发布时间:2019年07月16日 18时23分
本文字数:本文一共有1.4k字
原始链接:https://imzlp.me/posts/31203/
许可协议: CC BY-NC-SA 4.0
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!