ZH ·
🌏 English

编译 QtHttpServer 并封装为 C 接口动态库:解决 QCoreApplication 依赖问题

编译 QtHttpServer 模块

首先,从 GitHub 拉取代码并切换到对应分支:

git clone https://github.com/qt-labs/qthttpserver.git
cd qthttpserver
git checkout 5.15
git submodule update --init --recursive

然后使用 Qt Creator 打开工程并进行编译。

最简单的 Qt 控制台程序

一个最基础的 Qt 程序通常是一个没有 UI、在控制台运行的 HelloWorld。其核心代码结构如下:

#include <QtCore>

int main(int argc, char **argv) {
    QCoreApplication app(argc, argv);

    // qDebug() << "Hello World";

    return app.exec();
}

使用 QtHttpServer

QtHttpServer 目前尚未集成在 Qt 的主库中(预计在 Qt 6 会正式加入)。因此,我们需要自行下载并编译。项目地址:https://github.com/qt-labs/qthttpserver

编译与安装

如果你使用的是 Qt 5,建议切出 5.15 分支。编译成功后,需要手动将生成的头文件和动态库拷贝到 Qt 的安装路径下,以便其他项目引用。

以下是以 Linux 环境为例的拷贝参考(请根据实际路径修改):

# 进入编译输出目录
cd build-qthttpserver-Desktop_Qt_5_12_6_GCC_64bit-Release/

# 拷贝库文件
mv ./* /opt/Qt5.12.6/5.12.6/gcc_64/lib/

# 拷贝 CMake 配置文件
cd cmake/
mv ./* /opt/Qt5.12.6/5.12.6/gcc_64/lib/cmake/

# 拷贝 pkgconfig
cd ..
cd pkgconfig/
mv ./* /opt/Qt5.12.6/5.12.6/gcc_64/lib/pkgconfig/

# 拷贝头文件
cd ..
cd ..
cd include/
mv ./* /opt/Qt5.12.6/5.12.6/gcc_64/include/

# 拷贝模块配置文件
cd ..
cd mkspecs
cd modules
mv ./* /opt/Qt5.12.6/5.12.6/gcc_64/mkspecs/modules/

启动 QtHttpServer

在 Qt 官方博客 introducing-qt-http-server 中,介绍了一个最基本的 QHttpServer 用法:

#include <QtCore>
#include <QtHttpServer>

int main(int argc, char **argv) {
  QCoreApplication app(argc, argv);
  QHttpServer httpServer;
  httpServer.route("/", []() {
    return "Hello world";
  });
  httpServer.listen(QHostAddress::Any, 9527);
  return app.exec();
}

程序启动后,访问 http://127.0.0.1:9527 即可看到 “Hello world”。如果需要支持 POST 方法并接收数据,可以参考如下代码:

  httpServer.route("/post-body", "POST", [] (const QHttpServerRequest &request) {
        return request.body();
    });

更多关于路由的用法,可以参考这篇博客:qhttpserver-routing-api


封装主服务类

为了更好地管理 HttpServer,我们将其封装在 MainSVC 类中,作为整体服务的入口。

MainSVC 头文件

// FileName: mainsvc.h
class MainSVC : public QObject
{
	Q_OBJECT

public:
	MainSVC(QObject *parent=nullptr);
	~MainSVC();
	void start();
};

实现文件

注意:为了保证 QtHttpServer 实例在服务运行期间一直有效,这里将其定义为静态变量或类成员。

// FileName: mainsvc.cpp
#include <QHttpServer>
#include "mainsvc.h"

static QtHttpServer http_server;

MainSVC::MainSVC()
{
  http_server.route("/", []() {
    return "BigBookPlus Server Test.";
  });
}

MainSVC::~MainSVC()
{
}

void MainSVC::start()
{
  int port = 9527;
  http_server.listen(QHostAddress::Any, port);
}

在动态链接库中使用 Qt 组件并提供 C 语言 API

关于 QApplication / QCoreApplication 的坑

当你在一个非 Qt 的 C++ 工程中调用基于 Qt 开发的动态库时,经常会遇到类似“必须先创建 QApplication/QCoreApplication 才能运行”的报错。

其原因在于:

  1. QCoreApplication:管理非 GUI 程序的事件循环(Event Loop)。
  2. QApplication:继承自 QCoreApplication,管理 GUI 相关的资源(如 QWidget)。

Qt 的信号槽机制、网络模块等都极度依赖 exec() 开启的事件循环。在动态库场景下,我们需要在主线程之外手动维护这个循环。

解决方案:全局对象与子线程事件循环

在动态库内部,我们可以定义一个全局的 QCoreApplication 对象,并为其提供伪造的 argc/argv

int argc = 1;
char param[] = "test";
char *argv[1] = { param };
static QCoreApplication a(argc, argv);

服务初始化与同步

我们定义一个 init 函数,在其中创建服务对象并调用 a.exec()。由于 exec() 是阻塞的,我们必须使用 QtConcurrent::run 在子线程中启动它。

为了确保调用者在 svc_init 返回时,服务已经完全初始化,我们引入 QMutex 进行同步控制。

完整实现:

// FileName: svc_c_api.cpp
#include <QtCore>
#include <QtConcurrent>
#include "svc_c_api.h"
#include "mainsvc.h"

int argc = 1;
char param[] = "test";
char *argv[1] = { param };

static QCoreApplication a(argc, argv);
static QMutex init_lock;
MainSVC* main_svc = nullptr;

static int init(void)
{
    init_lock.lock();
    main_svc = new MainSVC(Q_NULLPTR);
    init_lock.unlock();
    return a.exec(); // 开启事件循环
}

int svc_init(void)
{
    QFuture<void> future = QtConcurrent::run(init);
  
    QThread::msleep(100); // 确保子线程已经运行并锁定 mutex
    init_lock.lock();     // 等待 main_svc 初始化完成
    init_lock.unlock();   
  
    return 0;
}

bool svc_start()
{
    if (main_svc != nullptr) {
        main_svc->start();
        return true;
    }
    return false;
}

C 语言接口文件

为了让非 C++ 或非 Qt 环境(如纯 C 或 Python)能够调用,我们需要使用 extern "C" 导出接口。

// FileName: svc_c_api.h
#ifndef SVC_C_API_H_
#define SVC_C_API_H_

#if defined(_MSC_VER)
# if defined(SVC_LIB)
#  define SVC_QTLIB_EXPORT __declspec(dllexport)
# else
#  define SVC_QTLIB_EXPORT __declspec(dllimport)
# endif
#else
# define SVC_QTLIB_EXPORT
#endif

#include <stdbool.h>

#ifdef __cplusplus
extern "C" {
#endif
    SVC_QTLIB_EXPORT int svc_init();
    SVC_QTLIB_EXPORT bool svc_start();
#ifdef __cplusplus
}
#endif

#endif

构建配置 (CMake)

推荐使用 CMake 来构建项目。通过设置 CMAKE_AUTOMOC 等选项,CMake 可以自动处理 Qt 的元对象编译。

cmake_minimum_required(VERSION 3.8.0)
project(test-svc VERSION 0.1.0 LANGUAGES CXX)

# 设置 Qt 路径(根据实际环境修改)
set(Qt5_DIR D:/Qt/Qt5.12.6/5.12.6/msvc2017_64/lib/cmake/Qt5)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 自动处理 MOC, RCC, UIC
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)

add_definitions(-D SVC_LIB)

find_package(Qt5 COMPONENTS Core Network Concurrent HttpServer REQUIRED)

set(USED_QT_LIBRARIES Qt5::Core Qt5::Network Qt5::Concurrent Qt5::HttpServer)

aux_source_directory(src SVC_SRCS)

add_library(snn-svc SHARED ${SVC_SRCS})
target_link_libraries(snn-svc ${USED_QT_LIBRARIES})

总结

通过在动态库中静态初始化 QCoreApplication 并在后台线程运行事件循环,我们可以完美地在非 Qt 项目中利用 Qt 的强大功能(如 QtHttpServer)。这种方式既保留了 Qt 信号槽的便利性,又通过 C 接口实现了良好的解耦。

欢迎留言探讨。