"Nothing is impossible!" Professor Hubert J. Farnsworth
Демонстрационный проект Unicode98 (ATL ActiveX, 32k)
Демонстрационный проект Unicode98b (WTL, 22k)
Файл Unicows.cpp для альтернативной загрузки unicows.dll (2k)
Однажды, в пятницу вечером, я получил письмо, из которого следовало, что заказчик очень хочет, чтобы проект, над которым я работаю, мог бы быть запущен из Windows98.
"А, может, Вам еще и поддержку Microsoft ® Windows ™ версии 2.0 подавай?", - подумал я, но, тем не менее, решил попытаться. Первое, что пришло мне на ум, это просто перекомпилировать все 30 модулей, из которых состоит проект, без директивы препроцессора _UNICODE. Идея здравая, но пришла в мою голову с опозданием месяцев в 6. К сожалению, некоторые из разработчиков, участвовавших в этом проекте, не понимают, как выяснилось, разницу между LPCTSTR и LPCWSTR. Некоторые даже умудрились использовать LPCTSTR при описании интерфейсов! Представляете, что получится, если код, реализующий некий интерфейс, трактует строки как двухбайтовые, а код, пользующийся этим интерфейсом, считает их однобайтовыми? Дело сильно осложнилось наличием нескольких библиотек, исходным кодом которых я не располагал, и, как следствие, не мог их пересобрать. Возможно, что если бы я располагал двумя-тремя неделями, я бы расставил по коду бесконечное количество перекладываний из пустого в порожнее и наоборот, но, увы. Времени у меня было в обрез, и я начал искать другой путь. Настало время разобраться с давным-давно вышедшей библиотекой поддержки уникода для Windows9x/Me.
Идея, реализованная в этой библиотеке, довольно проста: весь API, рассчитанный на двухбайтовые строки, эмулируется специальными заглушками, преобразующими все строковые параметры из двухбайтовых в однобайтовые, затем вызывается реализованная в Windows9x/Me неуникодная функция, а результат снова перекладывается в двухбайтовые строки. Именно таким образом в WindowsNT реализована поддержка "старого", неуникодного API. В этой системе однобайтовые строки превращаются в двухбайтовые, вызывается соответствующая функция, а результат снова урезают до однобайтовых строк. Странно, что подобный механизм не был встроен в Windows9x/Me изначально. Unicows.dll занимает всего 200k и реализует почти 500 заглушек для работы с двухбайтовыми строками. Давайте попробуем эту замечательную библиотеку.
Сначала создадим простой ATL проект и добавим в него ActiveX контрол. Теперь добавим поддержку уникода для Windows9x/Me. Процедура "прикручивания" unicows довольно сложная и интуитивно-непонятная. Но сводится она к тому, чтобы добавить Unicows.lib к списку прочих библиотек, причем непременно в самое начало:
Плюс нужно добавить вот такую строку куда-нибудь в StdAfx.cpp:
#pragma comment(linker, "/nod:kernel32.lib /nod:advapi32.lib /nod:user32.lib /nod:gdi32.lib /nod:shell32.lib /nod:comdlg32.lib /nod:version.lib /nod:mpr.lib /nod:rasapi32.lib /nod:winmm.lib /nod:winspool.lib /nod:vfw32.lib /nod:secur32.lib /nod:oleacc.lib /nod:oledlg.lib /nod:sensapi.lib") |
Компилируем, запускаем и... не работает! А как же! Я ведь совершенно забыл, что сначала нужно скопировать unicows.dll в системный каталог Windows. Инсталлируем unicows, запускаем... работает! Но, к сожалению, только Debug-версия. Release-версия никак не может создать окно для контрола. Небольшое расследование показало, что в этом виноват макрос _ATL_DLL, из-за которого CWindowImpl::Create вызывает функцию AtlModuleRegisterWndClassInfoW из ATL.DLL, а та, в свою очередь, обращается напрямую к RegisterClassExW из USER32.DLL. Вызов не попадает в unicows, потому что ATL.DLL ничего про нее не знает. Unicows подменяет вызовы только в тех модулях, в сборке которых участвовала unicows.lib
ПРИМЕЧАНИЕ Майкрософт рекомендует устанавливать unicows.dll не в системный каталог windows, а в "C:\Program Files\Common Files\Microsoft Shared\MSLU\" |
Проблема решается простым отключением _ATL_DLL. Это будет стоить всего лишь в 10к, на которые "подрастет" наш модуль. Если Вас не пугает необходимость статически линковать все необходимые библиотеки (типа MFC), можете дальше не читать. Впрочем, у Вас есть хорошая возможность расширить немного свой кругозор.
Итак, продолжим.
Возникает вполне уместный вопрос: "А каким образом это все устроено"? Очень просто. Хитрые манипуляции с unicows.lib необходимы для того, чтобы подменить уникодные функции из модулей kernel32, advapi32, user32, gdi32, shell32, comdlg32, version, mpr, rasapi32, winmm, winspool, vfw32, secur32, oleacc, oledlg и sensapi на функции с аналогичными именами из unicows. А все функции из unicows.lib выглядят примерно так:
UNICOWSAPI ATOM WINAPI user32_RegisterClassExW_Thunk(IN CONST WNDCLASSEXW *lpwcx) { ResolveThunk("user32", "RegisterClassExW", RegisterClassExW, Unicows_RegisterClassExW, GodotFailRegisterClassExW); RegisterClassExW(lpwcx); } |
Пусть Вас не пугает странное название этой функции. В .def файле она переименовывается просто в RegisterClassExW, который и находит линковщик.
ATOM WINAPI Unicows_RegisterClassExW (IN CONST WNDCLASSEXW *lpwcx)
{
// Делаем, что хотим
}
|
Тогда линковщик, найдя две разных функции, но с одинаковыми именами, отдаст предпочтение той, что находится в нашей программе.
До вызова ResolveThunk, значение RegisterClassExW совпадает с user32_RegisterClassExW_Thunk, а внутри этого вызова происходит изменение этого указателя на функцию из USER32, для WindowsNT/2k/XP, либо на Unicows_RegisterClassExW, для Windows9x/Me с установленной unicows.dll, либо на GodotFailRegisterClassExW, для Windows9x/Me без unicows.dll. В любом случае, user32_RegisterClassExW_Thunk уже не будет никогда вызываться. Первый и последний вызов user32_RegisterClassExW_Thunk был осуществлен через указатель на эту функцию – RegisterClassExW, и значение по этому адресу было исправлено посредством вызова ResolveThunk.
Интересно, что в случае WindowsNT/2k/XP загрузки unicows.dll в память не происходит. ResolveThunk и функции-заглушки находятся в нашей программе. Фактически, осуществляется отложенное связывание функций. Это означает, что никакой лишней работы в случае с уникодной версией Windows не будет. Unicows работает "прозрачно" в этих ОС. В Windows9x/Me имеет место небольшая задержка для инициализации unicows, не заметная, впрочем, на фоне общей "задумчивости" этих ОС.
У стандартного механизма подключения unicows есть большой недостаток: он бесполезен, если имеются уже готовые модули в виде DLL, а не в виде исходных файлов. Помните AtlModuleRegisterWndClassInfoW и как с ней пришлось бороться? Так вот, есть способ лучше. Можно загрузить DLL в память и поправить ее таблицу импорта (см Форматы РЕ и COFF объектных файлов).
Для этого придется просмотреть все импортируемые функции и, если функция с таким же именем присутствует в unicows.dll, переправить обращение к этой функции в unicows.dll. Напишем две функции. Первая загружает в память процесса unicows.dll и настраивает таблицы экспортируемых функций. Вторая исправляет таблицу импорта указанного модуля.
Листинг №1 Перенаправление функций в unicows.dll#define MakePtr(cast, base, offset) (cast)((DWORD_PTR)(base) + (DWORD_PTR)(offset)) //@////////////////////////////////////////////////////////////////////////// // Глобальные переменные static BOOL g_bUnicodeOS = FALSE; static HMODULE g_hModuleUnicows = NULL; static LPWORD g_pdwOrd = NULL; static LPDWORD g_pdwAddrs = NULL; static LPDWORD g_pdwNames = NULL; static DWORD g_dwNames = 0; //@////////////////////////////////////////////////////////////////////////// // Вспомогательная функция, возвращает адрес функции из unicows.dll // с указанным именем, если такая имеется static LPDWORD GetFunctionAddress(LPCSTR azName) { for (DWORD dwName = 0; dwName < g_dwNames; dwName++) { if (0 == ::lstrcmpiA(MakePtr(LPSTR, g_hModuleUnicows, g_pdwNames[dwName]), azName)) return MakePtr(LPDWORD, g_hModuleUnicows, g_pdwAddrs[g_pdwOrd[dwName]]); } return NULL; } //@////////////////////////////////////////////////////////////////////////// // Вспомогательная функция, возвращает имя функции по ее номеру // для указанного модуля static LPCSTR GetNameFromOrdinal(HANDLE hModule, DWORD dwOrdinal) { if (!hModule) return FALSE; // Нет такого модуля // Находим таблицу экспорта PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(hModule); if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER)) || IMAGE_DOS_SIGNATURE != pDosHeader->e_magic) return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE; PIMAGE_NT_HEADERS pNTHeaders = MakePtr(PIMAGE_NT_HEADERS, hModule, pDosHeader->e_lfanew); if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS)) || IMAGE_NT_SIGNATURE != pNTHeaders->Signature) return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE; IMAGE_DATA_DIRECTORY& expDir = pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; PIMAGE_EXPORT_DIRECTORY pExpDir = MakePtr(PIMAGE_EXPORT_DIRECTORY, hModule, expDir.VirtualAddress); LPWORD pdwOrd = MakePtr(LPWORD, hModule, pExpDir->AddressOfNameOrdinals); LPDWORD pdwNames = MakePtr(LPDWORD, hModule, pExpDir->AddressOfNames); dwOrdinal -= pExpDir->Base; for (DWORD iName = 0; iName < pExpDir->NumberOfNames; iName++) { // Проверяем все номера по порядку if (dwOrdinal == pdwOrd[iName]) return MakePtr(LPSTR, hModule, pdwNames[iName]); } return NULL; } //@////////////////////////////////////////////////////////////////////////// // Функция инициализации unicows.dll BOOL _UnicowsInit() { if (0 == (0x80000000 & ::GetVersion())) { // WinNT/2k/XP: Тут нам делать нечего g_bUnicodeOS = TRUE; return ::SetLastError(0), TRUE; } g_hModuleUnicows = ::LoadLibraryA("Unicows.dll"); if (!g_hModuleUnicows) return FALSE; // Ошибка при загрузке Unicows.dll // Находим таблицу экспорта PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(g_hModuleUnicows); if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER)) || IMAGE_DOS_SIGNATURE != pDosHeader->e_magic) return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE; PIMAGE_NT_HEADERS pNTHeaders = MakePtr(PIMAGE_NT_HEADERS, g_hModuleUnicows, pDosHeader->e_lfanew); if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS)) || IMAGE_NT_SIGNATURE != pNTHeaders->Signature) return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE; IMAGE_DATA_DIRECTORY& expDir = pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; PIMAGE_EXPORT_DIRECTORY pExpDir = MakePtr(PIMAGE_EXPORT_DIRECTORY, g_hModuleUnicows, expDir.VirtualAddress); // Запонимаем таблицу имен и адресов g_pdwOrd = MakePtr(LPWORD, g_hModuleUnicows, pExpDir->AddressOfNameOrdinals); g_pdwNames = MakePtr(LPDWORD, g_hModuleUnicows, pExpDir->AddressOfNames); g_pdwAddrs = MakePtr(LPDWORD, g_hModuleUnicows, pExpDir->AddressOfFunctions); g_dwNames = pExpDir->NumberOfNames; return TRUE; } //@////////////////////////////////////////////////////////////////////////// // Функция перенаправляющая вызовы в unicows.dll BOOL _UnicowsRebindImports(HMODULE hModule) { if (g_bUnicodeOS) return ::SetLastError(0), FALSE; // Находим таблицу импорта PIMAGE_DOS_HEADER pDosHeader = PIMAGE_DOS_HEADER(hModule); if (::IsBadReadPtr(pDosHeader, sizeof(IMAGE_DOS_HEADER)) || IMAGE_DOS_SIGNATURE != pDosHeader->e_magic) return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE; PIMAGE_NT_HEADERS pNTHeaders = MakePtr(PIMAGE_NT_HEADERS, hModule, pDosHeader->e_lfanew); if (::IsBadReadPtr(pNTHeaders, sizeof(IMAGE_NT_HEADERS)) || IMAGE_NT_SIGNATURE != pNTHeaders->Signature) return ::SetLastError(ERROR_INVALID_PARAMETER), FALSE; IMAGE_DATA_DIRECTORY& impDir = pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; PIMAGE_IMPORT_DESCRIPTOR pImpDesc = MakePtr(PIMAGE_IMPORT_DESCRIPTOR, hModule, impDir.VirtualAddress), pEnd = pImpDesc + impDir.Size / sizeof(IMAGE_IMPORT_DESCRIPTOR) - 1; while(pImpDesc < pEnd) { // Для каждого импортируемого модуля if (pImpDesc->OriginalFirstThunk) { PIMAGE_THUNK_DATA pNamesTable = MakePtr(PIMAGE_THUNK_DATA, hModule, pImpDesc->OriginalFirstThunk); while(pNamesTable->u1.AddressOfData) { LPCSTR azFunctionName; if (IMAGE_SNAP_BY_ORDINAL(pNamesTable->u1.Ordinal)) { // Функция импортируется по номеру, вычисляем имя azFunctionName = GetNameFromOrdinal( ::GetModuleHandleA(MakePtr(LPSTR, hModule, pImpDesc->Name)) , IMAGE_ORDINAL(pNamesTable->u1.Ordinal)); if (!azFunctionName) continue; } else { PIMAGE_IMPORT_BY_NAME pName = MakePtr(PIMAGE_IMPORT_BY_NAME, hModule, pNamesTable->u1.AddressOfData); azFunctionName = LPCSTR(pName->Name); } LPDWORD dwAddr = GetFunctionAddress(azFunctionName); if (dwAddr) { // Эта функция есть в Unicows.dll LPDWORD *pProc = MakePtr(LPDWORD *, pNamesTable, pImpDesc->FirstThunk - pImpDesc->OriginalFirstThunk); // Возможно, кто-то это уже сделал if (*pProc != dwAddr) { // Отключаем защиту DWORD dwOldProtect = 0; if (::VirtualProtect(pProc, sizeof(DWORD), PAGE_READWRITE, &dwOldProtect)) { *pProc = dwAddr; // Восстанавливаем защиту ::VirtualProtect(pProc, sizeof(DWORD), dwOldProtect, &dwOldProtect); } } } } } pImpDesc++; } return TRUE; } |
С помощью такого кода мы можем не только переправить "на ходу" вызовы уникодных функций из нашего модуля, но и из любого чужого, например ATL.DLL.
К сожалению, Windows, при загрузке DLL, автоматически вызывает функцию DllMain() с параметром DLL_PROCESS_ATTACH и этот вызов произойдет до того, как мы сможем поправить таблицу импорта этой DLL. В случае с ATL.DLL ничего страшного не происходит, версия этой библиотеки для Windows9x/Me не уникодная, но экспортирует несколько уникодных функций. Для истинно уникодных DLL, к исходному коду которых нет доступа, у меня сработал такой трюк: сначала я вручную вызывал DllMain с параметром DLL_PROCESS_DETACH, затем настраивал таблицу импорта и снова вызывал DllMain, но уже с параметром DLL_PROCESS_ATTACH. К счастью, хотя эти DLL и не могли как следует проинициализироваться при первом вызове DllMain загрузчиком Windows, они делали это молча. Не выдавая пугающих сообщений об ошибках. Возможно, что Вам повезет меньше. Тогда путь только один: писать свой собственный загрузчик, загружающий нужный модуль в память, настраивающий все нужные таблицы, поддержку уникода и лишь потом вызывающий DllMain. Интересно, что ::LoadLibraryEx() умеет загружать модули в память не вызывая DllMain, но... только в WindowsNT/2k/XP! Windows9x/Me флаг DONT_RESOLVE_DLL_REFERENCES не поддерживает. Уникодная версия atl.dll, например, выдает страшное предупреждение и возвращает FALSE в DllMain, завершая работу приложения.
Подобная, но, к счастью, разрешимая проблема имеется и для EXE приложений. Дело в том, что выполнение программы начинается не с WinMain, а с некоторой функции, инициализирующей CRT, а затем уже вызывающей WinMain. Подробнее здесь. Все, что нам нужно, это задать свою точку входа в программу:
и написать эту самую _UnicowsEntry:
Листинг №2 Собственная точка входа в программуextern "C" void _UnicowsEntry() { if (_UnicowsInit()) { _UnicowsRebindImports(::GetModuleHandleA(NULL)); _UnicowsRebindImports(::GetModuleHandleA("ATL")); } else { ::MessageBox(NULL, _T("Ошибка инициализации UNICOWS.DLL"), NULL, MB_OK | MB_ICONSTOP); ::ExitProcess(-1); } #ifdef _UNICODE void wWinMainCRTStartup(); wWinMainCRTStartup(); #else // _UNICODE void WinMainCRTStartup(); WinMainCRTStartup(); #endif // _UNICODE } |
Отложенная загрузка (подробнее) означает, что некоторые функции не присутствуют в таблице импорта, а связываются с нужными модулями по мере необходимости. Этот механизм очень похож на тот, который реализуют заглушки unicows.
Как таковой, проблемы не возникает. Все, что нам нужно – это доработать маленько наши функции и заодно подменить ::GetProcAddress() на нашу собственную функцию, а там мы сначала поищем в unicows.dll, а если не найдем нужной функции, то отправим вызов в настоящую ::GetProcAddress().
Листинг №3 Подмена ::GetProcAddress()static FARPROC WINAPI _UnicowsGetProcAddress(IN HMODULE hModule, IN LPCSTR azName); static FARPROC (WINAPI *_RealGetProcAddress)(IN HMODULE hModule, IN LPCSTR azName); static LPDWORD GetFunctionAddress(LPCSTR azName) { if (0 == ::lstrcmpiA("GetProcAddress", azName)) return LPDWORD(_UnicowsGetProcAddress); // Unicows for (DWORD dwName = 0; dwName < g_dwNames; dwName++) { if (0 == ::lstrcmpiA(MakePtr(LPSTR, g_hModuleUnicows, g_pdwNames[dwName]), azName)) return MakePtr(LPDWORD, g_hModuleUnicows, g_pdwAddrs[g_pdwOrd[dwName]]); } return NULL; } static FARPROC WINAPI _UnicowsGetProcAddress(IN HMODULE hModule, IN LPCSTR azName) { if (IS_INTRESOURCE(azName)) azName = GetNameFromOrdinal(hModule, WORD(azName)); LPDWORD dwAddr = GetFunctionAddress(azName); if (dwAddr) return FARPROC(dwAddr); return _RealGetProcAddress(hModule, azName); } |
Некоторые функции не имеют заглушек для уникодной версии в Windows9x/Me. Такими функциям, например, являются ::GetAltTabInfo() и ::RealGetWindowClass(). USER32.DLL из WindowsNT/2k/XP экспортирует по три функции для каждой из них, например, GetAltTabInfo, GetAltTabInfoA и GetAltTabInfoW. В USER32.DLL из Windows98 есть только GetAltTabInfo. USER32.DLL из Windows95 не имеет такой функции вообще. Интересно, что в WinUser.h определены именно GetAltTabInfoA и GetAltTabInfoW, таким образом, даже если ваше приложение скомпилировано без поддержки уникода, Windows9x все равно не сможет его загрузить. Тем не менее, эти функции есть в unicows.dll, и, если воспользоваться стандартным способом подключения unicows, приложение будет работать. Для альтернативного способа нам придется воспользоваться явным (GetProcAddress) или отложенным (delayload) связыванием. Оба пути приведут нас, в конце концов, в unicows.dll, где имеются уникодные версии этих функций.
С Module32First/Module32Next, Process32First/Process32Next из KERNEL32.DLL похожая ситуация. В WindowsNT/2k/XP есть Module32First и Module32FirstW, в Widows98 только Module32First. В Unicows.dll Module32FirstW также отсутствует. Для этих функций, впрочем, можно легко отказаться от уникода. Для этого можно просто отменить макрос UNICODE перед включением TlHelp32.h, а затем включить его обратно.
#ifdef UNICODE #undef UNICODE #include <Tlhelp32.h> #define UNICODE #else #include <Tlhelp32.h> #endif |
На прощание хочу обратить Ваше внимание на тот факт, что unicows не обеспечивает настоящей поддержки уникода под Windows9x/Me, Вам не удастся вывести одновременно японские и русские буквы, как в WindowsNT/2k/XP, но позволяет не компилировать и отлаживать две версии одной программы: с уникодом и без. В любом случае, если вам нужны одновременно и китайские и арабские буквы, обойтись без WindowsNT/2k/XP не получится.
Microsoft Layer for Unicode on Windows 95/98/Me Systems
Q259403 (загрузка Atl.dll с сайта Майкрософт)
Unicows.dll version 1.0
Atl.dll version 3.0