Library versioning under FreeBSD Symbol versioning - simple explanation -------------------------------------- Symbol versioning allows a library to define its exported symbols through the use of a map file. It also allows one library to provide multiple versions of the same symbol. In the past, without symbol versioning, this was accomplished by bumping the shared library version (libfoo.so.1, libfoo.so.2, etc.). Now one library version can provide multiple versions of the same symbol. Symbol versioning - example --------------------------- As a simple example, consider a hypothetical library libvector that provides the following interface: struct __vector; typedef struct __vector *vector_t; vector_t v_create(int initial, int max); int v_add(vector_t v, const void *obj); int v_remove(vector_t v, const void *obj); int v_elements_in(vector_t v); void *v_element_at(vector_t v, int index); int v_size_current(vector_t v); int v_size_max(vector_t v); The corresponding symbol map file would be: VER_1.0 { global: v_add; v_create; v_element_at; v_elements_in; v_remove; v_size_current; v_size_max; local: *; }; This symbol map defines the desired vector interfaces as global symbols. The asterisk (*) in the local section is a wildcard and specifies that all other symbols, regardless of whether they are declared static or not, are defined as local (hidden) symbols. When linking an application (or library) to libvector, only the global symbols are visible. Now suppose, after a release of libvector (as libvector.so.1), new interfaces are added: int v_remove_at(vector_t v, int index); int v_insert_at(vector_t v, int index, const void *obj); A new map file that defines the new version of libvector would be: VER_1.0 { global: v_add; v_create; v_element_at; v_elements_in; v_remove; v_size_current; v_size_max; local: *; }; VER_1.1 { global: v_remove_at; v_insert_at; } VER_1.0; The new symbol map defines VER_1.1 to provide the two new additional interfaces, and also to provide the same interfaces as VER_1.0. The new version of libvector is also released as libvector.so.1 and is compatible with the previous version. Older binaries or libraries that have linked to the previous version of libvector will continue to behave as before. Applications or libraries that are linked to the new version of libvector will be able to access the new interfaces. Now suppose an incompatible interface change is made to libvector: vector_t v_create(int initial, int extent, int max); The original v_create() created a vector with an initial size that could grow to a max size. When it needed to grow, it used a default size by which it grew. The new interface allows the application to specify what this size is, but the new interface is not compatible. Without symbol versioning, this would normally require releasing the library with a subsequent version, libvector.so.2. But symbol versioning allows the same library to support the original v_create() interface to older binaries, while providing the modified interface to new binaries. This is done using the assembler .symver pseudo opcode: .symver __v_create_old, v_create@VER_1.0 .symver __v_create_new, v_create@@VER_1.2 /* Note */ The library implementation provides two versions of v_create, __v_create_old() that provides the VER_1.0 and VER_1.1 interface, and __v_create_new() that provides the new (VER_1.2) interface. The second .symver statement above specifies the default interface for v_create() (using '@@' instead of '@'). There can be only one default interface, while there may be several non-default interfaces. (*) Note that if the implementation of the new interface is named the same as the symbol name, then the second .symver above is not needed. For this example, if the new version of v_create were named exactly that, or even if v_create were a weak reference to the actual implementation, then the second .symver above should not be issued. .symver __v_create_old, v_create@VER_1.0 __weak_reference(__v_create_new, v_create); vector_t __v_create_new(int initial, int extent, int max) { ... } vector_t __v_create_old(int initial, int max) { ... } The map file corresponding to VER_1.2 would be: VER_1.0 { global: v_add; /* v_create moved to VER_1.2 */ v_element_at; v_elements_in; v_remove; v_size_current; v_size_max; local: *; }; VER_1.1 { global: v_remove_at; v_insert_at; } VER_1.0; VER_1.2 { global: v_create; } VER_1.1; Note that you do not want to duplicate symbols in the map file. The .symver directives are all that is required to add compatibility symbols into old versions. Symbol versioning in libc ------------------------- We take a similar approach to symbol versioning as Linux does. There is a master version file that defines the libc versions; the versions are named in a similar numbering scheme as Solaris (SUNW1.0, SUNW1.1, etc.). The master version file lives in src/lib/libc (or perhaps in src/lib if a more general approach to version definitions is desired). This file does not contain any symbols, only the complete list of versions. It is of similar form as the map file in the example above: $ cat /usr/src/lib/libc/Versions.def FBSD_1.0 { }; FBSD_1.1 { # ABI change } FreeBSD_1.0; FBSD_1.1.1 { # no ABI change } FreeBSD_1.1; FBSD_1.2 { # ABI change } FreeBSD_1.1; Each sub-directory of libc that exports symbols defines a symbol map file (Symbol.map or whatever) that lists its symbols in a similar way as the example above. For example, libc/stdlib/Symbol.map would contain: $ cat /usr/src/lib/libc/stdlib/Symbol.map FBSD_1.0 { abort; abort2; abs; atexit; ... system; wctomb; wcstombs; }; It should be noted that these symbol map files do not specify version successors; they only define the list of exported symbols. When multiple versions of the same symbol need to be exported for compatibility reasons, they are listed multiple times in the symbol map file, just like the example above shows. is modified to provide the macro: #define __sym_compat(sym,impl,ver) \ __asm__(".symver " #impl ", " #sym "@" #ver) #define __sym_default(sym,impl,ver) \ __asm__(".symver " #impl ", " #sym "@@" #ver) that source files can use to define multiple versions of the same symbol: int __foo_ver1(FILE *); /* original */ int __foo_ver2(FILE *); /* FILE changed format */ int __foo_ver3(FILE *, int); /* added argument */ __sym_compat(foo, __foo_ver1, FreeBSD_1.0); __sym_compat(foo, __foo_ver2, FreeBSD_1.1); __sym_default(foo, __foo_ver3, FreeBSD_1.2); Each subdirectory of libc that defines a symbol map file, also needs to add this file to SYM_MAPS in its appropriate Makefile.inc. SYM_MAPS+= Symbol.map Tying it all together, there is a magical awk script that parses the master version file (libc/Versions.def), then each symbol map file to produce a master map file that can be used by the linker. Currently, it's installed as /usr/share/mk/version_gen.awk. It's run as (*): awk -f version_gen.awk -v vfile=Versions.def db/Symbol.map \ gdtoa/Symbol.map gen/Symbol.map ... with the master map being written to standard output. This is then provided as an argument to the linker's --version-script option. (*) We allow comments in symbol maps, so they should be first run through cpp to remove the comments before being supplied to version_gen.awk.