Article: Cross-platform library integration in Unreal Engine 4
A proposed workflow for simplifying the integration of third-party libraries into projects built with Epic Games’ Unreal Engine 4.
- Key issues
- Workflow overview
- Step 1: Conan wrappers for UE4-bundled third-party libraries
- Step 2: Building libraries against UE4 versions of dependencies
- Step 3: Consuming Conan packages in an Unreal project or plugin
- Future directions
The ability to integrate third-party libraries into Unreal Engine 4 projects and plugins allows developers to augment the existing capabilities of the Engine with the functionality of any arbitrary codebase. A number of resources are available online that provide guidance on how to do so (see 123 for examples from the Epic Wiki alone.) However, most of these guides utilise simple manual addition of build rules to a project’s
.Build.cs file (the set of rules that UnrealBuildTool uses to build a UE4 project or plugin.) This simplistic approach quickly becomes verbose when integrating multiple libraries and their dependencies, particularly when supporting versions for multiple operating systems.
In addition to the poor scalability inherent in the approach described by most existing materials, such guides rarely address the more complex issues that can be encountered when integrating third-party libraries. Nuances related to compiler toolchain invocations, linker behaviour, and Application Binary Interfaces (ABIs) can result in obstacles that require significant developer effort to diagnose and address. A manual workflow requiring manual solutions to these issues is entirely insufficient. There is a clear need for a workflow that automates as much of the library integration process as possible. Such a workflow should integrate smoothly with UnrealBuildTool and scale to arbitrary numbers of dependencies, whilst simultaneously abstracting away the solutions to the key issues that developers may face during the integration of third-party libraries.
Symbol interposition is a linker feature that allows executables to handle the existence of multiple instances of a given symbol within a process’s address space. When attempting to resolve a reference to a symbol for which multiple instances are available, a linker’s default behaviour will typically be to select the first instance that it finds. Under macOS and Linux, the
dlsym() function supports the
RTLD_NEXT flags, which provide functionality to control this symbol resolution behaviour. 4 Equivalent functionality is not provided by the Windows API because DLLs must explicitly export public symbols and applications must either explicitly link against the corresponding import libraries or else retrieve symbols programmatically.
Unreal Engine 4 bundles a number of prebuilt third-party libraries in the
Engine/Source/ThirdParty subdirectory of the Engine’s source tree. The prebuilt binaries for the majority of these dependencies are static libraries. As a consequence of symbol interposition, linking against additional third-party libraries which declare symbols with the same name as those that exist within a UE4-bundled library can cause unintended collisions. As an example, consider a scenario in which we are linking against a static build of the OpenCV computer vision library. If OpenCV has been built against a different version of libpng than the version which is bundled with UE4, then we will encounter a symbol collision when attempting to read a PNG image using the OpenCV API. The linker will select the symbols from the UE4 version of libpng instead of those from the OpenCV version, and the library version mismatch will result in an error.
Symbol interposition issues can occur under the following circumstances:
- When linking against shared libraries under macOS or Linux; and
- When linking against static libraries under Windows, macOS or Linux.
The most effective way to prevent symbol interposition issues is to ensure that all third-party libraries are built against the UE4-bundled versions of their own dependencies (where applicable.) Although this does require building each library from source, the use of a package management system and associated repository can restrict the occurrence of such builds to once per combination of operating system and Engine version.
STL and libc++ issues under Linux
The default implementation of the C++ standard library that ships with most Linux distributions is libstdc++, the GNU C++ Library. libstdc++ is licensed under the GNU General Public License, a strong copyleft license. To allow non-GPL licensed software to link against libstdc++, its license includes the GCC Runtime Library Exception, which provides additional clauses stating that any independent code that links against the runtime library is exempt from the terms of the GPL, so long as a set of eligibility criteria are met. However, the extent to which it is permissible to distribute libstdc++ itself (including its accompanying header files) alongside non-GPL software appears to be a matter of contention.
The Unreal Engine does not utilise libstdc++. Linux builds of the Engine and all of its bundled third-party libraries are built against libc++, the implementation of the C++ standard library from the LLVM project. libc++ is dual licensed under the MIT license and the BSD-like UIUC License, which are permissive, non-copyleft licenses. libc++ and its headers are bundled with the Engine in the
ThirdParty directory of the Engine’s source tree. This choice of runtime library is ostensibly due to legal concerns regarding the ability to distribute libstdc++ with the Engine 56.
libc++ is not ABI compatible with libstdc++, except for a small handful of low-level features. As a consequence, any code that has been compiled with libc++ and utilises features of the C++ standard library (such as the container classes from the STL) cannot interoperate with other code that uses these features and has been compiled against libstdc++ (and vice versa.) The practical upshot of this incompatibility is that any third-party libraries which have been built against libstdc++ can only communicate with UE4 C++ code through the use of pure C interfaces. In order to achieve full C++ interoperability, it is necesary to compile third-party libraries against libc++ (preferably the version of libc++ that is bundled with the Engine, for maximum compatibility.) This is effectively an extension of the solution to symbol interposition (building libraries from source against the UE4-bundled versions of their dependencies), whereby we treat libc++ as a dependency of all libraries when building under Linux.
To address the issues described in the sections above, I propose a workflow that automates the integration of third-party libraries with UE4 through the use of the Conan package management system. A high-level overview of the proposed workflow is depicted in Figure 1:
The workflow allows an Unreal Engine 4 project or plugin to seamlessly consume prebuilt third-party libraries, which have in turn been compiled against dependencies bundled in the
Engine/Source/ThirdParty subdirectory of the Engine’s source tree (including libc++ under Linux.) To facilitate this workflow, I have implemented several key components:
- In pull request #4074 (login required) of the Unreal Engine 4 GitHub repository, I modified the source code of UnrealBuildTool to incorporate additional build information (such as include directories and library paths) in the JSON output produced by the
-jsonexportflag. This pull request was subsequently merged and included in the release of Unreal Engine 4.19.
- I have implemented a command-line Python tool called ue4cli that provides an interface for interacting with UE4. In addition to commands for generating project files and building and cleaning projects, there are commands for retrieving the compiler flags required to build against UE4-bundled third-party libraries. Internally, ue4cli queries UnrealBuildTool to retrieve this information and extracts the relevant details from the JSON output.
- In the GitHub repository conan-ue4cli I have implemented a script to automatically generate Conan packages for all UE4-bundled third-party dependencies. These packages simply query ue4cli to retrieve the necessary build flags and then supply them to the build system. In addition to these generated packages and the base package upon which they rely for their common functionality, I have provided a special package for libc++ that abstracts away the details of compiling libraries against the UE4-bundled libc++ under Linux.
- In the GitHub repository ue4-opencv-demo I have provided an example Conan recipe for compiling the OpenCV computer vision library against the UE4-bundled versions of its dependencies, as well as an example UE4 project that consumes the built OpenCV library. The contents of this demo repository are included in the later sections of this article.
The Conan package manager was selected for the implementation of the proposed workflow for three reasons:
- It is cross-platform and supports all of the desktop platforms that are supported by UE4.
- It is agnostic of build system and is designed to be extensible to arbitrary build systems. This is crucial because the ability to produce JSON using the built-in
jsongenerator and then consume that information within UnrealBuildTool is the key to seamless integration with the build process for UE4 projects and plugins.
- It supports both public repositories on Bintray and self-hosted repositories through JFrog Artifactory Community Edition for C/C++, making it suitable for use with both open source and private, in-house projects.
Step 1: Conan wrappers for UE4-bundled third-party libraries
Note: the full code from this section is available in the conan-ue4cli GitHub repository.
The first step to integrating UE4-bundled libraries into any external system is to retrieve the relevant build flags from UnrealBuildTool. To abstract away the details of locating and invoking UnrealBuildTool, I created the ue4cli command-line tool. ue4cli handles the detection of installed Engine versions and automatically locates the relevant batch files and shell scripts that provide access to the internals of the UE4 build system. The command-line interface of ue4cli makes it simple to query the list of UE4-bundled third-party libraries and retrieve the necessary build flags for consumption by any external build system. Additional flags specific to the CMake build system are also supported.
Consuming the output of ue4cli from within Conan is extremely straightforward. The conan-ue4cli GitHub repository contains a script to generate wrapper packages for UE4-bundled libraries. The script performs the following steps:
- Creates a Conan profile named
ue4that will be used when building all UE4-related packages. Under Windows and macOS this profile simply contains the autodetected default values for the system. Under Linux the profile forces the use of clang even if GCC is the default system compiler, since UE4 only supports building with clang under Linux 6.
- Installs the base
ue4libpackage that contains functionality common to all wrapper packages, as well as a the
libcxxpackage for using libc++ under Linux.
- Queries ue4cli to retrieve the list of third-party libraries that are bundled with the installed version of UE4.
- Generates and installs the wrapper package for each UE4-bundled third-party library.
Each generated wrapper package contains only the name of the library. All other information is retrieved dynamically when the package is resolved during the
conan install process of a consuming conanfile. For example, the generated package for zlib looks like this:
package_info() method will be called when Conan consumes the package. The generated code simply invokes the
UE4Lib class from the base
ue4lib package, which in turn queries ue4cli for the relevant information. Under Windows and macOS, this simple mechanism is all that is necessary in order to consume UE4-bundled libraries. Under Linux, however, it is also necessary to build against the UE4-bundled libc++, which requires special treatment.
In theory, all that is necessary in order to build against the UE4-bundled libc++ and its accompanying headers is to specify the correct compiler and linker flags. These flags are listed most clearly in the Linux build script for OpenEXR (login required) from the Engine source tree, and are also hardcoded in ue4cli (the string
$3RDPARTY refers to the
Engine/Source/ThirdParty subdirectory of the Engine’s source tree):
Unfortunately, the order in which the linker flags appear in a compilation command is crucial to their functioning. If the linker flags appear after the input files being compiled or linked, then all references to symbols in
libc++abi.a will be resolved correctly. However, if the linker flags appear before the input files, the link operation will fail due to unresolved symbols. This ordering requirement is problematic when working with build systems such as autotools or CMake that generate compiler commands without providing any control over linker flag ordering. CMake in particular is notorious for placing linker flags before input files in its generated compiler commands.
To prevent meddling by the build system, the
libcxx package does not directly specify the compiler and linker flags for libc++. Instead, it provides two wrapper scripts,
clang++.py, that act as an intermediary between the build system and the real compiler. The build system is pointed to these scripts using the
CXX environment variables, and the scripts then inject the required flags before invoking the real compiler and returning its output to the build system. The relevant code from the
libcxx conanfile.py (full source here) is as follows:
The linker flag
---link is specified so that
clang++.py know when to inject the libc++ linker flags in addition to the necessary compiler flags (injecting the linker flags indiscriminately can cause issues with some of the autotools and CMake compiler detection routines, so it is best to only include them when an actual link operation is taking place.) The relevant code common to
clang++.py that receives this information is shown below (full source here):
The use of these wrapper scripts makes the presence of the UE4-bundled libc++ entirely transparent to any build system that is used to build packages which depend on UE4-bundled libraries. This is particularly important due to the build system-agnostic nature of Conan, since any arbitrary build system could conceivably be used to consume the UE4 wrapper packages. The platform check for Linux which enables this behaviour also allows the
libcxx package to be used harmlessly under Windows and macOS, as it is a no-op on these platforms.
Step 2: Building libraries against UE4 versions of dependencies
Note: the full code from this section is available in the ue4-opencv-demo GitHub repository.
The wrapper packages for UE4-bundled third-party libraries are of little use in isolation - after all, UE4 projects and plugins can already link against the bundled libraries simply by specifying them as dependencies in the relevant
.Build.cs file. The power of the workflow lies in the ability to build external third-party libraries against these UE4-bundled dependencies and then link against the custom-built versions of these external libraries.
Consuming the generated wrapper packages in the Conan recipe for another package is extremely straightforward, and requires minimal boilerplate code. Following on from the example of the OpenCV computer vision library disussed in the section Symbol interposition, we can create a custom build of OpenCV using the UE4-bundled zlib and libpng like so (full source here):
deps_cpp_info ConanFile attribute allows us to access the build flags from our dependency packages. In the case of the CMake build process for OpenCV, we need to pass the relevant information to the FindZLIB and FindPNG modules to ensure the UE4-bundled versions of zlib and libpng are found. In general, it should be sufficient to simply add the relevant include directories to the CMAKE_INCLUDE_PATH variable and the library directories to the CMAKE_LIBRARY_PATH variable to allow all FindXXX modules to see the UE4-bundled libraries. However, the FindPNG module appears to be somewhat finnicky about using these paths in certain versions of CMake, and must be forced to use the correct paths by setting the variables specific to that module.
As can be seen in the
build() method, only two lines of boilerplate code are required to enable the use of libc++ under Linux. The
libcxx package actually sets the
CXX environment variables in its
package_info() method, and ideally there should be no further work required to retain these changes. Unfortunately, if the
CXX environment variables are specified in the Conan profile then these values will override any changes specified by dependency packages. The environment variables could also conceivably be modified by other dependency packages that are loaded after
LibCxx.set_vars() function re-applies the changes from the
libcxx package to ensure they survive any external interference. If you know for certain that neither the Conan profile you are using or any of your recipe’s other dependency packages modify the values of the
CXX environment variables then you can omit the two boilerplate lines.
The one piece of information not contained in the example Conan recipe is the version of UE4 against which the package was built. There are a number of potential candidates for specifying this information:
- The package version string could be modified programmatically by the recipe to append the Engine version. However, communicating the Engine version to the recipe from the command-line becomes unwieldy and requires the use of a communication mechanism such as custom environment variables.
- The Conan settings attribute could be modified to include an additional field. However, this requires modifying the settings.yml file of the Conan installation to specify a list of all possible values, which becomes unwieldy as future versions of UE4 are released.
- The Conan options attribute could be included with a field that specifies the Engine version. However, this requires consumers to specify the Engine version option for each dependency package using the conanfile.txt
[options]section in a verbose
package_name:option = valuesyntax, which quickly becomes verbose when depending on multiple packages.
- The package channel (which forms the final part of the package’s fully-qualified name) could be used to specify the Engine version. This also requires specification when listing dependency packages, but is far less verbose because the modification is to the end of the package names as opposed to requiring an additional options list. The use of the channel also significantly improves the visibility and awareness of the version information to developers consuming the packages because the Engine version becomes part of the package name.
Note that the ue4cli invocation is
ue4 version short, which returns only the major and minor version numbers (e.g. “4.19”) and not the patch number (e.g. “4.19.1”). This is to accommodate the re-use of the built package when the Engine receives a hotfix, as such updates rarely modify the versions of the bundled third-party libraries.
Step 3: Consuming Conan packages in an Unreal project or plugin
Note: the full code from this section is available in the ue4-opencv-demo GitHub repository.
One of the strengths of the Conan package manager is that it is build system-agnostic. New build systems can simply be added as new generators, and the built-in
json generator produces JSON files containing all of the relevant build flags for consumption by build systems for which explicit support does not yet exist. Integrating Conan into UnrealBuildTool involves two simple steps:
- Add a
conanfile.txtin the root directory for our UE4 project or plugin that lists our custom-built dependencies and specifies
jsonin the list of generators.
- Modify our
.Build.csfile to execute the command
conan installas a child process and then parse the JSON output that it produces.
Following on from our OpenCV example in the previous section, the contents of our
conanfile.txt would be as follows:
Consuming the JSON output presents a slight problem due to the manner in which UnrealBuildTool processes
.Build.cs files. Each file is compiled into a .NET assembly and then dynamically loaded and invoked during the build process. As of Unreal Engine 4.19, the only assembly references that are included by default are
System and the UnrealBuildTool assembly itself 7. Fortunately, UnrealBuildTool includes JSON parsing functionality in the
Tools.DotNETCommon namespace. This makes it straightforward to simply leverage the UBT-provided functionality (although this is an implementation detail that could potentially change in future Engine versions.) As such, the
.Build.cs file for our OpenCV example looks like so:
When the project is built, the code from the
.Build.cs file will automatically invoke Conan and pass the generated build flags to UnrealBuildTool. So long as the specified packages are available in a repository that Conan is configured to use, the required libraries will be downloaded and linked against automatically. UnrealBuildTool will also propagate the include paths for the dependency packages when generating project files, so code completion in all IDEs supported by UE4 will also function correctly when including headers from the linked libraries.
Note that no runtime dependency information is specified (via
PublicDelayLoadDLLs or similar properties.) This is because OpenCV was built as a static library, and so introduces no additional runtime dependencies. If you are working with shared libraries, it will be necessary to modify the
ProcessDependencies() method to identify shared libraries and process them accordingly. However, since the Conan-based workflow makes it simple to build and consume static libraries, there is little reason to build shared libraries unless required to do so by the license terms of a dependency (e.g. the “Combined Works” section of the GNU Lesser General Public License.)
The code in the
.Build.cs file completes the information loop from UnrealBuildTool to Conan and back. This provides seamless integration between Conan and the UE4 build process across all platforms, significantly reducing the developer effort required to integrate third-party libraries. With the underlying details abstracted away, developers are free to focus on creating applications and games that combine the functionality of external libraries with the power of Unreal Engine 4.
The proposed workflow is just the first step in enhancing the development pipeline for UE4 projects and plugins that integrate third-party libraries. This workflow serves as the foundation for future work to enable a Continuous Integration (CI) pipeline for projects utilising external libraries. Going forward, there are a number of possibilities to be examined and addressed:
- Potential integration of the functionality from the example
.Build.csfile in the previous section into UnrealBuildTool itself (in particular, to remove the dependency on a UBT implementation detail in order to parse JSON data.) At the time of writing, UnrealBuildTool is in the process of migrating over to .NET core, and there is every possibility that the details (or even presence) of the
Tools.DotNETCommonnamespace will change in future Engine versions.
- Potential development of CI services for UE4 that incorporate conan-ue4cli to facilitate automated builds of UE4-compatible library packages.
- The possibility for a community-maintained repository of UE4-compatible packages for popular libraries.
- Potential considerations for compatibility of the proposed workflow with the distribution mechanisms for UE4 code plugins on the Unreal Marketplace. At present, UE4 code plugins must provide their source code and
.Build.csfile, so theoretically a plugin could already utilise Conan for its dependency management if all developers consuming the plugin were to already have Conan installed and correctly configured. For this to be practical, however, Conan would ideally need to be distributed along with the Engine itself. Given the roadmap plans for integrating the Python interpreter into the Engine, this could certainly be possible.
I look forward to exploring all of these possibilities as well as those that have yet to present themselves, and to seeing what can be created by developers who are empowered by these new tools and workflows.
UnrealBuildTool source code -
DynamicCompilation.cs(login required) ↩