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 int
mungkin 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 pack
arahan 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 . _cdecl
adalah konvensi pemanggilan default untuk fungsi C ++, bagaimanapun, jadi ini adalah satu hal yang tidak akan rusak kecuali Anda sengaja merusaknya dengan menentukan _stdcall
di satu tempat dan _cdecl
di 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
/ free
karena 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 GetCCDLL
mungkin rusak _Z8GetCCDLLv
di GCC dan ?GetCCDLL@@YAPAUCCDLL_v1@@XZ
di 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 GetProcAddress
nama 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 GetCCDLL
fungsi 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 pack
dan 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 pod
kelas khusus untuk setiap datatype dasar, sehingga int
secara otomatis akan dibungkus untuk int32_t
, uint
akan 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 ( bool
spesialisasi memiliki sedikit logika tambahan, karena itu diubah menjadi int8_t
dan kemudian int8_t
dibandingkan 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 pod
dirinya 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 ShowMessage
fungsinya:
#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 pod
ke normal wstring
dan 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.
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.