在 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-DLLPath的TMap
。
它内部的逻辑就是从 引擎 / 引擎插件 / 项目及插件 的 Binaries 路径依次调用FindModulePathsInDirectory
:
1 | void FModuleManager::FindModulePaths(const TCHAR* NamePattern, TMap<FName, FString> &OutModulePaths, bool bCanUseCache /*= true*/) const |
上面代码的上半部分从缓存路径中查找就不解释了,重要的是下面三个路径的查找:
FPlatformProcess::GetModulesDirectory()
EngineBinariesDirectories
GameBinariesDirectories
FPlatformProcess::GetModulesDirectory()
为相对于当前引擎的 Binaries:
1 | L"../../../Engine/Binaries/Win64" |
EngineBinariesDirectories与 GameBinariesDirectories 这两个数组均通过 FModuleManager::AddBinariesDirectory
函数添加进来的。
这个函数有三处调用:
FModuleManager::Get
FEngineLoop::PreInit
(Enterprise Project)FPluginManager::ConfigureEnabledPlugin
FPluginManager::MountNewlyCreatedPlugin
EngineBinariesDirectories
中存储的是引擎目录中 Plugins
的Binaries
的路径:
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 | // Runtime/Core/Private/Modules/ModuleManager.cpp |
这个调用的派发事件在 FModuleEnumerator::RegisterWithModuleManager()
中绑定,执行的函数为FModuleEnumerator::QueryModules
:
1 | void FModuleEnumerator::QueryModules(const FString& InDirectoryName, bool bIsGameDirectory, TMap<FString, FString>& OutModules) const |
FModuleManifest::GetFileName
返回的是传进来的 Binaries 目录下的 *.modules
文件,每一个编译出来的 Module 的 Binaries 路径下都会有这个文件,随便打开一个看一下内容:
1 | // Engime/Binaires/Win64/UE4Editor.mosules |
可以看到,其中的 json 对应了 ModuleName
和其 DLL。
然后调用 FModuleManifest::TryRead
将调用 FModuleManifest::GetFileName
得到的 *.modules
文件解析成一个 FModuleManifest
结构,包含 BuildId
与TMap<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 | ON_SCOPE_EXIT |
这段代码的意思是在退出当前的作用域 (Scope) 时执行 {}
中的逻辑,简单地来说,它定义了一个当前作用域的对象并托管了一个 Lambda,在离开当前作用域的时候通过 C++ 的 RAII 机制来调用托管的 Lambda,但它的具体实现不是这篇文章的主题,有时间再来单独分析。
FindModuleWithFailureReason
在 FModuleManager::AddModule
执行完毕之后,将指定模块的 DLL 信息,存放到了 FModuleManager::Modules
中,接下来就可以得到这个模块的句柄了:
1 | IModuleInterface* FModuleManager::LoadModuleWithFailureReason(const FName InModuleName, EModuleLoadResult& OutFailureReason, bool bWasReloaded /*=false*/) |
因为所有的模块在 ModuleName.cpp
中都使用了 IMPLEMENT_MODULE
及其衍生宏来注册模块,所以它们都继承了 IModuleInterface
,其中有StartupModule
和ShutdownModule
,获取到句柄,启动的时候调用的就是每个模块里定义的逻辑了。
注:模块的
StartupModule
是在FModuleManager::LoadModuleWithFailureReason
中调用的。不管是LoadModule
或者LoadModuleChecked
最终都是调用到LoadModuleWithFailureReason
进行实际的加载和启动的。同理,
FModuleManager::UnLoadModule
中执行了模块的ShutdownModule
。
UE 设计的这一套 Module 架构还是很方便的,但是也是 UE 的工具链实在是太完善了,自成一套体系,有些想要剥离出来某些功能比较麻烦。