The Libtool Library Versioning Scheme

The authors of Libtool tried hard to provide a versioning scheme that could be mapped to any of the schemes used by any Libtool platform. The Libtool versioning scheme is designed to be flexible enough to be forward compatible with reasonable future changes to existing Libtool platforms and even to new Libtool platforms.

Nevertheless, it's not a panacea. When extending Libtool for a new type of shared-library platform, situations have occurred (and continue to occur) that require some serious and careful evaluation. No one can be an expert on all systems, so the Libtool developers rely heavily on outside contributions to create proper mappings from the Libtool versioning scheme to the schemes of new or would-be Libtool platforms.

You should consciously avoid thinking of library version numbers (either Libtool's or those of a particular platform) as product major, minor, and revision values. In fact, these values have very specific meaning to the operating system loader, and they must be updated properly for each new library version in order to keep from confusing the loader. A confused loader could load the wrong version of a library based on incorrect version information assigned to the library.

Several years ago, I was working with my company's corporate versioning committee to come up with a software-versioning policy for the company as a whole. The committee wanted the engineers to ensure that the version numbers incorporated into our shared-library names were in alignment with the corporate software versioning strategy. It took me the better part of a day to convince them that a shared-library version was not related to a product version in any way, nor should such a relationship be established or enforced by them or by anyone else.

Here's why: The version number on a shared library is not really a library version but an interface version. The interface I'm referring to here is the application binary interface presented by a library to the user, another programmer desiring to call functions presented by the interface. An executable program has a single, well-defined, standard entry point (usually called main in the C language). But a shared library has multiple entry points that are generally not standardized in a manner that is widely understood. This makes it much more difficult to determine whether or not a particular version of a library is interface compatible with another version of the same library.

In Libtool's versioning scheme, shared libraries are said to support a range of interface versions, each identified by a unique integer value. If any publicly visible aspect of an interface changes between public releases, it can no longer be considered the same interface; it therefore becomes a new interface, identified by a new integer identifier. Each public release of a library in which the interface has changed simply acquires the next consecutive interface version number. Libraries that change in a backward-compatible manner between releases are said to support both the old and the new interface; thus a particular release of a library may support interface versions 2–5, for example.

Libtool library version information is specified on the libtool command line with the -version-info option, as shown in Example 7-1.

The Libtool developers wisely chose the colon separator over the period in an effort to keep developers from trying to directly associate Libtool version string values with the version numbers appended to the end of shared-library files on various platforms. The three values in the version string are respectively called the interface current, revision, and age values.

The current value represents the current interface version number. This is the value that changes when a new interface version must be declared because the interface has changed in some publicly visible way since the last public release of the library. The first interface in a library is given a version number of zero by convention. Consider a shared library in which the developer has added a new function to the set of functions exposed by this library since the last public release. The interface can't be considered the same in this new version because there's one additional function. Thus, its current number must be increased from zero to one.

The age value represents the number of back versions supported by the shared library. In mathematical terms, the library is said to support the interface range, currentage through current. In the example I just gave, a new function was added to the library, so the interface presented in this version of the library is not the same as it was in the previous version. However, the previous version is still fully supported, because the previous interface is a proper subset of the current interface. Therefore, the age value should also be incremented from zero to one.

The revision value merely represents a serial revision of the current interface. That is, if no publicly visible changes are made to a library's interface between releases—perhaps only an internal function was optimized—then the library name should change in some manner, if only to distinguish between the two releases. But both the current and age values would be the same, because the interface has not changed from the user's perspective. Therefore, the revision value is incremented to reflect the fact that this is a new release of the same interface. In the previous example, the revision value would be left at zero, because one or both of the other values was incremented.

To simplify the release process for shared libraries, the Libtool versioning algorithm should be followed step-wise for each new version of a library that is about to be publicly released:[96]

  1. Start with version information 0:0:0 for each new Libtool library. (This is done automatically if you simply omit the -version-info option from the list of linker flags passed to the libtool script.) For existing libraries, start with the previous public release's version information.

  2. If the library source code has changed at all since the last update, then increment revision (c:r:a becomes c:r+1:a).

  3. If any exported functions or data have been added, removed, or changed since the last update, increment current and set revision to 0.

  4. If any exported functions or data have been added since the last public release, increment age.

  5. If any exported functions or data have been removed since the last public release, set age to 0.

Keep in mind that this is an algorithm, and as such, it's designed to be followed step by step as opposed to jumping directly to the steps that appear to apply to your case. For example, if you removed an API function from your library since the last release, you would not simply jump to the last step and set age to zero. Rather, you would follow all of the steps until you reached the last step, and then set age to zero.

Note

Remember to update the version information only immediately before a public release of your software. More frequent updates are unnecessary and only guarantee that the current interface number becomes larger faster.

Let's look at an example. Assume that this is the second release of a library, and the first release used a -version-info string of 0:0:0. One new function was added to the library interface during this development cycle, and one existing function was deleted. The effect on the version information string for this new release of the library would be as follows:

  1. Begin with the previous version information: 0:0:0

  2. 0:0:0 becomes 0:1:0 (the library's source was changed)

  3. 0:1:0 becomes 1:0:0 (the library's interface was modified)

  4. 1:0:0 becomes 1:0:1 (one new function was added)

  5. 1:0:1 becomes 1:0:0 (one old function was removed)

It should be clear by now that there is no direct correlation between Libtool's current, revision, and age values and Linux's major, minor, and optional patch-level values. Instead, mapping rules are used to transform the values in one scheme to values in the other.

Returning to the above example, wherein a second release of a library added one function and removed one function, we ended up with a new Libtool version string of 1:0:0. The version string 1:0:0 indicates that the library is not backward compatible with the previous version (age is zero), so the Linux shared-library file would be named libname.so.1.0.0. This looks suspiciously like the Libtool version string—but don't be fooled. This fairly common coincidence is perhaps one of the most confusing aspects of the Libtool versioning abstraction.

Let's modify our example just a little to say that we've added a new library interface function but haven't removed anything. Start again with the original version information of 0:0:0 and follow the algorithm:

  1. Begin with the previous version information: 0:0:0

  2. 0:0:0 becomes 0:1:0 (the library's source was changed)

  3. 0:1:0 becomes 1:0:0 (the library's interface was modified)

  4. 1:0:0 becomes 1:0:1 (one new function was added)

  5. Not applicable (nothing was removed)

This time, we end up with a Libtool version string of 1:0:1, but the resulting Linux or Solaris shared-library filename is libname.so.0.1.0. Consider for a moment what it means, in the face of major, minor, and patch-level values, to have a nonzero age value in the Libtool version string. An age value of one (as in this case) means that we are effectively still supporting a Linux major value of zero, because this new version of the library is 100-percent backward compatible with the previous version. The minor value in the shared-library filename has been incremented from zero to one to indicate that this is, in fact, an updated version of the soname, libname.so.0. The patch-level value remains at zero because this value indicates a bug fix to a particular minor revision of an soname.

Once you fully understand Libtool versioning, you'll find that even this algorithm does not cover all possible interface modification scenarios. Consider, for example, version information of 0:0:0 for a shared library that you maintain. Now assume you add a new function to the interface for the next public release. This second release properly defines version information of 1:0:1 because the library supports both interface versions 0 and 1. However, just before the third release of the library, you realize that you didn't really need that new function after all, so you remove it. This is the only publicly visible change made to the library interface in this release. The algorithm would have set the version information string to 2:0:0. But in fact, you've merely removed the second interface and are now presenting the original interface once again. Technically, this library would be properly configured with a version information string of 0:1:0 because it presents a second release of version 0 of the shared-library interface. The moral of this story is that you need to fully understand the way Libtool versioning works, and then decide, based on that understanding, what the proper next-version values should be.

I'd also like to point out that the GNU Libtool Manual makes little effort to describe the myriad ways an interface can be different from one version of a library to another. An interface version indicates functional semantics as well as API syntax. If you change the way a function works semantically but leave the function signature untouched, you've still changed the function. If you change the network wire format of data sent by a shared library, then it's not really the same shared library from the perspective of consuming code. All the operating system loader really cares about when attempting to determine which library to load is: Will this library work just as well as that one? In these cases, the answer would have to be no, because even though the API interface is identical, the publicly visible way the two libraries do things is not the same.

These types of changes to a library's interface are so complex that project maintainers will often simply rename the library, thereby skirting library versioning issues entirely. One excellent way to rename your library is to use Libtool's -release flag. This flag adds a separate class of library versioning information into the base name of the library, effectively making it an entirely new library from the perspective of the operating system loader. The -release flag is used in the manner shown in Example 7-2.

In this example, I used -release and -version-info in the same set of Libtool flags, just to show you that they can be used together. You'll note here that the release string is specified as a series of dot-separated values. In this case, the final name of your Linux or Solaris shared library will be libname−2.9.0.so.0.0.0.

Another reason developers choose to use release strings is to provide some sort of correlation between library versions across platforms. As demonstrated above, a particular Libtool version information string will probably result in different library names across platforms because Libtool maps version information into library names differently from platform to platform. Release information remains stable across platforms, but you should carefully consider how you wish to use release strings and version information in your shared libraries, because the way you choose to use them will affect binary compatibility between releases of your libraries. The OS loader will not consider two versions of a library to be compatible if they have different release strings, regardless of the values of those strings.



[96] See the Free Software Foundation's GNU Libtool Manual at http://www.gnu.org/software/libtool/manual/.