/MT und geteilte Laufzeitumgebungen



  • Hallo,

    Ich habe eine kleine DLL-Bibliothek MyLib . Ich kompiliere mit /MT , d.h. ich linke die VC++-Runtime statisch. Ausserdem wird das Makro MYLIB_EXPORTS definiert.

    // === MyLib.hpp ==============================
    #ifdef MYLIB_EXPORTS
    	#define MYLIB_API __declspec(dllexport)
    #else
    	#define MYLIB_API __declspec(dllimport)
    #endif
    
    long MYLIB_API* create();
    
    // === MyLib.cpp ==============================
    #include "MyLib.hpp"
    
    long* create()
    {
    	return new long(73);
    }
    

    Im Weiteren habe ich ein Projekt, das MyLib.dll linkt. Es wird ebenfalls mit /MT kompiliert.

    // === MyProject.cpp ==========================
    #include "MyLib.hpp"
    
    #pragma comment(lib, "MyLib.lib")
    
    int main()
    {
    	long* x = create();
    	delete x;
    }
    

    Führe ich das Programm aus, kracht es mit einer Heap Corruption. Ich nehme an, der Grund dafür besteht darin, dass der in der DLL erzeugte long in einem anderen Freestore liegt, als der delete -Operator zu löschen versucht. Anscheinend verwenden bei /MT sowohl die .dll als auch die .exe ihre eigene Runtime, finde ich auch einleuchtend.

    Wie begegnet man solchen Problemen normalerweise? Wird von statischem Linken der Runtime generell abgeraten?

    Man kann zwar die Bibliothek so gestalten, dass sie genau schaut, was über DLL-Grenzen geteilt wird, aber das führt schnell zu sehr viel Boilerplate-Code. Ist nicht bereits sowas

    struct MyClass
    {
        MyClass(); // initialisiert vector
    
        std::vector<int> elements;
    };
    

    problematisch, da der Defaultkonstruktor in der .cpp-Datei (d.h. in der DLL), aber die Grossen Drei vom Compiler generiert werden (d.h. im Client) und folglich verschiedene Allokatoren verwendet weden?


  • Mod

    Wen Du eine Create Funktion in der DLL hast kannst Du auch einen Delete Funktion dazu bauen.

    Ansonsten funktioniert diese Form nur, wenn die CRT gleich ist. Zudem funktioniert sie auch nur "garantiert" dann, wenn DLL und EXE mit dem selben Compiler gebaut wurden.



  • Martin Richter schrieb:

    Ansonsten funktioniert diese Form nur, wenn die CRT gleich ist.

    Aber funktioniert es auch bei statisch gelinkter CRT? Werden mit /MT nicht immer zwei Instanzen der Runtime (eine für .dll, eine für .exe) verwendet?



  • Wo der Code liegt, ist, so lange die DLL nicht zur Laufzeit entladen wird, egal.
    Wichtig ist wo Daten liegen.
    D.h. bei inline Funktionen mit statischen Variablen kann es Probleme geben.
    Und natürlich bei Dingen wie Heap oder anderen globalen Datenstrukturen/Variablen ( std::cout , rand/srand etc.).

    MSVC verwendet zwar die OS Funktionen um Speicher anzufordern, allerdings wird nicht der Process-Default-Heap verwendet, sondern die erzeugt sich einen eigenen Heap. Und das wird wohl das Problem sein.

    Werden mit /MT nicht immer zwei Instanzen der Runtime (eine für .dll, eine für .exe) verwendet?

    Ja, richtig.
    Ich vermute Martin meint mit "gleich ist" dass die gleiche Instanz verwendet wird.



  • hustbaer schrieb:

    Wo der Code liegt, ist, so lange die DLL nicht zur Laufzeit entladen wird, egal.

    Naja, schliesslich entscheidet der Ort des Codes z.B. darüber, welches new aufgerufen wird (wenn auch indirekt über push_back() o.Ä.).

    Wenn ich das richtig sehe, bleibt der Code einer im Client aufgerufenen Funktion in der DLL, wenn entweder:

    • Die Funktionsdefinition in einer .cpp-Datei liegt
    • Die Funktionsdefinition inline ist und die Funktion exportiert wird
    • Die Memberfunktion vom Compiler generiert wird und ihre Klasse exportiert wird


  • Nexus schrieb:

    hustbaer schrieb:

    Wo der Code liegt, ist, so lange die DLL nicht zur Laufzeit entladen wird, egal.

    Naja, schliesslich entscheidet der Ort des Codes z.B. darüber, welches new aufgerufen wird (wenn auch indirekt über push_back() o.Ä.).

    Wenn ich das richtig sehe, bleibt der Code einer im Client aufgerufenen Funktion in der DLL, wenn entweder:

    • Die Funktionsdefinition in einer .cpp-Datei liegt
    • Die Funktionsdefinition inline ist und die Funktion exportiert wird
    • Die Memberfunktion vom Compiler generiert wird und ihre Klasse exportiert wird

    Nönönönö.
    Also Fall 1: ja. Passt.

    In Fall 2 ist aber nur sicher gestellt dass WENN der Compiler beschliesst dass Inlining in dem Fall keinen Sinn macht, dann und nur dann nimmt er den Code aus der DLL. (Ohne dllexport würde er die Funktion dann nämlich in der .exe nochmal instanzieren, und dann per normalen Aufruf die .exe Version aufrufen)
    Wenn der Compiler dagegen meint dass Inlining hier so richtig cool wäre, dann tut er auch inlinen.

    In Fall 3 ist es wenn ich mich richtig erinnere gleich wie bei Fall 2.



  • Ah, die Schlüsse aus meinen kurzen Tests waren wohl etwas voreilig 🙂

    Wenn man also sicher gehen will, dass der Code in der .dll bleibt, muss man ihn in die .cpp schreiben. Wird folglich auf massivstes Boilerplate-Coden hinauslaufen, da man jeweils schön die Grossen Drei definieren darf. Etwas, das ich seit kopierbaren Smart-Pointern fast immer vermieden habe...

    Übrigens, ist Code in der .cpp überhaupt komplett sicher (Linkzeit-Codegenerierung)?



  • Nexus schrieb:

    Übrigens, ist Code in der .cpp überhaupt komplett sicher (Linkzeit-Codegenerierung)?

    Ja, ist er.
    Derzeit zumindest.

    ps:
    Das mit dllexport/dllimport und inline steht hier:
    http://msdn.microsoft.com/en-us/library/xa0d9ste.aspx
    Da steht allerdings nix vonwegen compilergenerierten Funktionen. Ich gehe aber davon aus dass die gleichen Regeln gelten wie bei inline Funktionen.



  • Nexus schrieb:

    Wenn man also sicher gehen will, dass der Code in der .dll bleibt, muss man ihn in die .cpp schreiben. Wird folglich auf massivstes Boilerplate-Coden hinauslaufen, da man jeweils schön die Grossen Drei definieren darf. Etwas, das ich seit kopierbaren Smart-Pointern fast immer vermieden habe...

    Ich finde, peinlichst genau darauf zu achten, dass kein Code die DLL verlässt ist sogar der einzig gangbare Weg. Man (bzw zumindest ich) benutzt ja gerade DLLs um portable Module zu haben, die man "überall" einsetzen kann. Das bedeuetet auch, dass man die DLLs mit anderen Compilern verwendet, etc. Sobald da einem ein inline rausleaken würde, wäre es damit vorbei. IMHO sollte man daher auch darauf achten, nicht ganze Klassen zu exportieren. Macht man das doch, baut also eine unportable DLL, kann man sich den ganzen Aufwand auch sparen, die DLL als LIB erstellen und diese dann halt zu seinem Projekt dazulinken.



  • hustbaer schrieb:

    Das mit dllexport/dllimport und inline steht hier:
    http://msdn.microsoft.com/en-us/library/xa0d9ste.aspx

    Dort steht doch "You can define as inline a function with the dllexport attribute. In this case, the function is always instantiated and exported"... Also hängt es doch nicht davon ab, ob der Compiler Inlining für wichtig hält? Oder was verstehe ich falsch?

    Morle schrieb:

    Man (bzw zumindest ich) benutzt ja gerade DLLs um portable Module zu haben, die man "überall" einsetzen kann. Das bedeuetet auch, dass man die DLLs mit anderen Compilern verwendet, etc.

    Ich dachte, das C++ ABI wäre überall unterschiedlich (zum Teil schon bei verschiedenen Versionen des selben Compilers), daher müsste man bei solchen Vorhaben auf C-Schnittstellen zurückgreifen.



  • Nexus schrieb:

    Ich dachte, das C++ ABI wäre überall unterschiedlich (zum Teil schon bei verschiedenen Versionen des selben Compilers), daher müsste man bei solchen Vorhaben auf C-Schnittstellen zurückgreifen.

    Genau. Wenn Du portabel bleiben willst, darfst Du in der Schnittstelle nur simple POD Typen verwenden und immer eine DLL zur Verfügung stellen.
    Siehe WinAPI...



  • Nexus schrieb:

    hustbaer schrieb:

    Das mit dllexport/dllimport und inline steht hier:
    http://msdn.microsoft.com/en-us/library/xa0d9ste.aspx

    Dort steht doch "You can define as inline a function with the dllexport attribute. In this case, the function is always instantiated and exported"... Also hängt es doch nicht davon ab, ob der Compiler Inlining für wichtig hält? Oder was verstehe ich falsch?

    Oh boy.
    Natürlich wird die immer in der DLL instanziert und exportiert. Muss ja. Was exportiert wird ist bloss leider vollkommen egal, interessant ist nur was importiert wird.

    Wenn du 1.5 Sätze weiter gelesen hättest...

    You can also define as inline a function declared with the dllimport attribute. In this case, the function can be expanded (subject to /Ob specifications), but never instantiated.
    ...
    Exercise care when providing imported inline functions. For example, if you update the DLL, don't assume that the client will use the changed version of the DLL. To ensure that you are loading the proper version of the DLL, rebuild the DLL's client as well.

    Also genau was ich geschrieben habe: die Funktion kann dennoch inline expandiert werden, nur wird sie nie instanziert.

    🙄



  • Morle schrieb:

    Macht man das doch, baut also eine unportable DLL, kann man sich den ganzen Aufwand auch sparen, die DLL als LIB erstellen und diese dann halt zu seinem Projekt dazulinken.

    Weisst du, Freund Morle, es gibt auch noch andere gute Gründe dafür DLLs zu verwenden.


  • Mod

    hustbaer schrieb:

    Morle schrieb:

    Macht man das doch, baut also eine unportable DLL, kann man sich den ganzen Aufwand auch sparen, die DLL als LIB erstellen und diese dann halt zu seinem Projekt dazulinken.

    Weisst du, Freund Morle, es gibt auch noch andere gute Gründe dafür DLLs zu verwenden.

    Jo Zum Beispiel, wenn 10 Programme, die selbe Garif-Library, die selbe Packer-Lib, die gleiche statistische und matheamatische Bibliothek verwenden.
    Alle Exes stammen immer aus einem Guß mit den DLLs. Das spart aber immens Spiecher, wenn 10 dieser Proezesse gleichzeitig laufen, denn die DLLs sind dann zehnmal genutzt aber nur einmal im Speicher!

    Zudem ist der Pflegeaufwand geringer und das Setup auch. Ebenso werden Servicepacks einfacher.

    Keine dieser DLLs hat pure POD Schnittstellen.



  • @hustbaer, ich habe den ganzen Text gelesen, allerdings habe ich den dllimport -Teil falsch interpretiert. Danke für die Erklärung, ist aber dennoch kein Grund zum Aufregen 😉



  • Martin Richter schrieb:

    hustbaer schrieb:

    Morle schrieb:

    Macht man das doch, baut also eine unportable DLL, kann man sich den ganzen Aufwand auch sparen, die DLL als LIB erstellen und diese dann halt zu seinem Projekt dazulinken.

    Weisst du, Freund Morle, es gibt auch noch andere gute Gründe dafür DLLs zu verwenden.

    Jo Zum Beispiel, wenn 10 Programme, die selbe Garif-Library, die selbe Packer-Lib, die gleiche statistische und matheamatische Bibliothek verwenden.
    Alle Exes stammen immer aus einem Guß mit den DLLs.

    Genau. Gibts hier auch. Aus einem Guß heisst hier, dass die DLL in der Projektgruppe als Abhängigkeit der EXE mit drin ist. Hat zumindest fürs Kompilieren keinen Vorteil

    Das spart aber immens Spiecher, wenn 10 dieser Proezesse gleichzeitig laufen, denn die DLLs sind dann zehnmal genutzt aber nur einmal im Speicher!

    Zudem ist der Pflegeaufwand geringer und das Setup auch. Ebenso werden Servicepacks einfacher.

    Speichersparen unterschreibe ich sofort. Ist IMHO bei Gigabytes von Arbeitsspeicher nicht mehr ganz so wichtig. Wichtiger finde ich den Pflegeaufwand, und da kann ich nicht zustimmen: Denn aus einem Guß heisst, ich muss aufpassen, wer im Team was kompiliert, etc. Durch menschliche Fehler kann es vorgekommen, dass jemand seine Compileroptionen verändert und dadurch solche DLLs sofort inkompatibel werden.

    Daher habe ich hier für mich selbst eine Faustregel, dass es eine LIB wird, sofern die DLL in der selben Projektgruppe als Abhängigkeit zur EXE definiert wäre.


  • Mod

    Sicher hat es für das Kompilieren Vorteile.
    Es dauert nicht so lange. Die EXE ist schneller gelinkt.
    Der Pflegeaufwand bzgl. Hotfixes ist geringer.

    Wenn ich 10 15MB Projekte eine Lib staisch einlinke, wobei die hälfte des Codes aus DLLs stammen könnte macht dies auch im Entwicklungszyklus eine Beschleunigung aus.
    Zudem bleiben die Entwicklungseinheiten auch besser abgegrenzt.

    Das mit dem Compiler-Schalter kann ich nicht nachvollziehen.

    Im Team arbeiten wir mit dem TFS. Man kann also nicht "einfach was an den Projekteinstellungen ändern". Ausgeliefert wird nur, was aus dem Build-Server läuft. Nicht was von den Entwicklungsrechnern kommt.

    Aber jeder Entwickler hat möglichen Zugriff auf alle Teil Projekte/DLLs.


Anmelden zum Antworten