This post is aimed to highlight the differences between shared and static C++ runtimes in regards to Android NDK. The idea here is not new and it has been discussed in many places already, so this is nothing new. What I share here is what was useful for me in terms of Android NDK lingo and eco-system.
To work with C++ means most of the time that one will have to use its STL for various reasons. Below are just a few: - std::vector
for dynamic array - std::string
for string support - std::map
for hash maps - std::regex
for regex support - The list goes on
To use the STL, one would have to depend on a “shared library” [for simplicity, we’ll call it libc++.so
> in the system one is executing the binary in. There are two ways to link this “shared library”, either statically or dynamically.
NOTE: For the rest of our discussion, we will refer to this “shared library” as STL or C++ runtime. While the three terms are not exactly synonymous, for the purposes of this post, they are.
Statically means that the C++ runtime will be bundled with the C++ binary.
Dynamically means that the C++ binary will not include C++ runtime, but will utilize the system’s dynamic linker in order to find the necessary STL methods. Dynamic linking is a deep topic which will not be discussed, but I highly recommend the amazing Linker series of blog posts written by Ian Lance Taylor.
Statically linking the C++ runtime is sufficient for most purposes since it would contain every method or structure the C++ binary would need, but it would bloat the size of the binary in return. This is why dynamic linking was created: a shared runtime will be distributed and agreed upon by both the developer of the binary and the user of the binary, which would slash quite a bit of byte off of the binary size.
The first thing to note is that there’s no difference in content between the static and shared variant of the STL. As mentioned before, a static C++ runtime is bundled with the binary, not linked, while a shared C++ runtime is linked, not bundled. Furthermore, static vs shared STL is a link-time decision.
Quoting from the relevant pages in developer.android.com:
An application should not use more than one C++ runtime. The various STLs are not compatible with one another. As an example, the layout of std::string in libc++ is not the same as it is in gnustl. Code written against one STL will not be able to use objects written against another. This is just one example; the incompatibilities are numerous.
This rule extends beyond your code. All of your dependencies must use the same STL that you have selected. If you depend on a closed source third-party dependency that uses the STL and does not provide a library per STL, you do not have a choice in STL. You must use the same STL as your dependency.
It is possible that you will depend on two mutually incompatible libraries. In this situation, the only solutions are to drop one of the dependencies or ask the maintainer to provide a library built against the other STL.
The issue, as mentioned, is that one cannot use more than one STL per app. This would be perfectly fine if app development was done without the use of third-party dependencies, but this is not the case, especially for Android.
Consider the case of a 3rd-party library that uses GNU’s static runtime and an app that has some native code that uses LLVM’s C++ shared runtime.
So the situation is like this: - App has ANDROID_STL=gnustl_static
- 3rd-party library foo has ANDROID_STL=c++_shared
The above is a very tricky situation although very common and quite possible. We’ll have to break down the outcomes to understand it more.
Two STLs, GNU’s and LLVM’s, will exist in one app. Both STLs have functions and structures that will go through different name mangling processes which would produce unique function names. So for example, std::to_string
could exist with the mangled name of _ZN9to7string6E
in LLVM’s STL and _AX8to2string5D
in GNU’s STL. This is actually good. When calling one std::to_string
from a native function that is expecting GNU’s mangled name, it will get GNU’s version of std::to_string
and vice versa.
The issues occur when both STLs produce the same mangled name, which is very much the case in std::exception
, for example. Or another issue occurs when std::to_string
exists in one STL and does not exist at all in another STL.
Another case is when one 3rd-party library includes GNU’s static runtime and the app has some native code that includes LLVM’s C++ static runtime.
So the situation is like this: - App has ANDROID_STL=gnustl_static
- 3rd-party library foo has ANDROID_STL=c++_static
This situation differs from Case #1 since there is no dynamic linking neither in the app nor in the 3rd-party library foo. This is gonna cause two STLs to exist in the same app space where all global data, static constructors and imported functions to also exit in the app space, but that still should not cause any linkage issues only as long as the two runtimes have zero communication between each other. As mentioned in the official documentation:
In this situation, the STL, including and global data and static constructors, will be present in both libraries. The runtime behavior of this application is undefined, and in practice, crashes are very common. Other possible issues include:
- Memory allocated in one library, and freed in the other, causing memory leakage or heap corruption.
- Exceptions raised in libfoo.so going uncaught in libbar.so, causing your app to crash.
- Buffering of std::cout not working properly.
However, if there is an independent 3rd-party library that does not communicate nor allocate space in another runtime’s memory regions, the 3rd-party library should be a bit bloated but it should work just fine.
To note, Facebook’s Yoga builds two shared libraries natively, libfb.so
and libyoga.so
, both of which are built with, as of the time of writing, c++_static
, which is LLVM’s C++ static runtime variant. This means that they don’t have to worry about the app developer including GNU’s STL or some other STL in the mix. More on this issue here
Most code will no make use of every single element of the STL, so the ones that are not used can, in theory, be taken out of the statically-linked STL. This is actually the case with modern versions of NDK, which I think will be finalized in r18.
It’s important to always stick to the One STL Per-App principle. If the app developer can control the STLs in all their 3rd-party dependencies, its best to stick with c++_shared
as recommended by the folks who maintain NDK since GNU’s STL is on the way to being fully deprecated and because a shared runtime variant would work just as good with less bloat than its static variant. Performance might be affected in bottleneck situations but a smarter guy who made the metrics is a better fit to speak about performance here.
If one is developing a 3rd-party dependency and is not sure which runtime to go with, they can either go with c++_static
and make sure it is not communicating with other runtimes, which is the case for most libraries, or produce multiple versions of the 3rd-party dependency with both LLVM and GNU shared runtime variants and offer it to clients.