dll_import für Templates
« | 04 Mar 2023 | »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>;
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.