【问题标题】:Memory / heap management across DLLs跨 DLL 的内存/堆管理
【发布时间】:2011-01-16 23:55:20
【问题描述】:

虽然这似乎是一个很常见的问题,但我并没有收获太多信息:如何在 DLL 边界之间创建关于内存分配的安全接口?

众所周知

// in DLL a
DLLEXPORT MyObject* getObject() { return new MyObject(); }
// in DLL b 
MyObject *o = getObject();
delete o;

肯定会导致崩溃。 但由于像上面那样的交互——我敢说——并不少见,所以必须有一种方法来确保安全的内存分配。

当然可以提供

// in DLL a
DLLEXPORT void deleteObject(MyObject* o) { delete o; }

但也许有更好的方法(例如 smart_ptr?)。我也读到过在处理 STL 容器时使用自定义分配器。

所以我的询问更多是关于涉及该主题的文章和/或文献的一般指针。是否有需要注意的特殊谬误(异常处理?)?这个问题仅限于 DLL 还是 UNIX 共享对象也“造成”?

【问题讨论】:

  • Linux 中的共享库也会受到影响。

标签: c++ dll memory-management


【解决方案1】:

按照您的建议,您可以使用boost::shared_ptr 来处理该问题。在构造函数中,您可以传递一个自定义清理函数,它可以是创建指针的 dll 的 deleteObject-Method。示例:

boost::shared_ptr< MyObject > Instance( getObject( ), deleteObject );

如果您的 dll 不需要 C 接口,您可以让 getObject 返回一个 shared_ptr。

【讨论】:

  • 这也是Effective C++中推荐的。
  • 为了消除歧义,Effective C++ 建议返回 std::shared_ptr,而不是在 DLL 中公开 deleteObject 的选项。
【解决方案2】:

重载operator newoperator delete 等。 al 用于所有 DLL 类并在 DLL 中实现它们:

 void* MyClass::operator new(size_t numb) {
    return ::operator new(num_bytes);
 }

 void MyClass::operator delete(void* p) {
    ::operator delete(p);
 }
 ...

这可以很容易地放在 DLL 导出的所有类的公共基类中。

这样,分配和释放完全在 DLL 堆上完成。老实说,我不确定它是否有任何严重的缺陷或可移植性问题 - 但它对我有用。

【讨论】:

【解决方案3】:

您可能会说它“肯定会导致崩溃”。有趣 - “可能”的意思与“肯定”完全相反。

现在,无论如何,该声明大多是历史性的。有一个非常简单的解决方案:使用 1 个编译器,1 个编译器设置,并链接到 CRT 的 DLL 形式。 (你可能会跳过后者)

没有具体的文章可以链接,因为现在这不是问题。无论如何,您都需要 1 个编译器和 1 个设置规则。 sizeof(std::string) 之类的简单事情都依赖于它,否则您将违反大量 ODR。

【讨论】:

    【解决方案4】:
    【解决方案5】:

    在某些情况下可能适用的另一个选项是将所有分配和解除分配保留在 DLL 内,并防止对象跨越该边界。您可以通过提供一个句柄来做到这一点,以便创建MyObject 在 DLL 代码中创建它并返回一个简单的句柄(例如 unsigned int),通过该句柄执行客户端的所有操作:

    // Client code
    ObjHandle h=dllPtr->CreateObject();
    dllPtr->DoOperation(h);
    dllPtr->DestroyObject(h);
    

    由于所有分配都发生在 dll 中,您可以通过包装在 shared_ptr 中来确保将其清理干净。这几乎是 John Lakos 在 Large Scale C++ 中建议的方法。

    【讨论】:

    • 感谢您提醒我拿起并阅读/浏览 Lakos!它的声誉似乎被“两个迈耶”所掩盖,但又是一个不同的话题。
    【解决方案6】:

    众所周知

    // in DLL a
    DLLEXPORT MyObject* getObject() { return new MyObject(); }
    // in DLL b 
    MyObject *o = getObject();
    delete o;
    

    肯定会导致崩溃。

    上述是否具有明确定义的特征取决于MyObjecttype 的定义方式。

    如果该类有一个虚拟析构函数(并且该析构函数没有内联定义),那么它就不会崩溃并且会表现出明确定义的行为。

    这个崩溃的原因通常是 delete 做了两件事:

    • 调用析构函数
    • 释放内存(通过调用operator delete(void* ...)

    对于一个带有非虚析构函数的类,它可以“内联”做这些事情,这导致DLL“b”内的delete可能会尝试从“a”堆中释放内存==崩溃.

    但是,如果MyObject的析构函数是virtual那么在调用“free”函数之前,编译器needs to determine the actual run-time class of the pointer才可以将正确的指针传递给operator delete()

    C++ 要求您必须将完全相同的地址传递给操作员 删除作为运算符 new 返回的内容。当你分配一个对象时 使用 new,编译器隐式知道 对象(这是编译器用来传递正确内存的对象 例如,将大小转换为 operator new。)

    但是,如果您的班级有基础 具有虚拟析构函数的类,并且您的对象通过 指向基类的指针,编译器不知道具体类型 在呼叫站点,因此无法计算正确的地址 传递给操作员 delete()。为什么,你可能会问?因为在场 多重继承,基类指针的地址可能是 与对象在内存中的地址不同。

    那么,那会发生什么 情况是,当您删除具有虚拟析构函数的对象时, 编译器调用所谓的删除析构函数 而不是通常的 a 序列 调用正常的析构函数,然后调用操作员 delete() 来回收 记忆。

    由于删除析构函数是一个虚函数,在 运行时将调用具体类型的实现,并且 该实现能够计算正确的地址 内存中的对象。该实现所做的是调用 常规析构函数,计算对象的正确地址,以及 然后在该地址上调用 operator delete()。

    似乎 GCC(来自链接的文章)和 MSVC 都通过从“删除析构函数”的上下文中调用 dtor 和“free”函数来实现这一点。而且这个帮助程序必然存在于您的 DLL 中,并且将始终使用正确的堆,即使“a”和“b”有不同的堆。

    【讨论】:

      【解决方案7】:

      在“分层”架构(非常常见的场景)中,最深层次的组件负责提供有关问题的策略(可能返回shared_ptr&lt;&gt;,如上面建议的那样或“调用者负责删除此”或“从不删除它,但在完成后调用releaseFooObject() 并且之后不要访问它”或...),靠近用户的组件负责遵循该策略。

      双向信息流使职责更难描述。


      这个问题仅限于 DLL 还是 UNIX 共享对象也“造成”?

      实际上比这更糟糕:您可以使用静态链接库轻松解决此问题。正是在单个执行上下文中存在代码边界,这使得有机会滥用或误传某些设施。

      【讨论】:

      • @happy_emi:是的。有。问题是代码边界而不是操作系统的结果。 Unix 有一些编码传统,使其出现的频率较低,但仍有可能。
      【解决方案8】:

      我已经写了an article,关于使用 C++11 的 unique_ptr 的自定义删除器工具通过 DLL 边界(或 Linux 中的共享对象库)传递对象。文中描述的方法不会用删除器“污染”unique_ptr签名。

      【讨论】:

      • 我通读了这篇文章,它似乎很有用,但它留下了很多问题。我认为添加一个不涉及类 + 单例 + 静态 getInstance() 函数的示例,而是定义一个类和一些更全局的工厂方法,这将非常有帮助。我仍然不太清楚该内联函数是在 DLL 中还是在 main 中运行。不过这很有趣!
      • @netpoetica 单身确实是完全多余的,你是对的,我应该在没有它的情况下更新文章。为了确定内联函数是否确实在 DLL 中,您可以查看符号的二进制文件。
      • @netpoetica 我使用依赖项检查了 DLL 中 create() 方法的存在,令我惊讶的是,它实际上是 DLL 的一部分。感谢您提请我注意!
      • @netpoetica 好的,我修好了。问题来自 __declspec(dllexport/dllimport),它是在整个工厂类中设置的,我将它移动到 doCreate 函数,现在我在 DLL 的导出符号中看不到 create() 方法。当我查看调用堆栈时,调试器也证实了这一点,当我在 create() 函数中时,我看到了 ExecutableConsumer.exe!SafeFactory::create()。我将用这个和单例删除来更新这篇文章。再次感谢!
      猜你喜欢
      • 2011-05-01
      • 2013-05-06
      • 2012-01-18
      • 1970-01-01
      • 2015-10-09
      • 1970-01-01
      • 2011-01-05
      • 2021-04-28
      • 2012-03-07
      相关资源
      最近更新 更多