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

2024-05-12

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

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


这个问题的简短答案是don't。因为没有标准的C++ABI https://stackoverflow.com/questions/2171177/what-is-application-binary-interface-abi(应用程序二进制接口、调用约定、数据打包/对齐、类型大小等的标准),您将不得不跳过很多障碍来尝试并强制执行处理程序中的类对象的标准方法。甚至不能保证在您跳过所有这些麻烦后它会起作用,也不能保证在一个编译器版本中有效的解决方案在下一个编译器版本中也能有效。

只需使用以下命令创建一个普通的 C 接口extern "C",由于 C ABIis明确且稳定。


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

数据打包/对齐

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

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

您可以使用以下方法解决此问题#pragma pack https://stackoverflow.com/a/3318475/2245528预处理器指令,这将强制编译器应用特定的打包。如果您选择的包值大于编译器选择的值,编译器仍将应用默认打包 https://stackoverflow.com/questions/22754240/always-same-effect-of-pragma-pack16-and-pragma-pack8,因此,如果您选择较大的打包值,一个类在编译器之间仍然可以具有不同的打包。解决这个问题的方法是使用#pragma pack(1),这将强制编译器在一字节边界上对齐数据成员(本质上,不会应用任何打包)。这不是一个好主意,因为它可能会导致性能问题甚至在某些系统上崩溃。然而,它will确保类的数据成员在内存中对齐方式的一致性。

会员重新排序

如果你的班级不是标准布局 http://en.cppreference.com/w/cpp/types/is_standard_layout,编译器可以重新排列内存中的数据成员 https://stackoverflow.com/questions/11340028/order-of-storage-inside-a-structure-object。对于如何完成此操作没有标准,因此任何数据重新排列都可能导致编译器之间的不兼容。因此,将数据来回传递到 DLL 将需要标准布局类。

调用约定

有多个调用约定 http://msdn.microsoft.com/en-us/library/984x0h58.aspx给定的函数可以有。这些调用约定指定如何将数据传递给函数:参数是存储在寄存器中还是堆栈中?参数按什么顺序压入堆栈?函数完成后谁清理堆栈上剩余的参数?

保持标准的调用约定很重要;如果你将一个函数声明为_cdecl,C++ 的默认值,并尝试使用它来调用它_stdcall 坏事将会发生 https://stackoverflow.com/questions/3404372/stdcall-and-cdecl/12465133#12465133. _cdecl然而,这是 C++ 函数的默认调用约定,因此这是不会破坏的一件事,除非您故意通过指定_stdcall在一个地方和一个_cdecl在另一个。

数据类型大小

根据本文档 https://stackoverflow.com/questions/3404372/stdcall-and-cdecl/12465133#12465133,在 Windows 上,无论您的应用程序是 32 位还是 64 位,大多数基本数据类型都具有相同的大小。但是,由于给定数据类型的大小是由编译器强制执行的,而不是由任何标准强制执行的(所有标准保证是1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)),这是一个好主意,使用固定大小的数据类型 http://en.cppreference.com/w/cpp/types/integer尽可能确保数据类型大小兼容性。

堆问题

如果您的 DLL 链接到的 C 运行时版本与 EXE 不同,这两个模块将使用不同的堆 https://stackoverflow.com/questions/10820114/do-statically-linked-dlls-use-a-different-heap-than-the-main-program。鉴于模块是使用不同的编译器编译的,这是一个特别可能出现的问题。

为了缓解这种情况,所有内存都必须分配到共享堆中,并从同一堆中释放。幸运的是,Windows 提供了 API 来帮助解决此问题:获取进程堆 http://msdn.microsoft.com/en-us/library/windows/desktop/aa366569(v=vs.85).aspx将允许您访问主机 EXE 的堆,并且堆分配 http://msdn.microsoft.com/en-us/library/windows/desktop/aa366597(v=vs.85).aspx/HeapFree http://msdn.microsoft.com/en-us/library/windows/desktop/aa366701(v=vs.85).aspx将允许您在此堆中分配和释放内存。重要的是你不要使用正常的malloc/free因为无法保证它们会按照您期望的方式工作。

STL问题

C++ 标准库有其自己的一组 ABI 问题。有没有保证 https://stackoverflow.com/questions/20646989/is-it-safe-to-return-stdwstring-from-a-dll给定的 STL 类型在内存中以相同的方式布局,也不保证给定的 STL 类从一个实现到另一个实现具有相同的大小(特别是,调试版本可能会将额外的调试信息放入给定的 STL 类型中) 。因此,任何 STL 容器在穿过 DLL 边界并在另一端重新打包之前都必须先解包为基本类型。

名称修改

您的 DLL 可能会导出您的 EXE 想要调用的函数。然而,C++编译器没有修改函数名称的标准方法 https://stackoverflow.com/questions/10127982/why-is-name-mangling-not-standardized。这意味着一个名为GetCCDLL可能会被损坏_Z8GetCCDLLv在海湾合作委员会和?GetCCDLL@@YAPAUCCDLL_v1@@XZ在 MSVC 中。

您已经无法保证静态链接到 DLL,因为用 GCC 生成的 DLL 不会生成 .lib 文件,而在 MSVC 中静态链接 DLL 则需要一个。动态链接似乎是一个更干净的选择,但名称修改会妨碍您:如果您尝试GetProcAddress http://msdn.microsoft.com/en-us/library/windows/desktop/ms683212(v=vs.85).aspx错误的损坏名称,调用将失败并且您将无法使用 DLL。这需要一些技巧才能解决,并且是为什么跨 DLL 边界传递 C++ 类是一个坏主意的一个相当重要的原因。

您需要构建 DLL,然后检查生成的 .def 文件(如果生成了;这将根据您的项目选项而有所不同)或使用 Dependency Walker 等工具来查找损坏的名称。然后,你需要写你的own.def 文件,定义重整函数的未重整别名。作为示例,让我们使用GetCCDLL我在前面提到过的功能。在我的系统上,以下 .def 文件分别适用于 GCC 和 MSVC:

GCC:

EXPORTS
    GetCCDLL=_Z8GetCCDLLv @1

MSVC:

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

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

如果您这样做,整个过程会更简单创建一个界面 https://stackoverflow.com/a/318137/2245528以便您的 DLL 遵循,因为您只需为一个函数定义别名,而不需要为 DLL 中的每个函数创建别名。然而,同样的警告仍然适用。

将类对象传递给函数

这可能是困扰交叉编译器数据传递的最微妙和最危险的问题。就算你处理好其他一切对于如何将参数传递给函数没有标准 https://stackoverflow.com/questions/22676050/is-there-a-standard-procedure-for-passing-class-objects-by-value。这可能会导致没有明显原因的微妙崩溃,也没有简单的方法来调试它们 https://stackoverflow.com/questions/22594530/memory-corruption-with-getprocessheap-heapalloc。你需要通过all通过指针的参数,包括任何返回值的缓冲区。这是笨拙和不方便的,也是另一种可能有效也可能无效的解决方法。


将所有这些解决方法放在一起并在此基础上构建使用模板和运算符进行一些创造性的工作 https://stackoverflow.com/a/3565808/2245528,我们可以尝试安全地跨 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)

The pod类专门针对每种基本数据类型,因此int将自动换行至int32_t, uint将被包装到uint32_t等等。这一切都发生在幕后,这要归功于超载= and ()运营商。我省略了其余的基本类型专业化,因为除了底层数据类型(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。首先我们需要一个接口,因此我们只有一种方法来解决重整问题。

//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 和任何调用者都可以使用的基本接口。请注意,我们正在传递一个指向pod, not a 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 https://en.wikipedia.org/wiki/Proof_of_concept,不是一个完整的实用程序库。

现在我们可以构建 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 配合使用。


总之,如果你绝对must跨 DLL 边界传递 C++ 对象,这就是您的做法。但是,这些都不能保证适用于您或其他任何人的设置。其中任何一个都可能随时中断,并且可能会在您的软件计划发布主要版本的前一天中断。这条道路充满了黑客、风险和一般的愚蠢行为,我可能应该被枪杀。如果您确实走这条路,请极其谨慎地进行测试。真的……根本不要这样做。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何安全地将对象(尤其是 STL 对象)传入和传出 DLL? 的相关文章

随机推荐

  • 更改 ProgressDialog 的背景

    我正在尝试更改背景ProgressDialog 我在网上搜索并找到了各种建议 例如如何从对话框中删除边框 https stackoverflow com questions 8051581 how to remove border from
  • 如何在Java中命名HashMap?

    这可能是一个愚蠢的问题 但我从未找到一种令人满意的方式来命名类型变量HashMap
  • spring-cloud-stream 请求-回复消息传递模式

    是否有一种应该与 spring cloud stream 一起使用的请求 答复模式 我在 spring cloud stream 上找到的所有文档都是针对 MessageChannel send 即发即弃类型的生产者 并且我熟悉 sprin
  • 通过 AJAX jquery 更改表格背景颜色?

    设想 当我的网页加载时 自动搜索单元格已由用户输入并且具有价值 如果已输入 表格背景颜色将为红色 否则为绿色 假设该表尚未输入 桌子背景绿色是这样的 和表的源代码 table width 1023 height 200 border 1 t
  • Android Lollipop 上的海拔 + 透明度错误

    在 API 21 上为具有一些 alpha 例如 99fe0038 和一些高程的视图使用背景颜色会显示两个圆圈 一个用于视图本身 另一个用于内部 标高和背景颜色通过代码设置 view setElevation getResources ge
  • TwiML 应用程序 - 当用户回复 Twilio Number 的 STOP/START 时调用 AWS Lambda

    这是我的场景 我正在使用 Twilio 向我的客户发送短信 当用户决定不接收这些短信时 他们会回复 停止 并开始再次接收 这是由 Twilio 自动处理的 但是 我需要调用 AWS Lambda 函数并相应地更新我的数据库 这就是我到现在为
  • 尝试了解CMTime

    我见过一些examples https stackoverflow com questions 5808557 avassetwriterinputpixelbufferadaptor and cmtime of https stackov
  • 在 R 中索引数据帧

    再会 我不明白这里的主题 就像它有效但我不明白为什么 我有这个数据库 planets df is pre loaded in your workspace Use order to create positions positions lt
  • 具有少量父设备属性的 udev 规则

    我需要复杂且通用的udev规则来确定插入任何 USB 集线器的特定端口的 USB 设备 所以 我必须结合设备树不同层的父属性 我有这个 udevadm info query all name dev ttyUSB0 attribute wa
  • 点击后退按钮时,iCarousel 会显示在上一页

    当我按下后退按钮时 这iCarousel仍然显示 1 秒 为什么会发生这种情况以及如何阻止这种情况 我已经使用故事板创建了 iCarosel 视图 void viewDidUnload super viewDidUnload self ca
  • 为什么 ASP.NET 在内容更改后提交 TextBox 控件的原始值?

    我有一个 Web 表单 允许用户修改某些字段中的数据 主要是 TextBox 控件 还有几个 CheckBox DropDownList 和一个 RadioButtonList 控件 并使用提交按钮保存更改 相当标准的东西 问题是 我需要跟
  • 使用 SQLite 测试 NHibernate“没有这样的表” - 生成模式

    我正在尝试使用内存中的 SQLite 数据库来测试 NHibernate 提供的数据层 我读过很多关于如何进行此设置的博客和文章 但我现在很困惑为什么它不起作用 问题 当我运行单元测试时 我收到错误 没有这样的表 学生 我读过的文章表明这
  • Spring Data Jpa项目使用ManyToMany关系时生成查询

    我有以下实体映射 Entity Table name books public class Book implements Serializable ManyToMany JoinTable name books2categories jo
  • 如何在iOS应用程序中实现信号量?

    是否可以在ios应用程序中实现计数信号量 对的 这是可能的 有很多可用的同步工具 同步 NSLock NS条件 NS条件锁 GCD 信号量 并行线程锁 我建议阅读 线程编程指南 http developer apple com librar
  • 数据库的创建日期

    这是一个问题 起源于this https stackoverflow com questions 2522626 check how old an oracle database is 2523227 2523227杰米提出的问题 我想我会
  • 让Webpack不捆绑文件

    所以现在我正在使用一个原型 我们使用 webpack 用于构建 tsx 文件和复制 html 文件 和 webpack dev server 之间的组合来提供开发服务 正如您可以假设的那样 我们也使用 React 和 ReactDOM 作为
  • JSF EL 表达式检查属性文件中的空字符串?

    我必须检查我的属性文件对于某些标签是否为空 因为我必须渲染元素 但即使标签为空 我仍然会得到显示键的元素
  • 如何在 git diff 中按标点符号拆分单词?

    我对以下命令有一些运气 git diff color words lt gt space lt gt 但它似乎没有在第一个字符类中正确地否定方括号 我试过这个 git diff color words lt gt space lt gt 为
  • mod_http_upload - 使用 Ruby on Rails 上传 HTTP 文件 (XEP-0363)

    我想在我的聊天应用程序中的用户之间传输图像 我正在使用 ejabberd 服务器进行聊天 据我发现 可以做到这一点的模块是mod http upload HTTP 文件上传 XEP 0363 我不知道如何实现这一点 任何人都可以帮助我弄清楚
  • 如何安全地将对象(尤其是 STL 对象)传入和传出 DLL?

    如何将类对象 尤其是 STL 对象 传入和传出 C DLL 我的应用程序必须以 DLL 文件的形式与第三方插件交互 并且我无法控制这些插件是使用什么编译器构建的 我知道 STL 对象没有保证的 ABI 并且我担心这会导致我的应用程序不稳定