从链接到静态运行时(/MT或/MTd)的DLL函数返回非基本c++类型

Returning non-primitive C++ type from a DLL function linked with a static runtime (/MT or /MTd)

本文关键字:返回 函数 DLL 类型 c++ MTd 静态 链接 运行时 MT      更新时间:2023-10-16

考虑我们有一个动态库("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 switch)中,这个问题神奇地不会出现。我的假设是,发布版本只是没有看到错误,但它仍然存在。

经过一番小小的研究,我在MSDN上发现了这个页面,上面写着:

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

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

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

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

注::我真的不想依赖于MSVCR100.dll,让我的应用程序的用户安装任何可重新分发的包。

最大功率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关键字。

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

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

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

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