Come faccio a passare in modo sicuro oggetti, specialmente oggetti STL, a e da una DLL?


106

Come faccio a passare oggetti di classe, in particolare oggetti STL, a e da una DLL C ++?

La mia applicazione deve interagire con plugin di terze parti sotto forma di file DLL e non posso controllare con quale compilatore sono costruiti questi plugin. Sono consapevole del fatto che non esiste alcuna ABI garantita per gli oggetti STL e sono preoccupato di causare instabilità nella mia applicazione.


4
Se stai parlando della libreria standard C ++, probabilmente dovresti chiamarla così. STL può significare cose diverse a seconda del contesto. (Vedi anche stackoverflow.com/questions/5205491/...~~V~~singular~~3rd )
Micha Wiedenmann

Risposte:


156

La risposta breve a questa domanda è non farlo . Poiché non esiste uno standard C ++ ABI (interfaccia binaria dell'applicazione, uno standard per convenzioni di chiamata, impacchettamento / allineamento dei dati, dimensione del tipo, ecc.), Dovrai saltare molti ostacoli per cercare di applicare un modo standard di trattare con la classe oggetti nel programma. Non c'è nemmeno una garanzia che funzionerà dopo aver saltato tutti quei cerchi, né c'è una garanzia che una soluzione che funziona in una versione del compilatore funzionerà nella successiva.

Basta creare una semplice interfaccia C utilizzando extern "C", poiché l'ABI C è ben definito e stabile.


Se davvero, davvero vuole passare oggetti C ++ attraverso la frontiera di DLL, è tecnicamente possibile. Ecco alcuni dei fattori che dovrai tenere in considerazione:

Impacchettamento / allineamento dei dati

All'interno di una data classe, i singoli membri di dati vengono solitamente inseriti in memoria in modo speciale in modo che i loro indirizzi corrispondano a un multiplo della dimensione del tipo. Ad esempio, un fileint potrebbe essere allineato a un limite di 4 byte.

Se la DLL è compilata con un compilatore diverso dal tuo EXE, la versione della DLL di una determinata classe potrebbe avere un impacchettamento diverso rispetto alla versione dell'EXE, quindi quando l'EXE passa l'oggetto della classe alla DLL, la DLL potrebbe non essere in grado di accedere correttamente a un dato membro di dati all'interno di quella classe. La DLL tenterà di leggere dall'indirizzo specificato dalla propria definizione della classe, non dalla definizione dell'EXE, e poiché il membro di dati desiderato non è effettivamente memorizzato lì, risulterebbero valori in Garbage Collector.

È possibile aggirare questo problema utilizzando la #pragma packdirettiva del preprocessore, che costringerà il compilatore ad applicare un impacchettamento specifico. Il compilatore continuerà ad applicare il pacchetto predefinito se si seleziona un valore di pacchetto più grande di quello che il compilatore avrebbe scelto , quindi se si sceglie un valore di imballaggio grande, una classe può ancora avere un imballaggio diverso tra i compilatori. La soluzione è usare #pragma pack(1), che costringerà il compilatore ad allineare i membri dei dati su un limite di un byte (essenzialmente, non verrà applicato alcun impacchettamento). Questa non è una grande idea, in quanto può causare problemi di prestazioni o addirittura arresti anomali su determinati sistemi. Tuttavia, lo farà garantire la coerenza nel modo dati membro della vostra classe sono allineati in memoria.

Riordino dei membri

Se la tua classe non è di layout standard , il compilatore può riorganizzare i suoi membri di dati in memoria . Non esiste uno standard per come eseguire questa operazione, quindi qualsiasi riorganizzazione dei dati può causare incompatibilità tra i compilatori. Il passaggio di dati avanti e indietro a una DLL richiederà pertanto classi di layout standard.

Chiamata convenzione

Esistono più convenzioni di chiamata una determinata funzione può avere. Queste convenzioni di chiamata specificano come i dati devono essere passati alle funzioni: i parametri sono memorizzati nei registri o nello stack? In che ordine vengono inseriti gli argomenti nella pila? Chi ripulisce gli argomenti rimasti in pila al termine della funzione?

È importante mantenere una convenzione di chiamata standard; se dichiari una funzione come _cdecl, il valore predefinito per C ++ e provi a chiamarlo usando _stdcall cose cattive accadrà . _cdeclè la convenzione di chiamata predefinita per le funzioni C ++, tuttavia, questa è una cosa che non si interromperà a meno che tu non la interrompa deliberatamente specificando un _stdcallin un punto e un _cdeclin un altro.

Dimensione del tipo di dati

Secondo questa documentazione , su Windows, la maggior parte dei tipi di dati fondamentali ha le stesse dimensioni indipendentemente dal fatto che l'app sia a 32 o 64 bit. Tuttavia, poiché la dimensione di un dato tipo di dati è imposta dal compilatore, non da alcuno standard (tutte le garanzie standard lo sono 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)), è una buona idea usare tipi di dati a dimensione fissa per garantire la compatibilità della dimensione del tipo di dati ove possibile.

Problemi con l'heap

Se la DLL si collega a una versione diversa del runtime C rispetto all'EXE, i due moduli utilizzeranno heap diversi . Questo è un problema particolarmente probabile dato che i moduli vengono compilati con diversi compilatori.

Per mitigare questo problema, tutta la memoria dovrà essere allocata in un heap condiviso e deallocata dallo stesso heap. Fortunatamente, Windows fornisce API per aiutare con questo: GetProcessHeap ti consentirà di accedere all'heap EXE dell'host e HeapAlloc / HeapFree ti consentirà di allocare e liberare memoria all'interno di questo heap. È importante non utilizzare normali malloc/ freepoiché non vi è alcuna garanzia che funzioneranno come previsto.

Problemi STL

La libreria standard C ++ ha una propria serie di problemi ABI. Non vi è alcuna garanzia che un dato tipo di STL sia disposto allo stesso modo in memoria, né vi è garanzia che una data classe STL abbia la stessa dimensione da un'implementazione all'altra (in particolare, le build di debug possono inserire informazioni di debug aggiuntive in un dato tipo STL). Pertanto, qualsiasi contenitore STL dovrà essere decompresso in tipi fondamentali prima di essere passato attraverso il limite della DLL e reimballato sull'altro lato.

Nome mutilazione

La tua DLL presumibilmente esporterà le funzioni che il tuo EXE vorrà chiamare. Tuttavia, i compilatori C ++ non hanno un modo standard di modificare i nomi delle funzioni . Ciò significa che una funzione denominata GetCCDLLpotrebbe essere alterata _Z8GetCCDLLvin GCC e ?GetCCDLL@@YAPAUCCDLL_v1@@XZin MSVC.

Non sarai già in grado di garantire il collegamento statico alla tua DLL, poiché una DLL prodotta con GCC non produrrà un file .lib e il collegamento statico di una DLL in MSVC ne richiede uno. Il collegamento dinamico sembra un'opzione molto più pulita, ma la manipolazione del nome ti ostacola: se provi con GetProcAddressil nome alterato sbagliato, la chiamata fallirà e non sarai in grado di usare la tua DLL. Ciò richiede un po 'di pirateria informatica per aggirare, ed è una ragione abbastanza importante per cui passare le classi C ++ attraverso un confine DLL è una cattiva idea.

Dovrai creare la tua DLL, quindi esaminare il file .def prodotto (se ne viene prodotto uno; questo varierà in base alle opzioni del tuo progetto) o utilizzare uno strumento come Dependency Walker per trovare il nome alterato. Quindi, è necessario scrivere il proprio file DEF, che definisce un alias unmangled alla funzione maciullato. Ad esempio, usiamo la GetCCDLLfunzione che ho menzionato un po 'più in alto. Sul mio sistema, i seguenti file .def funzionano rispettivamente per GCC e MSVC:

GCC:

EXPORTS
    GetCCDLL=_Z8GetCCDLLv @1

MSVC:

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

Ricostruisci la tua DLL, quindi riesamina le funzioni che esporta. Un nome di funzione non confuso dovrebbe essere tra questi. Notare che non è possibile utilizzare le funzioni sovraccaricate in questo modo : il nome della funzione unmangled è un alias per uno specifico overload della funzione come definito dal nome alterato. Si noti inoltre che sarà necessario creare un nuovo file .def per la DLL ogni volta che si modificano le dichiarazioni di funzione, poiché i nomi alterati cambieranno. Ancora più importante, aggirando il nome alterando, stai ignorando qualsiasi protezione che il linker sta cercando di offrirti per quanto riguarda i problemi di incompatibilità.

L'intero processo è più semplice se crei un'interfaccia da seguire per la tua DLL, poiché avrai solo una funzione per la quale definire un alias invece di dover creare un alias per ogni funzione nella tua DLL. Tuttavia, si applicano ancora gli stessi avvertimenti.

Passaggio di oggetti di classe a una funzione

Questo è probabilmente il più sottile e pericoloso dei problemi che affliggono il passaggio di dati tra compilatori. Anche se gestisci tutto il resto, non esiste uno standard per il modo in cui gli argomenti vengono passati a una funzione . Ciò può causare piccoli arresti anomali senza motivo apparente e nessun modo semplice per eseguire il debug . Dovrai passare tutti gli argomenti tramite puntatori, inclusi i buffer per qualsiasi valore restituito. Questo è goffo e scomodo, ed è ancora un'altra soluzione alternativa hacky che può o non può funzionare.


Mettendo insieme tutte queste soluzioni alternative e basandosi su un lavoro creativo con modelli e operatori , possiamo tentare di passare in modo sicuro gli oggetti attraverso un limite DLL. Si noti che il supporto per C ++ 11 è obbligatorio, così come il supporto per #pragma packe le sue varianti; MSVC 2013 offre questo supporto, così come le versioni recenti di GCC e 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 è specializzata per ogni tipo di dati di base, quindi intverrà automaticamente inserito in int32_t,uint verrà eseguito il wrapping uint32_t, ecc. Tutto ciò avviene dietro le quinte, grazie agli operatori =e sovraccaricati (). Ho omesso il resto delle specializzazioni di tipo di base poiché sono quasi completamente le stesse tranne per i tipi di dati sottostanti (la boolspecializzazione ha un po 'di logica in più, poiché viene convertita in a int8_te quindi int8_tviene confrontata con 0 per riconvertirla bool, ma questo è abbastanza banale).

Possiamo anche avvolgere i tipi STL in questo modo, sebbene richieda un po 'di lavoro extra:

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

Ora possiamo creare una DLL che utilizza questi tipi di pod. Per prima cosa abbiamo bisogno di un'interfaccia, quindi avremo solo un metodo per capire il mangling.

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

Questo crea solo un'interfaccia di base utilizzabile sia dalla DLL che da qualsiasi chiamante. Nota che stiamo passando un puntatore a un filepod , non a podse stesso. Ora dobbiamo implementarlo sul lato 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;
}

E ora implementiamo il ShowMessage funzione:

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

Niente di troppo stravagante: questo copia solo il passato pod in un normale wstringe lo mostra in una casella di messaggio. Dopo tutto, questo è solo un POC , non una libreria di utilità completa.

Ora possiamo costruire la DLL. Non dimenticare gli speciali file .def per aggirare la modifica del nome del linker. (Nota: la struttura CCDLL che ho effettivamente costruito ed eseguito aveva più funzioni di quella che presento qui. I file .def potrebbero non funzionare come previsto.)

Ora per un EXE per chiamare 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;
}

Ed ecco i risultati. La nostra DLL funziona. Abbiamo raggiunto con successo i problemi ABI STL precedenti, i problemi ABI C ++ precedenti, i problemi di mangling passati e la nostra DLL MSVC funziona con un EXE GCC.

L'immagine che mostra il risultato dopo.


In conclusione, se è assolutamente necessario passare oggetti C ++ attraverso i confini DLL, questo è come lo fai. Tuttavia, niente di tutto ciò è garantito per funzionare con la tua configurazione o quella di qualcun altro. Tutto ciò potrebbe interrompersi in qualsiasi momento e probabilmente si interromperà il giorno prima che il tuo software abbia una versione principale. Questo percorso è pieno di hack, rischi e idiozia generale per cui probabilmente dovrei essere ucciso. Se segui questa strada, prova con estrema cautela. E davvero ... non farlo affatto.


1
Hmm, non male! Hai raccolto una discreta raccolta di argomenti contro l'utilizzo di tipi c ++ standard per interagire con una DLL di Windows e hai etichettato di conseguenza. Queste particolari restrizioni ABI non si applicheranno ad altri toolchain oltre a MSVC. Questo dovrebbe essere anche menzionato ...
πάντα ῥεῖ

12
@DavidHeffernan Right. Ma questo è il risultato di diverse settimane di ricerca per me, quindi ho pensato che sarebbe valsa la pena documentare ciò che ho imparato in modo che gli altri non abbiano bisogno di fare la stessa ricerca e quegli stessi tentativi di hackerare insieme una soluzione funzionante. Tanto più che questa sembra essere una domanda semi-comune da queste parti.
cf sta con Monica

@ πάνταῥεῖ Queste particolari restrizioni ABI non si applicheranno ad altri toolchain oltre a MSVC. Questo dovrebbe essere anche menzionato ... Non sono sicuro di averlo capito correttamente. Stai indicando che questi problemi ABI sono esclusivi di MSVC e, ad esempio, una DLL creata con clang funzionerà correttamente con un EXE creato con GCC? Sono un po 'confuso, dal momento che sembra contraddittorio con tutte le mie ricerche ...
cf sta con Monica

@computerfreaker No, sto dicendo che PE ed ELF utilizzano diversi formati ABI ...
πάντα ῥεῖ

3
@computerfreaker La maggior parte dei principali compilatori C ++ (GCC, Clang, ICC, EDG, ecc.) seguono l'ABI C ++ Itanium. MSVC no. Quindi sì, questi problemi ABI sono in gran parte specifici di MSVC, anche se non esclusivamente - anche i compilatori C su piattaforme Unix (e anche versioni diverse dello stesso compilatore!) Soffrono di interoperabilità non perfetta. Sono di solito abbastanza vicino, però, che non sarei affatto sorpreso di scoprire che si potrebbe collegare con successo una DLL Clang-costruito con una GCC-built eseguibile.
Stuart Olsen

17

@computerfreaker ha scritto un'ottima spiegazione del motivo per cui la mancanza di ABI impedisce il passaggio di oggetti C ++ oltre i limiti della DLL nel caso generale, anche quando le definizioni del tipo sono sotto il controllo dell'utente e la stessa identica sequenza di token viene utilizzata in entrambi i programmi. (Ci sono due casi che funzionano: classi con layout standard e interfacce pure)

Per i tipi di oggetti definiti nello standard C ++ (inclusi quelli adattati dalla libreria dei modelli standard), la situazione è molto, molto peggiore. I token che definiscono questi tipi NON sono gli stessi su più compilatori, poiché lo standard C ++ non fornisce una definizione di tipo completa, ma solo requisiti minimi. Inoltre, la ricerca del nome degli identificatori visualizzati in queste definizioni di tipo non risolve lo stesso problema. Anche sui sistemi in cui è presente un ABI C ++, il tentativo di condividere tali tipi oltre i confini del modulo si traduce in un comportamento massiccio non definito a causa delle violazioni della regola di una definizione.

Questo è qualcosa con cui i programmatori Linux non erano abituati a trattare, perché libstdc ++ di g ++ era uno standard de facto e praticamente tutti i programmi lo utilizzavano, soddisfacendo così l'ODR. la libc ++ di clang ha infranto questa ipotesi, e poi C ++ 11 è arrivato con modifiche obbligatorie a quasi tutti i tipi di libreria Standard.

Basta non condividere i tipi di libreria Standard tra i moduli. È un comportamento indefinito.


16

Alcune delle risposte qui rendono il passaggio di classi C ++ davvero spaventoso, ma mi piacerebbe condividere un punto di vista alternativo. Il puro metodo C ++ virtuale menzionato in alcune delle altre risposte si rivela effettivamente più pulito di quanto si possa pensare. Ho costruito un intero sistema di plugin attorno al concetto e funziona molto bene da anni. Ho una classe "PluginManager" che carica dinamicamente le dll da una directory specificata utilizzando LoadLib () e GetProcAddress () (e gli equivalenti Linux, quindi l'eseguibile per renderlo multipiattaforma).

Che tu ci creda o no, questo metodo perdona anche se fai alcune cose stravaganti come aggiungere una nuova funzione alla fine della tua interfaccia virtuale pura e provare a caricare le DLL compilate sull'interfaccia senza quella nuova funzione: verranno caricate bene. Ovviamente ... dovrai controllare un numero di versione per assicurarti che il tuo eseguibile chiami la nuova funzione solo per le dll più recenti che implementano la funzione. Ma la buona notizia è: funziona! Quindi, in un certo senso, hai un metodo grezzo per far evolvere la tua interfaccia nel tempo.

Un'altra cosa interessante delle interfacce virtuali pure: puoi ereditare tutte le interfacce che desideri e non ti imbatterai mai nel problema del diamante!

Direi che il più grande svantaggio di questo approccio è che devi stare molto attento ai tipi che passi come parametri. Nessuna classe o oggetto STL senza prima avvolgerli con interfacce virtuali pure. Nessuna struttura (senza passare attraverso il pragma pack voodoo). Solo tipi primari e puntatori ad altre interfacce. Inoltre, non puoi sovraccaricare le funzioni, il che è un inconveniente, ma non un ostacolo allo spettacolo.

La buona notizia è che con una manciata di righe di codice è possibile creare classi e interfacce generiche riutilizzabili per avvolgere stringhe STL, vettori e altre classi contenitore. In alternativa, puoi aggiungere funzioni alla tua interfaccia come GetCount () e GetVal (n) per consentire alle persone di scorrere gli elenchi.

Le persone che creano plugin per noi lo trovano abbastanza facile. Non devono essere esperti sul confine ABI o altro: ereditano semplicemente le interfacce a cui sono interessati, codificano le funzioni che supportano e restituiscono false per quelle che non lo fanno.

La tecnologia che fa funzionare tutto questo non è basata su alcuno standard per quanto ne so. Da quello che ho raccolto, Microsoft ha deciso di fare le proprie tabelle virtuali in questo modo in modo da poter creare COM, e altri autori di compilatori hanno deciso di seguire l'esempio. Ciò include GCC, Intel, Borland e la maggior parte degli altri principali compilatori C ++. Se hai intenzione di utilizzare un oscuro compilatore incorporato, questo approccio probabilmente non funzionerà per te. Teoricamente qualsiasi azienda di compilatori potrebbe cambiare le proprie tabelle virtuali in qualsiasi momento e rompere le cose, ma considerando l'enorme quantità di codice scritto nel corso degli anni che dipende da questa tecnologia, sarei molto sorpreso se qualcuno dei principali attori decidesse di rompere il rango.

Quindi la morale della storia è ... Con l'eccezione di alcune circostanze estreme, hai bisogno di una persona responsabile delle interfacce che possa assicurarsi che il confine ABI rimanga pulito con tipi primitivi ed eviti il ​​sovraccarico. Se sei d'accordo con questa clausola, non avrei paura di condividere interfacce con classi in DLL / SO tra compilatori. Condividere le classi direttamente == guai, ma condividere interfacce virtuali pure non è poi così male.


Questo è un buon punto ... avrei dovuto dire "Non aver paura di condividere le interfacce con le classi". Modificherò la mia risposta.
Ph0t0n

2
Ehi, è un'ottima risposta, grazie! Ciò che lo renderebbe ancora migliore secondo me sarebbero alcuni collegamenti a ulteriori letture che mostrano alcuni esempi delle cose che stai menzionando (o anche un po 'di codice) - ad esempio per il wrapping delle classi STL, ecc. Altrimenti sto leggendo questa risposta ma poi sono un po 'perso su come sarebbero effettivamente queste cose e su come cercarle.
Ela782

8

Non è possibile passare in modo sicuro oggetti STL oltre i limiti della DLL, a meno che tutti i moduli (.EXE e .DLL) non siano creati con la stessa versione del compilatore C ++ e le stesse impostazioni e versioni di CRT, che è altamente vincolante e chiaramente non è il tuo caso.

Se vuoi esporre un'interfaccia orientata agli oggetti dalla tua DLL, dovresti esporre le interfacce pure C ++ (che è simile a ciò che fa COM). Considera la lettura di questo interessante articolo su CodeProject:

HowTo: esportare classi C ++ da una DLL

Potresti anche prendere in considerazione l'idea di esporre un'interfaccia C pura al limite della DLL e quindi creare un wrapper C ++ nel sito del chiamante.
Questo è simile a quello che accade in Win32: il codice di implementazione di Win32 è quasi C ++, ma molte API Win32 espongono un'interfaccia C pura (ci sono anche API che espongono le interfacce COM). Quindi ATL / WTL e MFC racchiudono queste interfacce C pure con classi e oggetti C ++.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.