Author: | Michał Górny |
---|---|
Date: | 20 Aug 2012 |
Copyright: | http://creativecommons.org/licenses/by/3.0/ |
Source: | http://dev.gentoo.org/~mgorny/articles/the-impact-of-cxx-templates-on-library-abi.rst |
Preamble
The general aspect of maintaining binary compatibility of C++ library interfaces has been already covered thoroughly multiple times. A good reference of articles on the topic can be found on wiki page of ABI compliance checker tool [1]. Sadly, those articles usually consider the topic of C++ templates only briefly, if at all.
While in fact the topic is fairly complex, and I believe that considering the overall usefulness and popularity of the templates, it should be considered more thoroughly. Thus, in this article I will try to address the issues arising from use of templates, methods of dealing with them and trying to prevent them.
Both the overall topic of templates in respect to the programming techniques, and the wide topic of ABI are already explained in detail in many other articles and guides. Moreover, I believe that myself I am not fluent enough to be able to cover those topics in detail here. Thus, I will assume that a reader of this article is already familiar with both the general topic of templates in C++, and the basic aspects of an ABI and its compatibility.
Moreover, in the solutions and problems listed here I will assume that a particular toolchain in question does conform to the C++98 standard, and is able to properly support templates with regard to multiple instantiations.
[1] | http://ispras.linuxbase.org/index.php/ABI_compliance_checker#Articles |
ABI consistency in regular functions and class methods
Before proceeding further, I wish to quickly summarize the aspects of ABI compliance in regard to regular (non-template and non-inline) functions and class methods.
In shared libraries, the interface of library functions is defined within the header files which are installed along with the library. The implementation code, however, is defined in the source files which are compiled into the shared library itself. Effectively, the implementation is kept opaque to the library consumers; and those consumers (either programs or other shared libraries) dynamically link to the library [2].
Nevertheless, if the function ABI is to change, such a change can be signaled through changing the SONAME of the library. As executables and other shared libraries are linked to a specific SONAME, they have to be rebuilt in order to use the new version of the library. That way, it is ensured that the consumers will be rebuilt to use the new ABI before using the library.
Effectively, it is quite straightforward to maintain the ABI of a shared library using regular functions and classes. The number of ABI changes is minimized through keeping the implementation opaque to the consumers; and the actual ABI changes are propagated through use of SONAME and other symbol versioning techniques.
[2] | The topic of static linking is not covered here as of no relevance. |
The problem of handling template functions and classes
The requirements put on the compiler for template support made the solution presented above no longer possible. In order to achieve generic programming, the compiler must be able to adjust both the interface and the implementation of template functions and classes according to the template arguments. Considering that those arguments can practically hold any consumer-provided type or value (including types defined by consumers themselves), it is no longer possible to precompile the implementation code and provide it in a opaque shared library.
A completely different solutions need to be used in order to support templates. Firstly, both the interface and the implementation code must be public to the consumers; thus both are provided in the headers installed by the library. Effectively, a number of C++ libraries, Eigen [3] for instance, actually consist of header files only.
Secondly, compiler must be able to instantiate and compile the templates at will. Unlike with regular function which are compiled within a single source unit, templates have to be compiled on use, and can be used in multiple independent files. Effectively, the compiler must be able to handle multiple definitions of the same template sanely.
To sum up, templates basically no longer form a uniform shared library. They are propagated through header files, and can be instantiated practically anywhere — either in the library providing them (through use in other functions, or through forced instantiation), in a shared library using them or in the final executable. Moreover, the same template can be instantiated in multiple locations at the same time. The implementation part can no longer be opaque, and is spread along with the instantiations.
The lack of ability to provide opaque implementation makes it harder to change the code without breaking ABI compatibility. Moreover, the decentralization makes it even harder both to propagate implementation changes (including bug fixes) and to handle ABI changes.
[3] | http://eigen.tuxfamily.org/ |
Avoiding multiple, incompatible instantiations
One of the most common issues being result of a library ABI change is the occurrence of multiple, incompatible instantiations of the same template. I will explain this on a recent example.
With the recent improvement of C++11 standard support in gcc 4.7.1, the std::list<T> template class has been changed to hold the list length. The change has been made conditional to the C++11 standard being enabled; effectively, to the -std=c++11 option [4].
Now, if I have a particular program which uses std::list<int>, and I compile that program in the C++11 mode, the extended template will be used. To that moment, everything is fine.
However, if I would like to use a shared library using std::list<int> as well, and that library is compiled for another C++ standard (-std=c++98, for example), the program may no longer work correctly.
This is because both the program executable and the shared library would try to instantiate an incompatible versions of std::list<int>. Only one of those versions will be actually used, and the code written for the other one may break randomly.
The main issue solving that incompatibility is that if the library providing the template is based only on header files, it is no longer able to enforce propagation of the ABI change through rebuilding all of the libraries and executables using it.
[4] | As a side note, the change has been reverted and postponed to be introduced unconditionally in the next standard C++ library version. |
Using templates in final executables only
A one particular way in which the problem could be avoided is to limit the template instantiation to a single executable. For practical reasons, that executable would supposedly be the final program executable, allowing the programmer to use templates freely and all the libraries to use them equally.
This can be achieved through making all the implementation using templates… a template as well. For example, consider the following code:
#include <vector>
class C
{
public:
int f();
// ...
};
int C::f()
{
std::vector<int> value_vector;
// ...
}
Here function f() from class C uses a std::vector<int> internally. In order to avoid local instantiation of the vector, we need to make the class C a template class. In this particular case, we can achieve an additional benefit from that — allowing the user to specify the allocator for value_vector:
#include <memory>
#include <vector>
template < class allocator = std::allocator<void> >
class C
{
public:
int f()
{
typedef typename allocator::template rebind<int>
::other alloc_type;
std::vector<int, alloc_type> value_vector;
// ...
}
};
This way, as long as all classes using C are templates as well, the only template instantiation will take place in the final executable where the last template class is used. Of course, this has many implications for quite a little benefit. Most importantly:
- all template classes have to be used along with template parameters; instead of C, one has to use C<>;
- all template classes have to make the implementation public, and start to suffer from all template-related issues explained here;
- the code generation is deferred to the final executable. Code duplication occurs, shared libraries no longer serve a purpose and compiling the program becomes time- and memory-consuming.
Just as a note, the fore-mentioned approach can be used when there is no logical use for a template as well. An unused template parameter can be introduced then:
template <int unused = 0>
class C
{
// ...
};
Using inline methods and functions
There is one more solution which I am ought to mention, yet its use is limited and it is not guaranteed to work at all. The one remaining way of preventing multiple instantiations is to enforce inline instantiations of template methods.
This approach has a few limitations. Firstly, it requires modifying the template itself, so it won’t work with external templates, like fore-mentioned std::list. Additionally, since the compiler is free to ignore the inline keyword at will, this solution may only work partially or not work at all.
For completeness, I will present an example:
template <class T>
class C
{
T sth;
public:
inline T& get()
{
return sth;
}
inline void set(T& new_val)
{
sth = new_val;
}
};
The above code uses the inline keyword extensively, hoping that the compiler will inline all the method calls rather than instantiating them as separate methods. If it succeeds for all of the methods, external instantiations will not have anything to collide with.
Achieving a (partially) opaque implementation with templates
While the solutions listed above address a real issue, none of them could be considered a really good solution. They mostly try to circumvent a specific issue rather than addressing the deeper one which I would like to point out.
Those solutions are good as long as we’re only interested in keeping the executables mostly working. However, the template library works alike a static library there, suffering from the same limitations that a static library has.
Most importantly, it is impossible to change the implementation of a particular function or method, and propagate the change without rebuilding all the libraries and programs using the template. And this becomes important if the particular code suffers from security vulnerabilities or bugs which could result in data loss.
In order to solve that problem, we need to take a step back and see what can we do to make template implementation more opaque once again.
Explicitly instantiating opaque templates
One particular solution which solves the issue completely is to keep the implementation completely opaque, and force the compiler to explicitly instantiate necessary templates. However, a very important disadvantage of this solution is that the possible uses of the template are limited to the ones provided by the library.
For an example, please consider we’re having a MyList<T> container which works similarly to std::list<T>. In order to make the list implementation opaque, the header files contain only the interface of MyList<T>:
template <class T>
class MyList
{
struct priv_type;
priv_type* priv;
public:
// ...
};
As you can see in the above example, the PImpl (private implementation) idiom is used there to hide the private class members. That kind of PImpl implementation does not make sense with regular templates, since the private members would have to be explicitly listed along with the implementation. However, in this case we are keeping the implementation private.
The implementation along with the supported instantiations is provided in the source file of the library:
#include "mylist"
template class MyList<int>;
template class MyList<double>;
template class MyList<void*>;
// ...
With the above declarations, followed by the method and struct priv_type implementations will result in compiler outputting the template code for int, double and void* types. That code will be placed in the shared library, and made available for the consumers of the shared library.
Now, if a consumer uses MyList<int> and links to the shared library, it will use the opaque implementation from the library. Alike with regular functions, any ABI-safe changes will be propagated to the programs; and ABI-unsafe will result in necessity of rebuilding them, propagated through SONAME change.
However, if the consumer decides to use MyList<char>, the compiler will no longer be able to satisfy that requirement. The linker will refuse to link the program because of no implementation of MyList<char> methods.
Using common base classes and functions
Sadly, the fore-mentioned approach limits the possible uses of the template to a predefined parameter sets. This may be acceptable in very specific cases but usually it just invalidates the whole point of using templates.
An alternative approach is to use a partially opaque implementation, with the opaque being subject to easy implementation changes, and the public behaving like a regular template. This could be accomplished in a number of ways.
One of them is to develop a common, opaque base class which performs the most important tasks in a type-agnostic way and a derived template class which wraps the former in a nice API. For example, consider a pointer list like the following:
template <class T>
class PointerList;
template <>
class PointerList<void>
{
struct priv_data;
priv_data* priv;
public:
// ...
};
template <class T>
class PointerList
: private PointerList<void>
{
public:
// ...
};
The pointer list template is first specialized for the void* type. That specialization uses a PImpl idiom, private code and explicit instantiation (in the source file) to provide an opaque implementation of void* pointer list. On top of that, a public generic pointer list template is built.
With such a solution, the most important parts of implementation (in this case that would be the list handling code) are kept opaque, and thus changes in that code are propagated the usual way. The remaining public parts (in this case, that part would probably just cast types) suffer the regular issues with template classes.
It should be noted that the example presented above was very optimistic. Usually, providing a common opaque implementation is a more demanding task. Sometimes even it is not possible to split out a common amount of code for the base class, and only small pieces of code could be considered common. In these cases, the presented approach may not be beneficial at all.
Enforcing ABI versioning with template libraries
Up to the moment, I have mostly considered avoiding issues resulting from ABI incompatibilities, and avoiding introducing those incompatibilities. But even if the author protects executables from the fore-mentioned issues and provides a mostly-opaque implementation, he should still prepare the library for future ABI (or implementation) changes.
As mentioned before, the most common method used in POSIX-compliant systems is based on interface versioning (or even a more fine-grained symbol versioning in GNU systems). The concept is mostly straightforward — a shared library declares a range of supported interface versions, and the programs linking against it copy the current version into themselves. When the library is upgraded, and does not support the old interface anymore, the interface version in programs does not match the one in the library, and they refuse to use it without being rebuilt.
Although this technique is used in regular C and C++ libraries to handle ABI changes (effectively propagating the ABI change to consumers), it can be used to enforce implementation changes for templates as well. This can become particularly useful if a particular template code suffers from serious vulnerabilities or bugs.
The –as-needed problem
The fore-mentioned interface versioning technique requires the program using templates to link against a shared library where the version is defined. This becomes problematic if the template class does not use opaque implementation in the used code (i.e. all methods used by a particular consumer are defined in the template) and the –as-needed GNU linker option is used.
The intent of that option is to link the program executable only with those libraries which it uses directly. Although this often is advantageous and thus the option gained popularity, it thwarts our plans. Because the code using the templates does not call any function from the library we are forcing it to link, the linker will simply skip that library. Effectively, the ABI link will be broken, and interface changes from the library won’t be propagated to template consumers.
Although this specific problem could supposedly be worked around through disabling that option, that is not really feasible in this case. Most importantly, the task of disabling it would either need to be done by the library consumers directly (effectively, making it unlikely to happen) or handled through an external -config application which will hurt cross-compilation.
To cover the topic completely, I’ll note that such a solution would first have to check whether –as-needed is actually enabled. And due to the specific synopsis of -Wl compiler option, finding that would be a semi-difficult task (and different compilers will have different options). Then, it would have to add -Wl,–no-as-needed, the relevant libraries and re-enable -Wl,–as-needed.
Since that thing is neither easy to implement, nor portable, and we can’t forget that other (future) linkers can inhibit similar behavior using different options or even unconditionally, I believe we should look for another solution. One which works around the actual issue rather than trying to disable the feature exposing it.
Using dummy symbols to enforce linking
As mentioned earlier, the reason why linker silently ignores our library is that the program doesn’t actually use any symbol from it. So, in order to work around that, we simply need to define a dummy symbol and use it. Although that may look simple at first, we should give it some thought.
First of all, considering that the symbol has no real meaning, we should try hard to avoid unnecessary impact on the code, both in terms of performance and its size. Preferably, it should be as short as possible, and it should appear on unused (or likely-unused) code paths. However, we should also make sure that the code will actually be reachable — and thus the compiler won’t optimize it out.
For example, an obvious solution seems to add a dummy() method to our template class, and access the symbol there. Sadly, since the method is never actually used, the compiler doesn’t even instantiate it. After some thinking, you may reach a consensus that the only safe place for the call is in the constructor or destructor — but if the template class is used as a static class, these may not be used as well.
I described a similar problem on stackoverflow [5], and was given a pretty good tip there. Thanks to that, I assembled the following universal solution:
namespace mylib
{
extern int dummy;
namespace
{
class Dummy
{
public:
inline Dummy()
{
dummy = 0;
}
};
static const Dummy dummy;
}
}
With the int dummy variable being declared in the library code. The main (and possibly the only one) advantage of that solution is that it works with any code. The variable setting code may have side effects, thus the compiler is not allowed to optimize it out. It can however practically optimize it to a single mov instruction.
The disadvantage of that solution is that the assignment operation will be performed once for every single file including the header file. Thus, I would consider the fore-mentioned solution as a last resort, while preferring placing similar assignments in rarely-used (but always compiled) code, whenever possible.
For example, such a code may be within a rare error or exception handler. Just make sure that the compiler will not be allowed to assume that a particular branch will never be executed. For example, the following may be a bad idea:
template <type T>
void C<T>::f(int always_one = 1)
{
if (always_one != 1)
{
dummy = 0;
throw MyException();
}
// ...
}
Although most likely this code will work as expected, if a compiler decides to inline the function it may notice that it is always called with 1, effectively removing the whole branch. Similarly, the code should not be placed in unreachable code (because compiler may notice that) or assertions (because with NDEBUG they will be removed). And don’t forget that if f() will not be used directly at all, the whole function can be optimized out.
To sum up: if you have a safe, unlikely-called branch in your code, then it’s the best place for the assignment. If you don’t have one, then universal solution is probably the best you can get. Although it will cause unnecessary repetitions of the assignments, every other place is likely to cause even larger number of them.
[5] | http://stackoverflow.com/questions/11917014/enforce-linking-with-a-shared-library-with-wl-as-needed-when-only-templates |
A short summary
In this article I tried to point out a few common problems related to the ABI in shared libraries using templates. Firstly, I’d like to note I’ve focused only on issues arising from their use in the implementation code of a library, without exposing them in the public API.
If templates are exposed in the public API, i.e. used as function arguments or return types, their impact increases rapidly. In that case they become part of the library ABI, and changes in them effectively change the ABI of the library itself. There are solutions which try to avoid that, for example the bridge pattern [6] but they’re out of scope of this article.
Still, all the fore-mentioned issues apply. When designing a library of templates, you should be aware of them and know how to handle them. Most importantly, I believe that you should always be aware that your ABI may change at some point, even if it is very simple and dedicated to a very specific solution, and even if you are going to make it opaque, or take other precautions to avoid it changing in the future.
Thus, the important point here is to design your library in such a way that its consumers will be able to handle these ABI changes gracefully. The second but nevertheless important problem is actually avoiding ABI changes, either by good design (if possible) or through introducing new classes or methods rather than modifying the existing ones.
However, note that the latter method mentioned is going to require you to either permanently duplicate code (and effectively allowing the consumers to still use the old one), or just delay the ABI change until the compatibility code is removed in favor of requiring your consumers to update their code, thus effectively changing both the API and the ABI.
[6] | http://en.wikipedia.org/wiki/Bridge_pattern |