Dynamic Object Versioning |
Rod Evans Sunday August 22, 2004
For some time now, we've been versioning core system libraries.
You can display version definitions, and version requirements with
% pvs -d /lib/libelf.so.1 libelf.so.1; SUNW_1.5; SUNW_1.4; .... SUNWprivate_1.1;
So, what do these versions provide? Shared object versioning has often been established with various conventions of renaming the file itself with different major or minor (or micro) version numbers. However, as applications have become more complex, specifically because they are constructed from objects that are asynchronously delivered from external partners, this file naming convention can be problematic.
In developing the core Solaris libraries, we've been rather obsessed
with compatibility, and rather than expect customers to rebuild against
different shared object file names (i.e.,
Now you could maintain compatibility by retaining all existing public interfaces, and only adding new interfaces, without the versioning scheme. However, the version scheme has a couple of advantages:
consumers of the interface sets record their requirements on the version name they reference.
establishing interface sets removes unnecessary interfaces from the name-space.
the version sets provide a convenient means of policing interface evolution.
When a consumer references a versioned shared object, the version
name representing the interfaces the consumer references are
recorded. For example, an application that references the
% cc -o main main.c -lelf % pvs -r main libelf.so.1 (SUNW_1.4);
This version name requirement is verified at runtime. Therefore,
should this application be executed in an environment consisting
of an older
% pvs -dn /lib/libelf.so.1 SUNW_1.3; SUNWprivate_1.1; % main ld.so.1: ./main: fatal: libelf.so.1: version `SUNW_1.4' not found \\ (required by file ./main)
This verification might seem simplistic, and won't the application be terminated anyway if a required interface can't be located? Well yes, but function binding normally occurs at the time the function is first called. And this call can be some time after an application is started (think scientific applications that can run for days or weeks). It is far better to be informed that an interface can't be located when a library is first loaded, that to be killed some time later when a specific interface can't be found.
Defining a version typically results in the demotion of many other global symbols to local scope. This localization can prevent unintended symbol collisions. For example, most shared objects are built from many relocatable objects, each referencing one another. The interface that the developer wishes to export from the shared object is normally a subset of the number of global symbols that would normally remain visible.
Version definitions can be defined using a
% cat mapfile ISV_1.1 { global: foo1(); foo2(); local: \*; }; % cc -o libfoo.so.1 -G -Kpic -Mmapfile foo.c bar.c ... % pvs -dos libfoo.so.1 libfoo.so.1 - ISV_1.1: foo1; libfoo.so.1 - ISV_1.1: foo2;
The demotion of unnecessary global symbols to locals greatly reduces the relocation requirements of the object at runtime, and can significantly reduce the runtime startup cost of loading the object.
Of course, interface compatibility requires a disciplined approach to maintaining interfaces. In the previous example, should the signature of foo1() be changed, or foo2() be deleted, then the use of a version name is meaningless. Any application that had built against the original interfaces, will fail at runtime when the new library is delivered, even though the version name verification will have been satisfied.
With the core Solaris libraries we maintain compatibility as we evolve through new releases by maintaining existing public interfaces and only adding new version sets. Auditing of the version sets help catch any mistaken interface deletions or additions. Yeah, we fall foul of cut-and-paste errors too :-)
For more information on versioning refer to Interfaces and Versioning.
[6] Lazy Loading fall back | [8] Relocations and debugging flags |