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

标签 c++ windows dll stl abi

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

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

最佳答案

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

只需使用extern "C"创建一个普通的 C 接口(interface),因为 C ABI 定义明确且稳定。

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

数据打包/对齐

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

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

您可以使用 #pragma pack 预处理器指令解决此问题,该指令将强制编译器应用特定的打包。 The compiler will still apply default packing if you select a pack value bigger than the one the compiler would have chosen,所以如果你选择一个大的打包值,一个类在编译器之间仍然可以有不同的打包。对此的解决方案是使用#pragma pack(1),这将强制编译器在一个字节的边界上对齐数据成员(本质上,不会应用打包)。 这不是一个好主意,因为它可能会导致某些系统出现性能问题甚至崩溃。 但是,它将确保类的数据成员在内存中对齐方式的一致性。

成员(member)重新排序

如果您的类不是standard-layout,则编译器为can rearrange its data members in memory。没有关于如何完成的标准,因此任何数据重新排列都可能导致编译器之间的不兼容。因此,将数据来回传递到 DLL 将需要标准布局类。

调用约定

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

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

数据类型大小

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

堆问题

如果您的 DLL 链接到与您的 EXE 不同的 C 运行时版本,the two modules will use different heaps。鉴于模块是用不同的编译器编译的,这是一个特别可能的问题。

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

STL 发出

C++ 标准库有自己的一套 ABI 问题。有no guarantee表示给定的 STL 类型在内存中的布局方式相同,也不能保证给定的 STL 类从一个实现到另一个实现具有相同的大小(特别是,调试版本可能会将额外的调试信息放入给定的 STL 类型)。因此,任何 STL 容器在通过 DLL 边界并在另一侧重新打包之前都必须被解包为基本类型。

名称修改

您的 DLL 可能会导出您的 EXE 想要调用的函数。但是,C++ 编译器do not have a standard way of mangling function names。这意味着名为GetCCDLL的函数在 GCC 中可能会被重整为_Z8GetCCDLLv,在 MSVC 中可能会被重整为?GetCCDLL@@YAPAUCCDLL_v1@@XZ

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

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

海湾合作委员会:

EXPORTS
    GetCCDLL=_Z8GetCCDLLv @1

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

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

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

将类对象传递给函数

这可能是困扰交叉编译器数据传递的最微妙和最危险的问题。即使您处理其他所有事情,there's no standard for how arguments are passed to a function。这可能会导致subtle crashes with no apparent reason and no easy way to debug them。您需要通过指针传递所有参数,包括任何返回值的缓冲区。这是笨拙和不方便的,并且是另一种可能有效也可能无效的hacky解决方法。

将所有这些变通方法放在一起,并以some creative work with templates and operators为基础,我们可以尝试安全地跨 DLL 边界传递对象。请注意,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)
pod类专门用于每种基本数据类型,因此int将自动包装为int32_tuint将包装为uint32_t等。这一切都发生在幕后,感谢重载=()运算符。我省略了其余的基本类型特化,因为它们几乎完全相同,除了底层数据类型(bool特化有一点额外的逻辑,因为它被转换为int8_t,然后是int8_t ” 与 0 进行比较以转换回 bool ,但这相当简单)。

我们也可以用这种方式包装 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)

现在我们可以创建一个使用这些 pod 类型的 DLL。首先,我们需要一个接口(interface),所以我们只有一种方法来确定重整。
//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();

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

现在让我们实现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);
}

没什么特别的:这只是将传递的pod复制到普通的wstring中,并在消息框中显示。毕竟,这只是一个POC,而不是一个完整的实用程序库。

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

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

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

The image that showing the result afterward.

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

关于c++ - 如何安全地将对象(尤其是 STL 对象)传入和传出 DLL?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22797418/

相关文章:

python - 将 Flask 应用部署为 Windows 服务

c++ - 卸载 DLL 时的内存泄漏是否会导致主机进程发生泄漏?

c# - 创建 C# dll 并使用 powershell 方法

c# - .NET DLL/EXE PE 是否符合要求?

c++ - 将外部库添加到 CMakeList.txt c++

c++ - 如何将基类对象分配给派生类对象?

Windows 通用应用程序 pdf 查看

c++ - 使用模板重载运算符,但防止重新定义

c++ - 使用赋值是危险的,因为之前的值无效

sql - 使用自定义文件名路径复制 csv