使用静态运行时库(/MT或/MTd)链接的DLL函数返回非基本类型的C++类型

8
考虑我们有一个动态库("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释放内部内存)。据我所知,这就是出现错误的地方:尝试使用一个CRT分配的底层std :: string内存来释放另一个CRT。

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

因此,问题是:有没有可能解决这个问题?

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")

(function name gets decorated by the C++ compiler, the actual name can be retrieved with dumpbin.exe utility)。警告将消失,但问题仍然存在。
P.P.P.S:我认为提供库中每个这种情况的一对函数可能是一个解决方案:一个返回指向某些数据的指针,另一个删除这些数据的指针。在这种情况下,内存是由相同的CRT分配和释放的。但这种解决方案似乎非常丑陋和不友好,因为我们必须始终使用指针,并且程序员必须始终记住调用特殊的库函数来删除指针,而不是简单地使用delete关键字。
1个回答

11

是的,这是为什么 /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 自动化 abi 是一个很好的例子。


请注意,Visual C++ 2012 CRT 不会创建私有堆;它使用进程堆(这是与以前版本的不同之处)。 - James McNellis
2
哇,我在heapinit.c中看到了它,真是个全新的用户代码制造Windows代码失败的方式;)谢谢@James。 - Hans Passant

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