如何在动态加载的DLL中正确实现(C ++)线程本地存储?

4
在这种情况下,我的动态加载的DLL是由Windows资源管理器加载的,以便向文件/文件夹属性页面添加新的属性表(新选项卡)。
一个简单的例子是StrmExt.dll(下载源代码)。在这个例子中(由Microsoft提供的源代码),该DLL没有使用线程本地存储(TLS),因此在同时加载多个属性页时会造成重大问题。
在审查源代码后,该DLL需要一个基于线程的变量(文件路径)。...
static TCHAR g_szFile[MAX_PATH];

将这一行代码更改为:

_declspec (thread) TCHAR g_szFile[MAX_PATH];

我已经使该DLL支持多线程和多个属性表的实例。然而,我知道这种改变只能被Windows Vista及更高版本支持(在Windows 7上的测试非常积极)。例如,XP不会支持动态加载库的这种方式...并且它已知会导致应用程序崩溃(请参见最后一段)。

为了在XP上运行,我不能使用此声明。我怀疑需要增强其DLL入口点:

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
}

我希望你能将其转化为类似这样的内容... 如前所述这里

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;
}

这在第一次加载DLL时运行良好,无论我创建1个或2个线程。在释放DLL后,资源管理器在下一次加载库时崩溃。
我误解了什么?我注意到原始开发人员故意禁用了DLL进程附加通知的线程通知。为什么?
DisableThreadLibraryCalls(hInstance);

1
DisableThreadLibraryCalls只是一种优化。"Explorer崩溃"不是一个恰当的问题描述,您需要更好地记录崩溃细节。 - Hans Passant
公正的评论。我知道它是一种优化选项,但我很好奇是否还有其他原因。我现在使用的XP系统当前在另一个系统的虚拟机上,我会考虑在另一天提供异常报告。然而,我对示例中的通知代码表示怀疑。它看起来正确吗,特别是在Windows资源管理器的上下文中?即进程附加逻辑是否正确执行线程附加逻辑?TLS是否在线程分离和进程分离时正确释放? - clsturgeon
显然,您必须删除对DisableThreadLibraryCalls的调用,以便接收DLL_THREAD_ATTACHDLL_THREAD_DETACH。除此之外,我猜您可能会因为DllMain的不确定性而有一些泄漏问题。 - David Heffernan
2
我曾经遇到过一个全局变量的问题,所以我使用了TLS,现在我有了两个问题。如果不使用全局变量,你就不会有问题了。 - arx
请参阅有关DLLMain()中不应执行的操作的文章:http://www.voyce.com/index.php/2009/12/03/dont-do-anything-in-dllmain-please/ - Richard Chambers
显示剩余2条评论
1个回答

2
在这种情况下,最好完全避免这个问题。是的,您可能会有比进程更多的线程,每个属性表都将与一个线程关联,但反之并不保证。两个属性表可以共享一个线程,这取决于操作系统。(而这样的未记录决策在版本之间会发生变化)。
相反,使用PROPSHEETPAGElParam成员。它足够大,可以容纳指针,也适用于64位系统。将其指向您自己的类。生命周期管理比您尝试的DLL附加/分离要简单得多;Windows会在正确的时刻调用您的PropSheetPageProc

谢谢你的正确指导。现在我有一个32位和一个64位的工作DLL,它似乎可以在所有Windows操作系统版本上运行,包括XP及以上版本。 - clsturgeon

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