Я расскажу вам про маленький проект, позволяющий использовать 32 битные версии расширений в 64 битной версии Total Commander-а (далее TC). Проект находится на стадии demo и позволяет использовать WCX модули с минимальным, необходимым набором функции (просмотр и извлечение содержимого архивов или все что можно представить в виде архивов). Ну и в конце опрос о востребованности такого решения и доведения проекта до некого уровня, покрывающего всю возможную часть API и все возможные категории модулей.
Постановка задачи и её решение
Модули для 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) и положить рядом с модулем.
И хотелось бы узнать ваше мнение
Автор: BulldozerBSG