无GUI无线程DLL使用QSerialPort报QObject::startTimer错误咨询
我正在开发一款无GUI的动态链接库(项目配置TEMPLATE = lib),使用QSerialPort实现串口功能。没有创建额外线程也无相关需求:因为无图形界面,阻塞式串口操作完全符合使用预期。
执行如下代码时:
while (!serial_uart->isWritable()); while (!serial_uart->write(frame));
收到报错信息:
QObject::startTimer: Timers can only be used with threads started with QThread
咨询问题:如何在无GUI的库中使用QSerialPort且不触发该报错?
备注:最初误以为问题由serial_uart->waitForReadyRead(timeout)接口引发,但测试发现即便不调用该接口,仅执行serial_uart->write()也会出现相同报错。
test.cpp
#include "test.h" extern "C" { __declspec(dllexport) Test* new_Test() { return new Test(); } __declspec(dllexport) void DoTest(Test *t) { t->DoTest(); } } Test::Test() :QObject() { qDebug("Hello"); } void Test::DoTest() { this->serialport = new QSerialPort(); this->serialport ->setPortName("COM12"); this->serialport->setBaudRate(QSerialPort::Baud19200); this->serialport->open(QIODevice::ReadWrite); while (!this->serialport->isWritable()); while (!this->serialport->write("hello")); }
test.h
#include <QSerialPort> class Test : public QObject { Q_OBJECT public: Test(); void DoTest(); QSerialPort *serialport; };
test.pro
TEMPLATE = lib TARGET = test QT += serialport INCLUDEPATH += . HEADERS += test.h SOURCES += test.cpp
Python调用代码
通过ctypes调用编译生成的release/test.dll,执行代码如下:
from ctypes import * dll = CDLL(r"release\test.dll") dll.new_Test.restype = c_void_p dll.new_Test.argtypes = [] dll.DoTest.restype = None dll.DoTest.argtypes = [c_void_p] t = dll.new_Test() dll.DoTest(t)
运行输出:
Hello
QObject::startTimer: Timers can only be used with threads started with QThread
报错核心原因是:你在非Qt创建的线程(这里是Python解释器的主线程,不是通过QThread启动管理的线程)中使用了依赖Qt基础运行环境的QSerialPort组件。QSerialPort内部会使用定时器处理串口读写超时、状态检测等逻辑,而Qt的定时器要求运行线程必须存在可用的Qt核心环境,且线程由QThread创建管理。
哪怕你使用阻塞式接口,QSerialPort初始化时也会注册相关定时器资源,只要当前线程没有初始化Qt核心应用实例,就会触发这个报错。
方案1:在DLL中初始化QCoreApplication(推荐,适配阻塞调用场景)
因为是无GUI库,不需要依赖QApplication,只需要在DLL初始化阶段创建全局唯一的QCoreApplication实例,给Qt组件提供基础运行环境即可,不需要额外开线程,阻塞接口可以正常使用。
修改代码,在DLL入口或者第一次调用导出函数时初始化Qt核心环境:
#include <QCoreApplication> // 全局唯一的QCoreApplication实例 static QCoreApplication* g_app = nullptr; static int g_argc = 1; static char g_arg0[] = "test_dll"; static char* g_argv[] = {g_arg0, nullptr}; // 初始化Qt核心环境,保证全局只执行一次 void initQtEnv() { if (!g_app) { g_app = new QCoreApplication(g_argc, g_argv); } } // 在导出的实例创建函数中先调用初始化 extern "C" { __declspec(dllexport) Test* new_Test() { initQtEnv(); return new Test(); } // 其余导出函数逻辑保持不变 }
如果所有Qt串口操作都在同一个调用线程(即Python调用DLL的线程)执行,不需要手动启动全局事件循环,阻塞式的waitForXxx、write接口都可以正常工作,不会再触发定时器报错。如果遇到读写状态不更新的情况,可以在操作间隙周期性调用QCoreApplication::processEvents()处理待分发的Qt事件即可。
方案2:将QSerialPort逻辑移动到独立QThread中执行
如果不想在DLL里持有QCoreApplication实例,可以单独创建一个QThread,把QSerialPort实例的所有权移动到这个QThread中,保证串口所有操作都在QThread管理的、有事件循环的线程内执行即可。但这种方案需要处理跨线程调用的同步问题,对于阻塞调用的场景来说复杂度更高,不如方案1直接。
- 你写的两个while循环逻辑存在问题:
isWritable()用于返回设备是否可写,串口打开成功后该值就会为true,不需要空轮询等待;write()返回值是实际写入的字节数,写入失败时返回-1,使用!serial_uart->write(frame)判断的话,只要写入字节数不为0就会退出循环,若出现部分字节写入的情况会导致逻辑异常,正确的阻塞写入应该搭配waitForBytesWritten()接口使用,空轮询会占满CPU资源,不推荐使用。 - 一个进程内只能存在一个
QCoreApplication/QApplication实例,初始化时要做好单例判断,避免重复创建引发异常。
内容的提问来源于stack exchange,提问作者Basj

