DLLs und moderne OOP, geht das überhaupt zusammen?



  • Hallo,

    Ich beschäftige mich seit kurzem damit ein Library-Design zu entwerfen und habe dabei in der Implementierung und den Interfaces viele modernere Techniken wie smart-Pointer, STL-Container, Iteratoren usw. verwendet. Ich erfreue mich daran wie elegant die Codes zusammen mit neuen C++ 11 Features wie 'range-based for' usw. spielen. Soweit so gut!

    Nun gibt es auf einmal die Anforderung diesen Code in Form einer DLL (MSVC 11.0+) auszugliedern. Aber da fängt es nun an... STL-Typen oder boost-Sachen mag der Linker im Interface überhaupt nicht, immer gibt es bei der expliziten Instanziierung und dem Export irgendwelche Probleme. Versuche ich das PIMPL-Idiom umzusetzen, dann habe ich immer noch das Problem Methoden wie begin/end bereitzustellen und muss also die Typen der Iteratoren bekannt machen, was wieder die o.g. Probleme beim Export/Import aufwirft. Strings/Vektoren und sonstige Container als Parameter austauschen ist auch ein ziemlicher Gau, weil ich aufpassen muss, dass nicht in der DLL Speicher angefordert wird, den man außerhalb löscht (z.B. Im Destruktor von std::string, wenn dieser in der DLL gefüttert wurde).

    Langsam beginne ich zu verzweifeln und frage mich, ob es überhaupt der richtige Weg ist ein vorhandenes Design in eine DLL zu packen, oder ob man unter Beachtung einiger Regeln neu anfangen sollte. Was meint Ihr dazu, gibt es irgendwelche 'best practices' für DLLs die neuere Entwicklungen nicht ignorieren oder sehe ich bloß den Wald vor lauter Bäumen nicht? Wenn man mal googled findet man viele Threads nach dem Motto "lass es am besten" oder "ja, wenn der Compiler auf beiden Seiten gleich ist und Du gleiche Flags verwendest, dann könnte es u.U. vielleicht in der Hälfte der Fälle klappen." Das ist natürlich ein wenig unbefriedigend.

    Ich habe irgendwie keine List C++ wie vor 20 Jahren zu schreiben, nur damit ich den Code in eine DLL ausgliedern kann. Eine Möglichkeit die ich sehe wäre noch eine komplette lowlevel-Implementierung auf die man dann einen schönen C++ Wrapper setzt der nur auf Headern basiert, aber das wäre eine ziemliche Produktivitätsbremse und auch nicht wirklich schön.

    Wie löst man sowas? Freue mich auf hilfreiche Hinweise!

    Viele Grüße!



  • goi schrieb:

    Wie löst man sowas?

    tja ... Mir wäre die Lösung von Problemen mit verschiedenen Formaten von Linkage, object files, libraries bedeutend wichtiger als manch andere neue Sprachfeatures, so nett diese auch sein mögen.

    Insbesondere, da C/C++ ja einen Ruf als geeignete Sprache zur Programmierung von Libraries hat.



  • goi schrieb:

    viele Threads nach dem Motto "lass es am besten" oder "ja, wenn der Compiler auf beiden Seiten gleich ist und Du gleiche Flags verwendest, dann könnte es u.U. vielleicht in der Hälfte der Fälle klappen." Das ist natürlich ein wenig unbefriedigend.

    Ja, weil so viele Leute Idioten sind.
    Wenn der Compiler MSVC ist und man überall die selbe MSVC Version und die selbe DLL Runtime verwendet, dann klappt es. In allen Fällen.
    OK, ein paar weitere Bedingungen gibt es schon noch (Dinge wie Structure Packing und allgemein alles was die ABI beeinflusst müssen natürlich übereinstimmen). Aber "könnte sein dass es manchmal geht" ist kompletter Blödsinn. Glaub mir, das funktioniert. Zuverlässig.

    (Ich verwende das seit über 15 Jahren mit verschiedenen MSVC Versionen - ich denke ich sollte wissen wovon ich rede. Und wir verwenden im Interface *alles was wir wollen*. Inklusive Boost Typen, bzw. auch Typen aus anderen Libs wie Xerces, Crypto++ etc.)



  • Ja, wir machen das intern auch so. Weil natürlich alle unsere internen Dlls einheitlich gebaut werden.
    Trotzdem gefällt mir die Situation nicht so wirklich, weil man damit keine API Dll für externe Entwickler rausgeben kann. Welchen Compiler und welche Einstellungen die benutzen können oder wollen wir nicht beeinflußen. Aber damit muss man halt leben. Dann versucht man eben, die Schnittstelle nach außen möglichst schmal zu halten und die "gefährlichen" Typen zu wrappen.



  • Naja, Probleme mit unterschiedlichen Compilern kann man bei C++ kaum verhindern. Der Compiler hat da einfach zu viele Freiheiten was die Implementierung von Standard-Library Klassen angeht.
    Was ja klare Vorteile hat, aber was die Interoperability angeht natürlich auch Nachteile.

    Bzw. es ist ja nichtmal eine ABI vorgeschrieben. D.h. wenn beide Compiler nicht zufällig gerade die Itanium ABI verwenden, dann müssen nichtmal so einfach Dinge wie der Aufruf einer virtuellen Funktion funktionieren.



  • hustbaer schrieb:

    Ja, weil so viele Leute Idioten sind.
    Wenn der Compiler MSVC ist und man überall die selbe MSVC Version und die selbe DLL Runtime verwendet, dann klappt es. In allen Fällen.
    OK, ein paar weitere Bedingungen gibt es schon noch (Dinge wie Structure Packing und allgemein alles was die ABI beeinflusst müssen natürlich übereinstimmen). Aber "könnte sein dass es manchmal geht" ist kompletter Blödsinn. Glaub mir, das funktioniert. Zuverlässig.

    Das heißt so lange wir VC++ einsetzen und für jede Version Kompilate erzeugen sollte alles glatt gehen? Wie schaut das mit Exceptions und Speicherbereichen aus? Muss man da was besonderes beachten?



  • Wichtig ist das man die DLL Version der C Runtime linkt damit alle DLLs die gleiche Runtime benutzen.



  • Genau.
    DLL Runtime verwenden.
    Was Exceptions angeht: kein Problem. "It just works."
    Speicherbereiche -- meinst du new/delete malloc/free, und wenn Speicher in einer DLL angefordert wird und in einer anderen freigegeben?
    Das ist auch kein Problem, da eben DLL Runtime (mit dem selben Set an statischen Variablen) = alle verwenden die selbe Runtime = alle verwenden den selben Heap.

    Soweit ich mich erinnern kann passen auch alle Default-Einstellungen. D.h. wenn du ein neues Projekt erstellst musst du nichts umstellen damit alles wie gewünscht funktioniert. Ich würde überhaupt empfehlen möglichst alle Default-Einstellungen zu verwenden -- die machen ziemlich gut Sinn. Wenn ihr einen guten Grund habt etwas umzustellen, OK, aber ansonsten würde ich da nicht unnötig rumschrauben.
    Bzw. es bietet sich auch an gemeinsame Propertysheets für alle DLLs + EXEn zu verwenden. Dann hat man einen zentralen Punkt wo man Dinge umstellen kann die in allen Projekten gleich sein sollen.

    Was Linkerprobleme angeht: wenn die DLL gebaut wird __declspec(dllexport) verwenden, und wenn sie "konsumiert" wird __declspec(dllimport) verwenden.
    D.h. natürlich dass das selbe Headerfile 1x dllexport (beim Bauen der DLL) und 1x dllimport (beim Verwenden der DLL) verwenden muss.
    Dazu macht man sich einfach Makros ala

    #ifdef FOO_BUILDING
    #define FOO_EXPORT __declspec(dllexport)
    #else
    #define FOO_EXPORT __declspec(dllimport)
    #endif
    
    class FOO_EXPORT FooThing
    {
      // ...
    };
    

    Und in den Projekteistellungen für die Foo DLL dann einstellen dass das Makro FOO_BUILDING definiert sein soll.

    Und das selbe natürlich für jeden andere DLL.

    Was das "für jede Version" angeht: ja. Wobei ich dir leider nicht genau sagen kann was als unterschiedliche Version zählt. Ich schätze es müsste reichen wenn man ein Kompilat pro "Hauptversion" anbietet. D.h. z.B. Visual C++ 2012, 2012 Update 1, 2012 Update 2 etc. sollten kompatibel sein.
    Der Grund warum ich mir nicht sicher bin: diesen Fall haben wir nicht. Wir bauen immer alles mit dem exakt selben Compiler. Aber ich schätze, da die Runtime DLLs kompatibel sind sollten auch selbst gebaute DLLs kompatibel sein.
    (Bzw. sobald die Runtime DLLs nicht kompatibel sind, muss man natürlich auch unterschiedliche Kompilate von eigenen DLLs anbieten.)

    Was auch cool ist: Seit ich glaube Visual Studio 2012 kannst du die selben Solution- und Project-Files verwenden um mit verschiedenen älteren Visual Studio Versionen zu compilieren. Die älteren Visual Studio Versionen müssen dazu zwar installiert sein, aber du kannst die Solution immer mit der neuesten Visual Studio Version aufmachen und dann einfach mit einem älteren Toolset bauen. Du kannst sogar verschiedene Project-Configurations erstellen die verschiedene Toolsets verwenden.
    D.h. du kannst alles so einrichten dass alle benötigten Kompilate mit vier Klicks gebaut werden können (BUILD -> Batch Build -> Select All -> Build). Bzw. genau so über einen einzigen Commandline Aufruf.



  • hustbaer schrieb:

    goi schrieb:

    viele Threads nach dem Motto "lass es am besten" oder "ja, wenn der Compiler auf beiden Seiten gleich ist und Du gleiche Flags verwendest, dann könnte es u.U. vielleicht in der Hälfte der Fälle klappen."

    Ja, weil so viele Leute Idioten sind.

    sag mal, geht's noch? Wer eine DLL mit Compiler A bauen soll, und mit Code von Compiler B linken will, ist ja wohl kein Idiot!

    Die Bedenken sind gerechtfertigt. Ich habe viel mit Multiplattform-Zeug zu tun und Code aus verschiedenen Compilern. Ein einheitliches Format für linkage, objects usw wäre ein Segen. Gerade für C++, das einen Namen als Sprache für Libraries hat. Aber ein Vöglein hat mir gezwitschert, daß da was unterwegs sein soll in Hinsicht portable C++ ABI.

    hustbaer schrieb:

    Wenn der Compiler MSVC ist und man überall die selbe MSVC Version und die selbe DLL Runtime verwendet, dann klappt es.

    Toll. Und wie verwende ich die "selbe DLL Runtime", wenn die DLL vor Jahren von jemand anders auf einer anderen Compilerplattform gebaut wurde?

    hustbaer schrieb:

    In allen Fällen. OK, ein paar weitere Bedingungen gibt es schon noch

    immer schön das Kleingedruckte lesen 😃



  • großbuchstaben schrieb:

    hustbaer schrieb:

    goi schrieb:

    viele Threads nach dem Motto "lass es am besten" oder "ja, wenn der Compiler auf beiden Seiten gleich ist und Du gleiche Flags verwendest, dann könnte es u.U. vielleicht in der Hälfte der Fälle klappen."

    Ja, weil so viele Leute Idioten sind.

    sag mal, geht's noch? Wer eine DLL mit Compiler A bauen soll, und mit Code von Compiler B linken will, ist ja wohl kein Idiot!

    Wer Fälle wo der Compiler nicht gleich ist, als Argument gegen eine Aussage anführt, die explizit auf "Compiler ist gleich" eingeschränkt war, ist ein Idiot. In diesem Fall also du.

    Vielleicht hast du mich auch falsch verstanden. Ich meine nicht die Leute die gerne unterschiedliche Compiler verwenden möchten sind Idioten. Ich meine die Leute die behaupten dass es auch mit identischem Compiler "hit and miss" ist sind Idioten. Weil das eben einfach nicht stimmt.

    großbuchstaben schrieb:

    Die Bedenken sind gerechtfertigt. Ich habe viel mit Multiplattform-Zeug zu tun und Code aus verschiedenen Compilern.

    Darum ging es bloss nie.

    großbuchstaben schrieb:

    Ein einheitliches Format für linkage, objects usw wäre ein Segen. Gerade für C++, das einen Namen als Sprache für Libraries hat. Aber ein Vöglein hat mir gezwitschert, daß da was unterwegs sein soll in Hinsicht portable C++ ABI.

    Ich hab' auch nie behauptet dass ein einheitliches Format unnütz wäre.

    Ich glaube aber du überschätzt den Nutzen, da auch ne standardisierte ABI + Object Format dir nicht ermöglicht nen std::string von einem Compiler an einen anderen zu übergeben.



  • ganz schön übel, wenn einem die Argumente ausgehen, was?



  • Ich bedanke mich jedenfalls schonmal sehr für die ausführliche Antwort von hustbaer, denn die hilft mir wirklich weiter. Wir werden die nächsten 10 Jahre nicht von Visual C++ loskommen, und wenn das doch mal der Fall sein sollte, dann muss man halt was Arbeit reinstecken. Aber hier und jetzt ist es ein Segen gewisse Sachen einfach machen zu können weil man weiß, dass man nicht völlige Binärkompatibilität braucht, sondern die Sachen einfach so bauen kann wie man will. Rausgegeben wird davon ohnehin nichts, und wenn doch, dann kann man das immer noch gescheit wrappen.

    Schönes Wochenende erstmal noch!



  • hustbaer schrieb:

    Wer Fälle wo der Compiler nicht gleich ist, als Argument gegen eine Aussage anführt, die explizit auf "Compiler ist gleich" eingeschränkt war, ist ein Idiot. In diesem Fall also du.

    @hustbaer: Soll das den Versuch einer Beleidigung darstellen, Ja oder Nein?



  • Ich würde es als Beobachtung bezeichnen.



  • Wenn man die DLL das Speichermanagement machen lässt, kann man auch unterschiedliche Compiler und Runtime-Versionen benutzen. Das ist überigens bei COM/ActiveX auch so! 😃 Und das sind bestimmt keine Idioten gewesen, die sich das ausgedacht haben.

    Nur hat man dann natürlich Einschränkungen bzw. Disziplin nötig und kann nicht so schön frei in C++ programmieren.

    Für Inhouse-Projekte sehe ich aber keinen Grund, warum man sich nicht auf einen Compiler bzw. eine Runtime-Version einigen können soll? Verstehe nicht, warum selbst Inhouse sich viele Coder gegenseitig das Leben schwer machen wollen. 😞



  • Ein Grund könnte z.B. sein, dass man Plugins für irgendwelche Systeme schreibt, und die Systeme erwarten, dass die nachzuladenden Dlls mit einem bestimmten Compiler gebaut werden. Wir schreiben Plugins für viele Systeme, die alle unterschiedliche Compiler haben wollen, das ist schon recht lustig.



  • @Mechanics
    Ja, es gibt immer wieder Gründe warum man nicht alles mit dem selben Compiler/Runtime/Switches bauen kann.
    Nur wenn man es kann, und auch davon ausgehen kann dass sich dieser Zustand in Zukunft aufrechterhalten lässt, dann besteht mMn. kein Grund interne Schnittstellen zu kastrieren.



  • Wenn du ein gutes Buch über API Design suchst, dann kann ich dir das Buch
    von Martin Reddy - API Design for C++, empfehlen.



  • DKlay schrieb:

    Wenn du ein gutes Buch über API Design suchst, dann kann ich dir das Buch
    von Martin Reddy - API Design for C++, empfehlen.

    Sieht gut aus. Habs mir mal bestellt.


Anmelden zum Antworten