UE Modules:Find the DLL and load it

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
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);
}
}

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
2
3
// Runtime/Core/Private/Modules/ModuleManager.cpp
// FModuleManager::FindModulePathsInDirectory
QueryModulesDelegate.Execute(SearchDirectoryName, bIsGameDirectory, ValidModules);

This call is dispatched in FModuleEnumerator::RegisterWithModuleManager(), where the function executed is 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 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Engine/Binaries/Win64/UE4Editor.modules
{
"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"
}
}

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
2
3
4
ON_SCOPE_EXIT
{
FModuleManager::Get().AddModuleToModulesList(InModuleName, ModuleInfo);
};

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
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 to load!
LoadedModule = ModuleInfo->Module.Get();
// ....
}
// ...
}

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 in FModuleManager::LoadModuleWithFailureReason. Whether LoadModule or LoadModuleChecked, they ultimately call LoadModuleWithFailureReason for actual loading and starting.

Likewise, FModuleManager::UnLoadModule executes the module’s ShutdownModule.

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.

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:UE Modules:Find the DLL and load it
Author:LIPENGZHA
Publish Date:2019/07/16 18:23
Word Count:4.5k Words
Link:https://en.imzlp.com/posts/31203/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!