C++ 如何安全地将对象(尤其是 STL 对象)传入和传出 DLL?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/22797418/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-28 00:13:17  来源:igfitidea点击:

How do I safely pass objects, especially STL objects, to and from a DLL?

c++windowsdllstlabi

提问by cf stands with Monica

How do I pass class objects, especially STL objects, to and from a C++ DLL?

如何在 C++ DLL 之间传递类对象,尤其是 STL 对象?

My application has to interact with third-party plugins in the form of DLL files, and I can't control what compiler these plugins are built with. I'm aware that there's no guaranteed ABI for STL objects, and I'm concerned about causing instability in my application.

我的应用程序必须以 DLL 文件的形式与第三方插件交互,我无法控制这些插件是用什么编译器构建的。我知道 STL 对象没有保证的 ABI,我担心会导致我的应用程序不稳定。

回答by cf stands with Monica

The short answer to this question is don't. Because there's no standard C++ ABI(application binary interface, a standard for calling conventions, data packing/alignment, type size, etc.), you will have to jump through a lot of hoops to try and enforce a standard way of dealing with class objects in your program. There's not even a guarantee it'll work after you jump through all those hoops, nor is there a guarantee that a solution which works in one compiler release will work in the next.

这个问题的简短答案是不要。因为没有标准的 C++ ABI(应用程序二进制接口、调用约定、数据打包/对齐、类型大小等的标准),你将不得不跳过很多圈子来尝试和执行处理类的标准方式程序中的对象。甚至不能保证在您跳过所有这些箍之后它会起作用,也不能保证在一个编译器版本中工作的解决方案在下一个版本中工作。

Just create a plain C interface using extern "C", since the C ABI iswell-defined and stable.

只是创建一个普通的C接口使用extern "C",因为C ABI明确定义的和稳定的。



If you really, reallywant to pass C++ objects across a DLL boundary, it's technically possible. Here are some of the factors you'll have to account for:

如果您真的、真的想通过 DLL 边界传递 C++ 对象,那么这在技术上是可行的。以下是您必须考虑的一些因素:

Data packing/alignment

数据打包/对齐

Within a given class, individual data members will usually be specially placed in memory so their addresses correspond to a multiple of the type's size. For example, an intmight be aligned to a 4-byte boundary.

在给定的类中,单个数据成员通常会专门放置在内存中,因此它们的地址对应于类型大小的倍数。例如, anint可能与 4 字节边界对齐。

If your DLL is compiled with a different compiler than your EXE, the DLL's version of a given class might have different packing than the EXE's version, so when the EXE passes the class object to the DLL, the DLL might be unable to properly access a given data member within that class. The DLL would attempt to read from the address specified by its own definition of the class, not the EXE's definition, and since the desired data member is not actually stored there, garbage values would result.

如果您的 DLL 是使用与 EXE 不同的编译器编译的,则给定类的 DLL 版本可能与 EXE 的版本具有不同的打包方式,因此当 EXE 将类对象传递给 DLL 时,DLL 可能无法正确访问该类中的给定数据成员。DLL 会尝试从它自己的类定义指定的地址读取,而不是 EXE 的定义,并且由于所需的数据成员实际上没有存储在那里,因此会产生垃圾值。

You can work around this using the #pragma packpreprocessor directive, which will force the compiler to apply specific packing. The compiler will still apply default packing if you select a pack value bigger than the one the compiler would have chosen, so if you pick a large packing value, a class can still have different packing between compilers. The solution for this is to use #pragma pack(1), which will force the compiler to align data members on a one-byte boundary (essentially, no packing will be applied). This is not a great idea, as it can cause performance issues or even crashes on certain systems.However, it willensure consistency in the way your class's data members are aligned in memory.

您可以使用#pragma pack预处理器指令解决此问题,该指令将强制编译器应用特定的打包。如果您选择的包值大于编译器选择的值,编译器仍将应用默认打包,因此如果您选择大的打包值,类在编译器之间仍然可以具有不同的打包。对此的解决方案是使用#pragma pack(1),这将强制编译器在一个字节的边界上对齐数据成员(本质上,不会应用打包)。这不是一个好主意,因为它可能会导致性能问题甚至在某些系统上崩溃。但是,它确保在您的类的数据成员在内存中的对齐方式的一致性。

Member reordering

会员重新排序

If your class is not standard-layout, the compiler can rearrange its data members in memory. There is no standard for how this is done, so any data rearranging can cause incompatibilities between compilers. Passing data back and forth to a DLL will require standard-layout classes, therefore.

如果您的类不是标准布局,编译器可以在内存中重新排列其数据成员。没有关于如何完成的标准,因此任何数据重新排列都可能导致编译器之间的不兼容。因此,将数据来回传递到 DLL 将需要标准布局类。

Calling convention

调用约定

There are multiple calling conventionsa given function can have. These calling conventions specify how data is to be passed to functions: are parameters stored in registers or on the stack? What order are arguments pushed onto the stack? Who cleans up any arguments left on the stack after the function finishes?

一个给定的函数可以有多种调用约定。这些调用约定指定了如何将数据传递给函数:参数是存储在寄存器中还是存储在堆栈中?参数压入堆栈的顺序是什么?函数完成后,谁来清除堆栈中剩余的任何参数?

It's important you maintain a standard calling convention; if you declare a function as _cdecl, the default for C++, and try to call it using _stdcallbad things will happen. _cdeclis the default calling convention for C++ functions, however, so this is one thing that won't break unless you deliberately break it by specifying an _stdcallin one place and a _cdeclin another.

保持标准的调用约定很重要;如果您将函数声明_cdecl为 C++ 的默认值,并尝试使用_stdcall坏事调用它,则会发生_cdecl然而,这是 C++ 函数的默认调用约定,因此这是不会破坏的一件事,除非您通过_stdcall在一个地方指定一个而在另一个地方指定a来故意破坏它_cdecl

Datatype size

数据类型大小

According to this documentation, on Windows, most fundamental datatypes have the same sizes regardless of whether your app is 32-bit or 64-bit. However, since the size of a given datatype is enforced by the compiler, not by any standard (all the standard guarantees is that 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)), it's a good idea to use fixed-size datatypesto ensure datatype size compatibility where possible.

根据此文档,在 Windows 上,无论您的应用程序是 32 位还是 64 位,大多数基本数据类型都具有相同的大小。但是,由于给定数据类型的大小是由编译器强制执行的,而不是由任何标准强制执行的(所有标准保证都是这样1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)),因此最好使用固定大小的数据类型来确保尽可能与数据类型大小兼容。

Heap issues

堆问题

If your DLL links to a different version of the C runtime than your EXE, the two modules will use different heaps. This is an especially likely problem given that the modules are being compiled with different compilers.

如果您的 DLL 链接到与 EXE 不同的 C 运行时版本,则这两个模块将使用不同的堆。鉴于模块是用不同的编译器编译的,这是一个特别可能的问题。

To mitigate this, all memory will have to be allocated into a shared heap, and deallocated from the same heap. Fortunately, Windows provides APIs to help with this: GetProcessHeapwill let you access the host EXE's heap, and HeapAlloc/HeapFreewill let you allocate and free memory within this heap. It is important that you not use normal malloc/freeas there is no guarantee they will work the way you expect.

为了缓解这种情况,所有内存都必须分配到一个共享堆中,并从同一个堆中释放。幸运的是,Windows 提供了 API 来帮助解决这个问题:GetProcessHeap将允许您访问主机 EXE 的堆,而HeapAlloc/ HeapFree将允许您在此堆中分配和释放内存。重要的是你不要使用正常的malloc/free因为不能保证它们会按照你期望的方式工作。

STL issues

STL问题

The C++ standard library has its own set of ABI issues. There is no guaranteethat a given STL type is laid out the same way in memory, nor is there a guarantee that a given STL class has the same size from one implementation to another (in particular, debug builds may put extra debug information into a given STL type). Therefore, any STL container will have to be unpacked into fundamental types before being passed across the DLL boundary and repacked on the other side.

C++ 标准库有自己的一套 ABI 问题。有没有保证,一个给定的STL类型奠定了在内存中以同样的方式,也不是有保证给定的STL类有不同的实现到另一个相同的尺寸(特别是调试版本可以把额外的调试信息成给定的 STL 类型)。因此,任何 STL 容器在通过 DLL 边界并在另一侧重新打包之前都必须被解包为基本类型。

Name mangling

名称修改

Your DLL will presumably export functions which your EXE will want to call. However, C++ compilers do not have a standard way of mangling function names. This means a function named GetCCDLLmight be mangled to _Z8GetCCDLLvin GCC and ?GetCCDLL@@YAPAUCCDLL_v1@@XZin MSVC.

您的 DLL 可能会导出您的 EXE 想要调用的函数。但是,C++ 编译器没有修改函数名称的标准方法。这意味着一个名为的函数GetCCDLL可能会_Z8GetCCDLLv在 GCC 和?GetCCDLL@@YAPAUCCDLL_v1@@XZMSVC 中被修改。

You already won't be able to guarantee static linking to your DLL, since a DLL produced with GCC won't produce a .lib file and statically linking a DLL in MSVC requires one. Dynamically linking seems like a much cleaner option, but name mangling gets in your way: if you try to GetProcAddressthe wrong mangled name, the call will fail and you won't be able to use your DLL. This requires a little bit of hackery to get around, and is a fairly major reason why passing C++ classes across a DLL boundary is a bad idea.

您已经无法保证静态链接到您的 DLL,因为使用 GCC 生成的 DLL 不会生成 .lib 文件,并且在 MSVC 中静态链接 DLL 需要一个。动态链接似乎是一个更简洁的选择,但名称修改会妨碍您:如果您尝试GetProcAddress使用错误的修改名称,则调用将失败并且您将无法使用您的 DLL。这需要一点技巧来解决,这也是为什么跨 DLL 边界传递 C++ 类是个坏主意的一个相当重要的原因。

You'll need to build your DLL, then examine the produced .def file (if one is produced; this will vary based on your project options) or use a tool like Dependency Walker to find the mangled name. Then, you'll need to write your own.def file, defining an unmangled alias to the mangled function. As an example, let's use the GetCCDLLfunction I mentioned a bit further up. On my system, the following .def files work for GCC and MSVC, respectively:

您需要构建 DLL,然后检查生成的 .def 文件(如果生成了;这将根据您的项目选项而有所不同)或使用 Dependency Walker 之类的工具来查找损坏的名称。然后,您需要编写自己的.def 文件,为 mangled 函数定义一个 unmangled 别名。举个例子,让我们GetCCDLL进一步使用我提到的函数。在我的系统上,以下 .def 文件分别适用于 GCC 和 MSVC:

GCC:

海湾合作委员会:

EXPORTS
    GetCCDLL=_Z8GetCCDLLv @1

MSVC:

MSVC:

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

Rebuild your DLL, then re-examine the functions it exports. An unmangled function name should be among them. Note that you cannot use overloaded functions this way: the unmangled function name is an alias for one specific function overloadas defined by the mangled name. Also note that you'll need to create a new .def file for your DLL every time you change the function declarations, since the mangled names will change. Most importantly, by bypassing the name mangling, you're overriding any protections the linker is trying to offer you with regards to incompatibility issues.

重建您的 DLL,然后重新检查它导出的函数。其中应该有一个未混淆的函数名称。请注意,您不能以这种方式使用重载函数:未损坏的函数名称是由已损坏名称定义的特定函数重载的别名。另请注意,每次更改函数声明时,您都需要为 DLL 创建一个新的 .def 文件,因为损坏的名称会更改。最重要的是,通过绕过名称修改,您将覆盖链接器试图为您提供的有关不兼容问题的任何保护。

This whole process is simpler if you create an interfacefor your DLL to follow, since you'll just have one function to define an alias for instead of needing to create an alias for every function in your DLL. However, the same caveats still apply.

如果您为 DLL创建一个接口,则整个过程会更简单,因为您只需一个函数来定义别名,而不需要为 DLL 中的每个函数创建别名。但是,同样的警告仍然适用。

Passing class objects to a function

将类对象传递给函数

This is probably the most subtle and most dangerous of the issues that plague cross-compiler data passing. Even if you handle everything else, there's no standard for how arguments are passed to a function. This can cause subtle crashes with no apparent reason and no easy way to debug them. You'll need to pass allarguments via pointers, including buffers for any return values. This is clumsy and inconvenient, and is yet another hacky workaround that may or may not work.

这可能是困扰交叉编译器数据传递的最微妙和最危险的问题。即使您处理了其他所有事情,也没有关于如何将参数传递给函数的标准。这可能会导致没有明显原因的微妙崩溃,也没有简单的方法来调试它们。您需要通过指针传递所有参数,包括任何返回值的缓冲区。这是笨拙和不方便的,并且是另一种可能有效也可能无效的hacky解决方法。



Putting together all these workarounds and building on some creative work with templates and operators, we can attempt to safely pass objects across a DLL boundary. Note that C++11 support is mandatory, as is support for #pragma packand its variants; MSVC 2013 offers this support, as do recent versions of GCC and clang.

将所有这些变通方法放在一起,并利用模板和运算符进行一些创造性的工作,我们可以尝试安全地跨 DLL 边界传递对象。请注意,C++11 支持是强制性的,对 C++11#pragma pack及其变体的支持也是如此;MSVC 2013 提供了这种支持,最近版本的 GCC 和 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 podclass is specialized for every basic datatype, so that intwill automatically be wrapped to int32_t, uintwill be wrapped to uint32_t, etc. This all occurs behind the scenes, thanks to the overloaded =and ()operators. I have omitted the rest of the basic type specializations since they're almost entirely the same except for the underlying datatypes (the boolspecialization has a little bit of extra logic, since it's converted to a int8_tand then the int8_tis compared to 0 to convert back to bool, but this is fairly trivial).

pod班是专门为每一个基本数据类型,这样int将自动包裹int32_tuint将包裹uint32_t等,这一切都发生在幕后,由于超载=()运营商。我省略了其余的基本类型特化,因为它们几乎完全相同,除了底层数据类型(特化bool有一点额外的逻辑,因为它被转换为 aint8_t然后将 theint8_t与 0 进行比较以转换回bool,但这是相当微不足道的)。

We can also wrap STL types in this way, although it requires a little extra work:

我们也可以用这种方式包装 STL 类型,尽管它需要一些额外的工作:

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

Now we can create a DLL that makes use of these pod types. First we need an interface, so we'll only have one method to figure out mangling for.

现在我们可以创建一个使用这些 pod 类型的 DLL。首先,我们需要一个接口,所以我们只有一种方法来确定重整。

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

This just creates a basic interface both the DLL and any callers can use. Note that we're passing a pointer to a pod, not a poditself. Now we need to implement that on the DLL side:

这只是创建了一个 DLL 和任何调用者都可以使用的基本接口。请注意,我们传递的是指向 a 的指针pod,而不是 apod本身。现在我们需要在 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;
}

And now let's implement the ShowMessagefunction:

现在让我们实现这个ShowMessage功能:

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

Nothing too fancy: this just copies the passed podinto a normal wstringand shows it in a messagebox. After all, this is just a POC, not a full utility library.

没什么太花哨的:这只是将传递的内容复制pod到正常状态wstring并将其显示在消息框中。毕竟,这只是一个POC,而不是一个完整的实用程序库。

Now we can build the DLL. Don't forget the special .def files to work around the linker's name mangling. (Note: the CCDLL struct I actually built and ran had more functions than the one I present here. The .def files may not work as expected.)

现在我们可以构建 DLL。不要忘记特殊的 .def 文件来解决链接器的名称修改问题。(注意:我实际构建和运行的 CCDLL 结构比我在这里展示的有更多的功能。.def 文件可能无法按预期工作。)

Now for an EXE to call the DLL:

现在让 EXE 调用 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;
}

And here are the results. Our DLL works. We've successfully reached past STL ABI issues, past C++ ABI issues, past mangling issues, and our MSVC DLL is working with a GCC EXE.

这是结果。我们的 DLL 工作正常。我们已经成功解决了过去的 STL ABI 问题、过去的 C++ ABI 问题、过去的重整问题,并且我们的 MSVC DLL 正在使用 GCC EXE。

The image that showing the result afterward.

之后显示结果的图像。



In conclusion, if you absolutely mustpass C++ objects across DLL boundaries, this is how you do it. However, none of this is guaranteed to work with your setup or anyone else's. Any of this may break at any time, and probably will break the day before your software is scheduled to have a major release. This path is full of hacks, risks, and general idiocy that I probably should be shot for. If you do go this route, please test with extreme caution. And really... just don't do this at all.

总之,如果您绝对必须跨 DLL 边界传递 C++ 对象,那么您就是这样做的。但是,这些都不能保证适用于您或其他任何人的设置。其中任何一个都可能随时中断,并且可能会在您的软件计划发布主要版本的前一天中断。这条道路充满了黑客、风险和一般的白痴,我可能应该被枪杀。如果您确实要走这条路线,请非常小心地进行测试。真的......只是不要这样做。

回答by Ben Voigt

@computerfreaker has written a great explanation of why the lack of ABI prevents passing C++ objects across DLL boundaries in the general case, even when the type definitions are under user control and the exact same token sequence is used in both programs. (There are two cases which do work: standard-layout classes, and pure interfaces)

@computerfreaker 写了一个很好的解释,说明为什么缺乏 ABI 会阻止在一般情况下跨 DLL 边界传递 C++ 对象,即使类型定义在用户控制之下并且两个程序中使用完全相同的标记序列。(有两种情况可以工作:标准布局类和纯接口)

For object types defined in the C++ Standard (including those adapted from the Standard Template Library), the situation is far, far worse. The tokens defining these types are NOT the same across multiple compilers, as the C++ Standard does not provide a complete type definition, only minimum requirements. In addition, name lookup of the identifiers that appear in these type definitions don't resolve the same. Even on systems where there is a C++ ABI, attempting to share such types across module boundaries results in massive undefined behavior due to One Definition Rule violations.

对于 C++ 标准中定义的对象类型(包括那些改编自标准模板库的对象),情况要糟糕得多。定义这些类型的标记在多个编译器中并不相同,因为 C++ 标准不提供完整的类型定义,仅提供最低要求。此外,出现在这些类型定义中的标识符的名称查找不会解析相同。 即使在有 C++ ABI 的系统上,尝试跨模块边界共享此类类型也会由于违反单一定义规则而导致大量未定义行为。

This is something that Linux programmers weren't accustomed to dealing with, because g++'s libstdc++ was a de-facto standard and virtually all programs used it, thus satisfying the ODR. clang's libc++ broke that assumption, and then C++11 came along with mandatory changes to nearly all Standard library types.

这是 Linux 程序员不习惯处理的事情,因为 g++ 的 libstdc++ 是事实上的标准,几乎所有程序都使用它,从而满足 ODR。clang 的 libc++ 打破了这个假设,然后 C++11 对几乎所有标准库类型进行了强制性更改。

Just don't share Standard library types between modules. It's undefined behavior.

只是不要在模块之间共享标准库类型。这是未定义的行为。

回答by Ph0t0n

Some of the answers here make passing C++ classes sound really scary, but I'd like to share an alternate point of view. The pure virtual C++ method mentioned in some of the other responses actually turns out to be cleaner than you might think. I've built an entire plugin system around the concept and it's been working very well for years. I have a "PluginManager" class that dynamically loads the dlls from a specified directory using LoadLib() and GetProcAddress() (and the Linux equivalents so the executable to make it cross platform).

这里的一些答案让传递 C++ 类听起来很可怕,但我想分享一个不同的观点。其他一些回复中提到的纯虚拟 C++ 方法实际上比您想象的要干净。我围绕这个概念构建了一个完整的插件系统,并且多年来一直运行良好。我有一个“PluginManager”类,它使用 LoadLib() 和 GetProcAddress()(以及 Linux 等价物,因此可执行文件使其跨平台)从指定目录动态加载 dll。

Believe it or not, this method is forgiving even if you do some wacky stuff like add a new function at the end of your pure virtual interface and try to load dlls compiled against the interface without that new function - they'll load just fine. Of course... you'll have to check a version number to make sure your executable only calls the new function for newer dlls that implement the function. But the good news is: it works! So in a way, you have a crude method for evolving your interface over time.

信不信由你,即使你做了一些古怪的事情,比如在纯虚拟接口的末尾添加一个新函数,并尝试加载针对接口编译的 dll,而没有这个新函数,这种方法也是宽容的——它们会加载得很好。当然……您必须检查版本号以确保您的可执行文件只为实现该功能的较新 dll 调用新函数。但好消息是:它有效!因此,在某种程度上,随着时间的推移,您有一种粗略的方法来改进您的界面。

Another cool thing about pure virtual interfaces - you can inherit as many interfaces as you want and you'll never run into the diamond problem!

关于纯虚拟接口的另一个很酷的事情 - 您可以根据需要继承任意数量的接口,并且永远不会遇到菱形问题!

I would say the biggest downside to this approach is that you have to be very careful about what types you pass as parameters. No classes or STL objects without wrapping them with pure virtual interfaces first. No structs (without going through the pragma pack voodoo). Just primative types and pointers to other interfaces. Also, you can't overload functions, which is an inconvenience, but not a show-stopper.

我想说这种方法的最大缺点是你必须非常小心你作为参数传递的类型。没有类或 STL 对象,除非首先用纯虚拟接口包装它们。没有结构(无需通过 pragma pack voodoo)。只是原始类型和指向其他接口的指针。此外,您不能重载函数,这会带来不便,但不会阻止。

The good news is that with a handful of lines of code you can make reusable generic classes and interfaces to wrap STL strings, vectors, and other container classes. Alternatively, you can add functions to your interface like GetCount() and GetVal(n) to let people loop through lists.

好消息是,通过几行代码,您可以创建可重用的通用类和接口来包装 STL 字符串、向量和其他容器类。或者,您可以向界面添加函数,例如 GetCount() 和 GetVal(n),让人们循环浏览列表。

People building plugins for us find it quite easy. They don't have to be experts on the ABI boundary or anything - they just inherit the interfaces they're interested in, code up the functions they support, and return false for the ones they don't.

为我们构建插件的人发现这很容易。他们不必是 ABI 边界或任何方面的专家——他们只需继承他们感兴趣的接口,编写他们支持的功能,并为他们不喜欢的功能返回 false。

The technology that makes all this work isn't based on any standard as far as I know. From what I gather, Microsoft decided to do their virtual tables that way so they could make COM, and other compiler writers decided to follow suit. This includes GCC, Intel, Borland, and most other major C++ compilers. If you're planning on using an obscure embedded compiler then this approach probably won't work for you. Theoretically any compiler company could change their virtual tables at any time and break things, but considering the massive amount of code written over the years that depends on this technology, I would be very surprised if any of the major players decided to break rank.

据我所知,完成所有这些工作的技术并不基于任何标准。据我所知,Microsoft 决定以这种方式制作他们的虚拟表,以便他们可以制作 COM,而其他编译器编写者也决定效仿。这包括 GCC、Intel、Borland 和大多数其他主要的 C++ 编译器。如果您打算使用晦涩难懂的嵌入式编译器,那么这种方法可能对您不起作用。从理论上讲,任何编译器公司都可以随时更改他们的虚拟表并破坏一些东西,但考虑到多年来编写的大量代码依赖于这项技术,如果任何主要参与者决定打破排名,我会感到非常惊讶。

So the moral of the story is... With the exception of a few extreme circumstances, you need one person in charge of the interfaces who can make sure the ABI boundary stays clean with primitive types and avoids overloading. If you are OK with that stipulation, then I wouldn't be afraid to share interfaces to classes in DLLs/SOs between compilers. Sharing classes directly == trouble, but sharing pure virtual interfaces isn't so bad.

所以这个故事的寓意是......除了少数极端情况外,你需要一个负责接口的人来确保 ABI 边界与原始类型保持干净并避免过载。如果您同意该规定,那么我不会害怕在编译器之间共享 DLL/SO 中的类的接口。直接共享类 == 麻烦,但共享纯虚拟接口并没有那么糟糕。

回答by Mr.C64

You cannot safely pass STL objects across DLL boundaries, unless all the modules (.EXE and .DLLs) are built with the same C++ compiler version and the same settings and flavors of the CRT, which is highly constraining, and clearly not your case.

您不能安全地跨 DLL 边界传递 STL 对象,除非所有模块(.EXE 和 .DLL)都是使用相同的 C++ 编译器版本和相同的 CRT 设置和风格构建的,这是高度限制性的,显然不是您的情况。

If you want to expose an object-oriented interface from your DLL, you should expose C++ pure interfaces (which is similar to what COM does). Consider reading this interesting article on CodeProject:

如果你想从你的 DLL 中公开一个面向对象的接口,你应该公开 C++ 纯接口(这类似于 COM 所做的)。考虑阅读这篇关于 CodeProject 的有趣文章:

HowTo: Export C++ classes from a DLL

如何:从 DLL 导出 C++ 类

You may also want to consider exposing a pure C interface at the DLL boundary, and then building a C++ wrapper at the caller site.
This is similar to what happens in Win32: Win32 implementation code is almost C++, but lots of Win32 APIs expose a pure C interface (there are also APIs that expose COM interfaces). Then ATL/WTL and MFC wrap these pure C interfaces with C++ classes and objects.

您可能还需要考虑在 DLL 边界处公开纯 C 接口,然后在调用方站点构建 C++ 包装器。
这与 Win32 中发生的情况类似:Win32 实现代码几乎是 C++,但很多 Win32 API 公开了纯 C 接口(也有公开 COM 接口的 API)。然后 ATL/WTL 和 MFC 用 C++ 类和对象包装这些纯 C 接口。