Integrating third-party libraries as Unreal Engine plugins: ABI compatibility and Linux toolchain


The Unreal Build Tool (UBT) official documentation explains how to integrate a third-party library into Unreal Engine projects in a very broad way without focusing on the real problems that are (very) likely to occur while integrating the library. Every Operating System (OS) has its own peculiarities when it comes to integrating third-party software, but apart from the OS, there are only 2 possible scenarios when it comes to the integration of a third-party library in a C++ software:

  1. The third-party library comes as a pre-built binary.
  2. The third-party library source code is available and we have to compile it.

There’s also a third scenario, that’s when the library is header-only, but the integration is very easy and there are no packaging problems (more or less…).

The focus of this article will be - in fact - on the packaging problems that are very likely to occur when integrating a third-party library into a C++ project and how - when possible! - to solve them. There will be a second article that we’ll focus on the source code available scenario.

Unreal Engine comes into play because it is a C++ cross-platform framework that adds some additional layers of complexity:

  • It uses a non-standard build tool (UBT)
  • Depending on the OS, the compilation toolchain to use is different (sometimes is fixed and Unreal provides it, some other times it uses the OS toolchain)
  • On Linux, it uses libc++ instead of libstdc++ and this can cause lots of trouble when working with third-party libraries.

Thus, we can think about Unreal as the environment we use for simulating the scenarios presented above in a complex (and thus realistic) setting.

As previously introduced, every OS has its peculiarities when it comes to the integration of a third-party library - this article will focus on Linux.

The pre-built binary scenario

When a third-party library comes as a pre-built binary, we need to carefully look at all the details available regarding the compilation toolchain used. It’s of extreme importance to know the compiler used, the C++ standard, the standard library used and, of course, the target architecture. In fact, there’s no guarantee that binaries generated with a different toolchain are ABI compatible with the binaries produced by our toolchain even if the architecture matches.

To correctly simulate this scenario, we are going to create a library with different toolchains and see what happens when we link them to an Unreal Engine project.

Creating the library

The library just contains this single function:

// lib.cpp
#include <string>

std::string swap_concat(std::string &a, std::string &b) {
  std::string tmp{a};
  a = b;
  b = tmp;
  return a + b;
}

The function takes 2 strings, swaps them, creates a new string that’s the concatenation of the swapped strings and returns it. It’s not a case that this function uses std::string since, as we’ll see, strings are well-known for not being ABI compatible between libc++ and libstdc++.

Using clang we can change the C++ standard and the standard library used (that must be installed on the system) with ease. This simple bash script can generate the same shared library with the all the possible combinations of C++ standard (among 11, 14, 17) and standard library (among libc++ and libstdc++):

mkdir outs
for lib in libc++ libstdc++; do
    for std in 11 14 17; do
        clang++ -shared -std=c++$std -stdlib=$lib lib.cpp -o outs/$lib-clang++$std.so
    done
done

The clang version used is 13.0.1, the target is x86_64-pc-linux-gnu. Once executed the script, we end up with these 6 libraries:

libc++-clang++11.so  libc++-clang++14.so  libc++-clang++17.so  libstdc++-clang++11.so  libstdc++-clang++14.so  libstdc++-clang++17.so

Moreover, since we are creating a pre-built library we need to provide our users the header to use together with our shared objects.

// lib.h
#include <string>
std::string swap_concat(std::string &a, std::string &b);

Alright, now that we have our pre-built library we can start the integration with Unreal Engine.

Creating an Unreal Plugin

Mixing the information from the third-party library integrations and the tutorial on the plugin creation we can quite easily create a plugin for our library. The correct way for integrating a third-party library in an Unreal Engine project is by creating a plugin that wraps the library functionality and completely hides the library.

The reason is pretty easy: Unreal Engine is a cross-platform framework and for being such it adds an abstraction layer over every data structure whose behavior may change from platform to platform. For instance, instead of directly using std::string the recommendation is to use FString. Almost every data structure available in the standard library has its Unreal counterpart.

Wrapping the third-party library behind a plugin is a clever idea because:

  1. The plugin’s public interface only uses Unreal types and, thus, has a perfect integration with the Unreal environment.
  2. If the library comes as a pre-built library, it’s possible to configure the UBT - through the .Build.cs file - to include the correct third-party headers, and to link to the correct third-party library - depending on the target OS.
  3. Correctly organizing the third-party library in the ThirdParty folder conceptually helps the developer in separating the interfaces (the plugin interface vs the third-party interface) highlighting the strong separation between the two parts. This separation is so strong, that there are 2 different .Build.cs files

    • The build file of the external (Type = ModuleType.External) module. The build file configures the build system for exposing to its only user (the plugin) only certain headers, together with the libraries to use at linking & run time.
    • The build file of the plugin. This build file configures the build system for using the public headers of the external module only in its Private parts. At the same time, it exposes in its Public folder the plugin’s interface. That interface will only contain Unreal data types.

After this preamble that explains the reason why we are creating a plugin, let’s go straight to the code.

The Structure

The structure is enforced by the idea of having an external module for the third-party library and a module for the plugin.

Swap
├── Source
│   ├── Swap
│   │   ├── Private
│   │   │   └── Swap.cpp
│   │   ├── Public
│   │   │   └── Swap.h
│   │   └── Swap.Build.cs
│   └── ThirdParty
│       └── SwapLibrary
│           ├── include
│           │   └── lib.h
│           ├── linux
│           │   └── x64
│           │       ├── libc++-clang++11.so
│           │       ├── libc++-clang++14.so
│           │       ├── libc++-clang++17.so
│           │       ├── libstdc++-clang++11.so
│           │       ├── libstdc++-clang++14.so
│           │       └── libstdc++-clang++17.so
│           └── SwapLibrary.Build.cs
└── Swap.uplugin

SwapLibrary is our External module where we’ll place the pre-built libraries (all of them, for testing purposes), Swap instead is the plugin name. Let’s start by focusing on the latter.

The Plugin’s public headers

The idea is to expose the very same function offered by our library using only Unreal types.

IMPORTANT: do not expose the library headers. Hence, do NOT place an inclusion to the third-party library headers in any header present in the Public folder - otherwise, as a side effect, you’ll expose the library headers to the plugin’s users - and this is wrong!

Our Swap.h file looks like this:

#pragma once

#include "Containers/UnrealString.h"
#include "HAL/Platform.h"
#include "Modules/ModuleManager.h"

class SWAP_API FSwap : public IModuleInterface {
  public:
  // There's no need to override IModuleInterface
  // StartupModule & ShutdownModule methods.

  FString SwapConcat(FString &a, FString &b);
};

This is a public header that only contains Unreal types, and therefore it can be perfectly integrated into any Unreal project. The SwapConcat method is the Unreal wrapper of our third-party library swap_concat.

Focus: What is this $MODULE_API?

As already mentioned, Unreal Engine is a cross-platform framework that wraps every part of the code that can vary depending on the target OS.

The $MODULE_API (in our case SWAP_API) is a token that will be parsed by the Unreal Build Tool and mapped to the engine symbol (macro) DLLEXPORT. It’s possible to find this mapping in the Intermediate folder (generated by the UHT):

// Intermediate/[...]/Swap/Definitions.Swap.h
// [...]
#define UE_MODULE_NAME "Swap"
#define UE_PLUGIN_NAME "Swap"
#define SWAP_API DLLEXPORT
// [...]

The DLLEXPORT symbol is defined inside the engine in the correct header for the compilation target platform. In our case, we can find these lines

#define DLLEXPORT __attribute__((visibility("default")))

in the Engine/Source/Runtime/Core/Public/Unix/UnixPlatform.h header, while if we were on Windows, the definition would be

#define DLLEXPORT __declspec(dllexport)

in the Engine/Source/Runtime/Core/Public/Windows/WindowsPlatform.h header. The precompiler, thanks to the system-specific definitions, will include the correct header and thus use the correct definition for DLLEXPORT.

Unreal offers a HAL (Hardware Abstraction Layer) module that - as the name suggests - allows the users to don’t worry about these cross-platform nuances and write the code in a platform-agnostic way. The inclusion of HAL/Platform.h is required for being sure that the symbol DLLEXPORT is defined and, as a consequence, the correct platform-specific code will be used.

Now that we know that a new question naturally follows: what does the DLLEXPORT macro (thus the corresponding OS-specific implementation) do?

The UBT default behavior is to compile all the modules as libraries with all the symbols hidden (e.g. It’s like using gcc for creating a shared object, and passing the -fvisibility=hidden flag), which means that we need to manually choose which symbols to expose in our module.

The DLLEXPORT, thus, instructs the compiler to export the symbol that follows. In our case the FSwap class and its methods. This is an important thing to remember when creating modules in Unreal, otherwise, we end up with linking errors.

After this focus on the $module_API symbols, we can go back to our plugin implementation.

The plugin implementation

The implementation is the only part that can use the third-party library directly, thus, including the third-party library headers. The header inclusion, anyway, comes not for free. In fact, when including a third-party library we may find symbol clashes between Unreal and the library itself.

For this reason, Unreal offers some utilities that kinda restore the environment to the default state, so we can include the library freely.

#include "Swap.h"

#include "HAL/Platform.h"

#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
#include "Windows/WindowsHWrapper.h"
#endif

THIRD_PARTY_INCLUDES_START
#include "lib.h"
THIRD_PARTY_INCLUDES_END

#if PLATFORM_WINDOWS
#include "Windows/HideWindowsPlatformTypes.h"
#endif

#include "Modules/ModuleManager.h"

#include <string>

FString FSwap::SwapConcat(FString &a, FString &b) {
  std::string _a(TCHAR_TO_UTF8(*a));
  std::string _b(TCHAR_TO_UTF8(*b));

  std::string _ret = swap_concat(_a, _b);

  a = FString(UTF8_TO_TCHAR(_a.c_str()));
  b = FString(UTF8_TO_TCHAR(_b.c_str()));
  return a = FString(UTF8_TO_TCHAR(_ret.c_str()));
}

IMPLEMENT_MODULE(FSwap, Swap)

The various PLATFORM_* macros are all defined in the HAL/Platform.h header - these macros allow us to check the target platform and act accordingly. In particular, on Windows, there are clashes between system types and Unreal types. For this reason that boilerplate code that first enables and then disables the platform types is always required.

The THIRD_PARTY_INCLUDES_* macros, instead are enabling/disabling Unreal-specific compiler settings (things that Unreal treats as errors that can be warnings and vice versa). We can safely add the inclusion of the third-party library header only in this setting.

Then we can focus on the usage of the library: the FSwap::SwapConcat uses the swap_concat function and handles all the conversions required.

So far so good. We have created a plugin that correctly wraps the third-party library and only exposes - through a plugin - the library functionality using Unreal types.

The last part of the third-party integration is the effective third-party integration. So far we only worked using the header, but the pre-built library hasn’t come into play yet.

The build files

As mentioned at the beginning of this article, I decided to use Unreal Engine because it easily made us face problems that may come into play when creating complex cross-platform software. One of the additional difficulties is the usage of a custom build tool. Honestly speaking, the UBT is a pretty good build tool that makes it really easy to separate the responsibilities of the various modules, their dependencies, and the support for the cross-platform is straightforward.

For the implementation of our plugin we need to write 2 .Build.cs files:

  • SwapLibrary.Build.cs that will define our External module, and
  • Swap.Build.cs that’s the build file for the plugin, that will depend on the SwapLibrary module.
// SwapLibrary.Build.cs
using System.IO;
using UnrealBuildTool;

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

    PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "include"));

    if (Target.Platform == UnrealTargetPlatform.Linux) {
      string libname = "libstdc++-clang++17.so";
      string path = Path.Combine(ModuleDirectory, "linux", "x64", libname);
      PublicAdditionalLibraries.Add(path);
      RuntimeDependencies.Add(path);
    }
  }
}

In the SwapLibrary.Build.cs file needs to follow our defined structure. Thus, we need to expose the library headers (PublicIncludePaths) so we can include lib.h from every module that will depend on SwapLibrary (hence only the Swap plugin), and depending on the target platform add the public dependency to our pre-built library.

I separated the libname variable from the path so we can easily switch library to see what happens when using the same library compiled in different settings.

// Swap.Build.cs
using UnrealBuildTool;

public class Swap : ModuleRules {
  public Swap(ReadOnlyTargetRules Target) : base(Target) {
    PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;

    PublicDependencyModuleNames.AddRange(new string[] {
      "Core",
    });

    PrivateDependencyModuleNames.AddRange(new string[] {
      "SwapLibrary",
    });
  }
}

The Swap.Build.cs file is straightforward. The UBT automatically adds the Private and Public folders to the (Public|Private)IncludePaths hence the UBT is already aware of what we want to expose publicly and what not. The only thing to add is the private dependency on our SwapLibrary so that those who use the plugin only indirectly depends on the third-party library. In this way the third-party library that won’t be publicly exposed to the user.

Alright, we have defined the content of our plugin. We can add it to every Unreal project (add it to the .uproject file, to the .Build.cs file of your module and to the .Target.cs file too) and see what happens when we compile and link it.

libstdc++, libc++, and Unreal Engine

In the SwapLibrary.Build.cs we decided to use the library compiled with libstdc++ and C++17.

string libname = "libstdc++-clang++17.so";

and that’s what happen when we compile the Swap module and link it with the SwapLibrary:

Link (lld) libUnrealEditor-Swap-Linux-DebugGame.so
ld.lld: error: undefined symbol:
        swap_concat(
            std::__1::basic_string<char, std::__1::char_traits<char>,
            std::__1::allocator<char> >&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >&)
>>> referenced by Swap.cpp:26 (Private/Swap.cpp:26)
>>>               Swap.cpp.o:(FSwap::SwapConcat(FString&, FString&))
clang++: error: linker command failed with exit code 1 (use -v to see invocation)

Why the linker (lld) can’t find the symbol swap_concat? As I introduced earlier there’s a well-known compatibility issue between std::string coming from libc++ and std::string coming from libstdc++.

Although the API is the very same (std::string) the linker sees a different symbol at linking time. In fact, looking carefully the linker is complaining about std::__1::basic_string and not about std::string or std::basic_string.

So what’s that ::__1? It’s an inline namespace used to achieve ABI versioning. This is a clever design choice that allows finding, at linking time, problems that will otherwise cause runtime errors. In particular, the data structure of std::string from libc++ completely differs from the data structure used in libcstd++ and thus they cannot be used interchangeably. The inline namespace mechanism made it possible to let the linker identify the ABI compatibility issues and prevent catastrophic crashes at runtime. Bet let’s understand why in our example the function that has been searched has this signature:

swap_concat(
    std::<b>__1</b>::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >&,
    std::<b>__1::</b>basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >&)

Using nm we can list the symbols contained in object files. Thus, using nm together with grep and c++filt (for demangling) we can search for the swap_concat symbol in libstdc++-clang++17.so and read the signature in a human-readable way.

nm -D libstdc++-clang++11.so |grep swap_concat |c++filt

swap_concat(
    std::<b>__cxx11</b>::basic_string<char, std::char_traits<char>, std::allocator<char> >&,
    std::<b>__cxx11</b>::basic_string<char, std::char_traits<char>, std::allocator<char> >&)

The linker is looking for std::__1::basic_string while the library contains std::__cxx11::basic_string - a completely different symbol. The reason should be now pretty obvious: Unreal Engine uses libc++ and, thus, when we are using std::string in the FSwap::SwapConcat implementation, we are using a std::<b>__1</b>::basic_string and passing it to the swap_concat function. At compile time we have 0 problems because the library header is compatible (API compatibility) but thanks to the inline namespace mechanism the linker prevents us from linking completely different objects (ABI incompatible).

Thus, getting back to the main topic of this article. What can we do if the library is pre-built and we don’t have access to the source code?

Nothing

Precisely. There’s nothing we can do. The only failsafe option is asking the third-party library provider to re-compile the source code using a compatible toolchain. For being 100% sure that the third-party library is compatible with our toolchain, we should ask to use:

  • The same compiler, at the same version.
  • The same standard library.
  • The same C++ standard.

Only in this way we are sure that we can compile, link and execute without any problem.

Moving on, we have compiled the library using a mix of C++ standard and standard libraries. We now know that every library compiled with libstdc++ is somehow harmful to use when using Unreal and the linking phase will fail if we use functions with parameters whose signature is - at ABI level - different.

To verify, we can just replace the libname value in SwapLibrary.Build.cs to one of the above libraries compiled with libc++. All of them will work.

The project structure

We can now define a simple project that uses the plugin:

ThirdPartyProject/
├── Plugins
│   └── Swap
├── Source
│   ├── ThirdPartyProject
│   │   ├── MyActor.cpp
│   │   ├── MyActor.h
│   │   ├── ThirdPartyProject.Build.cs
│   │   ├── ThirdPartyProject.cpp
│   │   ├── ThirdPartyProjectGameModeBase.h
│   │   └── ThirdPartyProject.h
│   ├── ThirdPartyProjectEditor.Target.cs
│   └── ThirdPartyProject.Target.cs
└── ThirdPartyProject.uproject

The project structure is the Unreal’s default. We’ll focus only on the plugin usage in this part of the article. We can define an Actor (MyActor) and use the plugin when it gets spawned.

note: You can have a look at the complete project here: galeone/ue-plugin-tutorial.

#include "MyActor.h"

#include "Engine/Engine.h"

#include "Swap.h"

// Sets default values
AMyActor::AMyActor() { PrimaryActorTick.bCanEverTick = false; }

// Called when the game starts or when spawned
void AMyActor::BeginPlay() {
  Super::BeginPlay();

  FSwap *plugin = FModuleManager::GetModulePtr<FSwap>("Swap");

  FString a(TEXT("man"));
  FString b(TEXT("bat"));
  const FString Message = plugin->SwapConcat(a, b);

  uint64 Key{1};
  float TimeToDisplay{10};
  FColor DisplayColor{FColor::White};

  GEngine->AddOnScreenDebugMessage(Key, TimeToDisplay, DisplayColor, Message);
}

The usage only involves Unreal types - and when we spawn an actor in the scene, here’s what happens:

batman

It works!

A simplified version

The problem of integrating a third-party library as a plugin inside Unreal Engine presented in this article is a very very simplified version of what’s likely to occur in a more realistic scenario.

This pre-built library that uses libstdc++ instead of libc++ and exposes in the API a symbol whose implementation differs among the two standard libraries allowed us to understand one of the problems that may occur when integrating a third-party library.

However, things can become way more complex than this especially if the third-party library is more complicated than this, and depends upon other libraries that the engine itself provides.

What could happen if the third-party library has among its dependencies zlib or libpng?. zlib and libpng are libraries that Unreal Engine provides and, thus, uses during the linking phase of the various modules - especially when packaging a project.

We’ll try to answer this question in the next article where we cover the source code available scenario. In that article, we’ll see what happens if we use the system toolchain when compiling the library and what happens when we use the Unreal toolchain also for compiling the third-party library.

Bonus

The whole article talked about std::string, but the problem is totally general. There’s no standard ABI in C++ and, thus, there’s no ABI compatibility for every structure in libc++ and libstdc++.

Let’s take this final example:

The library header.

//lib.h
#include <memory>

class B {
};

class A {
    std::unique_ptr<B> _p;
    public:
    A(std::unique_ptr<B>&&);
};

The library source code.

//lib.cpp
#include "lib.h"

A::A(std::unique_ptr<B>&& p) : _p(std::move(p)) {}

The library compilation (libc++):

clang -stdlib=libc++ lib.cpp -shared -o liblib.so

The main program

//main.cpp

#include "lib.h"
#include <memory>

int main() {
    std::unique_ptr<B> p = std::make_unique<B>();

    A x(std::move(p));
    return 0;
}

The working compilation (compile with libc++) and the execution

clang++ -stdlib=libc++ main.cpp -o main -L. -llib
LD_LIBRARY_PATH=. ./main

The failing linking (compile with libstdc++):

clang++ main.cpp -o main -L. -llib
/usr/bin/ld: /tmp/main-857110.o: in function `main':
main.cpp:(.text+0x3a): undefined reference to `A::A(std::unique_ptr<B, std::default_delete<B> >&&)'
clang-13: error: linker command failed with exit code 1 (use -v to see invocation)

I hope this last example - with zero Unreal Engine - helps understanding the ABI compatibility problem.

Don't you want to miss the next article? Do you want to be kept updated?
Subscribe to the newsletter!

Related Posts

Code Coverage of Unreal Engine projects

Code coverage is a widely used metric that measures the percentage of lines of code covered by automated tests. Unreal Engine doesn't come with out-of-the-box support for computing this metric, although it provides a quite good testing suite. In this article, we dive into the Unreal Build Tool (UBT) - particularly in the Linux Tool Chain - to understand what has to be modified to add the support, UBT-side, for the code coverage. Moreover, we'll show how to correctly use the lcov tool for generating the code coverage report.

Wrap up of Advent of Code 2021 in pure TensorFlow

A wrap up of my solutions to the Advent of Code 2021 puzzles in pure TensorFlow

Advent of Code 2021 in pure TensorFlow - day 12

Day 12 problem projects us the world of graphs. TensorFlow can be used to work on graphs pretty easily since a graph can be represented as an adjacency matrix, and thus, we can have a tf.Tensor containing our graph. However, the "natural" way of exploring a graph is using recursion, and as we'll see in this article, this prevents us to solve the problem using a pure TensorFlow program, but we have to work only in eager mode.

Advent of Code 2021 in pure TensorFlow - day 11

The Day 11 problem has lots in common with Day 9. In fact, will re-use some computer vision concepts like the pixel neighborhood, and we'll be able to solve both parts in pure TensorFlow by using only a tf.queue as a support data structure.

Advent of Code 2021 in pure TensorFlow - day 10

The day 10 challenge projects us in the world of syntax checkers and autocomplete tools. In this article, we'll see how TensorFlow can be used as a generic programming language for implementing a toy syntax checker and autocomplete.

Advent of Code 2021 in pure TensorFlow - day 9

The day 9 challenge can be seen as a computer vision problem. TensorFlow contains some computer vision utilities that we'll use - like the image gradient - but it's not a complete framework for computer vision (like OpenCV). Anyway, the framework offers primitive data types like tf.TensorArray and tf.queue that we can use for implementing a flood-fill algorithm in pure TensorFlow and solve the problem.

Advent of Code 2021 in pure TensorFlow - day 8

The day 8 challenge is, so far, the most boring challenge faced 😅. Designing a TensorFlow program - hence reasoning in graph mode - would have been too complicated since the solution requires lots of conditional branches. A known AutoGraph limitation forbids variables to be defined in only one branch of a TensorFlow conditional if the variable is used afterward. That's why the solution is in pure TensorFlow eager.

Advent of Code 2021 in pure TensorFlow - day 7

The day 7 challenge is easily solvable with the help of the TensorFlow ragged tensors. In this article, we'll solve the puzzle while learning what ragged tensors are and how to use them.

Advent of Code 2021 in pure TensorFlow - day 6

The day 6 challenge has been the first one that obliged me to completely redesign for part 2 the solution I developed for part 1. For this reason, in this article, we'll see two different approaches to the problem. The former will be computationally inefficient but will completely model the problem, hence it will be easy to understand. The latter, instead, will be completely different and it will focus on the puzzle goal instead of the complete modeling.

Advent of Code 2021 in pure TensorFlow - day 5

The day 5 challenge is easily solvable in pure TensorFlow thanks to its support for various distance functions and the power of the tf.math package. The problem only requires some basic math knowledge to be completely solved - and a little bit of computer vision experience doesn't hurt.