Wrapping a third party library using an Unreal Engine Plugin

Introduction

Unreal Engine supports plugins, which contain code and game content. They can also wrap a third party library (a .dll file) and allow the functionality of that library to be called directly from C++ and indirectly using Blueprint objects which are part of the plugin.

In a previous article here we compiled the open source ffmpeg library. Now we will create a plugin which wraps the ffmpeg library in a form where it can be used in other Unreal Engine projects.

The point of this article is not to create an ffmpeg plugin, but to understand what is involved in making a third party plugin and explain how to deal with some of the problems which might arise.

Understanding Import Libraries

When a DLL is compiled from C or C++ source code two files are created, one with the extension ".dll" and one with the extension ".lib". The one with the .lib extension is the import library.

Typically a DLL contains a large number of symbols for all the functions and variables used in the code. Only some of these symbols are intended to be used by programs which load the DLL. These symbols are exported from the DLL either by making them with __declspec(dllexport) in the source code or naming them in a separate .DEF file which is used as part of the compilation process.

The import library contains information about all of the exported symbols from the DLL and is used in two ways:

  • by the linker when a program is compiled and linked to resolve symbols imported from the DLL
  • at runtime to tell the linker which DLL supplies the exported symbol

The .dll and .lib files are a pair and are distributed together in a plugin.

Minor Details

Windows static libraries also have a .lib extension. When using a DLL, symbols are resolved when the program is loaded. When using a static library, symbols are resolved at compile time - the needed parts of the static library are included in the executable file. This has consequences including larger executable files and not being able to share code which resides in a DLL.

Linux does not use import libraries. By default it exports all symbols in a .dll (usually a shared object .so file). This is a simpler approach but introduces its own complexities as thousands more symbols must be resolved at link time, and sometimes they resolve in undesirable ways.

Plugin Directory Structures

Epic has this to say about directory structures:

"The Unreal Engine (UE) source code includes several third-party libraries, which are stored under UnrealEngine/Engine/Source/ThirdParty/.. This is a convention for engine modules, and not required. When developing plugins that use third-party libraries, it is more convenient to include the third-party software within the plugin directory.

If you create a plugin from the UI, using Edit | Plugins | +Add | Third Party Library you end up with a plugin directory structure which looks like this (for clarity I have removed the non-Windows directories):

Plugin Build Files

The directories shown above show how the generated code is split into two parts:

  • the ThirdParty directory which contains files associated with the DLLs which the plugin wraps
  • the GeneratedExample module which contains module startup and shutdown code for the plugin

The most important file is GeneratedExampleLibrary.Build.cs. This contains (only the Windows content is shown):

using System.IO;
using UnrealBuildTool;

public class GeneratedExampleLibrary : ModuleRules
{
	public GeneratedExampleLibrary(ReadOnlyTargetRules Target) : base(Target)
	{
		Type = ModuleType.External;

		if (Target.Platform == UnrealTargetPlatform.Win64)
		{
			// Add the import library
			PublicAdditionalLibraries.Add(
				Path.Combine(ModuleDirectory, "x64", "Release", "ExampleLibrary.lib"));

			// Delay-load the DLL, so we can load it from the right place first
			PublicDelayLoadDLLs.Add("ExampleLibrary.dll");

			// Ensure that the DLL is staged along with the executable
			RuntimeDependencies.Add(
		"$(PluginDir)/Binaries/ThirdParty/GeneratedExampleLibrary/Win64/ExampleLibrary.dll");
   		 }
	}
}

Files we don't need

The GeneratedExampleLibrary.Build.cs contains this line:

 Type = ModuleType.External;

This indicates the ThirdParty module does not contain source which Unreal needs to build, it only contains the DLLs - these are assumed to be built outside of Unreal and do not need to be compiled by Unreal.

But the plugin generator has added ExampleLibrary.cpp and ExampleLibrary.h. These files were used once to generate the ExampleLibrary.dll and ExampleLibrary.lib when the project was created. But now ExampleLibrary.cpp is not used - if you change it nothing happens - ExampleLibrary.dll and ExampleLibrary.lib do not get rebuilt.

ExampleLibrary.h is used in the GeneratedExample module, where it runs a test just to show in the module startup code (in FGeneratedExampleModule::StartupModule()) that the function ExampleLibraryFunction() which is in the ExampleLibrary.dll can be called.

If we want to add our own DLLs to the project, using it as a starting point, we can delete

  • ExampleLibrary.h
  • ExampleLibrary.cpp
  • everything in FGeneratedExampleModule::StartupModule()

Binary files

It is important to understand that the file Binaries/ThirdParty/GeneratedExampleLibrary/Win64/ExampleLibrary.dll was put in that directory by the plugin example creation code in the Unreal Editor when we created the plugin. It is not put there by the build process. Although it is a copy of the file in ThirdParty\GeneratedExampleCode\x64\Release the project as generated does not have a mechanism to automatically copy it. If we delete Binaries/ThirdParty/GeneratedExampleLibrary/Win64/ExampleLibrary.dll and rebuild the project, it does not get regenerated or copied.

These lines in the .Build.cs files suggest the ExampleLibrary.dll will be copied to the Binaries directory - but this is not the case; a runtime dependency added in this way is only added if the file already exists under the Binaries directory:

// Ensure that the DLL is staged along with the executable
RuntimeDependencies.Add(
	 "$(PluginDir)/Binaries/ThirdParty/GeneratedExampleLibrary/Win64/ExampleLibrary.dll");

However you can copy a DLL to the Binaries directory from somewhere else like this:

RuntimeDependencies.Add(
      "$(PluginDir)/Binaries/ThirdParty/GeneratedExampleLibrary/Win64/ExampleLibrary.dll", 
      Path.Combine(ModuleDirectory, "x64", "Release", "ExampleLibrary.dll") );

If you do this the file will be copied when you build in the editor. This will also put the DLL in the Binaries directory when you package the plugin.

If the DLLs you want to distribute in the plugin are installed elsewhere on your system you can reference them in a similar way. These lines show how to add runtime dependencies for all the .dll files in the directory d:\tools\ffmpeg6\installed\bin:

DirectoryInfo d = new DirectoryInfo(@"d:\tools\ffmpeg6\installed\bin"); 

FileInfo[] Files = d.GetFiles("*.dll");
foreach (FileInfo file in Files)
{
	string FileName = Path.GetFileName(file.FullName);  
	RuntimeDependencies.Add(
		"$(PluginDir)/Binaries/ThirdParty/GeneratedExampleLibrary/Win64/" + FileName, 
		file.FullName);
}

When this code is compiled, or the plugin is packaged, all the .dll files in d:\tools\ffmpeg6\installed\bin will be copied to the /Binaries/ThirdParty/GeneratedExampleLibrary/Win64/ directory.

Import Libraries

To add an .lib file to the project it needs to be added to the additional libraries collection which Unreal uses to generate its compile commands. This is done by adding it to the PublicAdditionalLibraries collection in the GeneratedExampleLibrary.Build.cs like this:

DirectoryInfo d = new DirectoryInfo(@"d:\tools\ffmpeg6\installed\bin"); 

FileInfo[] Libs = d.GetFiles("*.lib");
foreach (FileInfo file in Libs)
{
	PublicAdditionalLibraries.Add( file.FullName );
}

The above code makes all the .lib file in d:\tools\ffmpeg6\installed\bin available to the project.

Its important to realise that Unreal does not use the project configuration of Visual Studio. In Visual Studio you can configure project paths in the project properties dialog:

This has no effect! Unreal does not use the paths set here, it uses the paths set in the .Build.cs files to create its compile commands.

Delayed Loading of DLLs

When a normal Windows process tries to load a .dll file it looks in places specified by the PATH environment variable. Unreal projects have their own idea of where a .dll might be, so that look in different places.

If a .dll cannot be found, we see a dialog box like this:

And output like this in the Output window of Visual Studio:

LogWindows: Failed to load 'D:/work/learn/PluginsGenerated/Plugins/GeneratedExample/Binaries/Win64/UnrealEditor-GeneratedExample.dll' (GetLastError=126)
LogWindows:   Missing import: avformat-60.dll
LogWindows:   Looked in: ../../../Engine/Binaries/Win64
LogWindows:   Looked in: D:\work\learn\PluginsGenerated\Binaries\Win64
LogWindows:   Looked in: D:\work\learn\PluginsGenerated\Plugins\GeneratedExample\Binaries\Win64
LogWindows:   Looked in: C:\Program Files\Epic Games\UE_5.1\Engine\Plugins\Editor\ModelingToolsEditorMode\Binaries\Win64
and another ~150 similar lines

Unreal by default looks in the Binaries\Win64 directory of every plugin, including:

  • D:\work\learn\PluginsGenerated\Binaries\Win64
  • D:\work\learn\PluginsGenerated\Plugins\GeneratedExample\Binaries\Win64

but not the Binaries directory of the ThirdParty module of our project

  • D:\work\learn\PluginsGenerated\Plugins\GeneratedExample\Binaries\ThirdParty\GeneratedExampleLibrary\Win64

So we can either:

  • move the files to D:\work\learn\PluginsGenerated\Plugins\GeneratedExample\Binaries\Win64 by changing the .Build.cs to:
FileInfo[] Files = d.GetFiles("*.dll");
foreach (FileInfo file in Files)
{
	string FileName = Path.GetFileName(file.FullName);
	RuntimeDependencies.Add( 
		"$(PluginDir)/Binaries/Win64/" + FileName, 
		file.FullName);
}

FileInfo[] Libs = d.GetFiles("*.lib");
foreach (FileInfo file in Libs)
{
	PublicAdditionalLibraries.Add( file.FullName);
	RuntimeDependencies.Add(
		"$(PluginDir)/Binaries/Win64/" + FileName,
		file.FullName);
}
  • or leave the files where they are and configure them for delayed loading.

To mark a .dll file for delayed load call PublicDelayLoadDLLs.Add(DLLName) like so:

FileInfo[] Files = d.GetFiles("*.dll");
foreach (FileInfo file in Files)
{
	string FileName = Path.GetFileName(file.FullName);
	RuntimeDependencies.Add( 
		"$(PluginDir)/Binaries/ThirdParty/GeneratedExampleLibrary/Win64/" + FileName, 
		file.FullName);
	PublicDelayLoadDLLs.Add(file.Name);
}

Note that the PublicDelayLoadDLLs() call takes just the file name not the path.

Delayed loading means before Unreal attempts to call a method in a DLL it gives us a change to load the library ourselves using a call to FPlatformProcess::GetDllHandle().

If our code calls FPlatformProcess::GetDllHandle() to load a DLL before any method from it is used, we can load the DLL from any location and it will be used.

For example in the module startup of our plugin (in FGeneratedExampleModule::StartupModule()) we could create a list of DLLs we want to load and load each one using a call to ```FPlatformProcess::GetDllHandle()`` like this:

	const FString DLLs[] = {
		"avutil-58.dll",
		"swresample-4.dll",
		"libx264-164.dll",
		"avcodec-60.dll",
		"avformat-60.dll"
	};

	FString Dir = "d:\\tools\\ffmpeg6\\installed\\bin\\";

	for (const FString& DLL : DLLs)
	{
		FString Where = Dir + DLL;
		void* Handle = FPlatformProcess::GetDllHandle(*Where);
		if (!Handle)
		{
			UE_LOG(LogTemp, Error, TEXT("Failed to open %s"), *Where);
		}
	}

	avformat_network_init();

where avformat_network_init() is the ffmpeg call we to load from the DLL.

Delayed Load DLL Ordering

In the above example we have this list of DLLs to load using FPlatformProcess::GetDllHandle().

const FString DLLs[] = {
	"avutil-58.dll",
	"swresample-4.dll",
	"libx264-164.dll",
	"avcodec-60.dll",
	"avformat-60.dll"
};

The function we want to call is in avformat-60.dll, all the others are dependencies that avformat-60.dll needs.

If we just try loading avcodec-60.dll first, we get log messages like this:

LogWindows: Failed to load 'd:\tools\ffmpeg6\installed\bin\avcodec-60.dll' (GetLastError=126)
LogWindows:   Missing import: swresample-4.dll
LogWindows:   Missing import: avutil-58.dll
LogWindows:   Missing import: libx264-164.dll

telling us we need to load those dependencies first.

Building a plugin from the command line

To speed up iteration you can package a plugin from the command line like this:

"C:/Program Files/Epic Games/UE_5.1/Engine/Build/BatchFiles/RunUAT.bat" BuildPlugin -Plugin="D:/work/learn/PluginsGenerated/Plugins/GeneratedExample/GeneratedExample.uplugin" -Package="D:/tmp/packaged/GeneratedExample/GeneratedExample" -CreateSubFolder" -nocompile -nocompileuat

Troubleshooting

If a method is called inside a DLL which is delay loaded and that DLL has not been loaded you get an exception like this:

Looking at the call stack we can see the problem is in the delayLoadHelper2 code.

To see which DLL is causing the problem look at the local variables:

From this we can see the dli->szDLL variable has the value "avformat-60.dll" so this is the DLL which has not been loaded yet.

Other plugin features

Include Paths

If your plugin has include files you can add the directory which contains them to the include path in the .Build.cs like this:

string IncPath = Path.Combine(ModuleDirectory, "include");
PublicSystemIncludePaths.Add(IncPath);

Alternative names for debug files

It is not uncommon to have different file names for release and debug builds, for example to have the character 'd' appended to the debug file names. In the .Build.cs file you can do something like this:

string LibName = "awesomename";
if (Target.Configuration == UnrealTargetConfiguration.Debug
	 && Target.bDebugBuildsActuallyUseDebugCRT)
{
	LibName += "d";
}
PublicAdditionalLibraries.Add(Path.Combine(LibPath, LibName + ".lib"));

Defining variables

In the .Build.cs you can define variables which can the be used inside your C++ code:

PublicDefinitions.Add("WITH_EXTRA_STUFF=1");

References

Epic Integrating third-party libraries into Unreal Engine