Вчера обнаружил весьма своеобразную проблему с AFX_MODULE_THREAD_STATE. Предусловия Проблема может возникать при выполнении следующих условий:
в данном случае CThreadLocal<>::GetData() вызывается неявно через CThreadLocal<>::operator TYPE*().либо (вне зависимости от того используется MFC как статическая библиотека или как DLL) код приложения использует члены m_nLastStatus или m_pLastStatus класса AFX_MODULE_THREAD_STATE. В будущем эта же самая проблема может возникать, если структуры TTTOOLINFOA и TTTOOLINFOW в следующих версиях <commctl.h> приобретут новые члены, возможно "включающиеся" соответствующими макросами, и пользователь начнет использовать новую версию PlatformSDK. Симптомы Непосредственно наблюдавшееся проявление заключалось в том, что приложение "падало" в функции CWnd::CancelToolTips(BOOL bKeys) при обращении к AFX_MODULE_THREAD_STATE::m_pLastStatus (...\atlmfc\src\mfc\wincore.cpp):
Также я полагаю, что эта же проблема может проявляться в похожих "падениях" или "стрельбе по памяти" в других местах при доступе к AFX_MODULE_THREAD_STATE::m_pLastStatus:
или AFX_MODULE_THREAD_STATE::m_nLastStatus:
Причина Т.к. AFX_MODULE_THREAD_STATE включает TOOLINFO m_lastInfo по значению, и sizeof(TOOLINFO) зависит от макросов _WIN32_IE и _WIN32_WINNT, очевидно, что при несовпадении значений этих макро-определений с теми, которые использовались при компиляции MFC, могут начинаться сюрпризы. В частности, любой непосредственный доступ к членам m_nLastStatus и m_pLastStatus класса AFX_MODULE_THREAD_STATE из кода приложения может приводить к получению неверных значений и/или порче памяти, т.к. эти члены будут расположены по другим смещениям, чем в версии AFX_MODULE_THREAD_STATE, которую "видит" MFC. Но проблема даже хуже, т.к. даже если пользовательский код не содержит обращений к упомянутым членам класса AFX_MODULE_THREAD_STATE, данные могут быть запорчены прямо изнутри кода MFC. Чтобы это произошло, достаточно, чтобы приложение было прилинковано к MFC статически, и чтобы пользовательский код содержал вызов CThreadLocal<AFX_MODULE_THREAD_STATE>::GetData() явно или неявно, через члены operator TYPE*() или operator->() шаблона CThreadLocal<>. Когда компилятор встречает подобный вызов в коде пользователя, инстанциируются члены GetData() и CreateObject() класса CThreadLocal<AFX_MODULE_THREAD_STATE>. Будучи инстанциирована из пользовательского кода со значениями _WIN32_IE или _WIN32_WINNT отличающимися от тех, что были использованы при компиляции MFC, CreateObject() будет создавать объекты AFX_MODULE_THREAD_STATE другого размера, нежели это ожидается MFC. Т.к. в приложении может быть только один экземпляр одного и того же члена специализации шаблона класса, компилятор вполне может выберать специализацию, порожденную из пользовательского кода. Более того, это даже более вероятно чем то, что будет выбрана специализация, содержащаяся в коде MFC. В свою очередь это приведет к тому, что CNoTrackObject::operator new(size_t nSize) будет вызван с nSize < sizeof(AFX_MODULE_THREAD_STATE), ожидаемого MFC. Конструктор AFX_MODULE_THREAD_STATE полагается на то, что CNoTrackObject::operator new заполнит память нулями, поэтому членm_pLastStatus останется неинициализированным. Если и _WIN32_IE < 0x0300 и _WIN32_WINNT < 0x0501, то конструкторAFX_MODULE_THREAD_STATE осуществит запись за пределами выделенного блока при инициализации члена m_nLastStatus. Решение Собственно говоря, текущий дизайн класса AFX_MODULE_THREAD_STATE можно считать дефектным, т.к. он слишком слабо устойчив к смене версии PlatformSDK и значений макросов _WIN32_IE и _WIN32_WINNT, используемых приложением. Так что настоящим решением был бы редизайн класса AFX_MODULE_THREAD_STATE так, чтобы члены не были открыты, и, например, член m_lastInfo хранился бы на куче. Т.к. это не в наших силах, лучшее, что можно сделать сегодня, — не трогать член AFX_MODULE_STATE::m_thread, а вместо этого пользоваться функцией AfxGetModuleThreadState(). В этом случае члены GetData() и CreateObject() класса CThreadLocal<AFX_MODULE_THREAD_STATE> из пользовательского кода инстанциироваться не будут. Если же приложение содержит обращения к членам m_nLastStatus или m_pLastStatus, то ничего, кроме как следить за синхронностью значений _WIN32_IE и _WIN32_WINNT с теми, что использовались для сборки MFC, не остается. |