Skip to main content

在动态链接库中以非GUI形式调用Qt组件并提供C语言形式API

BigBookAbout 6 minC/C++QtQtHttpServerCMakeSystem Design

编译 QtHttpServer 模块

首先拉去代码

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

然后用qtcreator打开工程,编译

最简单的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的主lib中,据说Qt6会正式加入。所以需要自行下载编译。open in new window

编译QtHttpServer

我使用的是Qt5,牵出5.15分支,并自己使用QtCreator编译:

git clone https://github.com/qt-labs/qthttpserver.git
git checkout 5.15

编译成功后,将头文件和动态库拷贝到所使用的Qt安装路径下的对应目录内即可。

cd build-qthttpserver-Desktop_Qt_5_12_6_GCC_64bit-Release/
mv ./* /opt/Qt5.12.6/5.12.6/gcc_64/lib/

cd cmake/
mv ./* /opt/Qt5.12.6/5.12.6/gcc_64/lib/cmake/

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官方blog introducing-qt-http-serveropen in new window,介绍了一个最基本的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();
}

程序启动后,浏览器打开 open in new window 就可以看到HelloWorld了,所以这里Server提供了Get方法。那么如果想提供POST方法并接受一段数据呢?

加入如下代码即可:

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

body中可以传入json等并进行解析。其他用法,推荐一篇详细介绍QtHttpServer接口routing方法的blog qhttpserver-routing-apiopen in new window

封装主服务类

在MainSVC中,对HttpServer进行管理。实际使用时MainSVC还可以封装了其他功能,作为整体服务的入口。

MainSVC头文件

// FileName: mainsvc.h
// Author: BigBookPlus
// Reference: http://bigbookplus.github.io

class MainSVC : public QObject
{
	Q_OBJECT

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

实现文件,加入QtHttpServer功能

MainSVC类的实现文件。这里要保证QtHttpServer是全局变量,否则Server无法正常运行。

// FileName: mainsvc.cpp
// Author: BigBookPlus
// Reference: http://bigbookplus.github.io

#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有关的报错信息,大概意思就是某些模块必须要QApplication/QCoreApplication才能运行。

具体来说,Non-GUI的QCore组件只需要依赖QCoreApplication对象;而涉及到GUI的例如QWidget组件则依赖QApplication对象。QApplication类其实继承自QCoreApplication。Qt主程序执行了它们的exec()方法,该Qt程序的EventLoop和EventDispatcher等功能才能正确运行,这样进而保证了信号和槽等机制的正常运转。实际使用时,应当令QApplication/QCoreApplication的对象变量位于主线程空间,而事件循环可以在其他线程执行,即exec()方法可以在子线程运行。因为exec()是个阻塞的方法,所以放在其他线程执行不会影响主线程,方便很多。

显而易见,不是所有的组件都需要必须有个QApplication/QCoreApplication,进一步说,即使需要QApplication/QCoreApplication对象,其实也可以只定义一个全局变量,而不需要真的执行它的exec方法。

在我的业务场景中,要给UI端以动态链接库的形式提供功能,除了一些一般的实时处理功能,还要在动态库的生命周期开启一个HttpServer,而不巧的是,QtHttpServer需要QCoreApplication对象。谨慎起见,同时我也决定在子线程中执行exec运行事件循环。最后,用户需要提供的是C接口,所以我将主服务接口以MainSVC对象进行封装完毕后,又定义了一套C形式的API。

QCoreApplication全局对象

首先在动态链接库的入口实现文件里,定义QCoreApplication全局对象,为了让对象正确初始化,我在这里定义了假的argc/argv参数变量:

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

服务初始化

然后定义一个init函数,函数内new了一个我封装的主服务类的对象,并开启了事件循环。

static int init(void)
{
  main_svc = new MainSVC(Q_NULLPTR);
  return a.exec();
}

这个函数将在动态链接库的导出的初始化函数svc_init中,开启一个子线程来执行。使用时,用户调用svc_init,即可完成服务的初始化。下面是svc_init的定义:

int svc_init(void)
{
  QFuture<void> future = QtConcurrent::run(init);
  return 0;
}

实现文件

但是有个新问题,用户直接调用svc_init进行测试,由于后面没有其他操作很快就退出了,因为初始化都是在线程中异步执行的。不能让用户在主线程中等待,简单的方法是在全局变量中增加一个QMutex对象进行控制,再再init和svc_init中访问此变量,即可简单达到同步的功能。初始化该动态库,会提供一个HttpServer供所有人调用。该动态库提供的服务在初始化阶段保证初始化完成后退出,提供给用户调用svc_start方法,启动服务,用户如需要服务一直运行,则后续自行控制主线程生命周期即可。完整实现如下:

// FileName: svc_c_api.cpp
// Author: BigBookPlus
// Reference: http://bigbookplus.github.io

#include <QtCore>
#include <QtCore/QVariant>
#include <QtConcurrent>

#include "svc_c_api.h"
#include "mainsvc.h"

int argc = 1;
char param[] = "test";    // magic string. nonsense.
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); // ensure init_lock mutex has been locked.
  init_lock.lock();     // ensure main_svc initilizition finished.
  init_lock.unlock();   // unlock
  
  return 0;
}

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

	return false;
}

接口文件

C语言形式的API的头文件实现如下:

// FileName: svc_c_api.h
// Author: BigBookPlus
// Reference: http://bigbookplus.github.io

#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(); //before init
	SVC_QTLIB_EXPORT bool svc_start();
#ifdef __cplusplus
}
#endif

#endif

后记:怎样构建(编译)

构建Qt项目的方法有很多,可以是Visual Studio + Qt插件,也可以用QtCreator。不过我更喜欢CMake的方式进行构建。基本的方法可以参考Qt官方关于CMake的指南cmake-get-startedopen in new window。Qt项目编译中的关键步骤:MOC、UIC、RCC都可以通过添加一行指令,让CMake自动完成。用CMake管理Qt项目,基本和普通的C++项目感觉不到太大区别。

这里提供一个我写的示例:

cmake_minimum_required(VERSION 3.8.0)

project(test-svc VERSION 0.1.0 LANGUAGES CXX)

set(Qt5_DIR D:/Qt/Qt5.12.6/5.12.6/msvc2017_64/lib/cmake/Qt5) # replace with path to your own version of Qt.

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

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_LIBRARYS 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_LIBRARYS})

END

欢迎留言探讨。

Last update:
Contributors: Xuling Chang