Câu trả lời ngắn gọn cho câu hỏi này là không . Vì không có C ++ ABI tiêu chuẩn (giao diện nhị phân ứng dụng, tiêu chuẩn để gọi quy ước, đóng gói / căn chỉnh dữ liệu, kích thước kiểu, v.v.), bạn sẽ phải vượt qua rất nhiều vòng để thử và thực thi cách xử lý tiêu chuẩn với lớp các đối tượng trong chương trình của bạn. Thậm chí không có gì đảm bảo rằng nó sẽ hoạt động sau khi bạn vượt qua tất cả các vòng đó, cũng không có gì đảm bảo rằng một giải pháp hoạt động trong một bản phát hành trình biên dịch sẽ hoạt động trong lần tiếp theo.
Chỉ cần tạo một giao diện C đơn giản bằng cách sử dụng extern "C"
, vì C ABI được xác định rõ ràng và ổn định.
Nếu bạn thực sự, thực sự muốn chuyển các đối tượng C ++ qua một ranh giới DLL, về mặt kỹ thuật thì điều đó hoàn toàn có thể. Dưới đây là một số yếu tố bạn sẽ phải tính đến:
Đóng gói / căn chỉnh dữ liệu
Trong một lớp nhất định, các thành viên dữ liệu riêng lẻ thường sẽ được đặt đặc biệt trong bộ nhớ để địa chỉ của chúng tương ứng với bội số kích thước của kiểu. Ví dụ: một int
có thể được căn chỉnh thành ranh giới 4 byte.
Nếu DLL của bạn được biên dịch bằng một trình biên dịch khác với EXE của bạn, thì phiên bản DLL của một lớp nhất định có thể có cách đóng gói khác với phiên bản của EXE, vì vậy khi EXE chuyển đối tượng lớp sang DLL, DLL có thể không thể truy cập đúng cách thành viên dữ liệu đã cho trong lớp đó. DLL sẽ cố gắng đọc từ địa chỉ được chỉ định bởi định nghĩa riêng của lớp, không phải định nghĩa của EXE, và vì thành viên dữ liệu mong muốn thực sự không được lưu trữ ở đó, các giá trị rác sẽ dẫn đến.
Bạn có thể giải quyết vấn đề này bằng cách sử dụng #pragma pack
chỉ thị tiền xử lý, lệnh này sẽ buộc trình biên dịch áp dụng đóng gói cụ thể. Trình biên dịch sẽ vẫn áp dụng đóng gói mặc định nếu bạn chọn giá trị gói lớn hơn giá trị mà trình biên dịch đã chọn , vì vậy nếu bạn chọn giá trị đóng gói lớn, một lớp vẫn có thể có đóng gói khác nhau giữa các trình biên dịch. Giải pháp cho điều này là sử dụng #pragma pack(1)
, điều này sẽ buộc trình biên dịch sắp xếp các thành viên dữ liệu trên một ranh giới một byte (về cơ bản, không có đóng gói nào sẽ được áp dụng). Đây không phải là một ý tưởng tuyệt vời, vì nó có thể gây ra các vấn đề về hiệu suất hoặc thậm chí là sự cố trên một số hệ thống nhất định. Tuy nhiên, nó sẽ đảm bảo tính nhất quán trong cách các thành viên dữ liệu của lớp bạn được căn chỉnh trong bộ nhớ.
Sắp xếp lại thành viên
Nếu lớp của bạn không phải là bố cục chuẩn , trình biên dịch có thể sắp xếp lại các thành viên dữ liệu của nó trong bộ nhớ . Không có tiêu chuẩn nào về cách thực hiện điều này, vì vậy bất kỳ sự sắp xếp lại dữ liệu nào cũng có thể gây ra sự không tương thích giữa các trình biên dịch. Do đó, việc chuyển dữ liệu qua lại tới một DLL sẽ yêu cầu các lớp bố cục tiêu chuẩn.
Quy ước gọi điện
Có nhiều quy ước gọi mà một hàm nhất định có thể có. Các quy ước gọi này chỉ định cách dữ liệu được truyền đến các hàm: các tham số được lưu trữ trong thanh ghi hay trên ngăn xếp? Thứ tự các đối số được đẩy lên ngăn xếp? Ai xóa bất kỳ đối số nào còn lại trên ngăn xếp sau khi hàm kết thúc?
Điều quan trọng là bạn phải duy trì một quy ước gọi tiêu chuẩn; nếu bạn khai báo một hàm là _cdecl
, mặc định cho C ++ và cố gắng gọi nó bằng cách sử dụng _stdcall
những điều xấu sẽ xảy ra . _cdecl
Tuy nhiên, là quy ước gọi mặc định cho các hàm C ++, vì vậy đây là một thứ sẽ không bị phá vỡ trừ khi bạn cố tình phá vỡ nó bằng cách chỉ định một _stdcall
ở một nơi và một _cdecl
ở một nơi khác.
Kích thước kiểu dữ liệu
Theo tài liệu này , trên Windows, hầu hết các kiểu dữ liệu cơ bản đều có cùng kích thước bất kể ứng dụng của bạn là 32 bit hay 64 bit. Tuy nhiên, vì kích thước của một kiểu dữ liệu nhất định được thực thi bởi trình biên dịch chứ không phải theo bất kỳ tiêu chuẩn nào (tất cả các tiêu chuẩn đều đảm bảo như vậy 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
), nên bạn nên sử dụng các kiểu dữ liệu có kích thước cố định để đảm bảo khả năng tương thích với kích thước kiểu dữ liệu nếu có thể.
Đống sự cố
Nếu DLL của bạn liên kết đến một phiên bản C runtime khác với EXE của bạn, hai mô-đun sẽ sử dụng các heap khác nhau . Đây là một vấn đề đặc biệt có thể xảy ra do các mô-đun đang được biên dịch bằng các trình biên dịch khác nhau.
Để giảm thiểu điều này, tất cả bộ nhớ sẽ phải được cấp phát vào một heap dùng chung và được phân bổ từ cùng một heap. May mắn thay, Windows cung cấp các API để trợ giúp việc này: GetProcessHeap sẽ cho phép bạn truy cập vào heap của máy chủ EXE và HeapAlloc / HeapFree sẽ cho phép bạn phân bổ và giải phóng bộ nhớ trong heap này. Điều quan trọng là bạn không sử dụng bình thường malloc
/ free
vì không có gì đảm bảo rằng chúng sẽ hoạt động theo cách bạn mong đợi.
Các vấn đề về STL
Thư viện tiêu chuẩn C ++ có bộ vấn đề ABI của riêng nó. Không có gì đảm bảo rằng một loại STL nhất định được trình bày theo cùng một cách trong bộ nhớ, cũng không có gì đảm bảo rằng một lớp STL nhất định có cùng kích thước từ một triển khai này sang một triển khai khác (cụ thể, các bản dựng gỡ lỗi có thể đưa thêm thông tin gỡ lỗi vào một loại STL đã cho). Do đó, bất kỳ vùng chứa STL nào sẽ phải được giải nén thành các loại cơ bản trước khi được chuyển qua ranh giới DLL và được đóng gói lại ở phía bên kia.
Tên mangling
DLL của bạn có lẽ sẽ xuất các hàm mà EXE của bạn sẽ muốn gọi. Tuy nhiên, các trình biên dịch C ++ không có một cách chuẩn mực cho các tên hàm . Điều này có nghĩa là một hàm được đặt tên GetCCDLL
có thể bị thay đổi _Z8GetCCDLLv
trong GCC và ?GetCCDLL@@YAPAUCCDLL_v1@@XZ
trong MSVC.
Bạn đã không thể đảm bảo liên kết tĩnh với DLL của mình, vì DLL được tạo bằng GCC sẽ không tạo ra tệp .lib và liên kết tĩnh DLL trong MSVC yêu cầu phải có. Liên kết động có vẻ như là một lựa chọn gọn gàng hơn nhiều, nhưng việc ghép tên sẽ cản trở bạn: nếu bạn cố gắng đặt GetProcAddress
sai tên bị xáo trộn , cuộc gọi sẽ thất bại và bạn sẽ không thể sử dụng DLL của mình. Điều này đòi hỏi một chút hackery để vượt qua và là một lý do khá chính tại sao việc chuyển các lớp C ++ qua ranh giới DLL là một ý tưởng tồi.
Bạn sẽ cần tạo DLL của mình, sau đó kiểm tra tệp .def đã tạo (nếu tệp được tạo; điều này sẽ thay đổi tùy theo tùy chọn dự án của bạn) hoặc sử dụng một công cụ như Dependency Walker để tìm tên bị xáo trộn. Sau đó, bạn sẽ cần viết tệp .def của riêng mình , xác định bí danh không bị nhầm lẫn cho hàm mangled. Ví dụ, hãy sử dụng GetCCDLL
hàm mà tôi đã đề cập ở trên. Trên hệ thống của tôi, các tệp .def sau đây tương ứng hoạt động cho GCC và MSVC:
GCC:
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC:
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
Xây dựng lại DLL của bạn, sau đó kiểm tra lại các chức năng mà nó xuất ra. Một tên hàm không bị nhầm lẫn nên nằm trong số đó. Lưu ý rằng bạn không thể sử dụng các hàm bị quá tải theo cách này : tên hàm không bị xáo trộn là một bí danh cho một hàm quá tải cụ thể như được định nghĩa bởi tên bị xáo trộn . Cũng lưu ý rằng bạn sẽ cần tạo tệp .def mới cho DLL của mình mỗi khi bạn thay đổi khai báo hàm, vì tên bị xáo trộn sẽ thay đổi. Quan trọng nhất, bằng cách bỏ qua việc xáo trộn tên, bạn đang ghi đè mọi biện pháp bảo vệ mà trình liên kết đang cố gắng cung cấp cho bạn liên quan đến các vấn đề không tương thích.
Toàn bộ quá trình này sẽ đơn giản hơn nếu bạn tạo một giao diện cho DLL của mình để làm theo, vì bạn sẽ chỉ có một chức năng để xác định bí danh thay vì cần tạo bí danh cho mọi hàm trong DLL của bạn. Tuy nhiên, các lưu ý tương tự vẫn được áp dụng.
Truyền các đối tượng lớp cho một hàm
Đây có lẽ là vấn đề phức tạp nhất và nguy hiểm nhất trong số các vấn đề mà dữ liệu trình biên dịch chéo truyền qua. Ngay cả khi bạn xử lý mọi thứ khác, không có tiêu chuẩn nào về cách các đối số được truyền vào một hàm . Điều này có thể gây ra sự cố nhỏ mà không có lý do rõ ràng và không có cách nào dễ dàng để gỡ lỗi chúng . Bạn sẽ cần chuyển tất cả các đối số qua con trỏ, bao gồm cả bộ đệm cho bất kỳ giá trị trả về nào. Điều này là vụng về và bất tiện, đồng thời là một giải pháp khắc phục sự cố khác có thể hoạt động hoặc không.
Tập hợp tất cả những cách giải quyết này lại với nhau và xây dựng trên một số công việc sáng tạo với các mẫu và toán tử , chúng ta có thể cố gắng chuyển các đối tượng qua ranh giới DLL một cách an toàn. Lưu ý rằng hỗ trợ C ++ 11 là bắt buộc, cũng như hỗ trợ cho #pragma pack
và các biến thể của nó; MSVC 2013 cung cấp hỗ trợ này, cũng như các phiên bản GCC và clang gần đây.
//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)
Các pod
lớp chuyên cho mỗi kiểu dữ liệu cơ bản, do đó int
sẽ tự động được bao bọc để int32_t
, uint
sẽ được bao bọc để uint32_t
, vv Điều này tất cả xảy ra đằng sau hậu trường, nhờ vào sự quá tải =
và ()
khai thác. Tôi đã bỏ qua phần còn lại của các chuyên ngành kiểu cơ bản vì chúng gần như hoàn toàn giống nhau ngoại trừ các kiểu dữ liệu cơ bản ( bool
chuyên ngành có một chút logic bổ sung, vì nó được chuyển đổi thành a int8_t
và sau đó int8_t
được so sánh với 0 để chuyển đổi trở lại bool
, nhưng điều này là khá tầm thường).
Chúng tôi cũng có thể bọc các loại STL theo cách này, mặc dù nó yêu cầu thêm một chút công việc:
#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)
Bây giờ chúng ta có thể tạo một DLL sử dụng các loại nhóm này. Đầu tiên, chúng tôi cần một giao diện, vì vậy chúng tôi sẽ chỉ có một phương pháp để tìm ra cách xử lý.
//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();
Điều này chỉ tạo ra một giao diện cơ bản mà cả DLL và bất kỳ người gọi nào đều có thể sử dụng. Lưu ý rằng chúng ta đang chuyển một con trỏ tới a pod
, không phải pod
chính nó. Bây giờ chúng ta cần triển khai điều đó ở phía 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;
}
Và bây giờ hãy triển khai ShowMessage
chức năng:
#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);
}
Không có gì quá lạ mắt: điều này chỉ sao chép thông tin được truyền pod
vào bình thường wstring
và hiển thị nó trong hộp thư. Rốt cuộc, đây chỉ là một POC , không phải là một thư viện tiện ích đầy đủ.
Bây giờ chúng ta có thể xây dựng DLL. Đừng quên các tệp .def đặc biệt để giải quyết vấn đề liên quan đến tên của trình liên kết. (Lưu ý: cấu trúc CCDLL mà tôi thực sự đã xây dựng và chạy có nhiều chức năng hơn cấu trúc mà tôi trình bày ở đây. Các tệp .def có thể không hoạt động như mong đợi.)
Bây giờ để EXE gọi 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;
}
Và đây là kết quả. DLL của chúng tôi hoạt động. Chúng tôi đã giải quyết thành công các vấn đề STL ABI trong quá khứ, các vấn đề C ++ ABI trước đây, các vấn đề xử lý trong quá khứ và MSVC DLL của chúng tôi đang làm việc với GCC EXE.
Tóm lại, nếu bạn nhất thiết phải chuyển các đối tượng C ++ qua ranh giới DLL, thì đây là cách bạn thực hiện. Tuy nhiên, không điều gì trong số này được đảm bảo sẽ hoạt động với thiết lập của bạn hoặc của bất kỳ ai khác. Bất kỳ điều gì trong số này có thể bị hỏng bất cứ lúc nào và có thể sẽ hỏng một ngày trước khi phần mềm của bạn được lên lịch phát hành chính. Con đường này đầy rẫy những mánh khóe, rủi ro và sự ngu ngốc nói chung mà tôi có lẽ nên bị bắt. Nếu bạn đi theo con đường này, vui lòng kiểm tra hết sức thận trọng. Và thực sự ... không làm điều này chút nào.