Làm cách nào để chuyển các đối tượng, đặc biệt là các đối tượng STL, đến và đi một cách an toàn trong một DLL?


106

Làm cách nào để chuyển các đối tượng lớp, đặc biệt là các đối tượng STL, đến và đi từ một C ++ DLL?

Ứng dụng của tôi phải tương tác với các plugin của bên thứ ba dưới dạng tệp DLL và tôi không thể kiểm soát trình biên dịch mà các plugin này được xây dựng. Tôi biết rằng không có ABI được đảm bảo cho các đối tượng STL và tôi lo ngại về việc gây ra sự không ổn định trong ứng dụng của mình.


4
Nếu bạn đang nói về Thư viện chuẩn C ++, thì có lẽ bạn nên gọi nó như vậy. STL có thể có nghĩa là những thứ khác nhau tùy thuộc vào ngữ cảnh. (Xem thêm stackoverflow.com/questions/5205491/… )
Micha Wiedenmann

Câu trả lời:


156

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 intcó 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 packchỉ 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 . _cdeclTuy 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/ freevì 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 GetCCDLLcó thể bị thay đổi _Z8GetCCDLLvtrong GCC và ?GetCCDLL@@YAPAUCCDLL_v1@@XZtrong 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 GetProcAddresssai 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 GetCCDLLhà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 packvà 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 podlớp chuyên cho mỗi kiểu dữ liệu cơ bản, do đó intsẽ tự động được bao bọc để int32_t, uintsẽ đượ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 =()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 ( boolchuyên ngành có một chút logic bổ sung, vì nó được chuyển đổi thành a int8_tvà 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 podchí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 ShowMessagechứ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 podvào bình thường wstringvà 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.

Hình ảnh hiển thị kết quả sau đó.


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.


1
Hừ, không tệ! Bạn đã thu thập được một bộ sưu tập khá tốt các đối số chống lại việc sử dụng các kiểu c ++ tiêu chuẩn để tương tác với DLL cửa sổ và được gắn thẻ tương ứng. Các hạn chế ABI cụ thể này sẽ không áp dụng cho các loại công cụ khác ngoài MSVC. Điều này thậm chí nên được đề cập ...
πάντα ῥεῖ

12
@DavidHeffernan Đúng. Nhưng đây là kết quả của vài tuần nghiên cứu đối với tôi, vì vậy tôi nghĩ sẽ rất đáng giá khi ghi lại những gì tôi đã học được để những người khác không cần phải thực hiện nghiên cứu tương tự và những nỗ lực tương tự để hack cùng một giải pháp hiệu quả. Còn hơn thế nữa vì đây dường như là một câu hỏi bán thường ở đây.
cf đứng với Monica

@ πάνταῥεῖ Những hạn chế ABI cụ thể này sẽ không áp dụng cho các loại công cụ khác ngoài MSVC. Điều này thậm chí nên được đề cập ... Tôi không chắc mình hiểu điều này một cách chính xác. Bạn có chỉ ra rằng các vấn đề ABI này là dành riêng cho MSVC và, giả sử, một DLL được xây dựng bằng clang sẽ hoạt động thành công với EXE được xây dựng bằng GCC? Tôi là một chút bối rối, vì đó dường như mâu thuẫn với tất cả các nghiên cứu của tôi ...
cf đứng với Monica

@computerfreaker Không, tôi đang nói rằng PE và ELF đang sử dụng các định dạng ABI khác nhau ...
πάντα ῥεῖ

3
@computerfreaker Hầu hết các trình biên dịch C ++ chính (GCC, Clang, ICC, EDG, v.v.) đều tuân theo Itanium C ++ ABI. MSVC không. Vì vậy, có, các vấn đề ABI này phần lớn là cụ thể đối với MSVC, mặc dù không phải riêng - ngay cả các trình biên dịch C trên nền tảng Unix (và thậm chí các phiên bản khác nhau của cùng một trình biên dịch!) Cũng bị ảnh hưởng bởi khả năng tương tác kém hoàn hảo. Tuy nhiên, chúng thường đủ gần, đến mức tôi sẽ không ngạc nhiên khi thấy rằng bạn có thể liên kết thành công một DLL do Clang tạo với một tệp thực thi do GCC xây dựng.
Stuart Olsen

17

@computerfreaker đã viết một lời giải thích tuyệt vời về lý do tại sao việc thiếu ABI ngăn cản việc chuyển các đối tượng C ++ qua ranh giới DLL trong trường hợp chung, ngay cả khi định nghĩa kiểu nằm dưới sự kiểm soát của người dùng và trình tự mã thông báo giống nhau chính xác được sử dụng trong cả hai chương trình. (Có hai trường hợp hoạt động: lớp bố cục tiêu chuẩn và giao diện thuần túy)

Đối với các kiểu đối tượng được xác định trong Tiêu chuẩn C ++ (bao gồm cả những kiểu được điều chỉnh từ Thư viện Mẫu Tiêu chuẩn), tình hình còn tệ hơn rất nhiều. Các mã thông báo xác định các loại này KHÔNG giống nhau trên nhiều trình biên dịch, vì Tiêu chuẩn C ++ không cung cấp định nghĩa loại hoàn chỉnh, chỉ có các yêu cầu tối thiểu. Ngoài ra, tra cứu tên của các số nhận dạng xuất hiện trong các định nghĩa loại này không giải quyết như nhau. Ngay cả trên các hệ thống có ABI C ++, việc cố gắng chia sẻ các loại như vậy qua các ranh giới mô-đun dẫn đến hành vi không xác định lớn do vi phạm Quy tắc Một Định nghĩa.

Đây là điều mà các lập trình viên Linux không quen xử lý, bởi vì libstdc ++ của g ++ là một tiêu chuẩn de-facto và hầu như tất cả các chương trình đều sử dụng nó, do đó đáp ứng ODR. libc ++ của clang đã phá vỡ giả định đó, và sau đó C ++ 11 ra đời cùng với những thay đổi bắt buộc đối với gần như tất cả các kiểu thư viện Chuẩn.

Chỉ cần không chia sẻ các loại thư viện Chuẩn giữa các mô-đun. Đó là hành vi không xác định.


16

Một số câu trả lời ở đây khiến việc vượt qua các lớp C ++ nghe có vẻ thực sự đáng sợ, nhưng tôi muốn chia sẻ một quan điểm thay thế. Phương thức C ++ thuần ảo được đề cập trong một số phản hồi khác thực sự hóa ra sạch hơn bạn có thể nghĩ. Tôi đã xây dựng toàn bộ hệ thống plugin xung quanh khái niệm này và nó đã hoạt động rất tốt trong nhiều năm. Tôi có một lớp "PluginManager" tự động tải các dlls từ một thư mục được chỉ định bằng cách sử dụng LoadLib () và GetProcAddress () (và các ứng dụng tương đương của Linux để tệp thực thi làm cho nó đa nền tảng).

Tin hay không thì tùy, phương pháp này có thể tha thứ ngay cả khi bạn thực hiện một số việc kỳ quặc như thêm một chức năng mới vào cuối giao diện ảo thuần túy của bạn và cố gắng tải các hình nền được biên dịch dựa trên giao diện không có chức năng mới đó - chúng sẽ tải tốt. Tất nhiên ... bạn sẽ phải kiểm tra số phiên bản để đảm bảo tệp thực thi của bạn chỉ gọi hàm mới cho các dll mới hơn triển khai chức năng. Nhưng tin tốt là: nó hoạt động! Vì vậy, theo một cách nào đó, bạn có một phương pháp thô sơ để phát triển giao diện của mình theo thời gian.

Một điều thú vị khác về giao diện ảo thuần túy - bạn có thể thừa hưởng bao nhiêu giao diện tùy thích và bạn sẽ không bao giờ gặp phải vấn đề kim cương!

Tôi sẽ nói rằng nhược điểm lớn nhất của cách tiếp cận này là bạn phải rất cẩn thận về những loại bạn chuyển vào làm tham số. Không có lớp hoặc đối tượng STL nào mà không bao bọc chúng bằng các giao diện ảo thuần túy trước. Không có cấu trúc (mà không cần thông qua pragma pack voodoo). Chỉ là các kiểu nguyên thủy và con trỏ đến các giao diện khác. Ngoài ra, bạn không thể làm quá tải các chức năng, đây là một sự bất tiện, nhưng không phải là một trình dừng.

Tin tốt là với một số ít dòng mã, bạn có thể tạo các lớp và giao diện chung có thể tái sử dụng để bọc chuỗi STL, vectơ và các lớp vùng chứa khác. Ngoài ra, bạn có thể thêm các chức năng vào giao diện của mình như GetCount () và GetVal (n) để cho phép mọi người xem qua danh sách.

Mọi người xây dựng plugin cho chúng tôi thấy khá dễ dàng. Họ không cần phải là chuyên gia về ranh giới ABI hay bất cứ thứ gì - họ chỉ kế thừa các giao diện mà họ quan tâm, viết mã các chức năng mà họ hỗ trợ và trả về false cho những giao diện mà họ không có.

Công nghệ làm cho tất cả những thứ này hoạt động không dựa trên bất kỳ tiêu chuẩn nào theo như tôi biết. Từ những gì tôi thu thập được, Microsoft đã quyết định làm các bảng ảo của họ theo cách đó để họ có thể tạo COM, và các nhà biên dịch khác cũng quyết định làm theo. Điều này bao gồm GCC, Intel, Borland và hầu hết các trình biên dịch C ++ lớn khác. Nếu bạn đang lên kế hoạch sử dụng một trình biên dịch nhúng khó hiểu thì cách tiếp cận này có thể sẽ không phù hợp với bạn. Về mặt lý thuyết, bất kỳ công ty biên dịch nào cũng có thể thay đổi bảng ảo của họ bất cứ lúc nào và phá vỡ mọi thứ, nhưng xem xét số lượng lớn mã được viết trong những năm phụ thuộc vào công nghệ này, tôi sẽ rất ngạc nhiên nếu bất kỳ người chơi lớn nào quyết định phá vỡ thứ hạng.

Vì vậy, đạo đức của câu chuyện là ... Ngoại trừ một số trường hợp cực đoan, bạn cần một người phụ trách các giao diện có thể đảm bảo ranh giới ABI luôn sạch sẽ với các loại nguyên thủy và tránh quá tải. Nếu bạn đồng ý với quy định đó, thì tôi sẽ không ngại chia sẻ giao diện cho các lớp trong DLL / SOs giữa các trình biên dịch. Chia sẻ trực tiếp lớp học == rắc rối, nhưng chia sẻ giao diện ảo thuần túy không quá tệ.


Đó là một điểm tốt ... Tôi nên nói "Đừng ngại chia sẻ giao diện cho các lớp". Tôi sẽ chỉnh sửa câu trả lời của mình.
Ph0t0n 27/09/17

2
Này, đó là một câu trả lời tuyệt vời, cảm ơn! Theo ý kiến ​​của tôi, điều gì sẽ làm cho nó thậm chí còn tốt hơn sẽ là một số liên kết để đọc thêm hiển thị một số ví dụ về những thứ mà bạn đang đề cập (hoặc thậm chí là một số mã) - ví dụ như gói các lớp STL, v.v. Nếu không, tôi đang đọc câu trả lời này nhưng sau đó tôi hơi mất hứng về cách những thứ này thực sự trông như thế nào và cách tìm kiếm chúng.
Ela782 09/09

8

Bạn không thể chuyển các đối tượng STL qua ranh giới DLL một cách an toàn, trừ khi tất cả các mô-đun (.EXE và .DLL) được xây dựng với cùng một phiên bản trình biên dịch C ++ và các cài đặt và hương vị giống nhau của CRT, điều này có nhiều ràng buộc và rõ ràng không phải trường hợp của bạn.

Nếu bạn muốn hiển thị giao diện hướng đối tượng từ DLL của mình, bạn nên hiển thị giao diện thuần C ++ (tương tự như những gì COM làm). Hãy xem xét việc đọc bài viết thú vị này trên CodeProject:

HowTo: Xuất các lớp C ++ từ DLL

Bạn cũng có thể muốn xem xét việc hiển thị giao diện C thuần túy ở ranh giới DLL, sau đó xây dựng trình bao bọc C ++ tại trang web người gọi.
Điều này tương tự như những gì xảy ra trong Win32: Mã thực thi Win32 gần như là C ++, nhưng rất nhiều API Win32 để lộ giao diện C thuần túy (cũng có các API hiển thị giao diện COM). Sau đó ATL / WTL và MFC bọc các giao diện C thuần túy này bằng các lớp và đối tượng C ++.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.