从与静态运行时链接的DLL函数(/ MT或/ MTd)返回非原始C ++类型

时间:2011-10-15 16:04:21

标签: c++ dll

考虑我们有一个动态库(“HelloWorld.dll”),该库是使用Microsoft Visual Studio 2010从以下源代码编译的:

#include <string>

extern "C" __declspec(dllexport) std::string hello_world()
{
    return std::string("Hello, World!"); // or just: return "Hello, World!";
}

我们还有一个可执行文件(“LoadLibraryExample.exe”),它使用 LoadLibrary WINAPI函数动态加载此DLL:

#include <iostream>
#include <string>

#include <Windows.h>

typedef std::string (*HelloWorldFunc)();

int main(int argc, char* argv[])
{
    if (HMODULE library = LoadLibrary("HelloWorld.dll"))
    {
        if (HelloWorldFunc hello_world = (HelloWorldFunc)GetProcAddress(library, "hello_world"))
            std::cout << hello_world() << std::endl;
        else
            std::cout << "GetProcAddress failed!" << std::endl;

        FreeLibrary(library);
    }
    else
        std::cout << "LoadLibrary failed!" << std::endl;
    std::cin.get();
}

与动态运行时库( / MD / MDd 开关)链接时,此方法正常。

当我将它们(库可执行文件)与调试版本的静态运行时库( / MTd 开关)链接时,会出现问题。该程序似乎有效(“Hello,World!”显示在控制台窗口中),但随后崩溃并出现以下输出:

HEAP[LoadLibraryExample.exe]: Invalid address specified to RtlValidateHeap( 00680000, 00413F60 )
Windows has triggered a breakpoint in LoadLibraryExample.exe.

This may be due to a corruption of the heap, which indicates a bug in LoadLibraryExample.exe or any of the DLLs it has loaded.

This may also be due to the user pressing F12 while LoadLibraryExample.exe has focus.

The output window may have more diagnostic information.

静态运行时库( / MT 开关)的发行版本不会出现这个问题。我的假设是发布版本没有看到错误,但它仍然存在。

经过一项小规模的研究后,我在MSDN上找到了this page,其中说明了以下内容:

  

使用静态链接的CRT意味着C运行时库保存的任何状态信息都将是CRT实例的本地信息。
  因为通过链接到静态CRT构建的DLL将具有其自己的CRT状态,所以不建议将其静态链接到DLL中的CRT,除非特别希望并理解其后果。

因此,库和可执行文件都有自己的CRT副本,这些副本都有自己的状态。在库中构造 std :: string 的实例(由库的CRT进行一些内部内存分配),然后返回到可执行文件。可执行文件显示它然后调用它的析构函数(导致可执行文件的CRT释放内部内存)。据我所知,这是发生错误的地方: std :: string 的底层内存分配了一个CRT,并试图用另一个CRT解除分配。

如果我们返回一个基本类型(int,char,float等)或来自DLL的指针,则不会出现问题,因为在这些情况下没有内存分配或解除分配。但是,尝试删除可执行文件中返回的指针会导致相同的错误(并且不删除指针显然会导致内存泄漏)。

所以问题是:是否有可能解决这个问题?

P.S。:我真的不想依赖MSVCR100.dll并让我的应用程序的用户安装任何可再发行的软件包。

P.P.S:上面的代码产生以下警告:

warning C4190: 'hello_world' has C-linkage specified, but returns UDT 'std::basic_string<_Elem,_Traits,_Ax>' which is incompatible with C

可以通过从库函数声明中删除 extern“C”来解决:

__declspec(dllexport) std::string hello_world()

并按以下方式更改 GetProcAddress 电话:

GetProcAddress(library, "?hello_world@@YA?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ")

(函数名称由C ++编译器修饰,可以使用 dumpbin.exe 实用程序检索实际名称)。警告随后消失,但问题仍然存在。

P.P.P.S:我看到了在库中为每种情况提供一对函数的可能解决方案:一个返回指向某些数据的指针,另一个删除指向这些数据的指针。在这种情况下,使用相同的CRT分配和释放存储器。但是这个解决方案似乎非常丑陋且不友好,因为我们必须始终使用指针进行操作,而且程序员必须始终记得调用特殊的库函数来删除指针而不是简单地使用 delete 关键字。

1 个答案:

答案 0 :(得分:11)

是的,这是/ MD首先存在的主要原因。使用/ MT构建DLL时,它将获得自己的嵌入式CRT副本。它创建自己的堆来分配。您返回的std :: string对象将在该堆上分配。

客户端代码尝试释放该对象时出现问题。它调用delete运算符并尝试在其自己的堆上释放内存。在Vista和Win7上,Windows内存管理器注意到它被要求释放不属于堆的堆块并且附加了调试器。它会生成一个自动调试器中断和一条诊断消息,以告诉您该问题。非常好btw。

显然/ MD解决了这个问题,你的DLL和客户端代码都将使用相同的CRT副本,因此使用相同的堆。它不是一个肯定的解决方案,你仍然会遇到麻烦,DLL是针对不同版本的CRT构建的。像msvcr90.dll而不是msvcr100.dll。

唯一完整的无错解决方案是限制从DLL公开的API。不要返回任何指向客户端代码需要释放的对象的指针。将对象的所有权分配给创建它的模块。引用计数是一种常见的解决方案如果必须使用由进程中的所有代码共享的堆,则默认进程堆(GlobalAlloc)或COM堆(CoTaskMemAlloc)符合条件。也不要允许例外跨越障碍,同样的问题。 COM Automation abi就是一个很好的例子。