Постановка задачи и её решение
Модули для TC представляют собой DLL фалы имеющие расширения WCX, WFX, WLX, WDX и содержащие определенный набор экспортируемых функций (согласно категории модуля). Все бы хорошо, только не все авторы позаботились об 64 битных версиях. А исходный код не доступен, как правило…
Вопрос — Можно использовать существующие 32 битные версии?
Ответ — Да, но не все так просто.
Если обобщить до загрузки динамической 32 битной библиотеки в 64 битный процесс, то окажется, что задача не нова и поиск решения в интернете не заставит ждать. Все сводится к созданию суррогатного процесса, способного загрузить библиотеку и взаимодействию с этим процессом посредством IPC (меж процессное взаимодействие). К исходникам TC у нас нет доступа и добавить механизм работы с суррогатным процессом мы не можем. Но можем создать библиотеку. Библиотека будет выдавать себя за модуль и общаться с суррогатным процессом, а тот в свою очередь будет дергать функции модуля и возвращать результат. И выгладить все это будет вот так:
С возможными вариантами IPC можно ознакомится на MSDN — Interprocess Communications. Для своего проекта я выбрал Pipes. Это возможно не самый быстрый способ, но он позволяет неявно следить за здоровьем суррогатного процесса. Если падает суррогатный процесс, то и разрывается канал pipe-а и наша библиотека узнает об этом. Далее описание происходящих процессов.
При подключении библиотеки
- генерация уникального имени для pipe-а
- создание pipe-а
- создание суррогатного процесса
- передача суррогатному процессу имени pipe-а
- ожидание и подключение клиента через pipe
Для генерации уникального имени воспользуемся функцией UuidCreate(). Она сгенерирует UUID(GUID). Преобразуем его в строку (UuidToString) и заполним путь для pipe. Создадим pipe(CreateNamedPipe) работающий в блокирующем режиме и передаче сообщений. Запустим суррогатный процесс (CreateProcess). Имя pipe-а передадим в качестве параметра командной строки. И будем ждать клиента (ConnectNamedPipe).
При отключении библиотеки
- отключить клиента от трубы
- завершить суррогатный процесс
- закрыть pipe (в общем освободить выделенные ресурсы)
Отключим клиент (DisconnectNamedPipe), завершим суррогатный процесс (TerminateProcess), закроем pipe и почистим ресурсы (CloseHandle)
При запуске суррогатного процесса
- получить имя pipe
- подключиться к pipe-у как клиент
- загрузить модуль
- ожидать сообщение
Подключимся к pipe-у (CreateFile) и сконфигурируем его на работу в блокирующем режиме и передачу сообщений. Загрузим модуль (LoadLibrary) и сохраним адреса экспортируемых функций (GetProcAddress). Войдем в цикл ожидания сообщений. В случае необходимости завершить процесс выйдем из цикла.
При завершении суррогатного процесса
- отключится от pipe-а
- выгрузить модуль
Отключимся от pipe-а (CloseHandle) и выгрузим модуль(FreeLibrary).
При вызове функции из библиотеки
- запаковать параметры в сообщение
- передать запрос через pipe
- получить ответ
- распаковать результат и выйти с функции
Вызов функции рассмотрим на примере
__declspec(dllexport) HANDLE __stdcall OpenArchive(tOpenArchiveData *ArchiveData) { if (s_init && ArchiveData) { // serialize uint8_t *p = s_buff; SET_FUNC(p, OPENARCHIVE) SET_CALLTYPE(p, CALL_QUERY) SET_STR_A(p, ArchiveData->ArcName) SET_INT(p, ArchiveData->OpenMode) // send DWORD writeSize = p - s_buff; DWORD writedSize; while (WriteFile(s_pipe, s_buff, writeSize, &writedSize, NULL)) { assert(writeSize == writedSize); // recv DWORD readedSize; if (ReadFile(s_pipe, s_buff, PIPE_BUFF_SIZE, &readedSize, NULL)) { // deserialize uint8_t *p = s_buff; uint8_t func; GET_FUNC(p, func) uint8_t callType; GET_CALLTYPE(p, callType) if (callType == CALL_ANSWER) { assert(func == OPENARCHIVE); GET_INT(p, ArchiveData->OpenResult) HANDLE r; GET_HANDLE(p, r) // result return r; } else if (callType == CALL_QUERY) { CALL_PROC } assert(0); } } ArchiveData->OpenResult = E_NOT_SUPPORTED; } return NULL; }
OpenArchive — первая функция которую вызывает TC после загрузки модуля. Ей передается указательна структуру типа tOpenArchiveData.
typedef struct { char* ArcName; int OpenMode; int OpenResult; char* CmtBuf; int CmtBufSize; int CmtSize; int CmtState; } tOpenArchiveData;
Мы не можем передать указатель на структуру, процессы изолированны и не видят память друг друга. Мы так же не можем передать структуру просто скопировав её в сообщение, из за указателя на строку (ArcName) и выравнивания полей. Плюс некоторые поля предназначены для передачи данных в функцию (ArcName, OpenMode), а некоторые служат буфером для возврата результата (OpenResult), последние и вовсе не используются (Cmt*). Мы должны произвести маршалинг, т. е. преобразовать данные в формат пригодный для передачи. Для этого служат ряд написанных макросов SET_*. SET_INT записывает int как 32 битное число в буфер. SET_STR_A записывает в буфер признак валидности указателя на строку и в случае валидности записывает размер строки с терминальным нулем и массив символов на который указывает указатель. В начало буфера помещаются два значения: что это за функция и что это — запрос. Далее надо посчитать размер данных записанных в буфер и записать их в pipe. Подождать ответа от другой стороны. При получении ответа прочитать два значения: что это за функция и что это — ответ или запрос на исполнение функции обратной связи. Если это ответ, получаем результат, записываем часть в структуру и выходим из функции. Если это запрос на вызов функции обратной связи, получаем параметры для неё, выполняем, возвращаем результат и ждем очередного ответа (всё это спрятано в макросе CALL_PROC). Отдельно стоит упомянуть тип результата рассмотренной функции. Это HANDLE, но в действительности указатель. Он понадобится в качестве параметра для вызова остальных функции самим TC. Но значимость его играет роль только в пределах модуля. В 32 битных процессах указатель 32 битный, в 64 соответственно 64 битный. И создается он в 32 битном процессе. Поэтому преобразование его в 64 а потом в 32 не приведет к потере данных.
Две функции (SetChangeVolProc, SetProcessDataProc) регистрируют функции обратной связи в модуле. Мы со своей стороны просто запомним их, а передадим сам факт регистрации. Они понадобятся в CALL_PROC.
При получении сообщения
- получить сообщение
- распаковать параметры
- вызвать функцию из расширения
- запаковать результат
- передать сообщение с результатом
Цикл получения сообщений
while (s_loop) { DWORD readedSize; if (ReadFile(s_pipe, s_buff, PIPE_BUFF_SIZE, &readedSize, NULL)) { // deserialize, process, serialize uint8_t *p = s_buff; uint8_t func; GET_FUNC(p, func) uint8_t callType; GET_CALLTYPE(p, callType) assert(callType == CALL_QUERY); if (func == OPENARCHIVE) { tOpenArchiveData openArchiveData = {0}; GET_STR_A(p, openArchiveData.ArcName) GET_INT(p, openArchiveData.OpenMode) HANDLE r = OpenArchive(&openArchiveData); p = s_buff; SET_FUNC(p, OPENARCHIVE) SET_CALLTYPE(p, CALL_ANSWER) SET_INT(p, openArchiveData.OpenResult) SET_HANDLE(p, r) } else ... ... { assert(0); } DWORD writeSize = p - s_buff; DWORD writedSize; if (!WriteFile(s_pipe, s_buff, writeSize, &writedSize, NULL) || writeSize != writedSize) { return -6; } } else if (GetLastError() != ERROR_TIMEOUT) { break; } }
Все приблизительно так же. Получим сообщение, выясним какую функцию просят вызвать, произведём процесс обратный маршалингу (GET_*), вызовем функцию, получим результат и отправим его библиотеке. В процессе вызова функции может произойти вызов функции обратной связи.
int __stdcall ChangeVolProc(char *ArcName, int Mode) { uint8_t *p = s_buff; SET_FUNC(p, CHANGEVOLPROC) SET_CALLTYPE(p, CALL_QUERY) SET_STR_A(p, ArcName) SET_INT(p, Mode) DWORD writeSize = p - s_buff; DWORD writedSize; assert(WriteFile(s_pipe, s_buff, writeSize, &writedSize, NULL) || writeSize == writedSize); DWORD readedSize; assert(ReadFile(s_pipe, s_buff, PIPE_BUFF_SIZE, &readedSize, NULL)); p = s_buff; uint8_t func; GET_FUNC(p, func) uint8_t callType; GET_CALLTYPE(p, callType) assert(func == CHANGEVOLPROC && callType == CALL_ANSWER); int r; GET_INT(p, r) return r; }
Вызываются наши подставные функции, которые проведут связь (с библиотекой).
Все это сопровождается обработкой ошибок в виде аварийного завершения суррогатного процесса, подстановкой функций-заглушек и возврат дефолных значений.
Отрицательная сторона решения: все это замедляет скорость работы модуля.
Пожалуй на этом все…
Что осталось
В действительности есть еще целый ряд вопросов, для которых надо выбрать решения. Реализован только минимум в рамках demo. Набор функций в рамках модуля расширения несколько больше, а о доступных возсожностях модуля говорит таблица экспорта. Динамически подстраиваться по это нельзя. Не все понятно с WLX модулями, в частности взаимодействие с окном. И т.д.
С полным исходным кодом можно ознакомится по ссылке source. Собрать можно с помощью Pelle С for Windows. Полученные приложение и библиотеку надо переименовать в соответствии с модулем (пример: модуль msi.wcx, программа msi.exe, библиотека msi.wcx64) и положить рядом с модулем.
И хотелось бы узнать ваше мнение
ссылка на оригинал статьи http://habrahabr.ru/post/202604/
Добавить комментарий