$darkmode
Elektra 0.11.0
Symbol Versioning

To enable backwards compatibility, while allowing the API to evolve, we use symbol versioning. This document focuses on the setup Elektra uses. For an explanation of symbol versioning itself, you may start here.

Infrastructure

The setup used is based on the one used by FreeBSD. The main script `version_gen.awk` is taken from their source (with small modifications to enable Alpine Linux compatibility).

The main script, is called by CMake during the build process. It parses the symbols.map files of all libraries (e.g. `/src/libs/elektra/symbols.map`) and combines them into a single file, which is then used by the compiler and the linker.

<tt>versions.def</tt>

All versions of Elektra symbols have to be declared in `versions.def`. Only the versions declared in this file can be used in the symbols.map files. No symbols should be declared in this file.

Each version corresponds to a release. After each release of Elektra, a new version must be added to versions.def. Only this version may be modified in the symbols.map files. Released version MUST NOT change. The newest and therefore unreleased version may change at any point before its release.

The libelektraprivate_1.0 is special. It MUST always declare the newest version (e.g. libelektra_0.9) as its base and it is the only private version (i.e. there is will be no libelektraprivate_1.1). The purpose of the private version is to allow the use of internal symbols across compile units. For example elektraAbort is part of this version. It should be usable in all of Elektra's modules, but not outside. Currently there is no mechanism to enforce this, however, developers can manually check, if the use any of the symbols declared in libelektraprivate_1.0. Any of these symbols may change or removed at any time. No compatibility guarantee is given for these symbols.

Exporting new symbols

To add a new symbol (probably a function), simply add its name to the symbols.map file of the relevant library. Make sure to always add the symbol to the newest version only. Only symbols listed in one of the symbols.map files will be accessible by the linker. This means only these symbols can be used outside of their compilation unit. Only export symbols, if this is your intention.

For public API this is an obvious decision. All public API function should be added to newest version that exists at the time of their creation.

For internal API the question is not so obvious. If possible use static symbols. Static symbols cannot be exported via symbols.map, so the question is trivial. If the symbol shall be used in different source files (and therefore cannot be static), but only inside a single compilation unit (°), you don't need to add the symbol. If you are unsure, first try it without adding the symbol to symbols.map and only if it doesn't work add the symbol to libelektraprivate_1.0.

(°) compilation units mostly correspond to libraries

Updating a symbol

If a symbol is part of the public API (i.e. not in libelektraprivate_1.0) and you want to make changes that are ABI incompatible (e.g. changing the signature of a function), you have to update the symbol version.

To do this, simply add the symbol name to the newest version in symbols.map and remove it from the version it is currently part of. Each symbol may only be defined in one version. If the symbol is already declared in the latest version, you don't need to do anything, unreleased version may be modified at any point in time.

For additional clarity, you may leave a comment in the old version noting where the symbols was moved to.

To actually implement symbol versioning, a few code changes are necessary. Explaining this is much easier, when using a concrete example.

We will use the function int foo (Key * k) and assume the new version will have the signature int foo (Key * k, int x). The old version shall be called oldversion and the new version shall be newversion.

The old function declaration

int foo (Key * k);

has to be replaced with the new declarations

int foo (Key * k, int x);
int ELEKTRA_SYMVER (foo, v1) (Key * k);
#define ELEKTRA_SYMVER(sym, impl)
Helper macro to create a versioned name of a symbol.
Definition: kdbmacros.h:150

The second line declares an old version of foo. Note: v1 may be replaced by any string that is a valid identifier. It is simply their to make the symbol name unique.

In the source files containing the definition of our function, we change the old definition

int foo (Key * k)
{
// old code ...
}

to use the compatibility symbol

int ELEKTRA_SYMVER (foo, v1) (Key * k)
{
// old code ...
}
ELEKTRA_SYMVER_DECLARE ("oldversion", foo, v1)
#define ELEKTRA_SYMVER_DECLARE(ver, sym, impl)
Declares another version of a symbol using the .symver assembler pseudo command.
Definition: kdbmacros.h:159

To actually enable backwards compatibility, we need to add some assembler .symver pseudo instructions. To make this easier, we have the helper macro ELEKTRA_SYMVER_DECLARE. We add these lines after the function declaration. Note: there MUST NOT be a ; after the ELEKTRA_SYMVER_DECLARE otherwise there will be errors on systems that don't support symbol versioning

Then we simply write the definition for the new version, as if it was an entirely new function:

int foo (Key * k, int x)
{
// new code ...
}

The current version of a symbol always uses the unversioned name of the symbol. That way we default to this version. Only older versions use a versioned name via ELEKTRA_SYMVER.

Removing a symbol

If a symbol becomes deprecated, it should NOT be removed from the symbols.map file. This would break backwards compatibility. Instead we remove the default implementation and only leave versions implemented via ELEKTRA_SYMVER and ELEKTRA_SYMVER_DECLARE. The existing default version must be converted into a versioned symbol for the last supported version.