KF5 Static Builds
Static linking has long gone out of fashion, at least on the average Linux desktop distribution. There are however good reasons to still (or again) support this for our frameworks. One is the rise of application bundles (Flatpak, Android APK, AppImage, etc).
Bundles often only contain a single executable, so there is no benefit of sharing a library (at least in most bundle formats, Flatpak is a bit different there). Still we need to ship everything the shared libraries provide, no matter if we need it or not.
Static linking is of course not the magic solution to this, but it’s a fairly generic way of allowing the compiler to drop unused code, reducing the application size. As application bundles are usually updated as a whole, we also don’t benefit from the ability to update shared libraries independently, unlike with a conventional distribution.
Besides application bundles, there are also single process embedded applications that can benefit from static linking, so this is relevant for the effort of bringing KF5 to Yocto. In particular on lower powered embedded devices the startup overhead of dynamic linking can be noticeable.
Build System
In order to make our frameworks usable as static libraries, there’s essentially two areas that might need a few adjustments, build system and code.
On the build system side there’s two things to look at. The first one is to not force libraries
to be built as shared libraries and instead allow the user to select this. That is, don’t use
the SHARED
keyword in the add_library
call. Normally CMake would default to static libraries
when doing that, but ECM’s KDECMakeSettings
changes that for us. To actually build static libraries, you need to set the BUILD_SHARED_LIBS
CMake option
to OFF
(example).
The other aspect that needs attention on the CMake side is how private library dependencies are handled. For shared libraries the consumer doesn’t need to know anything about those as this is encoded in the shared library file. A static library however is just a simple archive of object files, without such meta data, so public and private dependencies are conveyed in the CMake config file to the consumer. This however means that the consumer needs to also look for all private dependencies in order to link against those. That’s done by also listing those in the CMake config file for the static library, next to the public dependencies already listed there (example).
Static Initialization
One rather subtle but far reaching difference to dynamic libraries is how static initialization works. That is, code that is implicitly executed when loading the library (even before the application code is run). Static initialization is used in a number of places:
- Qt’s resource system
- ECM’s translation catalog loader
- Q_COREAPP_STARTUP_FUNCTION
- statically defined instances of a custom
struct
orclass
, triggering their constructor calls (which is how the above mechanisms are ultimately implemented)
With dynamic libraries this works on all platforms, with static libraries it doesn’t work in many cases and thus cannot be relied upon anymore. So, we need to change code affected by this.
This usually implies moving code that would run as part of static initialization to a later point in time, e.g. on first use of whatever is initialized. This can be beneficial for startup performance, but we have to be careful to not accidentally move potentially expensive operations on hot paths at runtime instead then (basic example, more exotic example).
Another potential place for such initialization code would be single entry points in to the library,
such as QCoreApplication
is for QtCore. The last resort approach is an explicit init()
function
as discussed here. That however changes API from a user
point-of-view, so I’d avoid that where possible.
Identifying all affected code is not always straightforward. Broad unit test coverage provides great
value there, but ultimately you probably want to look at all method calls in the .init_array
section
of the dynamic library (or the corresponding non-ELF counter-part on other platforms), e.g. using a
tool like ELF Dissector. Not everything in there
is automatically a problem, but all problems will be in there.
Further Challenges
Another thing that doesn’t make much sense in a statically linked setup is usage of dlopen
(or its
counter-parts on other platforms), most commonly used by plug-in systems. Qt has a solution for statically
linking plug-ins as part of QPluginLoader
. That can be a bit of work to use
in practice as all plugins need to be consumable as static libraries by the application too, and need manual
Q_IMPORT_PLUGIN
statements, but at least it’s nothing that requires creative solutions.
Static linking of course is not the complete solution to being able to create single application bundles, frameworks relying on multi-process architectures, daemons, IPC, etc need to be addressed independently of that still.
One problem we don’t have for KDE applications at least are license issues caused by static linking, that’s left to proprietary users ;)