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