Export std::map<T1,T2>?
-
Hallo,
ich versuche mich gerade an einer eigenen DLL, die ich zur Laufzeit nachladen kann. Ich denke das Thema mit dem export und dem import habe ich weitestgehend verstanden (also den Sinn bzgl. dem späteren Linken, etc.).
Nun möchte ich eine std::map<T1,T2> aus der STL nutzen, bekomme aber Warnungen und Fehler beim export. Ich habe versucht im Netz eine Lösung dafür zu finden. Was ich gefunden habe ist, dass man eine std::map<T1,T2> nicht exportieren kann.
Ist das soweit korrekt? Ich habe etwas von Wrappern gelesen. Das würde bedeuten, dass ich für jeden Containertyp, der nicht exportierbar ist, eine eigene Klasse implementieren muss?
Vielen Dank im Voraus
Torsten
-
Dir ist das Folgende bekannt?
-
Ich schätze, am ehesten könntest Du die Spezialisierung der Map per extern auslagern
extern template class map<T1, T2>;
.. und dann in einer anderen TU entsprechend explizit instantiieren, welche dann zur DLL kompiliert wird.
Die Wrapper, von denen Du gelesen hast, sind wahrscheinlich einfach pimpl (deklariere ein einfaches Interface, und die auf map basierende Implementierung wird durch die DLL geladen).Es sei erwähnt, dass man in der Praxis keine Spezialisierungen von STL Containern auslagert. Als einführendes Beispiel wäre eine eigene Klasse sicherlich viel dienlicher?
Edit: Ich bin ehrlich gesagt durch dem Begriff "export" verwirrt. Redest Du von Module exports?
-
Hallo nochmal,
ich habe mal ein minimales Beispiel vorbereitet. Es gibt eine Schnittstelle, damit ich später die DLL nachladen und nutzen kann. Weiterhin gibt es eine konkrete Klasse, also eine DLL an sich. Ich denke, ich habe mit dem Beispiel std::map<T1, T2> etwas Verwirrung gestiftet. Was ich umsetzen möchte, sieht man an den drei folgenden Quellen.
#pragma once #include <memory> #include <string> #ifdef LIBRARY_EXPORT #define LIBRARY_API __declspec(dllexport) #else #define LIBRARY_API __declspec(dllimport) #endif namespace DllExample { class LIBRARY_API AbstractLibrary { protected: AbstractLibrary() = default; AbstractLibrary(const AbstractLibrary&) = default; AbstractLibrary(AbstractLibrary&&) noexcept = default; ~AbstractLibrary() = default; AbstractLibrary& operator=(const AbstractLibrary&) = default; AbstractLibrary& operator=(AbstractLibrary&&) = default; virtual void addKeyValuePair(const std::string& key, const std::string& value) noexcept = 0; virtual const std::string& getValue(const std::string& key) const noexcept = 0; }; typedef std::shared_ptr<AbstractLibrary> AbstractLibraryPtr; }
#pragma once #include "abstractLibrary.hpp" #include <map> #ifdef LIBRARY_EXPORT #define LIBRARY_API __declspec(dllexport) #else #define LIBRARY_API __declspec(dllimport) #endif namespace DllExample { class LIBRARY_API Library : public AbstractLibrary { public: Library() = default; Library(const Library&) = default; Library(Library&&) noexcept = default; virtual ~Library() = default; Library& operator=(const Library&) = default; Library& operator=(Library&&) = default; void addKeyValuePair(const std::string& key, const std::string& value) noexcept override; const std::string& getValue(const std::string& key) const noexcept override; private: std::map<std::string, std::string> keyValuePairs; }; typedef std::shared_ptr<Library> LibraryPtr; }
#include "library.hpp" namespace DllExample { void Library::addKeyValuePair(const std::string& key, const std::string& value) noexcept { keyValuePairs.insert(std::pair<std::string, std::string>(key, value)); } const std::string& Library::getValue(const std::string& key) const noexcept { return keyValuePairs.at(key); } }
@Columbo : Was genau meinst du mit "Es sei erwähnt, dass man in der Praxis keine Spezialisierungen von STL Containern auslagert".
Wie müsste ich denn jetzt die Quellen anpassen, damit die Warnung "DllExample::Library::keyValuePairs": class "std::map<std::string,std::string,std::lessstd::string,std::allocator<std::pair<const std::string,std::string>>>" erfordert eine DLL-Schnittstelle, die von Clients von class "DllExample::Library" verwendet wird" nicht mehr auftaucht?
Torsten
-
Ich schätze mal, Du arbeitest mit Visual Studio und es handelt sich um die 'warning C4251'.
Ich arbeite u.a. mit Visual Studio Community 2019.
Diese Warnung hagelt bei mir ein, wenn ich z.B. ein Qt-Projekt kompiliere.
Ich ignoriere diese Warnung, weil diese bei mir global nicht abschaltbar ist.
Ich habe viel Zeit mit recherchieren verballert und keine Lösung gefunden.
-
@TorDev sagte in Export std::map<T1,T2>?:
Hallo nochmal,
Ich denke, ich habe mit dem Beispiel std::map<T1, T2> etwas Verwirrung gestiftet.
Die Verwirrung hast Du mit dem
export
angerichtet.
-
@Helmut-Jakoby Hm... Warnungen zu ignorieren oder gar zu unterdrücken ist eigentlich nicht mein Stil
@john-0 Sorry (es bezog sich auf __declspec(dllexport) / __declspec(dllimport)Torsten
-
Na ja, ich ignoriere eigentlich auch keine Warnungen (habe z.B. in Visual Studio Warnstufe 4).
Aber diese spezielle Warnung, das eine DLL-Schnittstelle benötigt wird, bekomme ich bei keinem anderen Compiler (MinGW, GCC oder clang) und die Applikationen verhalten sich gleich, egal mit welchem Compiler erstellt.
-
C4251 ist z.B. für folgende Fälle gedacht:
#ifdef MY_EXPORT #define MY_API __declspec(dllexport) #else #define MY_API __declspec(dllimport) #endif class Foo { // kein MY_API public: void doTheThing(); }; class MY_API Bar { public: void doTheThing() { m_foo.doTheThing(); } private: Foo m_foo; };
Ein Aufruf von
Bar::doTheThing
könnte hier inline erweitert werden. Das würde einen Linkerfehler erzeugen, weilFoo::doTheThing
nicht exportiert ist.
-
@TorDev sagte in Export std::map<T1,T2>?:
@Columbo : Was genau meinst du mit "Es sei erwähnt, dass man in der Praxis keine Spezialisierungen von STL Containern auslagert".
Damit meine ich, dass der Zweck von DLLs darin besteht, (typisch größere) Bibliotheken abzukoppeln, damit sie zwischen mehreren Programmen geteilt, oder individuell geupgraded werden können. Die STL Container sind weder sonderlich gross, noch benötigen sie upgrades. Und Platz spart man letztlich keinen, da sie Templates sind, und man nur seine eigenen Spezialisierungen auslagern kann (welche anderer Code wahrscheinlich nicht braucht).
@hustbaer sagte in Export std::map<T1,T2>?:
Ein Aufruf von
Bar::doTheThing
könnte hier inline erweitert werden. Das würde einen Linkerfehler erzeugen, weilFoo::doTheThing
nicht exportiert ist.Verstehe ich das richtig: Der Code ist falsch so wie er dort steht, weil
Bar::doTheThing
inline definiert wurde, und der importierende Code damit auf ein nicht importiertes Symbol verweist?
-
@TorDev
Ich kann dein minimales Beispiel nicht nachvollziehen. Darin sieht man nicht wie die DLL nachgeladen und dann verwendet wird. Und ich sehe nicht wie das mit dem von dir gezeigten Code funktionieren könnte.Die Funktionen in
AbstractLibrary
sind alleprotected
, d.h. das Programm das die DLL nachlädt kann diese nicht aufrufen. Weiters fehlt mir eine exportierte Funktion die das konkrete Objekt erzeugt.Ein übliches Muster wäre dieses:
Interface
class LIBRARY_API AbstractLibrary { public: virtual ~AbstractLibrary() = default; virtual void addKeyValuePair(const std::string& key, const std::string& value) = 0; virtual const std::string& getValue(const std::string& key) const = 0; }; using CreateLibraryFn = AbstractLibrary* (); extern "C" LIBRARY_API CreateLibraryFn createLibrary;
Implementierung
class Library : public AbstractLibrary { public: void addKeyValuePair(const std::string& key, const std::string& value) override { keyValuePairs.insert({key, value}); } const std::string& getValue(const std::string& key) const override { return keyValuePairs.at(key); } private: std::map<std::string, std::string> keyValuePairs; }; extern "C" LIBRARY_API AbstractLibrary* createLibrary() { return new Library(); }
Verwendung
int main() { auto const dll = LoadLibrary("library.dll"); if (!dll) { puts("meh"); exit(1); } CreateLibraryFn* createLibrary = reinterpret_cast<CreateLibraryFn*>(GetProcAddress(dll, "createLibrary")); if (!createLibrary) { puts("meh"); exit(1); } std::unique_ptr<AbstractLibrary> lib(createLibrary()); lib->addKeyValuePair("foo", "bar"); auto const value = lib->getValue("foo"); puts(value.c_str()); }
Das
LIBRARY_API
beiAbstractLibrary
könnte man auch noch wegbekommen wenn man möchte. Dazu müsste man das Löschen des Objekts ebenso in die DLL verschieben. z.B. indem man der KlasseAbstractLibrary
eine virtuelle "deleteMe" Funktion verpasst, und dannobj->deleteMe();
stattdelete obj;
in der exe verwendet.Weitere Anmerkung:
noexcept
ist nicht nötig, das Werfen von Exceptions über DLL Grenzen hinweg funktioniert wunderbar. Wenn du selbst definierte Exception Klassen auf der anderen Seite wieder fangen können möchtest, müssen diese natürlich auch exportiert werden.Und natürlich machst du dich damit ein wenig mehr von der ABI des Compilers abhängig.
-
@Columbo sagte in Export std::map<T1,T2>?:
@hustbaer sagte in Export std::map<T1,T2>?:
Ein Aufruf von
Bar::doTheThing
könnte hier inline erweitert werden. Das würde einen Linkerfehler erzeugen, weilFoo::doTheThing
nicht exportiert ist.Verstehe ich das richtig: Der Code ist falsch so wie er dort steht, weil
Bar::doTheThing
inline definiert wurde, und der importierende Code damit auf ein nicht importiertes Symbol verweist?Genau.
Wobei es natürlich auch funktionieren kann - z.B. wenn man einen Debug Build macht wo kein Inlining gemacht wird. In dem Fall würde es funktionieren, da dann die
Bar::doTheThing
Funktion aus der DLL verwendet wird statt sie inline im aufrufenden Code zu erweitern.
-
@Columbo
Das selbe Problem gibt es natürlich auch bei den implizit definierten Funktionen. Also wennFoo
jetzt z.B. einen nicht trivialen dtor oder assignment operator hat.Da diese für
Bar
implizit definiert werden, könnte z.B. auch~Bar
beim Aufrufer inline erweitert werden.~Bar
enthält aber einen Aufruf von~Foo
, und wenn~Foo
selbst nicht inlined wird, dann wäre hier ein export von~Foo
nötig. Der ja nicht da ist.
-
Vielleicht interessant bei dem Thema:
__declspec(dllexport)
auf einer Klasse führt dazu dass sämtliche Funktionen der Klasse exportiert werden, inklusive- implizit definierte Memberfunktionen
- "versteckte" ABI Funktionen wie der "scalar deleting destructor" und der "vector deleting destructor" (Hilfsfunktionen die für
delete p
bzw.delete [] p
verwendet werden)
__declspec(dllimport)
führt dazu dass alle Funktionen die nicht inline erweitert werden aus der DLL genommen werden. Also auch die implizit definierten Funktionen. Aber eben nur, so lange sie nicht inline erweitert werden!Inlining von Funktionen bleibt weiterhin möglich, und führt dann eben wie beschrieben u.U. dazu dass auf einmal exportierte Symbole benötigt werden, die man gar nicht direkt in seinem Programm verwendet.
U.a. dafür wurde Warning C4251 gemacht.