Search code examples
c++dllexenew-operatorheap-memory

Heap allocation failing in user DLL/EXE


Properly linked DLLs and EXEs are supposed to have one freestore from which they all can allocate heap-based objects. Here is Chis Becke’s Answer in Who allocates heap to a DLL?:

… it is the C++ runtime that is responsible for creating its freestore and deciding how to allocate it. Specifically, if you use the Dll runtime option, then a single dll - msvcrtxx.dll - manages a single freestore that is shared between all dll's, and the exe, that are linked against that dll

Since this is true, then I should be able to new objects in DLL/EXEs defined in other DLL/EXEs. According to Chris, the msvcrtxx.dll and compile-time/runtime linker take care of where the joint freestore for all the DLL/EXEs can be obtained.

That is not working for me.

To test this, I have generated two MFC dialog programs: NewFailMfc1 and NewFailMfc2. Running NewFailMfc2 which accesses NewFailMfc1’s Www function fails when doing the new.

// Code in NewFailMfc1.
void Www()
{
  char* ch { nullptr };
  ch = new char[ 100 ]; // error: attempts to allocate memory somewhere else than in the prescribed joint DLL/EXE freestore
  ch[ 0 ] = '\0';
}

// Calling code in NewFailMfc2.
Www();

Does someone with a better knowledge of how DLL/EXE freestore works than me know what the problem is?

(I attempted to ask this question once before in "Global function ::operator new fails when compiled in MyApp1 and MyApp2. During the asking process, I discovered that the problem was occurring more generally than in the <random> std lib.)

EDIT1:

In MSDN a nice virtual agent found Potential Errors Passing CRT Objects Across DLL Boundaries for me. Unfortunately, the only solution it recommends is compiling all your programs with the /MD compiler option, and not /MT which uses multiple copies of the CRT which automatically leads to crossing boundaries and memory access violations.

This is not good news for an app developer like me. What I need is a Best Practice so I can apply it and meet my delivery deadlines without having to deal with arcane low-level memory problems. How would I fx know there is a hidden call to the global ::operator new in the std:random_device type? I wouldn’t until it access-violated. Only now after all this research do I realize that by it calling the global new, it was crossing a boundary which gave my DLL/EXE an access violation. Very obscure.

EDIT2:

I have submitted a bug report in Visual Studio regarding the std::random_device implementation. See "std::random_device instantiation causes an access-violation in certain cases".


Solution

  • Explicit Instantiation

    Explicit Instantiation forces the compiler to generate code for a specific parameter list of a templated class or function. Without that code my imported DLL/EXE binaries were failing on runtime instantiations like ch = new char[ 100 ] and std::random_device rd; which implicitly does a global ::operator new. I have found no useful explanation for why this occurs. IPC discussions unfortunately don’t clearly distinguish between client-server runtime involving more than one running process, and runtime code that imports binary code (DLL/EXE) that was compiled and exported elsewhere.

    The solution was to add a template parameter to the class that was failing and add a .cpp file to each module that explicitly instantiated the class, like MyClsModule1.cpp, MyClsModule2.cpp, etc. In these files I explicitly instantiate the class for that module. I also add the .h files, like MyClsModule1.h, MyClsModule2.h, for each module which contain externs so duplicate code generations don’t occur within a particular module. With this approach each module generates module-specific class code at compile time, which forces the module’s threads to allow heap instantiations which access that module’s process heap.

    This modern C++ solution is elegant for me in that it keeps me from having to re-introduce complex IPC solutions like COM into my app code.

    From an app developer’s perspective I think that my original code ought to have worked or at least have generated errors that were more informative as to what the problem was, so I am leaving my bug report mentioned in EDIT2 in effect.