混合使用MSVCRT的版本问题

7

我有一个使用MSVCRT静态链接的C++库,我希望任何人都能够在任何版本的MSVC运行库中使用我的库。要实现这个目标,最好的方法是什么?

我已经非常小心地处理了各种情况:

  1. 内存不会跨DLL边界释放
  2. 运行时C++对象不会跨边界传递(即向量、映射等,除非它们是在该边界的一侧创建的)
  3. 不会在边界之间传递文件句柄或资源句柄

然而,我仍然有一些简单的代码会导致堆损坏。

我在我的库中有这样一个对象:

class Foos
{
public: //There is an Add method, but it's not used, so not relevant here
    DLL_API Foos();
    DLL_API ~Foos();

private:
    std::map<std::wstring, Foo*> map;
};

Foos::~Foos()
{
    // start at the begining and go to the end deleting the data object
    for(std::map<std::wstring, Foo*>::iterator it = map.begin(); it != map.end(); it++)
    {
        delete it->second;
    }
    map.clear();
}

然后我在我的应用程序中像这样使用它:

void bar() {
    Foos list;
}

从任何地方调用此函数后,会出现关于堆栈破坏的调试警告。如果我真的让它运行到底,它实际上会破坏堆栈并导致段错误。

我的调用应用程序使用Visual Studio 2012平台工具编译。库使用Visual Studio 2010平台工具进行编译。

这是我绝对不应该做的事情,还是我实际上正在违反使用多个运行时的规则?


如果您从析构函数中删除代码,错误是否仍然会发生?(我问这个问题是因为这段代码应该是一个无操作) - anatolyg
@anatolyg 是的,即使我从析构函数中删除了所有代码,我仍然会遇到堆栈损坏的问题。 - Earlz
它所做的只是创建map字段(因为它不是指针),然后将其析构。这显然会导致堆栈损坏。如果我将目标降级到VS2010并以这种方式构建我的应用程序,则可以正常工作。 - Earlz
4个回答

9
记忆永远不会越过DLL屏障,但实际上它确实会。事实上,您的应用程序在堆栈上为类对象创建了存储,并将指针传递给库中的方法,从构造函数调用开始,该指针在库代码内部称为“this”。在这种情况下,出现问题的原因是没有创建正确数量的存储空间。您让VS2012编译器查看了类声明。它使用VS2012实现的std::map。然而,您的库是使用VS2010编译的,它使用完全不同的std::map实现,并且大小完全不同,这要归功于C++11的巨大变化。这只是完全的内存损坏,应用程序中写入堆栈变量的代码将破坏std::map,反之亦然。跨模块边界公开C++类充满了陷阱。只有在可以保证所有内容都使用完全相同的编译器版本和完全相同的设置时才考虑。不能混合Debug和Release构建代码。制作库以使没有公开任何实现细节肯定是可能的,您必须遵守以下规则:
- 只公开具有虚方法的纯接口,参数类型必须是简单类型或接口指针。 - 使用类工厂创建接口实例。 - 使用引用计数进行内存管理,因此始终由库进行释放。 - 通过硬规则固定核心细节,例如打包和调用约定。 - 不要允许异常跨越模块边界,只使用错误代码。
这时您已经可以开始编写COM代码了,这也是例如DirectX中使用的样式。

3

map成员变量仍由应用程序创建,其中一些内部数据由应用程序分配而不是DLL(它们可能使用不同的map实现)。作为经验法则,不要使用来自DLL的堆栈对象,在您的DLL中添加类似于Foos * CreateFoos()的内容。


1
好的,基本上就是把所有的分配都包装起来。我尝试过 new Foos(),然后在 delete list 处会出现堆破坏。 - Earlz
2
@Earlz:情况比那更加复杂,通常情况下,您不希望暴露任何可能具有不同二进制布局或可能通过CRT功能分配内存的数据结构的内部(而STL子对象将具有不同的二进制布局并使用new分配内存)。在实践中,这意味着您几乎必须到处使用PIMPL惯用语。 - Matteo Italia
@MatteoItalia 实际上,那(你的评论)是最好的答案! - user2249683

3

在跨越屏障(例如向量、映射等)时,运行时 C++ 对象不会被传递(除非它们是在该屏障的一侧创建的)。

你正在做这件事。你的 Foos 对象由主程序在堆栈上创建,然后在库中使用。该对象包含了一个 map 作为其部分...

当编译主程序时,它会查看头文件等信息以确定为 Foos 对象分配多少堆栈空间。然后调用在库中定义的构造函数...该构造函数可能期望一个完全不同的对象布局/大小。


1
请注意,这不仅取决于CRT版本,甚至还取决于编译标志。当以调试或发布模式编译STL对象时,它们具有不同的内存布局。 - Matteo Italia
所以,如果我改变map位,使其成为指针,并在构造函数/析构函数中分配和释放,那么这可能是可以的,因为DLL将控制分配...我认为即使在头文件中列出private字段也是可选的,但是对于分配来说是必需的。 - Earlz
是的,那可能会起作用。不列出私有字段是不可选的 :) - jcoder

0

也许它并不符合你的需求,但是别忘了在头文件中实现整个东西可以简化问题(某种程度上):-)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接