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.
Posted: 12 April 2018
This content is no longer up-to-date.
The original text of this article is preserved here for historical reasons and reflects the state of the Unreal Engine at the time that it was written in April 2018. The current version of this information can be found here and reflects the state of the Unreal Engine today.
Contents
- Rationale
- 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
- References
Rationale
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.
Key issues
Symbol interposition
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_DEFAULT
and 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.
Workflow overview
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
-jsonexport
flag. 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
json
generator 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
ue4
that 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
ue4lib
package that contains functionality common to all wrapper packages, as well as a thelibcxx
package 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:
The 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++.a
and 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
and 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 CC
and 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
and 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
and 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):
The 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 CC
and CXX
environment variables in its package_info()
method, and ideally there should be no further work required to retain these changes. Unfortunately, if the CC
or 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
. The 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 CC
and 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 verbosepackage_name:option = value
syntax, 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.
Storing the Engine version string in the package channel offers the most advantages. The build.py script from the ue4-opencv-demo repository does exactly this:
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.txt
in the root directory for our UE4 project or plugin that lists our custom-built dependencies and specifiesjson
in the list of generators. - Modify our
.Build.cs
file to execute the commandconan install
as 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.
Future directions
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.cs
file 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 theTools.DotNETCommon
namespace 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.cs
file, 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.