Wie übergebe ich Objekte, insbesondere STL-Objekte, sicher an und von einer DLL?

106

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.

Siehe steht mit Monica
quelle
4
Wenn Sie über die C ++ - Standardbibliothek sprechen, sollten Sie sie wahrscheinlich so nennen. STL kann je nach Kontext unterschiedliche Bedeutungen haben. (Siehe auch stackoverflow.com/questions/5205491/… )
Micha Wiedenmann

Antworten:

156

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 intkö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 packPrä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 . _cdeclist jedoch die Standardaufrufkonvention für C ++ - Funktionen. Dies ist also eine Sache, die nur dann unterbrochen wird, wenn Sie sie absichtlich durch Angabe einer _stdcallan einer Stelle und einer _cdeclan 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/ freeda 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 GetCCDLLmö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, GetProcAddressden 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 GetCCDLLFunktion, die ich etwas weiter oben erwähnt habe. Auf meinem System funktionieren die folgenden .def-Dateien für GCC bzw. MSVC:

GCC:

EXPORTS
    GetCCDLL=_Z8GetCCDLLv @1

MSVC:

EXPORTS
    GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1

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 packund seine Varianten. MSVC 2013 bietet diese Unterstützung ebenso wie neuere Versionen von GCC und Clang.

//POD_base.h: defines a template base class that wraps and unwraps data types for safe passing across compiler boundaries

//define malloc/free replacements to make use of Windows heap APIs
namespace pod_helpers
{
  void* pod_malloc(size_t size)
  {
    HANDLE heapHandle = GetProcessHeap();
    HANDLE storageHandle = nullptr;

    if (heapHandle == nullptr)
    {
      return nullptr;
    }

    storageHandle = HeapAlloc(heapHandle, 0, size);

    return storageHandle;
  }

  void pod_free(void* ptr)
  {
    HANDLE heapHandle = GetProcessHeap();
    if (heapHandle == nullptr)
    {
      return;
    }

    if (ptr == nullptr)
    {
      return;
    }

    HeapFree(heapHandle, 0, ptr);
  }
}

//define a template base class. We'll specialize this class for each datatype we want to pass across compiler boundaries.
#pragma pack(push, 1)
// All members are protected, because the class *must* be specialized
// for each type
template<typename T>
class pod
{
protected:
  pod();
  pod(const T& value);
  pod(const pod& copy);
  ~pod();

  pod<T>& operator=(pod<T> value);
  operator T() const;

  T get() const;
  void swap(pod<T>& first, pod<T>& second);
};
#pragma pack(pop)

//POD_basic_types.h: holds pod specializations for basic datatypes.
#pragma pack(push, 1)
template<>
class pod<unsigned int>
{
  //these are a couple of convenience typedefs that make the class easier to specialize and understand, since the behind-the-scenes logic is almost entirely the same except for the underlying datatypes in each specialization.
  typedef int original_type;
  typedef std::int32_t safe_type;

public:
  pod() : data(nullptr) {}

  pod(const original_type& value)
  {
    set_from(value);
  }

  pod(const pod<original_type>& copyVal)
  {
    original_type copyData = copyVal.get();
    set_from(copyData);
  }

  ~pod()
  {
    release();
  }

  pod<original_type>& operator=(pod<original_type> value)
  {
    swap(*this, value);

    return *this;
  }

  operator original_type() const
  {
    return get();
  }

protected:
  safe_type* data;

  original_type get() const
  {
    original_type result;

    result = static_cast<original_type>(*data);

    return result;
  }

  void set_from(const original_type& value)
  {
    data = reinterpret_cast<safe_type*>(pod_helpers::pod_malloc(sizeof(safe_type))); //note the pod_malloc call here - we want our memory buffer to go in the process heap, not the possibly-isolated DLL heap.

    if (data == nullptr)
    {
      return;
    }

    new(data) safe_type (value);
  }

  void release()
  {
    if (data)
    {
      pod_helpers::pod_free(data); //pod_free to go with the pod_malloc.
      data = nullptr;
    }
  }

  void swap(pod<original_type>& first, pod<original_type>& second)
  {
    using std::swap;

    swap(first.data, second.data);
  }
};
#pragma pack(pop)

Die podKlasse ist auf jeden grundlegenden Datentyp spezialisiert, sodass dieser intautomatisch umbrochen int32_t, uintumbrochen uint32_tusw. 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 (die boolSpezialisierung hat ein wenig zusätzliche Logik, da sie in a konvertiert wird int8_tund dann int8_tmit 0 verglichen wird, um sie wieder zu konvertieren bool, aber das ist ziemlich trivial).

Wir können STL-Typen auch auf diese Weise verpacken, obwohl dies ein wenig zusätzliche Arbeit erfordert:

#pragma pack(push, 1)
template<typename charT>
class pod<std::basic_string<charT>> //double template ftw. We're specializing pod for std::basic_string, but we're making this specialization able to be specialized for different types; this way we can support all the basic_string types without needing to create four specializations of pod.
{
  //more comfort typedefs
  typedef std::basic_string<charT> original_type;
  typedef charT safe_type;

public:
  pod() : data(nullptr) {}

  pod(const original_type& value)
  {
    set_from(value);
  }

  pod(const charT* charValue)
  {
    original_type temp(charValue);
    set_from(temp);
  }

  pod(const pod<original_type>& copyVal)
  {
    original_type copyData = copyVal.get();
    set_from(copyData);
  }

  ~pod()
  {
    release();
  }

  pod<original_type>& operator=(pod<original_type> value)
  {
    swap(*this, value);

    return *this;
  }

  operator original_type() const
  {
    return get();
  }

protected:
  //this is almost the same as a basic type specialization, but we have to keep track of the number of elements being stored within the basic_string as well as the elements themselves.
  safe_type* data;
  typename original_type::size_type dataSize;

  original_type get() const
  {
    original_type result;
    result.reserve(dataSize);

    std::copy(data, data + dataSize, std::back_inserter(result));

    return result;
  }

  void set_from(const original_type& value)
  {
    dataSize = value.size();

    data = reinterpret_cast<safe_type*>(pod_helpers::pod_malloc(sizeof(safe_type) * dataSize));

    if (data == nullptr)
    {
      return;
    }

    //figure out where the data to copy starts and stops, then loop through the basic_string and copy each element to our buffer.
    safe_type* dataIterPtr = data;
    safe_type* dataEndPtr = data + dataSize;
    typename original_type::const_iterator iter = value.begin();

    for (; dataIterPtr != dataEndPtr;)
    {
      new(dataIterPtr++) safe_type(*iter++);
    }
  }

  void release()
  {
    if (data)
    {
      pod_helpers::pod_free(data);
      data = nullptr;
      dataSize = 0;
    }
  }

  void swap(pod<original_type>& first, pod<original_type>& second)
  {
    using std::swap;

    swap(first.data, second.data);
    swap(first.dataSize, second.dataSize);
  }
};
#pragma pack(pop)

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.

//CCDLL.h: defines a DLL interface for a pod-based DLL
struct CCDLL_v1
{
  virtual void ShowMessage(const pod<std::wstring>* message) = 0;
};

CCDLL_v1* GetCCDLL();

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 a podselbst. Jetzt müssen wir das auf der DLL-Seite implementieren:

struct CCDLL_v1_implementation: CCDLL_v1
{
  virtual void ShowMessage(const pod<std::wstring>* message) override;
};

CCDLL_v1* GetCCDLL()
{
  static CCDLL_v1_implementation* CCDLL = nullptr;

  if (!CCDLL)
  {
    CCDLL = new CCDLL_v1_implementation;
  }

  return CCDLL;
}

Und jetzt implementieren wir die ShowMessageFunktion:

#include "CCDLL_implementation.h"
void CCDLL_v1_implementation::ShowMessage(const pod<std::wstring>* message)
{
  std::wstring workingMessage = *message;

  MessageBox(NULL, workingMessage.c_str(), TEXT("This is a cross-compiler message"), MB_OK);
}

Nichts Besonderes: Dies kopiert nur das übergebene podin ein normales wstringund 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:

//main.cpp
#include "../CCDLL/CCDLL.h"

typedef CCDLL_v1*(__cdecl* fnGetCCDLL)();
static fnGetCCDLL Ptr_GetCCDLL = NULL;

int main()
{
  HMODULE ccdll = LoadLibrary(TEXT("D:\\Programming\\C++\\CCDLL\\Debug_VS\\CCDLL.dll")); //I built the DLL with Visual Studio and the EXE with GCC. Your paths may vary.

  Ptr_GetCCDLL = (fnGetCCDLL)GetProcAddress(ccdll, (LPCSTR)"GetCCDLL");
  CCDLL_v1* CCDLL_lib;

  CCDLL_lib = Ptr_GetCCDLL(); //This calls the DLL's GetCCDLL method, which is an alias to the mangled function. By dynamically loading the DLL like this, we're completely bypassing the name mangling, exactly as expected.

  pod<std::wstring> message = TEXT("Hello world!");

  CCDLL_lib->ShowMessage(&message);

  FreeLibrary(ccdll); //unload the library when we're done with it

  return 0;
}

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.

Das Bild, das das Ergebnis danach zeigt.


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.

Siehe steht mit Monica
quelle
1
Hmm, nicht schlecht! Sie haben eine ziemlich gute Sammlung von Argumenten gegen die Verwendung von Standard-C ++ - Typen für die Interaktion mit einer Windows-DLL gezogen und entsprechend gekennzeichnet. Diese besonderen ABI-Einschränkungen gelten nicht für andere Toolchains als MSVC. Dies sollte sogar erwähnt werden ...
πάντα ῥεῖ
12
@ DavidHeffernan Richtig. Dies ist jedoch das Ergebnis mehrwöchiger Recherchen für mich. Ich dachte, es lohnt sich zu dokumentieren, was ich gelernt habe, damit andere nicht dieselben Recherchen und dieselben Versuche unternehmen müssen, eine funktionierende Lösung zusammen zu hacken. Dies gilt umso mehr, als dies hier eine halbwegs häufige Frage zu sein scheint.
Siehe mit Monica
@ πάνταῥεῖ Diese speziellen ABI-Einschränkungen gelten nicht für andere Toolchains als MSVC. Dies sollte sogar erwähnt werden ... Ich bin nicht sicher, ob ich das richtig verstehe. Zeigen Sie an, dass diese ABI-Probleme nur bei MSVC auftreten und beispielsweise eine mit clang erstellte DLL erfolgreich mit einer mit GCC erstellten EXE funktioniert? Ich bin ein bisschen verwirrt, da dies allen meinen Nachforschungen zu widersprechen scheint ...
vgl. Steht mit Monica am
@computerfreaker Nein, ich sage, dass PE und ELF unterschiedliche ABI-Formate verwenden ...
πάντα ῥεῖ
3
@computerfreaker Die meisten wichtigen C ++ - Compiler (GCC, Clang, ICC, EDG usw.) folgen dem Itanium C ++ ABI. MSVC nicht. Ja, diese ABI-Probleme sind weitgehend spezifisch für MSVC, wenn auch nicht ausschließlich - selbst C-Compiler auf Unix-Plattformen (und sogar verschiedene Versionen desselben Compilers!) Leiden unter einer nicht perfekten Interoperabilität. Sie sind jedoch normalerweise so nah, dass ich mich nicht wundern würde, wenn Sie feststellen könnten , dass Sie eine von Clang erstellte DLL erfolgreich mit einer von GCC erstellten ausführbaren Datei verknüpfen können.
Stuart Olsen
17

@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.

Ben Voigt
quelle
16

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.

Ph0t0n
quelle
Das ist ein guter Punkt ... Ich hätte sagen sollen "Hab keine Angst, Schnittstellen zu Klassen zu teilen". Ich werde meine Antwort bearbeiten.
Ph0t0n
2
Hey, das ist eine großartige Antwort, danke! Was es meiner Meinung nach noch besser machen würde, wären einige Links zur weiteren Lektüre, die einige Beispiele für die Dinge zeigen, die Sie erwähnen (oder sogar einen Code) - zum Beispiel zum Umschließen der STL-Klassen usw. Ansonsten lese ich Diese Antwort, aber dann bin ich ein bisschen verloren, wie diese Dinge tatsächlich aussehen würden und wie man nach ihnen sucht.
Ela782
8

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:

HowTo: Exportieren Sie C ++ - Klassen aus einer DLL

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.

Mr.C64
quelle