Search code examples
c++windowsmultithreadingdllthread-local-storage

How do you properly implement (C++) thread local storage in a dynamically loaded DLL?


In this case my dynamically loaded DLL is loaded by Windows Explorer in order to add a new propertysheet (new tab) to the file/folder properties page.

A simple example of this is StrmExt.dll (download source). In this example (source provided by Microsoft) the DLL does NOT use thread local storage (TLS) and therefore causes major problems when loading multiple property pages at the same time.

Upon reviewing the source the DLL required one thread-base variable (the file path of the file)...

static TCHAR g_szFile[MAX_PATH];

Changing this one line of code to:

_declspec (thread) TCHAR g_szFile[MAX_PATH];

... Enabled the DLL to support multiple threads and therefore multiple instances of the propertysheet. However, I knew this change would only be supported by Windows Vista and newer (tests on Windows 7 have been very positive). XP, for example, would not support this for a dynamically loaded library... And it is known to crash the application. (See final paragraph).

In order to run on XP I could not use this declaration. I suspected I needed to enhanced their DLL entry point from:

extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        _Module.Init(ObjectMap, hInstance, &LIBID_STRMEXTLib);
        DisableThreadLibraryCalls(hInstance);
    }
    else if (dwReason == DLL_PROCESS_DETACH)
        _Module.Term();
    return TRUE;    // ok
}

...to something like this... As previously seen here

struct ThreadData {
    static TCHAR g_szFile[MAX_PATH];
};
...
DWORD g_dwThreadIndex;

extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, 
                      DWORD dwReason, LPVOID /*pReserved*/)
{
    ThreadData* pData;   
    switch (dwReason) {
        case DLL_PROCESS_ATTACH:

            g_dwThreadIndex = ::TlsAlloc();
            if (g_dwThreadIndex == TLS_OUT_OF_INDEXES)
                return FALSE;

           // execute the DLL_THREAD_ATTACH code

        case DLL_THREAD_ATTACH:

            // allocate memory for this thread
            pData = (ThreadData*) ::LocalAlloc(LPTR, sizeof(ThreadData));
            if (pData == 0)
                return FALSE;

            ::TlsSetValue(g_dwThreadIndex, (LPVOID) pData);
            break;

        case DLL_THREAD_DETACH:

            // release memory for this thread
            pData = (ThreadData*) ::TlsGetValue(g_dwThreadIndex);
            if (pData != 0)
                ::LocalFree((HLOCAL) pData);
            break;

        case DLL_PROCESS_DETACH:

            // release memory for this thread
            pData = (ThreadData*) ::TlsGetValue(g_dwThreadIndex);
            if (pData != 0)
                ::LocalFree((HLOCAL) pData);
            // release the TLS index
            ::TlsFree(g_dwThreadIndex);
            break;
    } 
    return TRUE;
}

This works fine during the first load of the DLL whether I create 1 or 2 threads. After the DLL is freed Explorer crashes on the next load of the library.

What am I misunderstanding? I noticed the original developer purposely disabled thread notification upon the DLL process attach notification. Why?

DisableThreadLibraryCalls(hInstance);

Solution

  • In this case, the problem is best avoided at all. Yes, you'll probably have more threads than processes, and yes, each property sheet will be associated with only one thread, but the reverse is not guaranteed. Two property sheets may share a single thread, that's up to the OS. (And such undocumented decisions change between versions).

    Instead, use the lParam member of PROPSHEETPAGE. It's big enough to hold a pointer, also on 64 bits systems. Point that to your own class. Lifetime management is a lot simpler than the DLL attach/detach you were trying; Windows calls your PropSheetPageProc at the right moment.