How to achieve separate heap handling in VS 2022 C++ DLLs ?

Muduli, Rupamudra 0 Reputation points
2026-08-27T11:24:49.96+00:00

Earlier Visual Studio versions (e.g. VS2010) used separate heap for each C++ DLLs and the main application (also C++).

In VS2022 the main application and the loaded DLLs share the same heap.

 

We would like to restore the original behavior: separate heap for each DLLs.

Is there a way to change back the DLLs behavior if it is compiled with VS2022?

Developer technologies | C++
Developer technologies | C++

A high-level, general-purpose programming language, created as an extension of the C programming language, that has object-oriented, generic, and functional features in addition to facilities for low-level memory manipulation.


5 answers

Sort by: Most helpful
  1. RLWA32 52,811 Reputation points
    2026-08-27T15:56:25.72+00:00

    I used a console application that loaded two dlls to test heap handles with GetProcessHeap and the CRT function _get_heap_handle. The console application and each of the loaded dlls called both functions.

    These are the results from running the test with a V2010 build and a VS2022 build.

    VS2010 results dynamic linking with CRT-

    VS2010 dynamic CRT

    VS2010 results static linking with CRT -

    VS2010 static CRT

    When dynamically linking to the CRT VS2010 created a single separate heap distinct from the process heap for the console application and the loaded dlls. When statically linking VS2010 created several separate heaps for the application and the loaded dlls that were distinct from the process heap.

    VS2022 exhibited completely different behavior.

    VS2022 results dynamic linking with CRT -

    VS2022 dynamic CRT

    VS2022 used the process heap for all as opposed to the VS2010 which used a single separate heap (not the process heap) for all.

    VS2022 results static linking with CRT -

    VS2022 static CRT

    Interestingly, the VS2022 call to _get_heap_handle returned the same heap for all (the process heap) even when linking everything with /MT(d).

    I'm not aware of any supported options to change this implementation.

    Was this answer helpful?

    1 person found this answer helpful.

  2. Bruce (SqlWork.com) 85,201 Reputation points
    2026-08-27T14:30:21.7466667+00:00

    The use of separate heaps is controlled by the build options (and coding) , not visual studio. You are probably using dynamic linking which causes modules to share the heap. use a static link /MT to get the old behavior

    Was this answer helpful?

    1 person found this answer helpful.

  3. RLWA32 52,811 Reputation points
    2026-09-03T09:01:52.6333333+00:00

    Another option is to specify your own dll entry point function so you can initialize the dlls Win32 private heap before the CRT initializes. In that case your dll globals will be allocated from the private heap.

    Dll entry point function

    #define WIN32_LEAN_AND_MEAN
    #include <Windows.h>
    #include <intrin.h>
    #include <new>
    #include <stdexcept>
    
    typedef int(__cdecl* NTVSPRINTFS)(char* szDest, size_t sizeOfBuffer, const char* szFormat, va_list argptr);
    
    extern "C" BOOL __stdcall _DllMainCRTStartup(HMODULE, DWORD, LPVOID);
    BOOL(__stdcall* dllstartup)(HMODULE, DWORD, LPVOID) = _DllMainCRTStartup;
    
    HANDLE g_DllHeap;
    NTVSPRINTFS ntvsprintf_s;
    
    void Report(LPCSTR pszFormat, ...);
    
    EXTERN_C BOOL __stdcall RawDllEntry(HMODULE p1, DWORD dw, PVOID p2)
    {
        switch (dw)
        {
        case DLL_PROCESS_ATTACH:
            if (!g_DllHeap)
            {
                ntvsprintf_s = (NTVSPRINTFS)GetProcAddress(GetModuleHandleA("ntdll.dll"), "vsprintf_s");
                g_DllHeap = HeapCreate(0, 65536, 0);
                if (!g_DllHeap)
                {
                    Report("HeapCreate failed with error %d\n", GetLastError());
                    return FALSE;
                }
    
                Report("Basic2Lib.dll private heap created at 0x%p\n", g_DllHeap);
            }
            return dllstartup(p1, dw, p2);
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
            return dllstartup(p1, dw, p2);
        case DLL_PROCESS_DETACH:
            Report("In DLL_PROCESS_DETACH due to %s\n", p2 != nullptr ? "terminating" : "unloading");
            BOOL result = dllstartup(p1, dw, p2);
            HeapDestroy(g_DllHeap);
            return result;
        }
        return TRUE;
    }
    
    // Override global new
    void* operator new(size_t size) {
        if (!g_DllHeap)
            __fastfail(FAST_FAIL_FATAL_APP_EXIT);
    
        void* ptr = HeapAlloc(g_DllHeap, 0, size);
        if (ptr)
        {
            Report("operator new allocated %Iu bytes at 0x%p\n", size, ptr);
            return ptr;
        }
        else
        {
            Report("HeapAlloc failed with error %d\n", GetLastError());
            throw std::bad_alloc();
        }
    }
    
    void* operator new[](size_t size) {
        if (!g_DllHeap)
            __fastfail(FAST_FAIL_FATAL_APP_EXIT);
    
        void* ptr = HeapAlloc(g_DllHeap, 0, size);
        if (ptr)
        {
            Report("operator new allocated %Iu bytes at 0x%p\n", size, ptr);
            return ptr;
        }
        else
        {
            Report("HeapAlloc failed with error %d\n", GetLastError());
            throw std::bad_alloc();
        }
    }
    
    // Override global delete
    void operator delete(void* ptr) noexcept {
        if (!g_DllHeap)
            __fastfail(FAST_FAIL_FATAL_APP_EXIT);
    
        if (HeapFree(g_DllHeap, 0, ptr))  // HeapFree documented to accept null pointer.
        {
            Report("operator delete deallocation of 0x%p\n", ptr);
        }
        else
        {
            Report("operator delete (HeapFree) failed with error %d\n", GetLastError());
        }
    }
    
    void operator delete[](void* ptr) noexcept {
        if (!g_DllHeap)
            __fastfail(FAST_FAIL_FATAL_APP_EXIT);
    
        if (HeapFree(g_DllHeap, 0, ptr))  // HeapFree documented to accept null pointer.
        {
            Report("operator delete deallocation of 0x%p\n", ptr);
        }
        else
        {
            Report("operator delete (HeapFree) failed with error %d\n", GetLastError());
        }
    }
    
    void Report(LPCSTR pszFormat, ...)
    {
        char szMsg[512]{};
        va_list pArg;
    
        va_start(pArg, pszFormat);
        ntvsprintf_s(szMsg, ARRAYSIZE(szMsg), pszFormat, pArg);
        va_end(pArg);
    
        OutputDebugStringA(szMsg);
    }
    

    The version of vsprintf_s from ntdll.dll is used to avoid reliance on the CRT.

    Dll code to be called by applications

    #include <Windows.h>
    #include <cstdio>
    #include <string>
    #include <memory>
    
    typedef struct {
        std::string str;
        int x;
    } PODSTRUCT, *PPODSTRUCT;
    
    std::string strglobal("This is a global std::string");
    
    BOOL APIENTRY DllMain( HMODULE hModule,
                           DWORD  ul_reason_for_call,
                           LPVOID lpReserved
                         )
    {
        switch (ul_reason_for_call)
        {
        case DLL_PROCESS_ATTACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
        }
        return TRUE;
    }
    
    EXTERN_C __declspec(dllexport) void Test1(size_t size)
    {
        printf_s("%s allocating and deleting %Id bytes\n", __FUNCTION__, size);
        char* p = new char[size];
        delete[] p;
    }
    
    EXTERN_C __declspec(dllexport) void Test2()
    {
        printf_s("%s allocating and deleting a structure\n", __FUNCTION__);
        PPODSTRUCT p = new PODSTRUCT{"Test struct", 42};
        delete p;
    }
    
    EXTERN_C __declspec(dllexport) void Test3(const char* text)
    {
        printf_s("%s allocating and deleting std::string\n", __FUNCTION__);
        std::string* s = new std::string(text);
        delete s;
    }
    
    EXTERN_C __declspec(dllexport) void Test4(const char* replacement)
    {
        printf_s("Current dll global is %s\n", strglobal.c_str());
        strglobal = replacement;
        printf_s("Replacement value of global is %s\n", strglobal.c_str());
    }
    
    EXTERN_C __declspec(dllexport) void Test5(size_t arraysize)
    {
        printf_s("create std::unique_ptr array - BYTE[%Id]\n", arraysize);
        std::unique_ptr<BYTE[]> p = std::make_unique<BYTE[]>(arraysize);
    }
    

    A console application to exercise the dll

    #define WIN32_LEAN_AND_MEAN
    #include <Windows.h>
    
    #include <cstdio>
    #include <tchar.h>
    #include <string>
    #include <thread>
    
    std::wstring str(L"This is a test");
    
    EXTERN_C __declspec(dllimport) void Test1(size_t size);
    EXTERN_C __declspec(dllimport) void Test2();
    EXTERN_C __declspec(dllimport) void Test3(const char* text);
    EXTERN_C __declspec(dllimport) void Test4(const char* text);
    EXTERN_C __declspec(dllimport) void Test5(size_t arraysize);
    
    typedef decltype(Test1) *T1;
    typedef decltype(Test2)* T2;
    typedef decltype(Test3)* T3;
    typedef decltype(Test4)* T4;
    typedef decltype(Test5)* T5;
    
    int _tmain(int argc, TCHAR *argv[])
    {
        HMODULE hmod = LoadLibrary(_T("Basic2Lib.dll"));
        T1 test1 = (T1)GetProcAddress(hmod, "Test1");
        T2 test2 = (T2)GetProcAddress(hmod, "Test2");
        T3 test3 = (T3)GetProcAddress(hmod, "Test3");
        T4 test4 = (T4)GetProcAddress(hmod, "Test4");
        T5 test5 = (T5)GetProcAddress(hmod, "Test5");
    
    
        HANDLE aHeaps[8]{};
        auto nHeaps = GetProcessHeaps(ARRAYSIZE(aHeaps), aHeaps);
        printf_s("Process heap is at 0x%p\n", GetProcessHeap());
        printf_s("Process has %i Win32 heaps\n", nHeaps);
        for (DWORD i = 0; i < nHeaps; i++)
        {
            printf_s("Heap %i is at 0x%p\n", i, aHeaps[i]);
        }
    
        test1(4096);
        test2();
        test3("this is test 3");
        test4("Replacement text for dll global variable");
        test5(16384);
    
        std::thread t = std::thread([&](){
            int x = printf_s("Lambda executing in std::thread\n");
            test3("lambda test");
            });
    
        t.join();
    
        FreeLibrary(hmod);
    
        return 0;
    }
    

    Was this answer helpful?

    0 comments No comments

  4. Danny Nguyen (WICLOUD CORPORATION) 8,540 Reputation points Microsoft External Staff Moderator
    2026-08-28T02:06:56.2533333+00:00

    Hi @Muduli, Rupamudra ,

    This isn't a setting that changed — it's how the CRT was redesigned.

    Before VS2015, each toolset shipped its own CRT DLL (msvcr100.dll, msvcr120.dll), and each copy had its own heap manager. In VS2015 the CRT was refactored into the Universal CRT, and ucrtbase.dll is now a Windows component shared by everything built with VS2015 and later. So when your EXE and DLLs all use /MD, they resolve to the same UCRT and share one heap. There's no switch to turn that off.

    If you need separate heaps per module, two options:

    1. Static CRT (/MT or /MTd) — each module gets its own CRT copy and its own heap. Closest to the old behavior. Caveat: memory allocated in one module must be freed in that same module, or you get heap corruption. That includes anything allocating internally, like a std::string returned across the boundary. See /MD, /MT, /LD.

    2. Private heaps — HeapCreate per DLL and route allocations through HeapAlloc/HeapFree, or override operator new/delete per module.

    What's the goal behind the separation? If it's tracking down corruption or leaks, Application Verifier or gflags page heap will find it without restructuring your build. If it's reclaiming everything a DLL allocated on unload, a private heap you destroy in one shot is the right fit.

    Hope this helps. If you found my response helpful or informative, I would greatly appreciate it if you could follow this guidance or provide feedback.

    Thank you.

    Was this answer helpful?


  5. Danny Nguyen (WICLOUD CORPORATION) 8,540 Reputation points Microsoft External Staff Moderator
    2026-08-28T01:06:25.2933333+00:00

    Hi @Muduli, Rupamudra I'm looking into this issue and will get back to you soon. Thank you for your patience.

    Was this answer helpful?

    0 comments No comments

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.