Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

dll

Consider we have a dynamic library ("HelloWorld.dll") which is compiled with Microsoft Visual Studio 2010 from the following source code:

#include <string>

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

And we also have an executable ("LoadLibraryExample.exe") which dynamically loads this DLL using LoadLibrary WINAPI function:

#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();
}

This works fine when being linked with a dynamic runtime library (/MD or /MDd switches).

The problem appears when I link them (the library and the executable) with a debug version of static runtime library (/MTd switch). The program seems to work ("Hello, World!" is displayed in the console window), but then crashes with the following output:

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.

The problem magically does not appear with a release version of static runtime library (/MT switch). My assumption is that the release version just doesn't see the error, but it is still there.

After a small research I found this page on MSDN which states the following:

Using the statically linked CRT implies that any state information saved by the C runtime library will be local to that instance of the CRT.
Because a DLL built by linking to a static CRT will have its own CRT state, it is not recommended to link statically to the CRT in a DLL unless the consequences of this are specifically desired and understood.

So the library and the executable have their own copies of CRT which have their own states. An instance of std::string is constructed in the library (with some internal memory allocations being made by the library's CRT) and then returned to the executable. The executable displays it and then calls its destructor (leading to deallocation of the internal memory by the executable's CRT). As I understand, this is where an error occurs: the underlying memory of std::string is allocated with one CRT and tried to be deallocated with another one.

The problem does not appear if we return a primitive type (int, char, float, etc) or a pointer from the DLL, because there are no memory allocations or deallocations in these cases. However, an attempt to delete the returned pointer in the executable results in the same error (and not deleting the pointer obviously results in a memory leak).

So the question is: Is it possible to work around this issue?

P.S.: I really don't want to have a dependency on MSVCR100.dll and make the users of my application to install any redistributable packages.

P.P.S: The code above produces the following warning:

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

which can be resolved by removing extern "C" from the library function declaration:

__declspec(dllexport) std::string hello_world()

and changing the GetProcAddress call as following:

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). The warning is then gone but the problem remains.

P.P.P.S: I see a possible solution in providing a pair of functions in the library for every such situation: one that returns a pointer to some data and the other that deletes a pointer to these data. In this case, the memory is allocated and deallocated with the same CRT. But this solution seems very ugly and non-friendly, as we must always operate with pointers and moreover a programmer must always remember to call a special library function to delete a pointer instead of simply using a delete keyword.

like image 357
Fedor Chunovkin Avatar asked Oct 15 '11 16:10

Fedor Chunovkin


1 Answers

Yes, this is the primary reason that /MD exists in the first place. When you build the DLL with /MT, it will get its own copy of the CRT embedded. Which creates its own heap to allocate from. The std::string object you return will be allocated on that heap.

Things go wrong when the client code tries to release that object. It calls the delete operator and that tries to release the memory on its own heap. On Vista and Win7, the Windows memory manager notices that it is asked to release a heap block that's not part of the heap and that a debugger is attached. It generates an automatic debugger break and a diagnostic message to tell you about the problem. Very nice btw.

Clearly /MD solves the problem, both your DLL and the client code will use the same copy of the CRT and thus the same heap. It is not a sure-fire solution, you will still run into trouble with the DLL is built against a different version of the CRT. Like msvcr90.dll instead of msvcr100.dll.

The only complete error free solution is to restrict the API you expose from the DLL. Don't return any pointers to any objects that need to be released by the client code. Assign ownership of objects to the module that created it. Reference counting is a common solution. And if you have to then use a heap that's shared by all code in a process, either the default process heap (GlobalAlloc) or the COM heap (CoTaskMemAlloc) qualify. Also don't allow exceptions to cross the barrier, same problem. The COM Automation abi is a good example.

like image 144
Hans Passant Avatar answered Nov 15 '22 22:11

Hans Passant