dll_import für Templates

Sind in für DLLs exportierten C++ Klassen unter Windows Datentypen enthalten, die auf Templates aufbauen, sieht man schnell den Fehler:

1warning C4251: ... needs to have dll-interface ...

Doch den bekommt man gar nicht so leicht weg, wenn man portabel bleiben möchte.


Vorgeschichte: C++ Templates

C++ Templates kann man nicht exportieren, sie sind in der Regel vollständig im Header implementiert und daher “einfach da”. Wenn sie aber benutzt und damit “instanziiert” werden, erzeugt der Compiler eine Implementierung, gegen die dann gelinkt wird.
Nachdem mehrere Module Templates instanziieren können stellt das eigentlich eine Verletzung der One-Definition-Rule (ODR) dar … doch für Templates wird eine Ausnahme gemacht und der Linker fügt die mehrfachen Varianten entweder in eine zusammen, oder behandelt sie wie private Inline-Codes.

Das Spiel endet aber an der Grenze der erzeugten Ausgabedatei.
Und wenn jetzt ein Projekt eine Bibliothek in ein Programm einbindet und eine Template-basierte Instanz zwischen beiden ausgetauscht werden soll, tja dann müssen wir hoffen und vertrauen, dass es “irgendwie” funktioniert.

Die C++ Sprache kennt keine Windows .dll oder Linux .so Bibliotheken, und somit sind es die Compiler-Hersteller, die den Austausch von Daten zwischen Bibliotheken implementieren, doch einen Standard gibt es nicht.

Aus diesem Grund können auch ein MSVC und ein MinGW GCC problemlos über C-Schnittstellen kommunizieren, denn diese Schnittstelle ist spezifiziert. C++ Codes funktionieren bibliotheksübergreifend jedoch nur mit genau ein und der selben Compiler-Version, ein Compiler-Mischbetrieb ist nicht möglich.

Idee: Template Instanzen exportieren

Will man einen std::vector<int> von einer Bibliothek zum Hauptprogramm weiterreichen, meldet uns die genannte Warnung, dass std::vector<int> kein DLL-Interface eingerichtet hat. Es funktioniert dann aber “magisch” trotzdem, weil der gleiche <vector> Header in beiden Komponenten vorhanden ist, und damit das Binäre Layout auf beiden Seiten identisch bestimmt wird.

Soll heißen, der Inhalt von std::vector<int> wird auf der Zielseite genau so “verstanden”, wie er auf der Erstellerseite angelegt wurde, weil beide Seiten eine Kopie des gleichen Codes enthalten.

Man kann jedoch aus diesem “Zufall” auch einen Standard machen, indem man die explizite Template-Instanziierung mit einem DLL-Interface exportiert und auf der Zielseite importiert.
Nun existiert der Code auf genau einer Seite und die zweite Seite nutzt ihn. Wir haben dann einen binären Standard und können Bugfixes an exakt einer Stelle vornehmen, anstatt sich sorgen zu müssen, dass Templates an jedem Ort anders interpretiert werden.

Beispiel Code

Code sagt mehr als 1000 Worte

 1// mylib.h
 2#include <utility>
 3
 4#if defined(mylib_EXPORTS)
 5#  if defined(_MSC_VER)
 6#    define MYLIB_API __declspec(dllexport)
 7#    define MYLIB_EXTERN
 8#  else
 9#    define MYLIB_API __attribute__((visibility ("default")))
10#    define MYLIB_EXTERN extern
11#  endif
12#else
13#  if defined(_MSC_VER)
14#    define MYLIB_API  __declspec(dllimport)
15#    define MYLIB_EXTERN extern
16#  else
17#    define MYLIB_API
18#    define MYLIB_EXTERN extern
19#  endif
20#endif
21
22// declare export of some std::pair instances
23LIB_EXTERN template struct LIB_API std::pair<int, int>;
24LIB_EXTERN template struct LIB_API std::pair<double, double>;
1// mylib.cpp
2#include "mylib.h"
3
4template struct std::pair<int, int>;
5template struct std::pair<double, double>;

Im Header wird jede gewollte Template-Spezialisierung deklariert, und in der .cpp Datei findet dann die explizite Instanziierung statt.

Der besondere Unterschied zwischen MSVC und GCC ist, dass unter Windows ein mit dllexport exportiertes Symbol nicht zusätzlich noch extern sein darf, während der GCC extern gestattet.

Wir brauchen daher ein zusätzliches “extern” - Makro, welches im Fall von dllexpoprt im Header einfach leer ist.
Unter Linux darf dieses extern nicht fehlen, ansonsten wird die Spezialisierung schon im Header generiert.

Fazit

Für eigene Datentypen ist die Methode also durchaus geeignet, um Templates in der Bibliothek für einige Typen in konkrete Datentypen umzuwandeln.

Die Sache hat nur einen Haken: std::string und alle Container nutzen intern jede Menge weitere private Hilfstypen, die selbst wieder Templates sein können. Und das Problem ist, dass man alle “exportieren” müsste, um alle bekannten MSVC Warnungen wegzubekommen.

Doch dieses Unterfangen ist nicht portabel, und aus diesem Grund setze ich die explizite Instanziierung nur selten für eigene Typen ein.