Wie übergebe ich Klassenobjekte, insbesondere STL-Objekte, an und von einer C ++ - DLL?
Meine Anwendung muss mit Plugins von Drittanbietern in Form von DLL-Dateien interagieren, und ich kann nicht steuern, mit welchem Compiler diese Plugins erstellt werden. Mir ist bewusst, dass es keine garantierte ABI für STL-Objekte gibt, und ich mache mir Sorgen, dass meine Anwendung instabil wird.
Antworten:
Die kurze Antwort auf diese Frage lautet nicht . Da es kein Standard-C ++ - ABI gibt (Anwendungsbinärschnittstelle, Standard für Aufrufkonventionen, Datenpacken / -ausrichtung, Typgröße usw.), müssen Sie durch viele Rahmen springen, um eine Standardmethode für den Umgang mit Klassen durchzusetzen Objekte in Ihrem Programm. Es gibt nicht einmal eine Garantie, dass es funktioniert, nachdem Sie durch all diese Reifen gesprungen sind, noch gibt es eine Garantie, dass eine Lösung, die in einer Compiler-Version funktioniert, in der nächsten funktioniert.
Erstellen nur eine einfache C - Schnittstelle
extern "C"
, da die C ABI ist gut definiert und stabil.Wenn Sie C ++ - Objekte wirklich, wirklich über eine DLL-Grenze übergeben möchten, ist dies technisch möglich. Hier sind einige der Faktoren, die Sie berücksichtigen müssen:
Packen / Ausrichten von Daten
Innerhalb einer bestimmten Klasse werden einzelne Datenelemente normalerweise speziell im Speicher abgelegt, sodass ihre Adressen einem Vielfachen der Größe des Typs entsprechen. Beispielsweise
int
könnte a an einer 4-Byte-Grenze ausgerichtet sein.Wenn Ihre DLL mit einem anderen Compiler als Ihre EXE-Datei kompiliert wird, hat die DLL-Version einer bestimmten Klasse möglicherweise ein anderes Paket als die EXE-Version. Wenn die EXE das Klassenobjekt an die DLL übergibt, kann die DLL möglicherweise nicht ordnungsgemäß auf eine zugreifen gegebenes Datenelement innerhalb dieser Klasse. Die DLL würde versuchen, von der Adresse zu lesen, die durch ihre eigene Definition der Klasse angegeben ist, nicht durch die Definition der EXE, und da das gewünschte Datenelement dort nicht tatsächlich gespeichert ist, würden sich Müllwerte ergeben.
Sie können dies mit der
#pragma pack
Präprozessor-Direktive umgehen, die den Compiler zwingt, bestimmte Packungen anzuwenden. Der Compiler wendet weiterhin die Standardverpackung an, wenn Sie einen Packwert auswählen, der größer ist als der vom Compiler gewählte . Wenn Sie also einen großen Packwert auswählen, kann eine Klasse zwischen den Compilern immer noch unterschiedliche Packungen aufweisen. Die Lösung hierfür ist die Verwendung#pragma pack(1)
, die den Compiler zwingt, Datenelemente an einer Ein-Byte-Grenze auszurichten (im Wesentlichen wird kein Packen angewendet). Dies ist keine gute Idee, da dies auf bestimmten Systemen zu Leistungsproblemen oder sogar zum Absturz führen kann. Dadurch wird jedoch die Konsistenz bei der Ausrichtung der Datenelemente Ihrer Klasse im Speicher sichergestellt.Neuordnung der Mitglieder
Wenn Ihre Klasse kein Standardlayout hat , kann der Compiler seine Datenelemente im Speicher neu anordnen . Es gibt keinen Standard dafür, daher kann jede Neuordnung von Daten zu Inkompatibilitäten zwischen Compilern führen. Für die Weitergabe von Daten an eine DLL sind daher Standardlayoutklassen erforderlich.
Aufruf Konvention
Es gibt mehrere Aufrufkonventionen, die eine bestimmte Funktion haben kann. Diese Aufrufkonventionen legen fest, wie Daten an Funktionen übergeben werden sollen: Werden Parameter in Registern oder auf dem Stapel gespeichert? In welcher Reihenfolge werden Argumente auf den Stapel geschoben? Wer bereinigt alle auf dem Stapel verbleibenden Argumente, nachdem die Funktion beendet wurde?
Es ist wichtig, dass Sie eine Standard-Anrufkonvention einhalten. Wenn Sie eine Funktion als deklarieren
_cdecl
, wird die Standardeinstellung für C ++ und der Versuch, sie mit_stdcall
schlechten Dingen aufzurufen, passieren ._cdecl
ist jedoch die Standardaufrufkonvention für C ++ - Funktionen. Dies ist also eine Sache, die nur dann unterbrochen wird, wenn Sie sie absichtlich durch Angabe einer_stdcall
an einer Stelle und einer_cdecl
an einer anderen Stelle brechen .Datentypgröße
Laut dieser Dokumentation haben die meisten grundlegenden Datentypen unter Windows die gleichen Größen, unabhängig davon, ob Ihre App 32-Bit oder 64-Bit ist. Da die Größe eines bestimmten Datentyps jedoch vom Compiler und nicht von einem Standard erzwungen wird (alle Standardgarantien sind dies
1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
), empfiehlt es sich, Datentypen mit fester Größe zu verwenden, um die Kompatibilität der Datentypgröße nach Möglichkeit sicherzustellen.Haufenprobleme
Wenn Ihre DLL eine Verbindung zu einer anderen Version der C-Laufzeit als Ihre EXE-Datei herstellt, verwenden die beiden Module unterschiedliche Heaps . Dies ist ein besonders wahrscheinliches Problem, da die Module mit verschiedenen Compilern kompiliert werden.
Um dies zu vermeiden, muss der gesamte Speicher einem gemeinsam genutzten Heap zugewiesen und von demselben Heap freigegeben werden. Glücklicherweise bietet Windows APIs, die Ihnen dabei helfen: Mit GetProcessHeap können Sie auf den Heap der Host-EXE zugreifen, und mit HeapAlloc / HeapFree können Sie Speicher innerhalb dieses Heaps zuweisen und freigeben . Es ist wichtig, dass Sie nicht normal verwenden
malloc
/free
da es keine Garantie gibt, dass sie so funktionieren, wie Sie es erwarten.STL-Probleme
Die C ++ - Standardbibliothek hat ihre eigenen ABI-Probleme. Es gibt keine Garantie dafür, dass ein bestimmter STL-Typ im Speicher auf dieselbe Weise angeordnet ist, und es gibt auch keine Garantie dafür, dass eine bestimmte STL-Klasse von einer Implementierung zur anderen dieselbe Größe hat (insbesondere können Debug-Builds zusätzliche Debug-Informationen in a einfügen gegebener STL-Typ). Daher muss jeder STL-Container in grundlegende Typen entpackt werden, bevor er über die DLL-Grenze geleitet und auf der anderen Seite neu verpackt wird.
Name Mangling
Ihre DLL exportiert vermutlich Funktionen, die Ihre EXE aufrufen möchte. C ++ - Compiler verfügen jedoch nicht über eine Standardmethode zum Verwalten von Funktionsnamen . Dies bedeutet, dass eine benannte Funktion in GCC und in MSVC
GetCCDLL
möglicherweise beschädigt wird._Z8GetCCDLLv
?GetCCDLL@@YAPAUCCDLL_v1@@XZ
Sie können bereits keine statische Verknüpfung mit Ihrer DLL garantieren, da eine mit GCC erstellte DLL keine LIB-Datei erstellt und für die statische Verknüpfung einer DLL in MSVC eine erforderlich ist. Die dynamische Verknüpfung scheint eine viel sauberere Option zu sein, aber das Mangeln von Namen steht Ihnen im Weg: Wenn Sie versuchen,
GetProcAddress
den falschen Namen zu verwenden, schlägt der Aufruf fehl und Sie können Ihre DLL nicht verwenden. Dies erfordert ein wenig Hacking, um herumzukommen, und ist ein ziemlich wichtiger Grund, warum das Übergeben von C ++ - Klassen über eine DLL-Grenze eine schlechte Idee ist.Sie müssen Ihre DLL erstellen und dann die erstellte .def-Datei untersuchen (falls eine erstellt wird; dies hängt von Ihren Projektoptionen ab) oder ein Tool wie Dependency Walker verwenden, um den verstümmelten Namen zu finden. Anschließend müssen Sie Ihre eigene .def-Datei schreiben und einen nicht verschränkten Alias für die entstellte Funktion definieren. Verwenden wir als Beispiel die
GetCCDLL
Funktion, die ich etwas weiter oben erwähnt habe. Auf meinem System funktionieren die folgenden .def-Dateien für GCC bzw. MSVC:GCC:
MSVC:
Erstellen Sie Ihre DLL neu und überprüfen Sie die exportierten Funktionen erneut. Ein nicht verwickelter Funktionsname sollte darunter sein. Beachten Sie, dass Sie überladene Funktionen nicht auf diese Weise verwenden können : Der Name der nicht verschränkten Funktion ist ein Alias für eine bestimmte Funktionsüberladung, wie durch den entstellten Namen definiert. Beachten Sie außerdem, dass Sie bei jeder Änderung der Funktionsdeklarationen eine neue .def-Datei für Ihre DLL erstellen müssen, da sich die entstellten Namen ändern. Am wichtigsten ist, dass Sie durch Umgehen des Namens Mangling alle Schutzmaßnahmen außer Kraft setzen, die der Linker Ihnen in Bezug auf Inkompatibilitätsprobleme bieten möchte.
Dieser gesamte Vorgang ist einfacher, wenn Sie eine Schnittstelle für Ihre DLL erstellen , da Sie nur eine Funktion zum Definieren eines Alias haben, anstatt für jede Funktion in Ihrer DLL einen Alias erstellen zu müssen. Es gelten jedoch weiterhin die gleichen Einschränkungen.
Klassenobjekte an eine Funktion übergeben
Dies ist wahrscheinlich das subtilste und gefährlichste Problem, das die Übergabe von Cross-Compiler-Daten plagt. Selbst wenn Sie alles andere erledigen, gibt es keinen Standard dafür, wie Argumente an eine Funktion übergeben werden . Dies kann zu subtilen Abstürzen ohne ersichtlichen Grund und ohne einfache Möglichkeit zum Debuggen führen . Sie müssen alle Argumente über Zeiger übergeben, einschließlich Puffer für alle Rückgabewerte. Dies ist ungeschickt und unpraktisch und eine weitere hackige Problemumgehung, die möglicherweise funktioniert oder nicht.
Wenn wir all diese Problemumgehungen zusammenstellen und auf kreativer Arbeit mit Vorlagen und Operatoren aufbauen , können wir versuchen, Objekte sicher über eine DLL-Grenze zu übergeben. Beachten Sie, dass die Unterstützung von C ++ 11 obligatorisch ist, ebenso wie die Unterstützung für
#pragma pack
und seine Varianten. MSVC 2013 bietet diese Unterstützung ebenso wie neuere Versionen von GCC und Clang.Die
pod
Klasse ist auf jeden grundlegenden Datentyp spezialisiert, sodass dieserint
automatisch umbrochenint32_t
,uint
umbrochenuint32_t
usw. wird. Dies alles geschieht dank der Überlastung=
und der()
Operatoren hinter den Kulissen . Ich habe den Rest der grundlegenden Typspezialisierungen weggelassen, da sie bis auf die zugrunde liegenden Datentypen fast identisch sind (diebool
Spezialisierung hat ein wenig zusätzliche Logik, da sie in a konvertiert wirdint8_t
und dannint8_t
mit 0 verglichen wird, um sie wieder zu konvertierenbool
, aber das ist ziemlich trivial).Wir können STL-Typen auch auf diese Weise verpacken, obwohl dies ein wenig zusätzliche Arbeit erfordert:
Jetzt können wir eine DLL erstellen, die diese Pod-Typen verwendet. Zuerst brauchen wir eine Schnittstelle, damit wir nur eine Methode haben, um das Mangeln herauszufinden.
Dadurch wird lediglich eine grundlegende Schnittstelle erstellt, die sowohl die DLL als auch alle Anrufer verwenden können. Beachten Sie, dass wir einen Zeiger auf a übergeben
pod
, nicht auf apod
selbst. Jetzt müssen wir das auf der DLL-Seite implementieren:Und jetzt implementieren wir die
ShowMessage
Funktion:Nichts Besonderes: Dies kopiert nur das übergebene
pod
in ein normaleswstring
und zeigt es in einer Messagebox. Immerhin ist dies nur ein POC , keine vollständige Dienstprogrammbibliothek.Jetzt können wir die DLL erstellen. Vergessen Sie nicht die speziellen .def-Dateien, um die Namensverknüpfung des Linkers zu umgehen. (Hinweis: Die CCDLL-Struktur, die ich tatsächlich erstellt und ausgeführt habe, hatte mehr Funktionen als die hier vorgestellte. Die .def-Dateien funktionieren möglicherweise nicht wie erwartet.)
Nun soll eine EXE die DLL aufrufen:
Und hier sind die Ergebnisse. Unsere DLL funktioniert. Wir haben erfolgreich frühere STL-ABI-Probleme, frühere C ++ ABI-Probleme und frühere Mangling-Probleme erreicht, und unsere MSVC-DLL arbeitet mit einer GCC-EXE.
Zum Schluss, wenn Sie unbedingt müssen C ++ Objekte über DLL Grenzen passieren, das ist , wie Sie es tun. Es ist jedoch garantiert, dass nichts davon mit Ihrem Setup oder dem eines anderen funktioniert. All dies kann jederzeit unterbrochen werden und wird wahrscheinlich am Tag vor der geplanten Veröffentlichung einer größeren Version Ihrer Software unterbrochen. Dieser Weg ist voller Hacks, Risiken und allgemeiner Idiotie, für die ich wahrscheinlich erschossen werden sollte. Wenn Sie diesen Weg gehen, testen Sie bitte mit äußerster Vorsicht. Und wirklich ... mach das einfach überhaupt nicht.
quelle
@computerfreaker hat eine großartige Erklärung dafür geschrieben, warum das Fehlen von ABI im Allgemeinen verhindert, dass C ++ - Objekte über DLL-Grenzen hinweg übertragen werden, selbst wenn die Typdefinitionen vom Benutzer gesteuert werden und in beiden Programmen genau dieselbe Token-Sequenz verwendet wird. (Es gibt zwei Fälle, die funktionieren: Standardlayoutklassen und reine Schnittstellen)
Bei im C ++ - Standard definierten Objekttypen (einschließlich der aus der Standardvorlagenbibliothek angepassten) ist die Situation weitaus schlimmer. Die Token, die diese Typen definieren, sind für mehrere Compiler NICHT gleich, da der C ++ - Standard keine vollständige Typdefinition bietet, sondern nur Mindestanforderungen. Darüber hinaus wird die Namenssuche der in diesen Typdefinitionen enthaltenen Bezeichner nicht gleich aufgelöst. Selbst auf Systemen, auf denen ein C ++ - ABI vorhanden ist, führt der Versuch, solche Typen über Modulgrenzen hinweg gemeinsam zu nutzen, aufgrund von Verstößen gegen die One Definition Rule zu einem massiven undefinierten Verhalten.
Dies ist etwas, mit dem Linux-Programmierer nicht vertraut waren, da libstdc ++ von g ++ ein De-facto-Standard war und praktisch alle Programme ihn verwendeten, wodurch die ODR erfüllt wurde. clangs libc ++ hat diese Annahme gebrochen, und dann hat C ++ 11 obligatorische Änderungen an fast allen Standardbibliothekstypen vorgenommen.
Teilen Sie Standardbibliothekstypen nur nicht zwischen Modulen. Es ist undefiniertes Verhalten.
quelle
Einige der Antworten hier lassen das Bestehen von C ++ - Klassen wirklich beängstigend klingen, aber ich möchte eine andere Sichtweise teilen. Die in einigen anderen Antworten erwähnte reine virtuelle C ++ - Methode erweist sich tatsächlich als sauberer als Sie vielleicht denken. Ich habe ein komplettes Plugin-System um das Konzept herum aufgebaut und es funktioniert seit Jahren sehr gut. Ich habe eine "PluginManager" -Klasse, die die DLLs aus einem angegebenen Verzeichnis mithilfe von LoadLib () und GetProcAddress () dynamisch lädt (und die Linux-Entsprechungen, damit die ausführbare Datei plattformübergreifend ist).
Ob Sie es glauben oder nicht, diese Methode ist verzeihend, selbst wenn Sie einige verrückte Dinge tun, wie das Hinzufügen einer neuen Funktion am Ende Ihrer reinen virtuellen Schnittstelle und versuchen, DLLs zu laden, die ohne diese neue Funktion gegen die Schnittstelle kompiliert wurden - sie werden einwandfrei geladen. Natürlich ... müssen Sie eine Versionsnummer überprüfen, um sicherzustellen, dass Ihre ausführbare Datei die neue Funktion nur für neuere DLLs aufruft, die die Funktion implementieren. Aber die gute Nachricht ist: Es funktioniert! In gewisser Weise haben Sie eine grobe Methode, um Ihre Benutzeroberfläche im Laufe der Zeit weiterzuentwickeln.
Eine weitere coole Sache bei reinen virtuellen Schnittstellen: Sie können so viele Schnittstellen erben, wie Sie möchten, und Sie werden nie auf das Diamantproblem stoßen!
Ich würde sagen, der größte Nachteil dieses Ansatzes ist, dass Sie sehr vorsichtig sein müssen, welche Typen Sie als Parameter übergeben. Keine Klassen oder STL-Objekte, ohne sie zuerst mit reinen virtuellen Schnittstellen zu versehen. Keine Strukturen (ohne das Pragma Pack Voodoo durchzugehen). Nur primitive Typen und Zeiger auf andere Schnittstellen. Außerdem können Sie Funktionen nicht überladen, was eine Unannehmlichkeit ist, aber kein Show-Stopper.
Die gute Nachricht ist, dass Sie mit einer Handvoll Codezeilen wiederverwendbare generische Klassen und Schnittstellen erstellen können, um STL-Zeichenfolgen, Vektoren und andere Containerklassen zu verpacken. Alternativ können Sie Ihrer Benutzeroberfläche Funktionen wie GetCount () und GetVal (n) hinzufügen, damit Benutzer Listen durchlaufen können.
Leute, die Plugins für uns erstellen, finden es ziemlich einfach. Sie müssen keine Experten für die ABI-Grenze sein oder so - sie erben nur die Schnittstellen, an denen sie interessiert sind, codieren die von ihnen unterstützten Funktionen und geben false für diejenigen zurück, die sie nicht unterstützen.
Die Technologie, mit der all dies funktioniert, basiert meines Wissens nicht auf einem Standard. Soweit ich weiß, hat Microsoft beschlossen, ihre virtuellen Tabellen auf diese Weise zu erstellen, damit sie COM erstellen können, und andere Compiler-Autoren haben beschlossen, diesem Beispiel zu folgen. Dies umfasst GCC, Intel, Borland und die meisten anderen wichtigen C ++ - Compiler. Wenn Sie vorhaben, einen obskuren eingebetteten Compiler zu verwenden, funktioniert dieser Ansatz wahrscheinlich nicht für Sie. Theoretisch könnte jede Compilerfirma ihre virtuellen Tabellen jederzeit ändern und Dinge kaputt machen, aber angesichts der enormen Menge an Code, die über die Jahre geschrieben wurde und von dieser Technologie abhängt, wäre ich sehr überrascht, wenn sich einer der Hauptakteure dazu entschließen würde, den Rang zu brechen.
Die Moral der Geschichte lautet also ... Mit Ausnahme einiger extremer Umstände benötigen Sie eine Person, die für die Schnittstellen verantwortlich ist und sicherstellen kann, dass die ABI-Grenze bei primitiven Typen sauber bleibt und eine Überlastung vermieden wird. Wenn Sie mit dieser Bestimmung einverstanden sind, hätte ich keine Angst davor, Schnittstellen zu Klassen in DLLs / SOs zwischen Compilern zu teilen. Das direkte Teilen von Klassen == Probleme, aber das Teilen von reinen virtuellen Schnittstellen ist nicht so schlecht.
quelle
Sie können STL-Objekte nicht sicher über DLL-Grenzen hinweg übergeben, es sei denn, alle Module (.EXE und .DLLs) werden mit derselben C ++ - Compilerversion und denselben Einstellungen und Varianten der CRT erstellt, was sehr einschränkend ist und eindeutig nicht in Ihrem Fall ist.
Wenn Sie eine objektorientierte Schnittstelle aus Ihrer DLL verfügbar machen möchten, sollten Sie reine C ++ - Schnittstellen verfügbar machen (ähnlich wie bei COM). Lesen Sie diesen interessanten Artikel über CodeProject:
Möglicherweise möchten Sie auch eine reine C-Schnittstelle an der DLL-Grenze verfügbar machen und anschließend einen C ++ - Wrapper am Aufruferstandort erstellen.
Dies ähnelt dem, was in Win32 passiert: Der Win32-Implementierungscode ist fast C ++, aber viele Win32-APIs stellen eine reine C-Schnittstelle bereit (es gibt auch APIs, die COM-Schnittstellen verfügbar machen). Dann umschließen ATL / WTL und MFC diese reinen C-Schnittstellen mit C ++ - Klassen und -Objekten.
quelle