Qt

Qt QCoreApplication или EventLoop в DLL

Потребовалось мне создать динамическую dll для сторонней программы (не Qt).
Разумеется, хотелось использовать все возможности Qt, а именно сигналы/слоты/QTCPSocket и прочее.

Но для их работы нужна очередь событий (EventLoop), то есть либо QCoreApplication, либо QEventLoop. Но в dll этого нет.

Можно, конечно, сигналы-слоты использовать с Qt::DirectConnect (прямой вызов), но тогда всё равно не будут нормально работать QTCPSocket (потому что для их работы в WndProс должно приходить уведомление), поэтому придётся вызывать методы waitForBytesWriten() и т.д.) и прочее, кто работает асинхронно и ждёт событий от системы.

Была бы программа, в которую подключается dll сама написана на Qt, то таких танцев с бубном не потребовалось (т.к. там уже есть QApplication и ещё одного не нужно)

В dll основная функция, с которой всё начинается, это DllMain (при загрузке/выгрузке в процесс или подключении/отключении к потоку она автоматически вызывается системой).

extern "C" BOOL __stdcall DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch(fdwReason) {
        case DLL_PROCESS_ATTACH: 
          QCoreApplication app(argc, nullptr); //-- Нельзя так!
          ...
          app.exec(); //-- Дальше загрузка остальных библиотек и хост-программа целиком зависнет, т.к. exec() запускает почти бесконечный цикл и из DllMain не выйдем.
        break;

        case DLL_PROCESS_DETACH: 
          ...
        break;
    }

    return true;
}

Кажется, что достаточно поместить QCoreApplication app(argc, nullptr); в блок DLL_PROCESS_ATTACH, но тогда загрузка всех остальных библиотек зависнет наглухо, про это чётко сказано в официальной документации: https://docs.microsoft.com/en-us/windows/win32/dlls/dllmain

Что делать? Самым простым способом, это будет создать новый поток (но в документации прописано, что это вроде не есть гуд, но не понятно почему) и запустить его на выполнение, а внутри него уже можно запускать бесконечный цикл. Сделаем функцию, которую поток запустит при запуске и сделаем запуск потока.

/**
* @brief В отдельном потоке
*/
DWORD WINAPI run(HMODULE hInst)
{
    int argC=0;
    QCoreApplication app(argC, nullptr);
    ... //-- Можем использовать все возможности Qt
    return app.exec(); //-- Бесконечный цикл запущен, пока не завершим, из этой функции не выйдем
}

/**
* @brief При загрузке библиотеки в процесс
*/
void init(HINSTANCE hInst)
{
    toLog("INIT\n");
    CreateThread(nullptr, 0,  (LPTHREAD_START_ROUTINE)run, (LPVOID)hInst, 0, nullptr);
}

/**
* @brief При выгрузке из процесса
*/
void deInit(HINSTANCE hInst)
{    
   ...
}

extern "C" BOOL __stdcall DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    Q_UNUSED(lpvReserved);

    switch(fdwReason) {
        case DLL_PROCESS_ATTACH: init(hinstDLL); break;
        case DLL_PROCESS_DETACH: deInit(hinstDLL); break;
    }

    return true;
}
CONFIG += qt

И вроде бы всё хорошо, сигналы начнут ходить, слоты начнут вызываться.

ВАЖНО:
1. Почему используется winapi функция создания потока CreateThread()?
Потому, что при завершении программы система сама убъёт поток.
В случае, к примеру, QtConcurrent::run(..) нам придётся самостоятельно отслеживать закрытие всех окон и делать выход из QCoreApplicaton. Если всё же пойдёте этим путём, учтите, что напрямую из другого потока вызывать будет бесполезно! Придётся делать через «postMessage» (т.к. sendMessage сразу вызывает) или флаг ставить, и по таймеру обрабатывать.

2. Кстати, так же внутри классов нельзя вызывать системные функции, которые могут заблокировать поток Qt. К примеру, winapi MessageBoxW(nullptr, ….)! Как избежать?
Первым параметром идёт указатель на окно, которое будет принимать события (а следовательно и поток) (точнее, обработкой то занимается WndProc), нам всего лишь нужно поставить туда указатель на окно от QCoreApplication. Да, Вы можете возразить, что его нет! На самом деле есть, просто скрытое. Найти его можно с помощью winapi FindWindowEx по id процесса (можно из из Qt достать, но создатели вроде бы не предусмотрели таких извращений, поэтому без модификации файлов фреймворка не получится).
ИД процесса (не путить с ИД процесса приложения!) можно получить: QCoreApplication::applicationPid().
Чуть ниже выложу реализацию, запускать так:
HWND hApp = findwind(nullptr, L»», L»», 1, QCoreApplication::applicationPid())

3. Что бы библиотека загрузилась, ей для работы нужны Qt шные библиотеки (той же версии, которой компилируется текущая), это как минимум:
1. libwinpthread-1.dll
2. libgcc_s_dw2-1.dll
3. libstdc++-6.dll
4. Qt5Core.dll
5. Остальные

Порядок загрузки важен!
Положите либо рядом с загружаемой, либо в System32 (в случае Винды).
Либо при загрузке библиотеки используйте loadLibraryEx которая сама подгрузит нужные (в случае, если Вы используете injectdll (инъекцию dll)).

P.S. Функции вывода в лог (принцип такой же, как и у printf, не забудьте заголовочники подключить):

void toLog(const char *fmt, ...)
{        
    va_list arg;
    va_start(arg, fmt);
    char buffer[4096];
    int rc = vsnprintf(buffer, sizeof(buffer), fmt, arg);
    va_end(arg);

    FILE * fp = fopen("C:/DELME/Log.txt", "a");
    fprintf(fp, buffer);
    fclose(fp);
}

void toLogW(const wchar_t *fmt, ...)
{
    va_list arg;
    va_start(arg, fmt);
    wchar_t buffer[4096];
    int rc = vsnwprintf(buffer, sizeof(buffer), fmt, arg);
    va_end(arg);

    FILE * fp = fopen("C:/DELME/Log.txt", "a");
    fwprintf(fp, buffer);
    fclose(fp);
}

Поиск окна по нескольким параметрам:

HWND findwind(HWND parent, std::wstring text, std::wstring className, int instance = 1, DWORD processID=0, bool fromStart=true)
{
    WCHAR bufferText[1024];
    WCHAR bufferClass[1024];
    DWORD dwPID = 0;
    HWND hWnd = parent;

    while (true) {
        hWnd = FindWindowEx(nullptr, hWnd, nullptr, nullptr);

        if ( hWnd==nullptr ) break;
        if ( !IsWindow(hWnd) ) { continue; }

        if( !text.empty() ) {
            GetWindowText(hWnd, bufferText, 1000);
            wchar_t * pos = wcsstr(bufferText, text.c_str());
            if ( (pos==nullptr) || (fromStart && pos-bufferText!=0) ) { continue; }
        }

        if ( processID>0) {
            GetWindowThreadProcessId(hWnd, &dwPID);
            if ( dwPID!=processID ) { continue; }
        }

        if ( !className.empty() ) {
            GetClassName(hWnd, bufferClass, 1000);
            if ( wcsstr(bufferClass, className.c_str())-bufferClass!=0 ) { continue; }
        }

        if ( --instance==0 ) { return hWnd; }
    }

    return nullptr;
}

P.P.S. Так же бесконечный цикл/очередь событий нужна, если ставиться SetWindowHookEx, иначе без этого он просто сброситься и хук вызываться не будет. Это же применимо и для injectDLL.
Кстати, при срабатывании хука напрямую Qt слоты вызывать не стоит, т.к. хук в другом потоке. Лучше использовать «QCoreApplication::postEvent(…)» а в обработчике переопределить функцию bool event(QEvent * event) override; Вот так примерно:

#include <QEvent>
#include "minwindef.h"
#include "winuser.h"
/**
* @brief Тип события для обработки хука
*/
const QEvent::Type WndProcHookEventType = static_cast<QEvent::Type>(QEvent::User+1000);
class WndProcHookEvent: public QEvent
{
public:
    WndProcHookEvent(WPARAM wParam, LPARAM lParam): QEvent(WndProcHookEventType) {
        _cmd = wParam;
        //-- Нужно сразу получать значения всего интересующего, иначе указатели после выхода их хука могут стать не валидными
        _cwps = *reinterpret_cast<CWPSTRUCT*>(lParam); 
    }

    uint cmd() const { return _cmd; }
    CWPSTRUCT cwps() { return _cwps; }

private:
    uint _cmd;
    CWPSTRUCT _cwps;

};

void run(HMODULE hInst)
{
   ...
   hHook = SetWindowsHookExW(WH_CALLWNDPROC, hookCB, 0, GetWindowThreadProcessId(<base program window handle>, nullptr));
   ...
}

LRESULT WINAPI hookCB(int nCode, WPARAM wParam, LPARAM lParam)
{
    if( nCode<0 ) {
        CallNextHookEx(hHook, nCode, wParam, lParam );
        return 0;
    }

    QCoreApplication::postEvent(_handler, new WndProcHookEvent(wParam, lParam));

    return CallNextHookEx(hHook, nCode, wParam, lParam);
}

bool event(QEvent* event) override 
{
   if ( event->type()!=WndProcHookEventType ) { return false; }
   CWPSTRUCT cwps = wndProc->cwps();
   if ( cwps.message==WM_COMMAND ) {
      ...
   }
   return true;
}

Вот как-то так.

Related posts

QML Сделать задержку перед началом анимации

QtCreator не открывает диалог выбора файлов, проектов, не открывает проект

Сборка Qt 6.8 (Dev) из исходников в Docker контейнере (Linux)

1 comment

Toxen 19 апреля 2024 - 14:53
Я просто оставлю это здесь: Код проверки наличия QCoreApplicaiton в вызывающем приложении (сам код использовать в dll) if(qApp == nullptr) { new QCoreApplication(argc, argv); } далее работа с QEventLoop: QEventLoop event; QObject::connect(task, SIGNAL(finished()), &event, SLOT(quit())); event.exec();
Add Comment