I'm writing up notes on the challenges of C++ on Android and how it affects
distributing Swift runtime libraries and Swift binaries on Android.
This came up during the Android workgroup meeting and I was asked to document it
further for discussion.
As of Android 18, the Android distribution no longer includes a full C++ runtime
as part of the OS. Android applications that use C++ are expected to bundle
the C++ runtime as part of the application, either as a shared library
(libc++.so) bundled in the application, or statically linked into the
application binary (libc++.a). For a standalone application, this model is fine.
Everything is coming from a single version of the NDK, which is used to build
the application and any supporting dependencies. We'll call this the
"Application NDK". Different versions of the NDK may ship different versions of
the C++ runtimes, which can break ABI between them, but because the entire
application is only built with a single version of the NDK, it doesn't matter.
Introducing Swift complicates things. The Swift runtime depends on the C++
runtime, and the distributor of the Swift runtime only has control over the
runtime used to compile the Swift runtime. We'll call this the "Swift Runtime
NDK".
If both the "Swift Runtime NDK" and the "Application NDK" are the same version,
then the layouts of structures are the same and nothing will break. Otherwise,
the layouts of structures can change between versions of the runtime.
What is the essence of the ABI break? The C/C++ compiler use headers to describe
the layout of structures, and then generate address-offset loads into the
structure to access the members. To keep things simplified, lets say we're
writing an application that operates on 2D points and we have another library
consuming it, that our application also uses.
In an earlier version of the library, we declare a Point2D with the x first,
and the y second.
struct Point2D {
int x;
int y;
};
Then in a later version, we decided to flip them:
struct Point2D {
int y;
int x;
};
Then in a helper library, we create a function to extract the x value.
The helper implementation access the x directly and the header only exposes
the getX function.
int getX(struct Point2D pt) { return pt.x; }
If we build the helper library against the earlier version, the pt.x will
point at the first member of the Point2D structure. If we compile the
application against the later version of the Point2D library, the x will
actually be in the second position. If we construct
#include "helper.h"
#include "point2d.h"
struct Point2D pt = { 1, 2 };
printf("%d, %d\n", pt.x, getX(pt));
This will print '2, 1'.
Both versions will compile and link. The types are named the same way with the
same members, and the symbol names don't change. The x changes offsets though,
which results in the wrong member being accessed in the code compiled against
the old version of the library.
In this example, the Point2D library is analogous to the C++ runtime, the
library exposing getX(struct Point2D) is the Swift runtime built against an
older version of the C++ runtime, and the application is the application, built
against the headers for the newer version of the C++ runtime library.
In what is one of the more egregious versions of this, changes to the C++
standard resulted in an ABI break in std::string between C++03 and C++11.
Android does not make guarantees about the stability of the C++ runtime, and can
make this kind of change. More often than not though, most changes will likely
result in changes to symbol names which will result in a linker error instead
of quietly misbehaving at runtime.
With the exception of the C++ interop library, the Swift runtimes do not expose
C++ symbols. Dynamic libraries (shared objects) have facilities for hiding
private symbols. We can statically link the C++ runtime into the
libswiftCore.so, effectively hiding the dependency on the C++ runtime and we
can actually build the standard library against any version of C++ that we
choose.
Unlike dynamic libraries, static archives do not have facilities to hide symbols
like this. We can't link and resolve the C++ uses in the libswiftCore.a
archive. While one can technically use the link-flag ordering on POSIX linkers
to influence which C++ runtime symbols from each library are resolved against,
doing so is fraught with peril. Furthermore, lld is not a POSIX linker, so we
cannot use this trick here. I don't know that we can safely offer the Swift
runtime as a static archive that works with mismatched NDK versions.
The libCxxStdlib.a static archive that is part of the C++ interoperability
story does expose C++ symbols from the library. Shipping this library as a
dynamic library would not help us, as memory objects are passing the
library-boundary and can be accessed and manipulated by external C++ code.
This outlines the issues with the mismatched C++ runtimes coming from different
versions of the Android NDK. I've thrown out half-a-solution of only shipping
dynamic version of the Swift standard library, which would then allow us to hide
the C++ runtime dependency. This doesn't help us with the C++ interoperability
libraries though, which expose C++ symbols for consumption. Those libraries need
to build against a matching set of definitions as what is consuming it.