Engine Source Analysis: Module Loading and Startup

引擎源码分析:模块的加载和启动

UE is a modular architecture, where Engine/Game Project/StandaloneApplication/Plugins are all Modules (Unreal Engine API Reference lists the Modules provided by the Engine). This article analyzes how UE’s Modules are loaded and started via FModuleManager::LoadModule using the code from FModuleManager.

Forward

This article references UE code from version 4.21.

Note: The IS_MONOLITHIC mentioned in this article refers to the LinkType in the user’s *.target.cs, which can be specified using TargetLinkType.Monolithic.

  • The meaning of TargetLinkType.Monolithic (Monolithic mode) is that all the code is placed into a single executable file, and during compilation, it uses methods such as implementation inclusion or static linking, without relying on other Modules’ DLLs.

For more on Module API Specifiers, see: Programming/Modules Module API Specifiers

The call to FModuleManager::LoadModule or FModuleManager::LoadModuleWithFailureReason will execute the StartupModule of the loaded Module.

Module Implementation

In UE, the project’s macro is IMPLEMENT_PRIMARY_GAME_MODULE, which is usually defined in ProjectName.cpp after creating the project:

1
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, VRExpansion, "VRExpansion" );

FDefaultGameModuleImpl is a class that inherits from FDefaultModuleImpl and indirectly from IModuleInterface, without overriding any of IModuleInterface‘s functions, serving merely as a layer of encapsulated default implementation (game projects do not need to drive startup through Module’s StartupModule). You can pass your own class inheriting from IModuleInterface into the Module, overriding its interfaces to perform specific actions when the Module is loaded (the interfaces of IModuleInterface are listed later).

The IMPLEMENT_PRIMARY_GAME_MODULE macro first gets replaced with IMPLEMENT_GAME_MODULE, which then continues to replace with IMPLEMENT_MODULE.
In the current engine implementation, these three macros have no distinction:

  • IMPLEMENT_PRIMARY_GAME_MODULE
  • IMPLEMENT_GAME_MODULE
  • IMPLEMENT_MODULE

Generally, what is commonly used in plugins is IMPLEMENT_MODULE (this macro is defined in Runtime\Core\Public\Modules\ModuleManger.h):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#if IS_MONOLITHIC

// If we're linking monolithically we assume all modules are linked in with the main binary.
#define IMPLEMENT_MODULE( ModuleImplClass, ModuleName ) \
/** Global registrant object for this module when linked statically */ \
static FStaticallyLinkedModuleRegistrant< ModuleImplClass > ModuleRegistrant##ModuleName( #ModuleName ); \
/** Implement an empty function so that if this module is built as a statically linked lib, */ \
/** static initialization for this lib can be forced by referencing this symbol */ \
void EmptyLinkFunctionForStaticInitialization##ModuleName(){} \
PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)

#else

#define IMPLEMENT_MODULE( ModuleImplClass, ModuleName ) \
\
/**/ \
/* InitializeModule function, called by module manager after this module's DLL has been loaded */ \
/**/ \
/* @return Returns an instance of this module */ \
/**/ \
extern "C" DLLEXPORT IModuleInterface* InitializeModule() \
{ \
return new ModuleImplClass(); \
} \
PER_MODULE_BOILERPLATE \
PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)

#endif //IS_MONOLITHIC

This macro defines and exports a generic module interface for external control of this Module’s startup/shutdown or other functionalities. This macro is essential for UE’s module mechanism to function.

IS_MONOLITHIC

When the LinkType in the compilation target’s target.cs is TargetLinkType.Monolithic, the logic used is IS_MONOLITHIC. In this mode, the IMPLEMENT_MODULE macro constructs a static object of FStaticallyLinkedModuleRegistrant<ModuleImplClass>, which in its constructor calls FModuleManager::RegisterStaticallyLinkedModule to add it to a TMap object (FModuleManager::StaticallyLinkedModuleInitializers) for subsequent LoadModule lookups:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
template< class ModuleClass >
class FStaticallyLinkedModuleRegistrant
{
public:

/**
* Explicit constructor that registers a statically linked module
*/
FStaticallyLinkedModuleRegistrant( const ANSICHAR* InModuleName )
{
// Create a delegate to our InitializeModule method
FModuleManager::FInitializeStaticallyLinkedModule InitializerDelegate = FModuleManager::FInitializeStaticallyLinkedModule::CreateRaw(
this, &FStaticallyLinkedModuleRegistrant<ModuleClass>::InitializeModule );

// Register this module
FModuleManager::Get().RegisterStaticallyLinkedModule(
FName( InModuleName ), // Module name
InitializerDelegate ); // Initializer delegate
}

/**
* Creates and initializes this statically linked module.
*
* The module manager calls this function through the delegate that was created
* in the @see FStaticallyLinkedModuleRegistrant constructor.
*
* @return A pointer to a new instance of the module.
*/
IModuleInterface* InitializeModule( )
{
return new ModuleClass();
}
};

class FModuleManager
{
// ...
public:
void RegisterStaticallyLinkedModule( const FName InModuleName, const FInitializeStaticallyLinkedModule& InInitializerDelegate )
{
StaticallyLinkedModuleInitializers.Add( InModuleName, InInitializerDelegate );
}

private:
/** Map of module names to a delegate that can initialize each respective statically linked module */
typedef TMap< FName, FInitializeStaticallyLinkedModule > FStaticallyLinkedModuleInitializerMap;
FStaticallyLinkedModuleInitializerMap StaticallyLinkedModuleInitializers;
// ...
};

In the context of IS_MONOLITHIC, the function FModuleManager::LoadModuleWithFailureReason (via FModuleManager::LoadModule) forwards to the TMap ModuleName-InitializerDelegate (FStaticallyLinkedModuleRegistrant::InitializeModule) to obtain the IModuleInterface of the specified module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// FModuleManager::LoadModuleWithFailureReason (Runtime\Core\Private\Modules\ModuleManager.cpp)
IModuleInterface* FModuleManager::LoadModuleWithFailureReason(const FName InModuleName, EModuleLoadResult& OutFailureReason)
{
// something....

// Make sure this isn't a module that we had previously loaded, and then unloaded at shutdown time.
//
// If this assert goes off, your trying to load a module during the shutdown phase that was already
// cleaned up. The easiest way to fix this is to change your code to query for an already-loaded
// module instead of trying to load it directly.
checkf((!ModuleInfo->bWasUnloadedAtShutdown), TEXT("Attempted to load module '%s' that was already unloaded at shutdown. FModuleManager::LoadModule() was called to load a module that was previously loaded, and was unloaded at shutdown time. If this assert goes off, your trying to load a module during the shutdown phase that was already cleaned up. The easiest way to fix this is to change your code to query for an already-loaded module instead of trying to load it directly."), *InModuleName.ToString());

// Check if we're statically linked with the module. Those modules register with the module manager using a static variable,
// so hopefully we already know about the name of the module and how to initialize it.
const FInitializeStaticallyLinkedModule* ModuleInitializerPtr = StaticallyLinkedModuleInitializers.Find(InModuleName);
if (ModuleInitializerPtr != nullptr)
{
const FInitializeStaticallyLinkedModule& ModuleInitializer(*ModuleInitializerPtr);

// Initialize the module!
ModuleInfo->Module = TUniquePtr<IModuleInterface>(ModuleInitializer.Execute());

if (ModuleInfo->Module.IsValid())
{
// Startup the module
ModuleInfo->Module->StartupModule();
// The module might try to load other dependent modules in StartupModule. In this case, we want those modules shut down AFTER this one because we may still depend on the module at shutdown.
ModuleInfo->LoadOrder = FModuleInfo::CurrentLoadOrder++;

// Module was started successfully! Fire callbacks.
ModulesChangedEvent.Broadcast(InModuleName, EModuleChangeReason::ModuleLoaded);

// Set the return parameter
LoadedModule = ModuleInfo->Module.Get();
}
else
{
UE_LOG(LogModuleManager, Warning, TEXT("ModuleManager: Unable to load module '%s' because InitializeModule function failed (returned nullptr.)"), *InModuleName.ToString());
OutFailureReason = EModuleLoadResult::FailedToInitialize;
}
}
#if IS_MONOLITHIC
else
{
// Monolithic builds that do not have the initializer were *not found* during the build step, so return FileNotFound
// (FileNotFound is an acceptable error in some case - ie loading a content only project)
UE_LOG(LogModuleManager, Warning, TEXT("ModuleManager: Module '%s' not found - its StaticallyLinkedModuleInitializers function is null."), *InModuleName.ToString());
OutFailureReason = EModuleLoadResult::FileNotFound;
}
#endif
// something....
};

!IS_MONOLITHIC

In the case of !IS_MONOLITHIC, it exports a symbol IModuleInterface* InitializeModule() (when LinkType is not specified as Monolithic in the target.cs - each Module is a separate linked library (lib/dll)).
When using FModuleManager::LoadModuleWithFailureReason to load a specified Module, it first retrieves the Module’s linked library (DLL) path through FModuleManager::FindModulePaths:
It will search through:

  • FPlatformProcess::GetModulesDirectory
  • EngineBinariesDirectories
  • GameBinariesDirectories
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#if !IS_MONOLITHIC
void FModuleManager::FindModulePaths(const TCHAR* NamePattern, TMap<FName, FString> &OutModulePaths, bool bCanUseCache /*= true*/) const
{
if (!ModulePathsCache)
{
ModulePathsCache.Emplace();
const bool bCanUseCacheWhileGeneratingIt = false;
FindModulePaths(TEXT("*"), ModulePathsCache.GetValue(), bCanUseCacheWhileGeneratingIt);
}

if (bCanUseCache)
{
// Try to use cache first
if (const FString* ModulePathPtr = ModulePathsCache->Find(NamePattern))
{
OutModulePaths.Add(FName(NamePattern), *ModulePathPtr);
return;
}

// Wildcard for all items
if (FCString::Strcmp(NamePattern, TEXT("*")) == 0)
{
OutModulePaths = ModulePathsCache.GetValue();
return;
}

// Wildcard search
if (FCString::Strchr(NamePattern, TEXT('*')) || FCString::Strchr(NamePattern, TEXT('?')))
{
bool bFoundItems = false;
FString NamePatternString(NamePattern);
for (const TPair<FName, FString>& CacheIt : ModulePathsCache.GetValue())
{
if (CacheIt.Key.ToString().MatchesWildcard(NamePatternString))
{
OutModulePaths.Add(CacheIt.Key, *CacheIt.Value);
bFoundItems = true;
}
}

if (bFoundItems)
{
return;
}
}
}

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

void FModuleManager::FindModulePathsInDirectory(const FString& InDirectoryName, bool bIsGameDirectory, const TCHAR* NamePattern, TMap<FName, FString> &OutModulePaths) const
{
// Figure out the BuildId if it's not already set.
if (!BuildId.IsSet())
{
FString FileName = FModuleManifest::GetFileName(FPlatformProcess::GetModulesDirectory(), false);

FModuleManifest Manifest;
if (!FModuleManifest::TryRead(FileName, Manifest))
{
UE_LOG(LogModuleManager, Fatal, TEXT("Unable to read module manifest from '%s'. Module manifests are generated at build time, and must be present to locate modules at runtime."), *FileName)
}

BuildId = Manifest.BuildId;
}

// Find all the directories to search through, including the base directory
TArray<FString> SearchDirectoryNames;
IFileManager::Get().FindFilesRecursive(SearchDirectoryNames, *InDirectoryName, TEXT("*"), false, true);
SearchDirectoryNames.Insert(InDirectoryName, 0);

// Enumerate the modules in each directory
for(const FString& SearchDirectoryName: SearchDirectoryNames)
{
FModuleManifest Manifest;
if (FModuleManifest::TryRead(FModuleManifest::GetFileName(SearchDirectoryName, bIsGameDirectory), Manifest) && Manifest.BuildId == BuildId.GetValue())
{
for (const TPair<FString, FString>& Pair : Manifest.ModuleNameToFileName)
{
if (Pair.Key.MatchesWildcard(NamePattern))
{
OutModulePaths.Add(FName(*Pair.Key), *FPaths::Combine(*SearchDirectoryName, *Pair.Value));
}
}
}
}
}
#endif

After obtaining the linked library’s path, FPlatformProcess::GetDllExport is used to get the function pointer for IModuleInterface::InitializeModule in the DLL, followed by calling IModuleInterface::StartupModule to start this module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// FModuleManager::LoadModuleWithFailureReason (Runtime\Core\Private\Modules\ModuleManager.cpp)
IModuleInterface* FModuleManager::LoadModuleWithFailureReason(const FName InModuleName, EModuleLoadResult& OutFailureReason)
{
// something....
// Make sure that any UObjects that need to be registered were already processed before we go and
// load another module. We just do this so that we can easily tell whether UObjects are present
// in the module being loaded.
if (bCanProcessNewlyLoadedObjects)
{
ProcessLoadedObjectsCallback.Broadcast();
}

// Try to dynamically load the DLL

UE_LOG(LogModuleManager, Verbose, TEXT("ModuleManager: Load Module '%s' DLL '%s'"), *InModuleName.ToString(), *ModuleInfo->Filename);

if (ModuleInfo->Filename.IsEmpty() || !FPaths::FileExists(ModuleInfo->Filename))
{
TMap<FName, FString> ModulePathMap;
FindModulePaths(*InModuleName.ToString(), ModulePathMap);

if (ModulePathMap.Num() != 1)
{
UE_LOG(LogModuleManager, Warning, TEXT("ModuleManager: Unable to load module '%s' - %d instances of that module name found."), *InModuleName.ToString(), ModulePathMap.Num());
OutFailureReason = EModuleLoadResult::FileNotFound;
return nullptr;
}

ModuleInfo->Filename = MoveTemp(TMap<FName, FString>::TIterator(ModulePathMap).Value());
}

// Determine which file to load for this module.
const FString ModuleFileToLoad = FPaths::ConvertRelativePathToFull(ModuleInfo->Filename);

// Clear the handle and set it again below if the module is successfully loaded
ModuleInfo->Handle = nullptr;

// Skip this check if file manager has not yet been initialized
if (FPaths::FileExists(ModuleFileToLoad))
{
ModuleInfo->Handle = FPlatformProcess::GetDllHandle(*ModuleFileToLoad);
if (ModuleInfo->Handle != nullptr)
{
// First things first. If the loaded DLL has UObjects in it, then their generated code's
// static initialization will have run during the DLL loading phase, and we'll need to
// go in and make sure those new UObject classes are properly registered.
{
// Sometimes modules are loaded before even the UObject systems are ready. We need to assume
// these modules aren't using UObjects.
if (bCanProcessNewlyLoadedObjects)
{
// OK, we've verified that loading the module caused new UObject classes to be
// registered, so we'll treat this module as a module with UObjects in it.
ProcessLoadedObjectsCallback.Broadcast();
}
}

// Find our "InitializeModule" global function, which must exist for all module DLLs
FInitializeModuleFunctionPtr InitializeModuleFunctionPtr =
(FInitializeModuleFunctionPtr)FPlatformProcess::GetDllExport(ModuleInfo->Handle, TEXT("InitializeModule"));
if (InitializeModuleFunctionPtr != nullptr)
{
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();
}
else
{
// Initialize the module!
ModuleInfo->Module = TUniquePtr<IModuleInterface>(InitializeModuleFunctionPtr());

if ( ModuleInfo->Module.IsValid() )
{
// Startup the module
ModuleInfo->Module->StartupModule();
// The module might try to load other dependent modules in StartupModule. In this case, we want those modules shut down AFTER this one because we may still depend on the module at shutdown.
ModuleInfo->LoadOrder = FModuleInfo::CurrentLoadOrder++;

// Module was started successfully! Fire callbacks.
ModulesChangedEvent.Broadcast(InModuleName, EModuleChangeReason::ModuleLoaded);

// Set the return parameter
LoadedModule = ModuleInfo->Module.Get();
}
else
{
UE_LOG(LogModuleManager, Warning, TEXT("ModuleManager: Unable to load module '%s' because InitializeModule function failed (returned nullptr.)"), *ModuleFileToLoad);

FPlatformProcess::FreeDllHandle(ModuleInfo->Handle);
ModuleInfo->Handle = nullptr;
OutFailureReason = EModuleLoadResult::FailedToInitialize;
}
}
}
else
{
UE_LOG(LogModuleManager, Warning, TEXT("ModuleManager: Unable to load module '%s' because InitializeModule function was not found."), *ModuleFileToLoad);

FPlatformProcess::FreeDllHandle(ModuleInfo->Handle);
ModuleInfo->Handle = nullptr;
OutFailureReason = EModuleLoadResult::FailedToInitialize;
}
}
else
{
UE_LOG(LogModuleManager, Warning, TEXT("ModuleManager: Unable to load module '%s' because the file couldn't be loaded by the OS."), *ModuleFileToLoad);
OutFailureReason = EModuleLoadResult::CouldNotBeLoadedByOS;
}
}
else
{
UE_LOG(LogModuleManager, Warning, TEXT("ModuleManager: Unable to load module '%s' because the file '%s' was not found."), *InModuleName.ToString(), *ModuleFileToLoad);
OutFailureReason = EModuleLoadResult::FileNotFound;
}
// something....
}

IModuleInterface

Regardless of whether it’s through IS_MONOLITHIC or !IS_MONOLITHIC, the outcome is the IModuleInterface of the Module (Runtime\Core\Public\Modules\ModuleInterface.h), which declares the common Module interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class IModuleInterface
{
public:
// Note: Even though this is an interface class we need a virtual destructor here because modules are deleted via a pointer to this interface
virtual ~IModuleInterface(){}

// Called right after the module DLL has been loaded and the module object has been created
// Load dependent modules here, and they will be guaranteed to be available during ShutdownModule. ie:
// FModuleManager::Get().LoadModuleChecked(TEXT("HTTP"));
virtual void StartupModule(){}

// Called before the module has been unloaded
virtual void PreUnloadCallback(){}

// Called after the module has been reloaded

virtual void PostLoadCallback(){}

// Called before the module is unloaded, right before the module object is destroyed.
// During normal shutdown, this is called in reverse order that modules finish StartupModule().
// This means that, as long as a module references dependent modules in its StartupModule(), it
// can safely reference those dependencies in ShutdownModule() as well.
virtual void ShutdownModule(){}

// Override this to set whether your module is allowed to be unloaded on the fly
// @return Whether the module supports shutdown separate from the rest of the engine.
virtual bool SupportsDynamicReloading(){return true;}

// Override this to set whether your module would like cleanup on application shutdown
// @return Whether the module supports shutdown on application exit
virtual bool SupportsAutomaticShutdown(){return true;}

// Returns true if this module hosts gameplay code
// @return True for "gameplay modules", or false for engine code modules, plugins, etc.
virtual bool IsGameModule() const{return false;}
};

Load Modules when the Engine Launch

As mentioned earlier, UE is a modular architecture, and the engine’s implementation relies on various modules combined to drive it. Therefore, when the engine starts, various corresponding modules will be initiated. The entry point for engine startup is in the Launch module, which is forwarded from the main function of each platform to GuardedMain (Engine\Source\Runtime\Launch\Private\Launch.cpp). In GuardedMain, GEngineLoop.PreInit(CmdLine) and GEngineLoop.Init() are executed in sequence.

In FEngineLoop::PreInit, the engine loads the essential modules for startup by sequentially calling FEngineLoop::LoadCoreModules (LaunchEngineLoop.cpp#L1480) and FEngineLoop::LoadPreInitModules (LaunchEngineLoop.cpp#L1572). The loading order of the modules is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// LaunchEngineLoop.cpp
FEngineLoop::PreInit()
{
// ...
// L1480
// Load Core modules required for everything else to work (needs to be loaded before InitializeRenderingCVarsCaching)
if (!LoadCoreModules())
{
UE_LOG(LogInit, Error, TEXT("Failed to load Core modules."));
return 1;
}
// ...

// L1572
LoadPreInitModules();
// ...

// L2015
// note: Since 4.20 add ELoadingPhase::PreEarlyLoadingScreen Support.
// Load up all modules that need to hook into the loading screen
if (!IProjectManager::Get().LoadModulesForProject(ELoadingPhase::PreEarlyLoadingScreen) || !IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PreEarlyLoadingScreen))
{
return 1;
}
// ...

// L2202
if ( !LoadStartupCoreModules() )
{
// At least one startup module failed to load, return 1 to indicate an error
return 1;
}
// ...

// L2211
// Load up all modules that need to hook into the loading screen
if (!IProjectManager::Get().LoadModulesForProject(ELoadingPhase::PreLoadingScreen) || !IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PreLoadingScreen))
{
return 1;
}
// ...

// L2310
if ( !LoadStartupModules() )
{
// At least one startup module failed to load, return 1 to indicate an error
return 1;
}
// ...

// L2457
// Load all the post-engine init modules
ensure(IProjectManager::Get().LoadModulesForProject(ELoadingPhase::PostEngineInit));
ensure(IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PostEngineInit));
// ...

// L3044
// Load all the post-engine init modules
if (!IProjectManager::Get().LoadModulesForProject(ELoadingPhase::PostEngineInit) || !IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PostEngineInit))
{
GIsRequestingExit = true;
return 1;
}
// ...

// L4279
// Load "pre-init" plugin modules
if (!ProjectManager.LoadModulesForProject(ELoadingPhase::PostConfigInit) || !PluginManager.LoadModulesForEnabledPlugins(ELoadingPhase::PostConfigInit))
{
return false;
}
// ...
}

The implementation of several functions for loading modules during EnginePreInit: ### FEngineLoop::LoadCoreModules

1
2
3
4
5
6
7
8
9
10
// LaunchEngineLoop.cpp#L2679
bool FEngineLoop::LoadCoreModules()
{
// Always attempt to load CoreUObject. It requires additional pre-init which is called from its module's StartupModule method.
#if WITH_COREUOBJECT
return FModuleManager::Get().LoadModule(TEXT("CoreUObject")) != nullptr;
#else
return true;
#endif
}

FEngineLoop::LoadPreInitModules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// LaunchEngineLoop.cpp#L2690
void FEngineLoop::LoadPreInitModules()
{
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Loading PreInit Modules"), STAT_PreInitModules, STATGROUP_LoadTime);

// GGetMapNameDelegate is initialized here
#if WITH_ENGINE
FModuleManager::Get().LoadModule(TEXT("Engine"));

FModuleManager::Get().LoadModule(TEXT("Renderer"));

FModuleManager::Get().LoadModule(TEXT("AnimGraphRuntime"));

FPlatformApplicationMisc::LoadPreInitModules();

#if !UE_SERVER
if (!IsRunningDedicatedServer() )
{
if (!GUsingNullRHI)
{
// This needs to be loaded before InitializeShaderTypes is called
FModuleManager::Get().LoadModuleChecked<ISlateRHIRendererModule>("SlateRHIRenderer");
}
}
#endif

FModuleManager::Get().LoadModule(TEXT("Landscape"));

// Initialize ShaderCore before loading or compiling any shaders,
// But after Renderer and any other modules which implement shader types.
FModuleManager::Get().LoadModule(TEXT("ShaderCore"));

#if WITH_EDITORONLY_DATA
// Load the texture compressor module before any textures load. They may
// compress asynchronously and that can lead to a race condition.
FModuleManager::Get().LoadModule(TEXT("TextureCompressor"));
#endif

#endif // WITH_ENGINE

#if (WITH_EDITOR && !(UE_BUILD_SHIPPING || UE_BUILD_TEST))
// Load audio editor module before engine class CDOs are loaded
FModuleManager::Get().LoadModule(TEXT("AudioEditor"));
FModuleManager::Get().LoadModule(TEXT("AnimationModifiers"));
#endif
}

FPlatformApplicationMisc::LoadPreInitModules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// FPlatformApplicationMisc::LoadPreInitModules
void FWindowsPlatformApplicationMisc::LoadPreInitModules()
{
// D3D11 is not supported on WinXP, so in this case we use the OpenGL RHI
if(FWindowsPlatformMisc::VerifyWindowsVersion(6, 0))
{
//#todo-rco: Only try on Win10
const bool bForceD3D12 = FParse::Param(FCommandLine::Get(), TEXT("d3d12")) || FParse::Param(FCommandLine::Get(), TEXT("dx12"));
if (bForceD3D12)
{
FModuleManager::Get().LoadModule(TEXT("D3D12RHI"));
}
FModuleManager::Get().LoadModule(TEXT("D3D11RHI"));
}
FModuleManager::Get().LoadModule(TEXT("OpenGLDrv"));
}

FEngineLoop::LoadStartupCoreModules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// LaunchEngineLoop.cpp#L2739
bool FEngineLoop::LoadStartupCoreModules()
{
FScopedSlowTask SlowTask(100);

DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Loading Startup Modules"), STAT_StartupModules, STATGROUP_LoadTime);

bool bSuccess = true;

// Load all Runtime modules
SlowTask.EnterProgressFrame(10);
{
FModuleManager::Get().LoadModule(TEXT("Core"));
FModuleManager::Get().LoadModule(TEXT("Networking"));
}

SlowTask.EnterProgressFrame(10);
FPlatformApplicationMisc::LoadStartupModules();

// initialize messaging
SlowTask.EnterProgressFrame(10);
if (FPlatformProcess::SupportsMultithreading())
{
FModuleManager::LoadModuleChecked<IMessagingModule>("Messaging");
}

// Init Scene Reconstruction support
#if !UE_SERVER
if (!IsRunningDedicatedServer())
{
FModuleManager::LoadModuleChecked<IMRMeshModule>("MRMesh");
}
#endif

SlowTask.EnterProgressFrame(10);
#if WITH_EDITOR
FModuleManager::LoadModuleChecked<IEditorStyleModule>("EditorStyle");
#endif //WITH_EDITOR

// Load UI modules
SlowTask.EnterProgressFrame(10);
if ( !IsRunningDedicatedServer() )
{
FModuleManager::Get().LoadModule("Slate");

#if !UE_BUILD_SHIPPING
// Need to load up the SlateReflector module to initialize the WidgetSnapshotService
FModuleManager::Get().LoadModule("SlateReflector");
#endif // !UE_BUILD_SHIPPING
}

#if WITH_EDITOR
// In dedicated server builds with the editor, we need to load UMG/UMGEditor for compiling blueprints.
// UMG must be loaded for runtime and cooking.
FModuleManager::Get().LoadModule("UMG");
#else
if ( !IsRunningDedicatedServer() )
{
// UMG must be loaded for runtime and cooking.
FModuleManager::Get().LoadModule("UMG");
}
#endif //WITH_EDITOR

// Load all Development modules
SlowTask.EnterProgressFrame(20);
if (!IsRunningDedicatedServer())
{
#if WITH_UNREAL_DEVELOPER_TOOLS
FModuleManager::Get().LoadModule("MessageLog");
FModuleManager::Get().LoadModule("CollisionAnalyzer");
#endif //WITH_UNREAL_DEVELOPER_TOOLS
}

#if WITH_UNREAL_DEVELOPER_TOOLS
FModuleManager::Get().LoadModule("FunctionalTesting");
#endif //WITH_UNREAL_DEVELOPER_TOOLS

SlowTask.EnterProgressFrame(30);
#if (WITH_EDITOR && !(UE_BUILD_SHIPPING || UE_BUILD_TEST))
// HACK: load BT editor as early as possible for statically initialized assets (non cooked BT assets needs it)
// cooking needs this module too
FModuleManager::Get().LoadModule(TEXT("BehaviorTreeEditor"));

// Ability tasks are based on GameplayTasks, so we need to make sure that module is loaded as well
FModuleManager::Get().LoadModule(TEXT("GameplayTasksEditor"));

IAudioEditorModule* AudioEditorModule = &FModuleManager::LoadModuleChecked<IAudioEditorModule>("AudioEditor");
AudioEditorModule->RegisterAssetActions();

// Load the StringTableEditor module to register its asset actions
FModuleManager::Get().LoadModule("StringTableEditor");

if( !IsRunningDedicatedServer() )
{
// VREditor needs to be loaded in non-server editor builds early, so engine content Blueprints can be loaded during DDC generation
FModuleManager::Get().LoadModule(TEXT("VREditor"));
}
// -----------------------------------------------------

// HACK: load EQS editor as early as possible for statically initialized assets (non cooked EQS assets needs it)
// cooking needs this module too
bool bEnvironmentQueryEditor = false;
GConfig->GetBool(TEXT("EnvironmentQueryEd"), TEXT("EnableEnvironmentQueryEd"), bEnvironmentQueryEditor, GEngineIni);
if (bEnvironmentQueryEditor
#if WITH_EDITOR
|| GetDefault<UEditorExperimentalSettings>()->bEQSEditor
#endif // WITH_EDITOR
)
{
FModuleManager::Get().LoadModule(TEXT("EnvironmentQueryEditor"));
}

// We need this for blueprint projects that have online functionality.
//FModuleManager::Get().LoadModule(TEXT("OnlineBlueprintSupport"));

if (IsRunningCommandlet())
{
FModuleManager::Get().LoadModule(TEXT("IntroTutorials"));
FModuleManager::Get().LoadModule(TEXT("Blutility"));
}

#endif //(WITH_EDITOR && !(UE_BUILD_SHIPPING || UE_BUILD_TEST))

#if WITH_ENGINE
// Load runtime client modules (which are also needed at cook-time)
if( !IsRunningDedicatedServer() )
{
FModuleManager::Get().LoadModule(TEXT("Overlay"));
}

FModuleManager::Get().LoadModule(TEXT("MediaAssets"));
#endif

FModuleManager::Get().LoadModule(TEXT("ClothingSystemRuntime"));
#if WITH_EDITOR
FModuleManager::Get().LoadModule(TEXT("ClothingSystemEditor"));
#endif

FModuleManager::Get().LoadModule(TEXT("PacketHandler"));

return bSuccess;
}

FPlatformApplicationMisc::LoadStartupModules

1
2
3
4
5
6
7
8
9
10
11
12
// FPlatformApplicationMisc::LoadStartupModules
void FWindowsPlatformApplicationMisc::LoadStartupModules()
{
#if !UE_SERVER
FModuleManager::Get().LoadModule(TEXT("XAudio2"));
FModuleManager::Get().LoadModule(TEXT("HeadMountedDisplay"));
#endif // !UE_SERVER

#if WITH_EDITOR
FModuleManager::Get().LoadModule(TEXT("SourceCodeAccess"));
#endif //WITH_EDITOR
}

FEngineLoop::LoadStartupModules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// LaunchEngineLoop.cpp#L2884
bool FEngineLoop::LoadStartupModules()
{
FScopedSlowTask SlowTask(3);

SlowTask.EnterProgressFrame(1);
// Load any modules that want to be loaded before default modules are loaded up.
if (!IProjectManager::Get().LoadModulesForProject(ELoadingPhase::PreDefault) || !IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PreDefault))
{
return false;
}

SlowTask.EnterProgressFrame(1);
// Load modules that are configured to load in the default phase
if (!IProjectManager::Get().LoadModulesForProject(ELoadingPhase::Default) || !IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::Default))
{
return false;
}

SlowTask.EnterProgressFrame(1);
// Load any modules that want to be loaded after default modules are loaded up.
if (!IProjectManager::Get().LoadModulesForProject(ELoadingPhase::PostDefault) || !IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PostDefault))
{
return false;
}

return true;
}

From the code above, it’s clear which modules the engine’s PreInit starts and the modules for the started plugins (determined by the plugin’s LoadingPhase).

Load Plugin Modules

In the previously organized code, it can be seen that the project’s plugin module is also loaded in FEngineLoop::PreInit. Those who have written UE plugins know that the UE plugin configuration (.uplugin) has two options: Type and LoadingPhase:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "TaskTools",
"Description": "",
"Category": "Other",
"CreatedBy": "",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"CanContainContent": true,
"IsBetaVersion": false,
"Installed": false,
"Modules": [
{
"Name": "TaskTools",
"Type": "Runtime",
"LoadingPhase": "Default"
}
]
}

The Type and LoadingPhase in the Modules section determine whether the module of the plugin is loaded at engine startup and when it is loaded. Type is a required parameter, while LoadingPhase is optional, defaulting to Default.

  • Type: Determines whether the module is loaded in different runtime environments.
  • LoadingPhase: Determines when the module is loaded.

Plugin:Type (Required)

Sets the type of Module. Valid options are RuntimeRuntimeNoCommandletDeveloperEditorEditorNoCommandlet, and Program. This type determines which types of applications this Plugin’s Module is suitable for loading in. For example, some plugins may include modules that are only designed to be loaded when the editor is running. Runtime modules will be loaded in all cases, even in shipped games. Developer modules will only be loaded in development runtime or editor builds, but never in shipping builds. Editor modules will only be loaded when the editor is starting up. Your Plugin can use a combination of modules of different types.

The selectable options for Type are as follows (read from json in FModuleDescriptor::Read):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace EHostType
{
enum Type
{
// Any target using the UE4 runtime
Runtime,
// Any target except for commandlet
RuntimeNoCommandlet,
// Any target or program
RuntimeAndProgram,
// Loaded only in cooked builds
CookedOnly,
// Loaded only when the engine has support for developer tools enabled
Developer,
// Loaded only by the editor
Editor,
// Loaded only by the editor, except when running commandlets
EditorNoCommandlet,
// Loaded only by programs
Program,
// Loaded only by servers
ServerOnly,
// Loaded only by clients
ClientOnly,
// NOTE: If you add a new value, make sure to update the ToString() method below!

Max
};
// ...
}

Plugin:LoadingPhase (Optional)

.uplugin Module LoadingPhase Descriptors.

If specified, controls when the plugin is loaded at start-up. This is an advanced option that should not normally be required. The valid options are Default (which is used when no LoadingPhase is specified), PreDefault, and PostConfigInitPostConfigInit enables the module to be loaded before the engine has finished starting up key subsystems. PreDefault loads just before the normal phase. Typically, this is only needed if you expect game modules to depend directly on content within your plugin, or types declared within the plugin’s code.

If the LoadingPhase option is not specified in the .uplugin, it defaults to Default. The optional options for LoadingPhase are as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace ELoadingPhase
{
enum Type
{
/** Loaded before the engine is fully initialized, immediately after the config system has been initialized. Necessary only for very low-level hooks */
PostConfigInit,

/** Loaded before the engine is fully initialized for modules that need to hook into the loading screen before it triggers */
PreLoadingScreen,

/** Right before the default phase */
PreDefault,

/** Loaded at the default loading point during startup (during engine init, after game modules are loaded.) */
Default,

/** Right after the default phase */
PostDefault,

/** After the engine has been initialized */
PostEngineInit,

/** Do not automatically load this module */
None,

// NOTE: If you add a new value, make sure to update the ToString() method below!
Max
};
}

FProjectManager::LoadModulesForProject

In FEngineLoop::PreInit, the specified LoadingPhase plugin modules are loaded via the call to IProjectManager::Get().LoadModulesForProject:

1
2
// Runtime\Project\Private\ModuleDescriptor.cpp
bool FProjectManager::LoadModulesForProject(const ELoadingPhase::Type LoadingPhase)

Inside, the module loading is forwarded to FModuleDescriptor::LoadModulesForPhase and error checking is performed on the results (the compilation failures of plugins usually indicate issues at this step):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
bool FProjectManager::LoadModulesForProject(const ELoadingPhase::Type LoadingPhase)
{
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Loading Game Modules"), STAT_GameModule, STATGROUP_LoadTime);

bool bSuccess = true;

if (CurrentProject.IsValid())
{
TMap<FName, EModuleLoadResult> ModuleLoadFailures;
FModuleDescriptor::LoadModulesForPhase(LoadingPhase, CurrentProject->Modules, ModuleLoadFailures);

if (ModuleLoadFailures.Num() > 0)
{
FText FailureMessage;
for (auto FailureIt = ModuleLoadFailures.CreateConstIterator(); FailureIt; ++FailureIt)
{
const EModuleLoadResult FailureReason = FailureIt.Value();

if (FailureReason != EModuleLoadResult::Success)
{
const FText TextModuleName = FText::FromName(FailureIt.Key());

if (FailureReason == EModuleLoadResult::FileNotFound)
{
FailureMessage = FText::Format(LOCTEXT("PrimaryGameModuleNotFound", "The game module '{0}' could not be found. Please ensure that this module exists and that it is compiled."), TextModuleName);
}
else if (FailureReason == EModuleLoadResult::FileIncompatible)
{
FailureMessage = FText::Format(LOCTEXT("PrimaryGameModuleIncompatible", "The game module '{0}' does not appear to be up to date. This may happen after updating the engine. Please recompile this module and try again."), TextModuleName);
}
else if (FailureReason == EModuleLoadResult::FailedToInitialize)
{
FailureMessage = FText::Format(LOCTEXT("PrimaryGameModuleFailedToInitialize", "The game module '{0}' could not be successfully initialized after it was loaded."), TextModuleName);
}
else if (FailureReason == EModuleLoadResult::CouldNotBeLoadedByOS)
{
FailureMessage = FText::Format(LOCTEXT("PrimaryGameModuleCouldntBeLoaded", "The game module '{0}' could not be loaded. There may be an operating system error or the module may not be properly set up."), TextModuleName);
}
else
{
ensure(0); // If this goes off, the error handling code should be updated for the new enum values!
FailureMessage = FText::Format(LOCTEXT("PrimaryGameModuleGenericLoadFailure", "The game module '{0}' failed to load for an unspecified reason. Please report this error."), TextModuleName);
}

// Just report the first error
break;
}
}

FMessageDialog::Open(EAppMsgType::Ok, FailureMessage);
bSuccess = false;
}
}

return bSuccess;
}

FModuleDescriptor::LoadModulesForPhase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Runtime\Project\Private\ModuleDescriptor.cpp
// LoadingPhase - is want load Module LoadingPhase Descriptor
// Modules - is current project all modules(CurrentProject->Modules)
// ModuleLoadErrors - is module-moduleLoadError mapping
void FModuleDescriptor::LoadModulesForPhase(ELoadingPhase::Type LoadingPhase, const TArray<FModuleDescriptor>& Modules, TMap<FName, EModuleLoadResult>& ModuleLoadErrors)
{
FScopedSlowTask SlowTask(Modules.Num());
for (int Idx = 0; Idx < Modules.Num(); Idx++)
{
SlowTask.EnterProgressFrame(1);
const FModuleDescriptor& Descriptor = Modules[Idx];

// Don't need to do anything if this module is already loaded
if (!FModuleManager::Get().IsModuleLoaded(Descriptor.Name))
{
if (LoadingPhase == Descriptor.LoadingPhase && Descriptor.IsLoadedInCurrentConfiguration())
{
// @todo plugin: DLL search problems. Plugins that statically depend on other modules within this plugin may not be found? Need to test this.

// NOTE: Loading this module may cause other modules to become loaded, both in the engine or game, or other modules
// that are part of this project or plugin. That's totally fine.
EModuleLoadResult FailureReason;
IModuleInterface* ModuleInterface = FModuleManager::Get().LoadModuleWithFailureReason(Descriptor.Name, FailureReason);
if (ModuleInterface == nullptr)
{
// The module failed to load. Note this in the ModuleLoadErrors list.
ModuleLoadErrors.Add(Descriptor.Name, FailureReason);
}
}
}
}
}

The implementation of this function is to iterate over all modules of the current project. When the LoadingPhase of the Module matches the passed LoadingPhase parameter and the Type of the Module meets the current runtime environment (judged by FModuleDescriptor::IsLoadedInCurrentConfiguration), the Module is loaded.### FModuleDescriptor::IsLoadedInCurrentConfiguration
FModuleDescriptor::IsLoadedInCurrentConfiguration‘s role is to determine whether the current module’s Type matches the current runtime environment, and it returns a bool to decide whether to load the Module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
bool FModuleDescriptor::IsLoadedInCurrentConfiguration() const
{
// Check that the module is built for this configuration
if(!IsCompiledInCurrentConfiguration())
{
return false;
}

// Check that the runtime environment allows it to be loaded
switch (Type)
{
case EHostType::RuntimeAndProgram:
#if (WITH_ENGINE || WITH_PLUGIN_SUPPORT)
return true;
#endif
break;

case EHostType::Runtime:
#if (WITH_ENGINE || WITH_PLUGIN_SUPPORT) && !IS_PROGRAM
return true;
#endif
break;

case EHostType::RuntimeNoCommandlet:
#if (WITH_ENGINE || WITH_PLUGIN_SUPPORT) && !IS_PROGRAM
if(!IsRunningCommandlet()) return true;
#endif
break;

case EHostType::CookedOnly:
return FPlatformProperties::RequiresCookedData();

case EHostType::Developer:
#if WITH_UNREAL_DEVELOPER_TOOLS
return true;
#endif
break;

case EHostType::Editor:
#if WITH_EDITOR
if(GIsEditor) return true;
#endif
break;

case EHostType::EditorNoCommandlet:
#if WITH_EDITOR
if(GIsEditor && !IsRunningCommandlet()) return true;
#endif
break;

case EHostType::Program:
#if WITH_PLUGIN_SUPPORT && IS_PROGRAM
return true;
#endif
break;

case EHostType::ServerOnly:
return !FPlatformProperties::IsClientOnly();

case EHostType::ClientOnly:
return !IsRunningDedicatedServer();

}

return false;
}

Thus, the engine modules and project plugin modules loaded during the engine startup have been successfully started.

ChangeLog

  • 2019-03-20 16:24:32 Added content for loading modules during engine startup
  • 2019-03-21 12:41:52 Added content for starting plugin modules
The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:Engine Source Analysis: Module Loading and Startup
Author:LIPENGZHA
Publish Date:2019/03/19 11:11
Update Date:2019/03/30 15:38
Word Count:7.8k Words
Link:https://en.imzlp.com/posts/24007/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!