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?


  • Mod

    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, weil Foo::doTheThing nicht exportiert ist.


  • Mod

    @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, weil Foo::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 alle protected, 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 bei AbstractLibrary 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 Klasse AbstractLibrary eine virtuelle "deleteMe" Funktion verpasst, und dann obj->deleteMe(); statt delete 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, weil Foo::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 wenn Foo 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.


Log in to reply