воскресенье, 11 апреля 2010 г.

Особенность InitCommonControlsEx в Windows XP

   В Windows XP вызов функции DialogBoxParamW может сразу возвращать управление вместо отображения диалогового окна, при выполнении следующих условий:
  1. у исполняемого файла есть манифест с name="Microsoft.Windows.Common-Controls" в <assemblyIdentity>
  2. в таблице импорта нет ссылок на comctl32.dll

   Типичное решение этой проблемы – предварительный вызов InitCommonControlsEx  из comctl32.dll. И на самом деле вызов InitCommonControlsEx решает проблему. Самое интересно, что вызов этой функции не обязательно вставлять перед DialogBoxParamW – можно в любом месте программы (и вообще, как будет показано ниже, главное, чтобы в таблице импорта была ссылка на comctl32).

   Проведенное расследование показало, что на самом деле вызов InitCommonControlsEx вовсе не обязателен для корректной работы, т.к. эта функция в любом случае вызывается в точке входа comctl32.dll при получении уведомления DLL_PROCESS_ATTACH (ниже приведена часть стека).
comctl32.dll!InitCommonControlsEx
comctl32.dll!_ProcessAttach
comctl32.dll!LibMain(DLL_PROCESS_ATTACH)
...
   А корень "проблемы" содержится во внутренней функции VerNtUserCreateWindowEx.

   Как оказалось, все, что нужно для корректной работы – чтобы на момент вызова DialogBoxParamW библиотека comctl32 уже была загружена в адресное пространство процесса (речь идет об Windows XP). Этого можно достичь двумя способами:
  • вставить в код вызов InitCommonControlsEx (на самом деле можно вызвать любую функцию и в любом месте программы), что приведет к появлению ссылки в таблице импорта и загрузке comctl32 во время инициализации процесса
  • предварительно загрузить comctl32.dll динамически – LoadLibrary("comctl32.dll") (такой себе "костыль")

   Для примера рассмотрим следующий шаблон диалогового окна
TESTDLG DIALOGEX 0, 0, 330, 115
STYLE DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
EXSTYLE WS_EX_DLGMODALFRAME | WS_EX_CONTROLPARENT
CAPTION "Test Program"
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
FONT 8, "Ms Shell Dlg 2"
{
  CONTROL "Label Caption", 1001, STATIC, SS_LEFT | SS_NOPREFIX | WS_CHILD | 
    WS_VISIBLE | WS_CLIPSIBLINGS | WS_GROUP, 65, 15, 194, 14
}

   Создадим тестовый проект, не содержащий статических ссылок на comctl32.dll, и рассмотрим, что же происходит "под капотом".

   Тестовое приложение вызываем функцию DialogBoxParamW (comctl32.dll при старте в память не загружается)
  1. Вызывается VerNtUserCreateWindowEx для создания окна "Test Program"
    Внутри происходит вызов NtUserCreateWindowEx, который завершается успешно
  2. Вызывается VerNtUserCreateWindowEx для создания окна "Label Caption" (STATIC CONTROL)
    Внутри происходит вызов NtUserCreateWindowEx, который завершается с ошибкой LastError = 1407 ("Cannot find window class").
    Что собственное и не удивительно, т.к. comctl32.dll не загружалась и, соответственно, никакие классы не регистрировала.
   После этого происходит интересный момент: динамически загружается comctl32 – LoadLibraryW("comctl32.dll"). При этом инициализируются все классы контролов (в точке входа). После загрузки ищется адрес процедуры "RegisterClassNameW" – GetProcAddress(comctl32, "RegisterClassNameW"). Но таковой не находится – "The specified procedure could not be found" (127). И со "спокойной" душой библиотека выгружается.

   На момент вызова VerNtUserCreateWindowEx стек выглядит примерно так
user32!VerNtUserCreateWindowEx
user32.dll!InternalCreateDialog
user32.dll!InternalDialogBox
user32.dll!DialogBoxIndirectParamAorW
user32.dll!DialogBoxParamW
...

   Управления возвращается в InternalCreateDialog и, по цепочке, в – DialogBoxParamW без отображения окна и какого либо признака ошибки.


   После возврата управления в InternalCreateDialog код последней ошибки = 1411 ("Class does not exist") – такая ошибка получается в точке входа shlwapi при обработке DLL_PROCESS_DETACH в результате дерегистрации классов "WorkerA", "WorkerW" (SHUnregisterClassesA). Ошибка эта возвращается функцией user32!GetClassInfoA


   Есть неподтвержденная информация, будто бы в старых версиях MSDN утверждается "Windows XP: If a manifest is used, InitCommonControlsEx is not required". В новых редакциях такого комментария нет.


   В Windows Vista+ "ошибка" исправлена.

   Теперь VerNtUserCreateWindowEx вызывает внутреннюю функцию VersionRegisterClass, которая также загружает comctl32 динамически и пытается найти адрес процедуры "RegisterClassNameW".
   GetProcAddress отрабатывает успешно (в отличие от предыдущих систем), после чего вызывается RegisterClassNameW("Static").

   А вызов InitCommonControlsEx из comctl32!DllMain теперь стал условным:
...
if IsRunningIn16BitProcess() then
  InitCommonControlsEx(...)
...



Ссылки
[1] If InitCommonControls doesn't do anything, why do you have to call it?

Комментариев нет:

Отправить комментарий