Bagaimana cara mengirimkan objek dengan aman, terutama objek STL, ke dan dari DLL?

106

Bagaimana cara mengirimkan objek kelas, terutama objek STL, ke dan dari C ++ DLL?

Aplikasi saya harus berinteraksi dengan plugin pihak ketiga dalam bentuk file DLL, dan saya tidak dapat mengontrol kompiler apa yang dibuat untuk plugin ini. Saya sadar bahwa tidak ada ABI yang dijamin untuk objek STL, dan saya khawatir akan menyebabkan ketidakstabilan dalam aplikasi saya.

cf berdiri dengan Monica
sumber
4
Jika Anda berbicara tentang C ++ Standard Library, maka Anda mungkin harus menyebutnya begitu. STL dapat memiliki arti yang berbeda tergantung pada konteksnya. (Lihat juga stackoverflow.com/questions/5205491/… )
Micha Wiedenmann

Jawaban:

156

Jawaban singkat untuk pertanyaan ini adalah jangan . Karena tidak ada C ++ ABI standar (antarmuka biner aplikasi, standar untuk konvensi pemanggilan, pengemasan / penyelarasan data, ukuran tipe, dll.), Anda harus melewati banyak rintangan untuk mencoba dan menerapkan cara standar menangani kelas. objek dalam program Anda. Bahkan tidak ada jaminan itu akan berhasil setelah Anda melewati semua rintangan itu, juga tidak ada jaminan bahwa solusi yang berfungsi dalam satu rilis kompiler akan berfungsi di rilis berikutnya.

Hanya membuat C polos antarmuka menggunakan extern "C", karena C ABI adalah didefinisikan dengan baik dan stabil.


Jika Anda benar- benar ingin meneruskan objek C ++ melintasi batas DLL, secara teknis itu mungkin. Berikut beberapa faktor yang harus Anda perhitungkan:

Pengepakan / penyelarasan data

Dalam kelas tertentu, anggota data individu biasanya akan ditempatkan secara khusus di memori sehingga alamat mereka sesuai dengan beberapa ukuran tipe. Misalnya, an intmungkin disejajarkan dengan batas 4 byte.

Jika DLL Anda dikompilasi dengan kompiler yang berbeda dari EXE Anda, versi DLL dari kelas yang diberikan mungkin memiliki pengemasan yang berbeda dari versi EXE, jadi ketika EXE meneruskan objek kelas ke DLL, DLL mungkin tidak dapat mengakses anggota data yang diberikan dalam kelas itu. DLL akan mencoba membaca dari alamat yang ditentukan oleh definisi kelasnya sendiri, bukan definisi EXE, dan karena anggota data yang diinginkan tidak benar-benar disimpan di sana, nilai sampah akan dihasilkan.

Anda dapat mengatasinya dengan menggunakan #pragma packarahan preprocessor, yang akan memaksa kompilator untuk menerapkan pengemasan tertentu. Kompilator akan tetap menerapkan pengemasan default jika Anda memilih nilai paket yang lebih besar dari yang akan dipilih oleh kompilator , jadi jika Anda memilih nilai pengemasan yang besar, sebuah kelas masih dapat memiliki pengemasan yang berbeda di antara kompiler. Solusi untuk ini adalah dengan menggunakan #pragma pack(1), yang akan memaksa kompilator untuk menyelaraskan anggota data pada batas satu byte (pada dasarnya, tidak ada pengemasan yang akan diterapkan). Ini bukan ide yang bagus, karena dapat menyebabkan masalah kinerja atau bahkan crash pada sistem tertentu. Namun, ini akan memastikan konsistensi dalam cara anggota data kelas Anda diselaraskan dalam memori.

Penataan ulang anggota

Jika kelas Anda bukan tata letak standar , kompilator dapat mengatur ulang anggota datanya di memori . Tidak ada standar untuk bagaimana hal ini dilakukan, sehingga pengaturan ulang data apa pun dapat menyebabkan ketidakcocokan antar kompiler. Oleh karena itu, meneruskan data bolak-balik ke DLL akan membutuhkan kelas tata letak standar.

Konvensi panggilan

Ada beberapa konvensi pemanggilan yang dapat dimiliki fungsi tertentu. Konvensi pemanggilan ini menentukan bagaimana data akan diteruskan ke fungsi: apakah parameter disimpan dalam register atau di stack? Urutan apa yang mendorong argumen ke tumpukan? Siapa yang membersihkan argumen yang tersisa di tumpukan setelah fungsi selesai?

Anda harus mempertahankan konvensi panggilan standar; jika Anda mendeklarasikan suatu fungsi sebagai _cdecl, default untuk C ++, dan mencoba memanggilnya menggunakan _stdcall hal-hal buruk akan terjadi . _cdecladalah konvensi pemanggilan default untuk fungsi C ++, bagaimanapun, jadi ini adalah satu hal yang tidak akan rusak kecuali Anda sengaja merusaknya dengan menentukan _stdcalldi satu tempat dan _cdecldi tempat lain.

Ukuran tipe data

Menurut dokumentasi ini , di Windows, sebagian besar tipe data fundamental memiliki ukuran yang sama terlepas dari apakah aplikasi Anda 32-bit atau 64-bit. Namun, karena ukuran tipe data tertentu diberlakukan oleh compiler, bukan oleh standar apa pun (semua jaminan standar adalah itu 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)), sebaiknya gunakan tipe data ukuran tetap untuk memastikan kompatibilitas ukuran tipe data jika memungkinkan.

Masalah heap

Jika DLL Anda tertaut ke versi runtime C yang berbeda dari EXE Anda, kedua modul akan menggunakan heaps yang berbeda . Ini adalah masalah yang mungkin terjadi karena modul sedang dikompilasi dengan kompiler yang berbeda.

Untuk mengurangi hal ini, semua memori harus dialokasikan ke heap bersama, dan dialokasikan dari heap yang sama. Untungnya, Windows menyediakan API untuk membantu hal ini: GetProcessHeap akan memungkinkan Anda mengakses heap EXE host, dan HeapAlloc / HeapFree akan membiarkan Anda mengalokasikan dan mengosongkan memori dalam heap ini. Penting agar Anda tidak menggunakan normal malloc/ freekarena tidak ada jaminan mereka akan bekerja seperti yang Anda harapkan.

Masalah STL

Pustaka standar C ++ memiliki kumpulan masalah ABI-nya sendiri. Tidak ada jaminan bahwa tipe STL tertentu ditata dengan cara yang sama dalam memori, juga tidak ada jaminan bahwa kelas STL yang diberikan memiliki ukuran yang sama dari satu implementasi ke implementasi lainnya (khususnya, build debug dapat memasukkan informasi debug tambahan ke dalam diberi tipe STL). Oleh karena itu, setiap kontainer STL harus dibongkar menjadi tipe dasar sebelum diteruskan melintasi batas DLL dan dikemas ulang di sisi lain.

Beri nama mangling

DLL Anda mungkin akan mengekspor fungsi yang ingin dipanggil oleh EXE Anda. Namun, kompiler C ++ tidak memiliki cara standar untuk mengatur nama fungsi . Ini berarti fungsi bernama GetCCDLLmungkin rusak _Z8GetCCDLLvdi GCC dan ?GetCCDLL@@YAPAUCCDLL_v1@@XZdi MSVC.

Anda sudah tidak dapat menjamin penautan statis ke DLL Anda, karena DLL yang diproduksi dengan GCC tidak akan menghasilkan file .lib dan secara statis menautkan DLL di MSVC memerlukannya. Menautkan secara dinamis tampaknya merupakan opsi yang jauh lebih bersih, tetapi nama mangling menghalangi Anda: jika Anda mencoba GetProcAddressnama rusak yang salah, panggilan akan gagal dan Anda tidak akan dapat menggunakan DLL Anda. Ini membutuhkan sedikit peretasan untuk menyiasatinya, dan merupakan alasan yang cukup utama mengapa meneruskan kelas C ++ melintasi batas DLL adalah ide yang buruk.

Anda harus membangun DLL Anda, lalu memeriksa file .def yang dihasilkan (jika ada yang diproduksi; ini akan bervariasi berdasarkan opsi proyek Anda) atau gunakan alat seperti Dependency Walker untuk menemukan nama yang rusak. Kemudian, Anda harus menulis file .def Anda sendiri , menentukan alias yang tidak diubah ke fungsi yang rusak. Sebagai contoh, mari gunakan GetCCDLLfungsi yang saya sebutkan lebih jauh. Di sistem saya, file .def berikut berfungsi untuk GCC dan MSVC, masing-masing:

GCC:

EXPORTS
    GetCCDLL=_Z8GetCCDLLv @1

MSVC:

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

Buat ulang DLL Anda, lalu periksa kembali fungsi yang diekspornya. Nama fungsi yang tidak diubah harus ada di antara mereka. Perhatikan bahwa Anda tidak dapat menggunakan fungsi yang kelebihan beban dengan cara ini : nama fungsi yang tidak diubah adalah alias untuk satu kelebihan fungsi tertentu seperti yang ditentukan oleh nama yang rusak. Perhatikan juga bahwa Anda harus membuat file .def baru untuk DLL Anda setiap kali Anda mengubah deklarasi fungsi, karena nama yang rusak akan berubah. Yang terpenting, dengan mengabaikan nama mangling, Anda mengganti perlindungan apa pun yang coba ditawarkan oleh linker kepada Anda terkait dengan masalah ketidakcocokan.

Keseluruhan proses ini lebih sederhana jika Anda membuat antarmuka untuk diikuti DLL, karena Anda hanya memiliki satu fungsi untuk mendefinisikan alias daripada perlu membuat alias untuk setiap fungsi di DLL Anda. Namun, peringatan yang sama tetap berlaku.

Meneruskan objek kelas ke suatu fungsi

Ini mungkin masalah yang paling tidak kentara dan paling berbahaya yang mengganggu pengiriman data lintas-kompiler. Bahkan jika Anda menangani yang lainnya, tidak ada standar tentang bagaimana argumen diteruskan ke suatu fungsi . Hal ini dapat menyebabkan kerusakan halus tanpa alasan yang jelas dan tidak ada cara mudah untuk men-debugnya . Anda harus meneruskan semua argumen melalui pointer, termasuk buffer untuk nilai kembalian apa pun. Ini kikuk dan tidak nyaman, dan merupakan solusi hacky lain yang mungkin berhasil atau tidak.


Dengan menggabungkan semua solusi ini dan mengembangkan beberapa pekerjaan kreatif dengan templat dan operator , kami dapat mencoba untuk meneruskan objek dengan aman melintasi batas DLL. Perhatikan bahwa dukungan C ++ 11 bersifat wajib, begitu juga dukungan untuk #pragma packdan variannya; MSVC 2013 menawarkan dukungan ini, seperti halnya GCC versi terbaru dan 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)

The podkelas khusus untuk setiap datatype dasar, sehingga intsecara otomatis akan dibungkus untuk int32_t, uintakan dibungkus untuk uint32_t, dll semua ini terjadi di belakang layar, berkat kelebihan beban =dan ()operator. Saya telah menghilangkan spesialisasi tipe dasar lainnya karena mereka hampir seluruhnya sama kecuali untuk tipe data yang mendasarinya ( boolspesialisasi memiliki sedikit logika tambahan, karena itu diubah menjadi int8_tdan kemudian int8_tdibandingkan dengan 0 untuk dikonversi kembali ke bool, tapi ini cukup sepele).

Kita juga dapat membungkus tipe STL dengan cara ini, meskipun itu membutuhkan sedikit kerja ekstra:

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

Sekarang kita dapat membuat DLL yang menggunakan tipe pod ini. Pertama kita membutuhkan antarmuka, jadi kita hanya memiliki satu metode untuk mencari tahu 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();

Ini hanya membuat antarmuka dasar yang dapat digunakan DLL dan semua pemanggil. Perhatikan bahwa kita memberikan pointer ke a pod, bukan ke poddirinya sendiri. Sekarang kita perlu mengimplementasikannya di sisi 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;
}

Dan sekarang mari kita terapkan ShowMessagefungsinya:

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

Tidak ada yang terlalu mewah: ini hanya menyalin lulus podke normal wstringdan menunjukkannya di kotak pesan. Bagaimanapun, ini hanyalah POC , bukan pustaka utilitas lengkap.

Sekarang kita bisa membangun DLL. Jangan lupa file .def khusus untuk mengatasi kerusakan nama linker. (Catatan: struct CCDLL yang sebenarnya saya buat dan jalankan memiliki lebih banyak fungsi daripada yang saya sajikan di sini. File .def mungkin tidak berfungsi seperti yang diharapkan.)

Sekarang untuk EXE untuk memanggil 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;
}

Dan inilah hasilnya. DLL kami bekerja. Kami telah berhasil mencapai masalah STL ABI sebelumnya, masalah C ++ ABI sebelumnya, masalah mangling sebelumnya, dan MSVC DLL kami bekerja dengan GCC EXE.

Gambar yang menunjukkan hasil setelahnya.


Kesimpulannya, jika Anda benar - benar harus meneruskan objek C ++ melintasi batas DLL, inilah cara Anda melakukannya. Namun, semua ini tidak dijamin akan berfungsi dengan penyiapan Anda atau orang lain. Semua ini dapat rusak kapan saja, dan mungkin akan rusak sehari sebelum perangkat lunak Anda dijadwalkan untuk rilis utama. Jalan ini penuh dengan peretasan, risiko, dan kebodohan umum yang mungkin harus saya lakukan. Jika Anda mengikuti rute ini, harap uji dengan sangat hati-hati. Dan sungguh ... jangan lakukan ini sama sekali.

cf berdiri dengan Monica
sumber
1
Hmm, lumayan! Anda menarik kumpulan argumen yang cukup bagus untuk tidak menggunakan tipe c ++ standar untuk berinteraksi dengan Windows DLL dan diberi tag yang sesuai. Pembatasan ABI khusus ini tidak akan berlaku untuk toolchain selain MSVC. Ini bahkan harus disebutkan ...
πάντα ῥεῖ
12
@Tokopedia Tetapi ini adalah hasil dari beberapa minggu penelitian bagi saya, jadi saya pikir akan bermanfaat untuk mendokumentasikan apa yang telah saya pelajari sehingga orang lain tidak perlu melakukan penelitian yang sama dan upaya yang sama untuk meretas solusi yang berfungsi. Terlebih lagi karena ini sepertinya pertanyaan semi-umum di sekitar sini.
cf berdiri dengan Monica
@ πάνταῥεῖ Pembatasan ABI khusus ini tidak akan berlaku untuk toolchain selain MSVC. Ini bahkan harus disebutkan ... Saya tidak yakin saya mengerti ini dengan benar. Apakah Anda menunjukkan bahwa masalah ABI ini eksklusif untuk MSVC, dan, katakanlah, DLL yang dibangun dengan clang akan berhasil bekerja dengan EXE yang dibuat dengan GCC? Saya agak bingung, karena tampaknya bertentangan dengan semua penelitian saya ...
cf berdiri dengan Monica
@computerfreaker Tidak, saya katakan bahwa PE dan ELF menggunakan format ABI yang berbeda ...
πάντα ῥεῖ
3
@computerfreaker Sebagian besar kompiler C ++ (GCC, Clang, ICC, EDG, dll.) mengikuti Itanium C ++ ABI. MSVC tidak. Jadi ya, masalah ABI ini sebagian besar khusus untuk MSVC, meskipun tidak secara eksklusif — bahkan kompiler C pada platform Unix (dan bahkan versi berbeda dari kompilator yang sama!) Mengalami interoperabilitas yang kurang sempurna. Mereka biasanya cukup dekat, meskipun, bahwa aku tidak akan sama sekali terkejut untuk menemukan bahwa Anda bisa berhasil menghubungkan DLL dentang-dibangun dengan GCC dibangun executable.
Stuart Olsen
17

@computerfreaker telah menulis penjelasan yang bagus tentang mengapa kekurangan ABI mencegah penerusan objek C ++ melintasi batas DLL dalam kasus umum, bahkan ketika definisi tipe berada di bawah kendali pengguna dan urutan token yang sama persis digunakan di kedua program. (Ada dua kasus yang berfungsi: kelas tata letak standar, dan antarmuka murni)

Untuk tipe objek yang ditentukan dalam C ++ Standard (termasuk yang diadaptasi dari Standard Template Library), situasinya jauh, jauh lebih buruk. Token yang mendefinisikan tipe ini TIDAK sama di beberapa kompiler, karena C ++ Standard tidak menyediakan definisi tipe lengkap, hanya persyaratan minimum. Selain itu, pencarian nama pengenal yang muncul dalam definisi jenis ini tidak menyelesaikan masalah yang sama. Bahkan pada sistem di mana terdapat C ++ ABI, mencoba untuk berbagi tipe seperti itu melintasi batas modul menghasilkan perilaku tidak terdefinisi masif karena pelanggaran Aturan Satu Definisi.

Ini adalah sesuatu yang tidak biasa dihadapi oleh programmer Linux, karena libstdc ++ g ++ adalah standar de-facto dan hampir semua program menggunakannya, sehingga memenuhi ODR. libc ++ clang mematahkan asumsi itu, dan kemudian C ++ 11 datang bersama dengan perubahan wajib ke hampir semua jenis pustaka Standar.

Hanya saja, jangan berbagi tipe pustaka standar antar modul. Ini perilaku yang tidak terdefinisi.

Ben Voigt
sumber
16

Beberapa jawaban di sini membuat kelulusan kelas C ++ terdengar sangat menakutkan, tetapi saya ingin berbagi sudut pandang lain. Metode virtual C ++ murni yang disebutkan di beberapa tanggapan lain ternyata lebih bersih dari yang Anda kira. Saya telah membangun seluruh sistem plugin di sekitar konsep tersebut dan telah bekerja dengan sangat baik selama bertahun-tahun. Saya memiliki kelas "PluginManager" yang secara dinamis memuat dll dari direktori tertentu menggunakan LoadLib () dan GetProcAddress () (dan setara dengan Linux sehingga dapat dieksekusi untuk membuatnya lintas platform).

Percaya atau tidak, metode ini memaafkan bahkan jika Anda melakukan beberapa hal aneh seperti menambahkan fungsi baru di akhir antarmuka virtual murni Anda dan mencoba memuat dll yang dikompilasi terhadap antarmuka tanpa fungsi baru itu - semuanya akan dimuat dengan baik. Tentu saja ... Anda harus memeriksa nomor versi untuk memastikan eksekusi Anda hanya memanggil fungsi baru untuk dll yang lebih baru yang mengimplementasikan fungsi tersebut. Namun kabar baiknya adalah: berhasil! Jadi di satu sisi, Anda memiliki metode kasar untuk mengembangkan antarmuka Anda dari waktu ke waktu.

Hal keren lainnya tentang antarmuka virtual murni - Anda dapat mewarisi antarmuka sebanyak yang Anda inginkan dan Anda tidak akan pernah mengalami masalah berlian!

Saya akan mengatakan kelemahan terbesar dari pendekatan ini adalah Anda harus sangat berhati-hati tentang jenis apa yang Anda berikan sebagai parameter. Tidak ada kelas atau objek STL tanpa membungkusnya dengan antarmuka virtual murni terlebih dahulu. Tidak ada struct (tanpa melalui voodoo paket pragma). Hanya tipe primatif dan penunjuk ke antarmuka lain. Selain itu, Anda tidak dapat membebani fungsi secara berlebihan, yang merupakan ketidaknyamanan, tetapi bukan penghenti pertunjukan.

Kabar baiknya adalah bahwa dengan beberapa baris kode Anda dapat membuat kelas dan antarmuka umum yang dapat digunakan kembali untuk menggabungkan string STL, vektor, dan kelas kontainer lainnya. Sebagai alternatif, Anda dapat menambahkan fungsi ke antarmuka Anda seperti GetCount () dan GetVal (n) agar orang dapat menelusuri daftar.

Orang yang membuat plugin untuk kami merasa cukup mudah. Mereka tidak harus ahli dalam batasan ABI atau apa pun - mereka hanya mewarisi antarmuka yang mereka minati, membuat kode fungsi yang mereka dukung, dan mengembalikan false untuk yang tidak mereka sukai.

Teknologi yang membuat semua ini bekerja sejauh yang saya tahu tidak didasarkan pada standar apa pun. Dari apa yang saya kumpulkan, Microsoft memutuskan untuk melakukan tabel virtual mereka dengan cara itu sehingga mereka dapat membuat COM, dan penulis kompiler lainnya memutuskan untuk mengikutinya. Ini termasuk GCC, Intel, Borland, dan sebagian besar kompiler C ++ utama lainnya. Jika Anda berencana menggunakan kompiler tertanam yang tidak jelas, maka pendekatan ini mungkin tidak akan berhasil untuk Anda. Secara teoritis perusahaan kompiler mana pun dapat mengubah tabel virtual mereka kapan saja dan merusak banyak hal, tetapi mengingat sejumlah besar kode yang ditulis selama bertahun-tahun yang bergantung pada teknologi ini, saya akan sangat terkejut jika ada pemain utama yang memutuskan untuk memecahkan peringkat.

Jadi inti dari cerita ini adalah ... Dengan pengecualian beberapa keadaan ekstrim, Anda memerlukan satu orang yang bertanggung jawab atas antarmuka yang dapat memastikan batas ABI tetap bersih dengan tipe primitif dan menghindari kelebihan muatan. Jika Anda setuju dengan ketentuan itu, maka saya tidak akan takut untuk berbagi antarmuka ke kelas di DLL / SOs antara kompiler. Berbagi kelas secara langsung == masalah, tetapi berbagi antarmuka virtual murni tidak terlalu buruk.

Ph0t0n
sumber
Itu poin yang bagus ... Saya seharusnya mengatakan "Jangan takut untuk berbagi antarmuka ke kelas". Saya akan mengedit jawaban saya.
Ph0t0n
2
Hei, itu jawaban yang bagus, terima kasih! Apa yang membuatnya lebih baik menurut saya adalah beberapa tautan ke bacaan lebih lanjut yang menunjukkan beberapa contoh hal-hal yang Anda sebutkan (atau bahkan beberapa kode) - misalnya untuk membungkus kelas STL, dll. Jika tidak, saya sedang membaca jawaban ini, tetapi kemudian saya agak bingung bagaimana sebenarnya hal-hal ini akan terlihat dan bagaimana mencarinya.
Ela782
8

Anda tidak dapat meneruskan objek STL dengan aman melintasi batas DLL, kecuali semua modul (.EXE dan .DLLs) dibuat dengan versi compiler C ++ yang sama dan pengaturan serta ragam CRT yang sama, yang sangat membatasi, dan jelas bukan kasus Anda.

Jika Anda ingin mengekspos antarmuka berorientasi objek dari DLL Anda, Anda harus mengekspos antarmuka murni C ++ (yang mirip dengan apa yang dilakukan COM). Pertimbangkan untuk membaca artikel menarik ini di CodeProject:

HowTo: Ekspor kelas C ++ dari DLL

Anda mungkin juga ingin mempertimbangkan untuk mengekspos antarmuka C murni di batas DLL, lalu membuat pembungkus C ++ di situs pemanggil.
Ini mirip dengan apa yang terjadi di Win32: kode implementasi Win32 hampir C ++, tetapi banyak Win32 API mengekspos antarmuka C murni (ada juga API yang mengekspos antarmuka COM). Kemudian ATL / WTL dan MFC membungkus antarmuka C murni ini dengan kelas dan objek C ++.

Tuan C64
sumber