On Windows, UE modules are loaded via DLL lookup in non-IS_MONOLITHIC
(Monolithic) mode, which means they are not packed into a single executable file. You can call functions like LoadModuleWithFailureReason
or LoadModuleChecked
from FModuleManager
to load a module by passing its string name. This article serves as an extension and supplement to UE4 Modules: Load and Startup, focusing on the details of DLL lookup and loading for modules rather than the timing and order of engine startup and module loading.
First, do you remember the purpose of the MODULE_NAME_API
macro? On the Windows platform, its macro definition is __declspec(dllexport)
, which means that UE modules are DLLs.
The basic call flow for loading a module using FModuleManager::LoadModuleWithFailureReason
(these functions are all under FModuleManager
) involves calling AddModule
within LoadModuleWithFailureReason
, which in turn calls FindModulePaths
-> FindModulePathsInDirectory
to locate the module.
FindModulePaths
The logic of FModuleManager::FindModulePaths
is quite simple; it receives the module name and returns a TMap<FName, FString>
of ModuleName-DLLPath
.
Internally, it calls FindModulePathsInDirectory
on the Binaries paths of the engine, engine plugins, project, and other plugins sequentially:
1 | void FModuleManager::FindModulePaths(const TCHAR* NamePattern, TMap<FName, FString> &OutModulePaths, bool bCanUseCache /*= true*/) const |
The upper part of the above code explaining the cache path lookup will not be elaborated. The important part is the lookup of the following three paths:
FPlatformProcess::GetModulesDirectory()
EngineBinariesDirectories
GameBinariesDirectories
FPlatformProcess::GetModulesDirectory()
is relative to the current engine’s Binaries:
1 | L"../../../Engine/Binaries/Win64" |
Both EngineBinariesDirectories
and GameBinariesDirectories
arrays are added via the FModuleManager::AddBinariesDirectory
function.
This function is called in three places:
FModuleManager::Get
FEngineLoop::PreInit
(Enterprise Project)FPluginManager::ConfigureEnabledPlugin
FPluginManager::MountNewlyCreatedPlugin
EngineBinariesDirectories
stores the Binaries paths of Plugins
in the engine directory:
1 | L"../../../Engine/Plugins/" |
GameBinariesDirectories
stores the path for the current project and the Binaries paths for plugins in the project directory, such as Binaries/Win64
:
In summary: the paths checked when UE loads a module are as follows:
- Engine Binaries path (Engine/Binaries/Win64)
- Plugin Binaries path in the engine (Engine/Plugins/${PLUGIN_NAME}/Win64)
- Game project’s Binaries and Binaries paths for all plugins in the project directory (${PROJECT_NAME}/Binaries and ${PROJECT_NAME}/Plugins/${PLUGIN_NAME}/Binaries).
FindModulePathsInDirectory
Also, in FModuleManager::FindModulePathsInDirectory
, for each path provided, valid modules in the current path are obtained via FModuleEnumerator::QueryModules
.
1 | // Runtime/Core/Private/Modules/ModuleManager.cpp |
This call is dispatched in FModuleEnumerator::RegisterWithModuleManager()
, where the function executed is FModuleEnumerator::QueryModules
:
1 | void FModuleEnumerator::QueryModules(const FString& InDirectoryName, bool bIsGameDirectory, TMap<FString, FString>& OutModules) const |
FModuleManifest::GetFileName
returns the *.modules
file in the provided Binaries directory. Each compiled module will have this file in its Binaries path. You can open one to check its content:
1 | // Engine/Binaries/Win64/UE4Editor.modules |
As you can see, the JSON corresponds to the ModuleName
and its DLL.
Then, FModuleManifest::TryRead
will call FModuleManifest::GetFileName
to parse the *.modules
file into an FModuleManifest
structure, which contains BuildId
and a TMap<FString,FString>
mapping ModuleName-DLLName
.
At this point, the tasks of FModuleManager::FindModulePaths
are completed, and it has obtained all module info and corresponding DLL information. The workflow then returns to FModuleManager::AddModule
.
AddModule
The subsequent execution of FModuleManager::AddModule
is standard. It extracts the necessary module information from the obtained map and forms a ModuleInfoRef
object:
1 | typedef TSharedRef<FModuleInfo, ESPMode::ThreadSafe> ModuleInfoRef; |
It adds this to the modules list using (FModuleManager::Get().AddModuleToModulesList(InModuleName, ModuleInfo)
).
P.S.: In the code of FModuleManager::AddModule
, there is a clever trick:
1 | ON_SCOPE_EXIT |
This piece of code means that the logic inside {}
will be executed when exiting the current scope. In simple terms, it defines an object for the current scope and manages a Lambda, which is called when leaving the current scope through C++’s RAII mechanism.
FindModuleWithFailureReason
After executing FModuleManager::AddModule
, the DLL information for the specified module is stored in FModuleManager::Modules
, allowing you to get the handle for this module:
1 | IModuleInterface* FModuleManager::LoadModuleWithFailureReason(const FName InModuleName, EModuleLoadResult& OutFailureReason, bool bWasReloaded /*=false*/) |
Because all the modules use IMPLEMENT_MODULE
and its derived macros in ModuleName.cpp
to register, they all inherit from IModuleInterface
, which includes StartupModule
and ShutdownModule
. Upon obtaining the handle, the logic defined in each module is called during startup.
Note: The module’s
StartupModule
is called inFModuleManager::LoadModuleWithFailureReason
. WhetherLoadModule
orLoadModuleChecked
, they ultimately callLoadModuleWithFailureReason
for actual loading and starting.Likewise,
FModuleManager::UnLoadModule
executes the module’sShutdownModule
.
The Module architecture designed by UE is quite convenient. However, the UE toolchain is so well-structured that it creates its own ecosystem, making it somewhat tricky to separate certain functionalities.