Comment transmettre en toute sécurité des objets, en particulier des objets STL, vers et depuis une DLL?

106

Comment passer des objets de classe, en particulier des objets STL, vers et depuis une DLL C ++?

Mon application doit interagir avec des plugins tiers sous la forme de fichiers DLL, et je ne peux pas contrôler avec quel compilateur ces plugins sont créés. Je suis conscient qu'il n'y a pas d'ABI garanti pour les objets STL et je crains de provoquer une instabilité dans mon application.

cf se tient avec Monica
la source
4
Si vous parlez de la bibliothèque standard C ++, vous devriez probablement l'appeler ainsi. STL peut signifier différentes choses selon le contexte. (Voir aussi stackoverflow.com/questions/5205491/… )
Micha Wiedenmann

Réponses:

156

La réponse courte à cette question est non . Parce qu'il n'y a pas d' ABI C ++ standard (interface binaire d'application, un standard pour les conventions d'appel, l'empaquetage / alignement des données, la taille du type, etc.), vous devrez sauter à travers de nombreux obstacles pour essayer d'appliquer une manière standard de gérer la classe objets dans votre programme. Il n'y a même pas de garantie que cela fonctionnera après avoir franchi tous ces obstacles, ni de garantie qu'une solution qui fonctionne dans une version du compilateur fonctionnera dans la suivante.

Créez simplement une interface C simple en utilisant extern "C", car l'ABI C est bien défini et stable.


Si vous voulez vraiment, vraiment passer des objets C ++ à travers une limite DLL, c'est techniquement possible. Voici quelques-uns des facteurs dont vous devrez tenir compte:

Emballage / alignement des données

Dans une classe donnée, les membres de données individuels seront généralement placés spécialement en mémoire afin que leurs adresses correspondent à un multiple de la taille du type. Par exemple, un intpeut être aligné sur une limite de 4 octets.

Si votre DLL est compilée avec un compilateur différent de votre EXE, la version de la DLL d'une classe donnée peut avoir un emballage différent de la version de l'EXE, donc lorsque l'EXE passe l'objet de classe à la DLL, la DLL peut être incapable d'accéder correctement à un donnée membre de cette classe. La DLL essaierait de lire à partir de l'adresse spécifiée par sa propre définition de la classe, et non par la définition de l'EXE, et puisque le membre de données souhaité n'y est pas réellement stocké, des valeurs inutiles en résulteraient.

Vous pouvez contourner ce #pragma packproblème à l'aide de la directive préprocesseur, qui forcera le compilateur à appliquer une compression spécifique. Le compilateur appliquera toujours l'empaquetage par défaut si vous sélectionnez une valeur de paquet plus grande que celle que le compilateur aurait choisie , donc si vous choisissez une valeur d'emballage élevée, une classe peut toujours avoir un empaquetage différent entre les compilateurs. La solution pour cela est d'utiliser #pragma pack(1), ce qui forcera le compilateur à aligner les données membres sur une limite d'un octet (essentiellement, aucun compactage ne sera appliqué). Ce n'est pas une bonne idée, car cela peut entraîner des problèmes de performances ou même des plantages sur certains systèmes. Cependant, il va assurer la cohérence de la façon dont les membres de données de votre classe sont alignés en mémoire.

Réorganisation des membres

Si votre classe n'est pas à disposition standard , le compilateur peut réorganiser ses données membres en mémoire . Il n'y a pas de norme sur la façon dont cela est fait, donc toute réorganisation des données peut provoquer des incompatibilités entre les compilateurs. La transmission de données dans les deux sens à une DLL nécessitera donc des classes de mise en page standard.

Convention d'appel

Une fonction donnée peut avoir plusieurs conventions d'appel . Ces conventions d'appel spécifient comment les données doivent être passées aux fonctions: les paramètres sont-ils stockés dans des registres ou sur la pile? Dans quel ordre les arguments sont-ils poussés sur la pile? Qui nettoie les arguments laissés sur la pile une fois la fonction terminée?

Il est important que vous mainteniez une convention d'appel standard; si vous déclarez une fonction comme _cdecl, la valeur par défaut pour C ++, et essayez de l'appeler en utilisant de _stdcall mauvaises choses se produira . _cdeclest la convention d'appel par défaut pour les fonctions C ++, cependant, c'est une chose qui ne cassera pas à moins que vous ne la rompiez délibérément en spécifiant un _stdcallà un endroit et un _cdeclà un autre.

Taille du type de données

Selon cette documentation , sous Windows, la plupart des types de données fondamentaux ont les mêmes tailles, que votre application soit 32 bits ou 64 bits. Cependant, étant donné que la taille d'un type de données donné est imposée par le compilateur, et non par aucune norme (toutes les garanties standard sont que 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)), c'est une bonne idée d'utiliser des types de données de taille fixe pour assurer la compatibilité de la taille des types de données lorsque cela est possible.

Problèmes de tas

Si votre DLL est liée à une version différente du runtime C que votre EXE, les deux modules utiliseront des tas différents . C'est un problème particulièrement probable étant donné que les modules sont compilés avec différents compilateurs.

Pour atténuer cela, toute la mémoire devra être allouée dans un tas partagé et désallouée à partir du même tas. Heureusement, Windows fournit des API pour vous aider: GetProcessHeap vous permettra d'accéder au tas de fichiers EXE de l'hôte, et HeapAlloc / HeapFree vous permettra d'allouer et de libérer de la mémoire dans ce tas. Il est important que vous n'utilisiez pas normal malloc/ freecar il n'y a aucune garantie qu'ils fonctionneront comme vous le souhaitez.

Problèmes de STL

La bibliothèque standard C ++ a son propre ensemble de problèmes ABI. Il n'y a aucune garantie qu'un type STL donné est disposé de la même manière en mémoire, ni aucune garantie qu'une classe STL donnée a la même taille d'une implémentation à une autre (en particulier, les versions de débogage peuvent mettre des informations de débogage supplémentaires dans un donné le type de STL). Par conséquent, tout conteneur STL devra être décompressé en types fondamentaux avant d'être passé à travers la limite DLL et reconditionné de l'autre côté.

Nom mutilation

Votre DLL exportera vraisemblablement les fonctions que votre EXE voudra appeler. Cependant, les compilateurs C ++ n'ont pas de méthode standard pour modifier les noms de fonctions . Cela signifie qu'une fonction nommée GetCCDLLpeut être transformée _Z8GetCCDLLven GCC et ?GetCCDLL@@YAPAUCCDLL_v1@@XZMSVC.

Vous ne pourrez déjà pas garantir la liaison statique vers votre DLL, car une DLL produite avec GCC ne produira pas de fichier .lib et la liaison statique d'une DLL dans MSVC en nécessite un. La liaison dynamique semble être une option beaucoup plus propre, mais la modification des noms vous gêne: si vous essayez GetProcAddressd'utiliser le mauvais nom mutilé, l'appel échouera et vous ne pourrez pas utiliser votre DLL. Cela nécessite un peu de piratage pour se déplacer, et c'est une raison assez importante pour laquelle le passage de classes C ++ à travers une limite DLL est une mauvaise idée.

Vous devrez créer votre DLL, puis examiner le fichier .def produit (le cas échéant; cela variera en fonction des options de votre projet) ou utiliser un outil comme Dependency Walker pour trouver le nom mutilé. Ensuite, vous devrez écrire votre propre fichier .def, définissant un alias démêlé pour la fonction mutilée. À titre d'exemple, utilisons la GetCCDLLfonction que j'ai mentionnée un peu plus haut. Sur mon système, les fichiers .def suivants fonctionnent respectivement pour GCC et MSVC:

GCC:

EXPORTS
    GetCCDLL=_Z8GetCCDLLv @1

MSVC:

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

Reconstruisez votre DLL, puis réexaminez les fonctions qu'elle exporte. Un nom de fonction démêlé devrait en faire partie. Notez que vous ne pouvez pas utiliser les fonctions surchargées de cette manière : le nom de la fonction non mélangée est un alias pour une surcharge de fonction spécifique telle que définie par le nom mutilé. Notez également que vous devrez créer un nouveau fichier .def pour votre DLL chaque fois que vous modifiez les déclarations de fonction, car les noms mutilés changeront. Plus important encore, en contournant le nom de la déformation, vous remplacez toutes les protections que l'éditeur de liens essaie de vous offrir en ce qui concerne les problèmes d'incompatibilité.

Tout ce processus est plus simple si vous créez une interface à suivre pour votre DLL, car vous n'aurez qu'une seule fonction pour définir un alias au lieu de devoir créer un alias pour chaque fonction de votre DLL. Cependant, les mêmes mises en garde s'appliquent toujours.

Passer des objets de classe à une fonction

C'est probablement le plus subtil et le plus dangereux des problèmes qui affectent le passage des données entre les compilateurs. Même si vous gérez tout le reste, il n'y a pas de norme sur la façon dont les arguments sont passés à une fonction . Cela peut provoquer des plantages subtils sans raison apparente et sans moyen facile de les déboguer . Vous devrez passer tous les arguments via des pointeurs, y compris les tampons pour toutes les valeurs de retour. C'est maladroit et peu pratique, et c'est encore une autre solution de contournement hacky qui peut ou non fonctionner.


En rassemblant toutes ces solutions de contournement et en nous appuyant sur un travail créatif avec des modèles et des opérateurs , nous pouvons tenter de transmettre en toute sécurité des objets à travers une limite de DLL. Notez que la prise en charge de C ++ 11 est obligatoire, de même que la prise en charge #pragma packet ses variantes; MSVC 2013 offre ce support, tout comme les versions récentes de GCC et 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)

La podclasse est spécialisée pour chaque type de données de base, de sorte qu'il intsera automatiquement enveloppé int32_t, uintsera enveloppé uint32_t, etc. Tout cela se produit dans les coulisses, grâce aux opérateurs surchargés =et (). J'ai omis le reste des spécialisations de type de base car elles sont presque entièrement les mêmes, à l'exception des types de données sous-jacents (la boolspécialisation a un peu de logique supplémentaire, car elle est convertie en a int8_t, puis int8_test comparée à 0 pour la reconvertir en bool, mais c'est assez trivial).

Nous pouvons également envelopper les types STL de cette manière, même si cela nécessite un peu de travail supplémentaire:

#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)

Nous pouvons maintenant créer une DLL qui utilise ces types de pod. Tout d'abord, nous avons besoin d'une interface, nous n'aurons donc qu'une seule méthode pour déterminer la manipulation.

//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();

Cela crée simplement une interface de base que la DLL et tous les appelants peuvent utiliser. Notez que nous passons un pointeur vers a pod, pas vers podlui - même. Nous devons maintenant implémenter cela du côté DLL:

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;
}

Et maintenant, implémentons la ShowMessagefonction:

#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);
}

Rien d'extraordinaire: cela copie simplement le passé poddans une normale wstringet l'affiche dans une boîte de message. Après tout, ce n'est qu'un POC , pas une bibliothèque d'utilitaires complète.

Nous pouvons maintenant créer la DLL. N'oubliez pas les fichiers .def spéciaux pour contourner le changement de nom de l'éditeur de liens. (Remarque: la structure CCDLL que j'ai réellement construite et exécutée avait plus de fonctions que celle que je présente ici. Les fichiers .def peuvent ne pas fonctionner comme prévu.)

Maintenant, pour un EXE pour appeler la DLL:

//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;
}

Et voici les résultats. Notre DLL fonctionne. Nous avons réussi à atteindre les problèmes STL ABI passés, les problèmes ABI C ++ passés, les problèmes antérieurs de déformation, et notre DLL MSVC fonctionne avec un EXE GCC.

L'image qui montre le résultat par la suite.


En conclusion, si vous devez absolument transmettre des objets C ++ au-delà des limites de la DLL, voici comment procéder. Cependant, rien de tout cela n'est garanti pour fonctionner avec votre configuration ou celle de quelqu'un d'autre. Tout ceci peut être interrompu à tout moment, et probablement le jour avant que votre logiciel ne soit prévu pour une version majeure. Ce chemin est plein de hacks, de risques et d'idioties générales pour lesquelles je devrais probablement me faire tirer dessus. Si vous suivez cette voie, veuillez tester avec une extrême prudence. Et vraiment ... ne faites pas ça du tout.

cf se tient avec Monica
la source
1
Hmm, pas mal! Vous avez extrait une assez bonne collection d'arguments contre l'utilisation de types C ++ standard pour interagir avec une DLL Windows et étiquetés en conséquence. Ces restrictions ABI particulières ne s'appliquent pas à d'autres chaînes d'outils que MSVC. Cela devrait même être mentionné ...
πάντα ῥεῖ
12
@DavidHeffernan C'est vrai. Mais c'est le résultat de plusieurs semaines de recherche pour moi, alors j'ai pensé qu'il serait intéressant de documenter ce que j'ai appris pour que les autres n'aient pas besoin de faire ces mêmes recherches et ces mêmes tentatives pour pirater ensemble une solution de travail. D'autant plus que cela semble être une question semi-courante ici.
cf se tient avec Monica le
@ πάνταῥεῖ Ces restrictions ABI particulières ne s'appliqueront pas à d'autres chaînes d'outils que MSVC. Cela devrait même être mentionné ... Je ne suis pas sûr de bien comprendre cela. Êtes-vous en train d'indiquer que ces problèmes ABI sont exclusifs à MSVC, et, par exemple, une DLL construite avec clang fonctionnera avec succès avec un EXE construit avec GCC? Je suis un peu confus, car cela semble contradictoire avec toutes mes recherches ...
cf se tient aux côtés de Monica
@computerfreaker Non, je dis que PE et ELF utilisent des formats ABI différents ...
πάντα ῥε
3
@computerfreaker La plupart des principaux compilateurs C ++ (GCC, Clang, ICC, EDG, etc.) suivent l'ABI Itanium C ++. MSVC ne le fait pas. Alors oui, ces problèmes ABI sont largement spécifiques à MSVC, mais pas exclusivement - même les compilateurs C sur les plates-formes Unix (et même les différentes versions du même compilateur!) Souffrent d'une interopérabilité moins que parfaite. Ils sont généralement assez proches, cependant, que je ne serais pas du tout surpris de constater que vous pourriez réussir à lier une DLL construite par Clang avec un exécutable construit par GCC.
Stuart Olsen
17

@computerfreaker a écrit une excellente explication de la raison pour laquelle l'absence d'ABI empêche le passage d'objets C ++ à travers les limites de la DLL dans le cas général, même lorsque les définitions de type sont sous le contrôle de l'utilisateur et que la même séquence de jetons est utilisée dans les deux programmes. (Il y a deux cas qui fonctionnent: les classes de disposition standard et les interfaces pures)

Pour les types d'objets définis dans la norme C ++ (y compris ceux adaptés de la bibliothèque de modèles standard), la situation est bien, bien pire. Les jetons définissant ces types ne sont PAS les mêmes sur plusieurs compilateurs, car la norme C ++ ne fournit pas une définition de type complète, uniquement des exigences minimales. De plus, la recherche de nom des identificateurs qui apparaissent dans ces définitions de type ne résout pas la même chose. Même sur les systèmes où il existe une ABI C ++, la tentative de partage de ces types à travers les limites du module entraîne un comportement indéfini massif en raison de violations de la règle d'une définition.

C'est quelque chose que les programmeurs Linux n'étaient pas habitués à traiter, parce que libstdc ++ de g ++ était un standard de facto et pratiquement tous les programmes l'utilisaient, satisfaisant ainsi l'ODR. La libc ++ de clang a cassé cette hypothèse, puis C ++ 11 est venu avec des modifications obligatoires à presque tous les types de bibliothèques Standard.

Ne partagez simplement pas les types de bibliothèques Standard entre les modules. C'est un comportement indéfini.

Ben Voigt
la source
16

Certaines des réponses ici rendent le passage des cours C ++ vraiment effrayant, mais j'aimerais partager un autre point de vue. La méthode C ++ virtuelle pure mentionnée dans certaines des autres réponses s'avère en fait plus propre que vous ne le pensez. J'ai construit tout un système de plugins autour du concept et cela fonctionne très bien depuis des années. J'ai une classe "PluginManager" qui charge dynamiquement les dll à partir d'un répertoire spécifié en utilisant LoadLib () et GetProcAddress () (et les équivalents Linux donc l'exécutable pour le rendre multiplateforme).

Croyez-le ou non, cette méthode est indulgente même si vous faites des choses farfelues comme ajouter une nouvelle fonction à la fin de votre interface virtuelle pure et essayer de charger des dll compilées contre l'interface sans cette nouvelle fonction - elles se chargeront très bien. Bien sûr ... vous devrez vérifier un numéro de version pour vous assurer que votre exécutable n'appelle la nouvelle fonction que pour les dll plus récentes qui implémentent la fonction. Mais la bonne nouvelle est: ça marche! Donc, d'une certaine manière, vous disposez d'une méthode grossière pour faire évoluer votre interface au fil du temps.

Une autre chose intéressante à propos des interfaces virtuelles pures - vous pouvez hériter d'autant d'interfaces que vous le souhaitez et vous ne rencontrerez jamais le problème du diamant!

Je dirais que le plus gros inconvénient de cette approche est que vous devez faire très attention aux types que vous passez en paramètres. Pas de classes ou d'objets STL sans les encapsuler d'abord avec des interfaces virtuelles pures. Pas de structs (sans passer par le pragma pack vaudou). Juste des types primatifs et des pointeurs vers d'autres interfaces. En outre, vous ne pouvez pas surcharger les fonctions, ce qui est un inconvénient, mais pas un spectacle.

La bonne nouvelle est qu'avec une poignée de lignes de code, vous pouvez créer des classes et des interfaces génériques réutilisables pour encapsuler des chaînes STL, des vecteurs et d'autres classes de conteneurs. Vous pouvez également ajouter des fonctions à votre interface comme GetCount () et GetVal (n) pour permettre aux gens de parcourir les listes.

Les gens qui créent des plugins pour nous trouvent cela assez facile. Ils n'ont pas besoin d'être des experts de la limite ABI ou de quoi que ce soit - ils héritent simplement des interfaces qui les intéressent, codent les fonctions qu'ils prennent en charge et renvoient false pour celles qu'ils ne font pas.

La technologie qui fait tout ce travail n'est basée sur aucune norme à ma connaissance. D'après ce que j'ai compris, Microsoft a décidé de faire ses tables virtuelles de cette façon afin de pouvoir créer COM, et d'autres auteurs de compilateurs ont décidé de faire de même. Cela inclut GCC, Intel, Borland et la plupart des autres compilateurs C ++ majeurs. Si vous prévoyez d'utiliser un compilateur intégré obscur, cette approche ne fonctionnera probablement pas pour vous. Théoriquement, toute entreprise de compilation pourrait changer ses tables virtuelles à tout moment et casser des choses, mais compte tenu de l'énorme quantité de code écrit au fil des ans qui dépend de cette technologie, je serais très surpris si l'un des principaux acteurs décidait de rompre le rang.

Donc, la morale de l'histoire est ... À l'exception de quelques circonstances extrêmes, vous avez besoin d'une personne en charge des interfaces qui peut s'assurer que la frontière ABI reste propre avec les types primitifs et évite la surcharge. Si vous êtes d'accord avec cette stipulation, je n'aurais pas peur de partager des interfaces avec des classes dans des DLL / SO entre des compilateurs. Partager des classes directement == problème, mais partager des interfaces virtuelles pures n'est pas si mal.

Ph0t0n
la source
C'est un bon point ... J'aurais dû dire "N'ayez pas peur de partager des interfaces avec des classes". Je modifierai ma réponse.
Ph0t0n
2
Hé c'est une excellente réponse, merci! Ce qui le rendrait encore meilleur à mon avis, ce serait des liens vers des lectures supplémentaires qui montrent des exemples des choses que vous mentionnez (ou même du code) - par exemple pour envelopper les classes STL, etc. Sinon, je lis cette réponse, mais je suis un peu perdu sur la façon dont ces choses ressembleraient réellement et comment les rechercher.
Ela782
8

Vous ne pouvez pas transmettre en toute sécurité des objets STL à travers les limites de la DLL, à moins que tous les modules (.EXE et .DLL) ne soient construits avec la même version de compilateur C ++ et les mêmes paramètres et saveurs du CRT, ce qui est très contraignant, et clairement pas votre cas.

Si vous souhaitez exposer une interface orientée objet à partir de votre DLL, vous devez exposer des interfaces pures C ++ (ce qui est similaire à ce que fait COM). Pensez à lire cet article intéressant sur CodeProject:

Comment: exporter des classes C ++ à partir d'une DLL

Vous pouvez également envisager d'exposer une interface C pure à la limite de la DLL, puis de créer un wrapper C ++ sur le site de l'appelant.
Ceci est similaire à ce qui se passe dans Win32: le code d'implémentation Win32 est presque C ++, mais de nombreuses API Win32 exposent une interface C pure (il existe également des API qui exposent des interfaces COM). Puis ATL / WTL et MFC encapsulent ces interfaces C pur avec des classes et des objets C ++.

Monsieur C64
la source