加载 Qt 共享库时 Qt 小部件不显示

Qt widgets not show up when Qt shared lib loaded

要求:Qt 小部件在 Qt 共享库 加载 时显示,用于 none-Qt 应用程序。

经过网络搜索,我发现:

  1. 所有Qt widgets必须存在于"main thread","main thread"是Qt对象创建的第一个线程。所以,创建一个 none-Qt 线程 (std::thread),然后,创建 该线程中的 QApplication 和其他一些小部件应该可以工作,但不能。

  2. 在 QApplication 创建之前,不要在 none-Qt 线程中创建任何 Qt 相关对象或调用任何 Qt 相关静态方法。

  3. 线程解决方案无法移植Mac OS,我的目标平台只有Windows,所以没关系。

  4. 在我的例子中,如果应用程序加载我的 Qt 库,并调用显示小部件的方法, 有用。但由于某些原因,调用者无法手动调用我的库方法。

  5. 如果宿主应用程序(加载共享库的应用程序)是 Qt 应用程序,您应该调用 QApplication::processEvents(),而不是 QApplication::exec()。就我而言,我应该在该线程中调用 QApplication::exec()。

源代码在这里:

BOOL APIENTRY DllMain(HMODULE hModule,
                      DWORD ul_reason_for_call,
                      LPVOID lpReserved)
{
    if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
        auto t = std::thread([]() {
            // setCodecForLocale is in the same thread, 
            // call it before QApplication created should be OK.

            QTextCodec::setCodecForLocale(QTextCodec::codecForName("GBK"));
            int i = 0;
            int argc = 0;
            QApplication app(argc, 0);

            auto dialogLogin = new DialogLogin(); // custom widget

            dialogLogin->setModal(true);
            dialogLogin->show();
            app.exec(); // app.processEvents() not work, too.
        });

        t.join(); // wait for thread ends in dllMain should be BAD, test only
    }

    return true;
}
class LibExecutor {
public:
    LibExecutor()
    {
        auto t = std::thread([]() {
            QTextCodec::setCodecForLocale(QTextCodec::codecForName("GBK"));
            int argc = 0;
            QApplication app(argc, 0);

            auto dialogLogin = new DialogLogin();
            dialogLogin->setModal(true);
            dialogLogin->show();
            app.exec();
        });

        t.join();
    }
};

static LibExecutor libExecutor;

两个版本都成功调用了小部件初始化内容,但小部件没有 出现。

这是我测试它的方法,使用 Qt 加载库,但是,事件我使用 Win32 API 加载库,也失败了。

#include "mainwindow.h"
#include <QApplication>
#include <QLibrary>

int main(int argc, char* argv[])
{
    QLibrary lib("F:/lib_location/lib_name.dll");

    if (lib.load()) {
        qDebug() << "load ok!";
    } else {
        qDebug() << "load error!";
    }
}

这是一个工作示例。使用 Qt 5.12 和 MSVC2017 和 MinGW 进行测试。

// main.cpp
int main(int argc, char *argv[])
{
    run_mylib_t *f= nullptr;
    HMODULE lib = LoadLibraryA("..\mylib\debug\mylib.dll");

    if (!lib) {
        qDebug() << "Failed to load library;";
        return -1;
    }

    f = reinterpret_cast<run_mylib_t *>(GetProcAddress(lib, "run_mylib"));

    if (!f) {
        qDebug() << "Failed to get function";
        return -1;
    }

    f(argc, argv);      

    return 0;
}

// mylib.h
extern "C" MYLIBSHARED_EXPORT int run_mylib(int argc, char *argv[]);
using run_mylib_t = int(int, char *[]);

// mylib.cpp
int loop(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

int run_mylib(int argc, char *argv[])
{
    auto lambda = [argc, argv]() {loop(argc, argv); };

    std::thread thread(lambda);
    thread.join();

    return 0;
}

注意,如果在创建线程之前使用Qt函数,Qt会检测到它不在主线程中,进程就会崩溃。这就是为什么我不使用 QLibrary.

Qt 不支持此用例。所以如果你现在让它工作,你不能保证它将来会工作。

你不能像这样同时加载 2 个 dll。

根据您在主应用程序中执行的操作,某些 Qt 功能可能无法按预期工作。例如,Qt 可能需要来自 Windows 的消息,但永远不会得到它们,因为它们将由真正的主线程处理。

关于 DllMain

来自 Windows 文档:

Warning

There are significant limits on what you can safely do in a DLL entry point. See General Best Practices for specific Windows APIs that are unsafe to call in DllMain. If you need anything but the simplest initialization then do that in an initialization function for the DLL. You can require applications to call the initialization function after DllMain has run and before they call any other functions in the DLL.

-- https://docs.microsoft.com/en-us/windows/desktop/dlls/dllmain

和动态-Link 库最佳实践:

You should never perform the following tasks from within DllMain:

  • Call LoadLibrary or LoadLibraryEx (either directly or indirectly). This can cause a deadlock or a crash.
  • Call GetStringTypeA, GetStringTypeEx, or GetStringTypeW (either directly or indirectly). This can cause a deadlock or a crash.
  • Synchronize with other threads. This can cause a deadlock.
  • Acquire a synchronization object that is owned by code that is waiting to acquire the loader lock. This can cause a deadlock.
  • Initialize COM threads by using CoInitializeEx. Under certain conditions, this function can call LoadLibraryEx.
  • Call the registry functions. These functions are implemented in Advapi32.dll. If Advapi32.dll is not initialized before your DLL, the DLL can access uninitialized memory and cause the process to crash.
  • Call CreateProcess. Creating a process can load another DLL.
  • Call ExitThread. Exiting a thread during DLL detach can cause the loader lock to be acquired again, causing a deadlock or a crash.
  • Call CreateThread. Creating a thread can work if you do not synchronize with other threads, but it is risky.
  • Create a named pipe or other named object (Windows 2000 only). In Windows 2000, named objects are provided by the Terminal Services DLL. If this DLL is not initialized, calls to the DLL can cause the process to crash.
  • Use the memory management function from the dynamic C Run-Time (CRT). If the CRT DLL is not initialized, calls to these functions can cause the process to crash.
  • Call functions in User32.dll or Gdi32.dll. Some functions load another DLL, which may not be initialized.
  • Use managed code.

-- https://docs.microsoft.com/en-us/windows/desktop/dlls/dynamic-link-library-best-practices

据此我可以告诉您,您将无法从 DllMain 创建 QApplication 和 运行 Qt 应用程序,至少有以下原因:

  • Qt 将使用 LoadLibrary 加载插件(至少 qwindows.dll)。如果您使用任何音频或图像或 sql 数据库,Qt 也会尝试加载相应的插件(例如 qjpeg.dll)。
  • Qt 也可能会尝试访问注册表,特别是如果您使用 QSettings 原生格式。
  • Qt 可以创建线程。特别是如果您使用网络或 Qt Quick。
  • Qt 将使用 mallocfree 等内存管理函数。