Дружим Total Commander (64 bits) и plugins (32 bits)

от автора

Я расскажу вам про маленький проект, позволяющий использовать 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) и положить рядом с модулем.

И хотелось бы узнать ваше мнение

Довести проект до ума

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Проголосовал 1 человек. Воздержавшихся нет.

ссылка на оригинал статьи http://habrahabr.ru/post/202604/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *