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

в 9:45, , рубрики: plugins, Total Commander, windows, Программирование, метки: ,

Я расскажу вам про маленький проект, позволяющий использовать 32 битные версии расширений в 64 битной версии Total Commander-а (далее TC). Проект находится на стадии demo и позволяет использовать WCX модули с минимальным, необходимым набором функции (просмотр и извлечение содержимого архивов или все что можно представить в виде архивов). Ну и в конце опрос о востребованности такого решения и доведения проекта до некого уровня, покрывающего всю возможную часть API и все возможные категории модулей.

Постановка задачи и её решение

Модули для TC представляют собой DLL фалы имеющие расширения WCX, WFX, WLX, WDX и содержащие определенный набор экспортируемых функций (согласно категории модуля). Все бы хорошо, только не все авторы позаботились об 64 битных версиях. А исходный код не доступен, как правило…

Вопрос — Можно использовать существующие 32 битные версии?
Ответ — Да, но не все так просто.

Если обобщить до загрузки динамической 32 битной библиотеки в 64 битный процесс, то окажется, что задача не нова и поиск решения в интернете не заставит ждать. Все сводится к созданию суррогатного процесса, способного загрузить библиотеку и взаимодействию с этим процессом посредством IPC (меж процессное взаимодействие). К исходникам TC у нас нет доступа и добавить механизм работы с суррогатным процессом мы не можем. Но можем создать библиотеку. Библиотека будет выдавать себя за модуль и общаться с суррогатным процессом, а тот в свою очередь будет дергать функции модуля и возвращать результат. И выгладить все это будет вот так:

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

С возможными вариантами 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js