0%

Qt 元对象系统

学习 Qt 源码,避不开断点调试和 pdb 文件。有三种思路:

  • 使用 Qt 4.8.7,安装包中源码和 pdb 文件都有,版本太旧;

  • 使用 Qt5.12.x,有离线安装包,但

    • 缺失 pdb 调试文件,单独下载 pdb 文件后与源码版本不一致,断点位置偏移;
    • 缺少 moc_qobject.cpp 等构建过程中生成的中间文件,影响理解;
  • 使用 Qt5.15.x 等任意版本,从源码构建,难度大,收益也大。

摸索之后,选择了从源码构建 qtbase 基础模块,用于学习核心特性足够。稍后出一键构建脚本。

先避开图形界面程序 Qt Application,通过 Qt Console Application 学习,只使用 Core 模块。

学习目标:

  • 元对象系统,搞定 moc_qobject.cpp 就差不多学会了
  • 事件循环,核心实现都在 QThreadData 类中
  • 信号槽,= 元对象系统中的函数调用能力
  • Qt 跨模块(dll/exe) 内存分配

思考:元对象系统在完成信号槽之外,还实现了哪些特性?

分离接口和实现

QObjectd_ptr 指针,指向 QObjectPrivateQWidgetd_ptr 指针,指向 QWidgetPrivate

Qt 内部实现上使用了宏,本质上是 pImpl 手法,分离接口和具体实现。

指针类型是抽象类 QObjectData ,它是所有 QXxxPrivate 类型的基类。

QScopedPointer<QObjectData> d_ptr

元对象系统就定义在 QObjectData 中:元对象针对类型,而非某个具体的对象实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 摘自 qobject.h  版本 Qt 5.12.12
class Q_CORE_EXPORT QObjectData {
public:
virtual ~QObjectData() = 0;
QObject *q_ptr;
QObject *parent;
QObjectList children;

uint isWidget : 1;
uint blockSig : 1;
uint wasDeleted : 1;
uint isDeletingChildren : 1;
uint sendChildEvents : 1;
uint receiveChildEvents : 1;
uint isWindow : 1; //for QWindow
uint deleteLaterCalled : 1;
uint unused : 24;
int postedEvents;
QDynamicMetaObjectData *metaObject;
QMetaObject *dynamicMetaObject() const;
};

元对象类型

要理解 QObject ,先看 QMetaObject

The QMetaObject class contains meta-information about Qt objects.

The QMetaMethod class provides meta-data about a member function.

struct QMetaObject 有很多方法,实际只有一个(语法上)公有的成员变量 d 但在语义上却是 private data 。为什么如此处理?

1
2
3
4
5
6
7
8
9
10
// 摘自 `qobjectdefs.h` 头文件
struct { // private data
SuperData superdata;
const QByteArrayData *stringdata;
const uint *data;
typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
StaticMetacallFunction static_metacall;
const SuperData *relatedMetaObjects;
void *extradata; //reserved for future use
} d;

文件 moc_qobject.cpp 去哪里查看呢? 构建过程生成的中间文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 摘自 moc_qobject.cpp Qt5.15.18 ,类型的静态成员变量
QT_INIT_METAOBJECT const QMetaObject QObject::staticMetaObject = { {
nullptr,
qt_meta_stringdata_QObject.data,
qt_meta_data_QObject,
qt_static_metacall, // 函数调用的中转站
nullptr,
nullptr
} };

const QMetaObject *QObject::metaObject() const
{
// QObject::d_ptr->metaObject 一般是 nullptr
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}

// SIGNAL 2
void QObject::objectNameChanged(const QString & _t1, QPrivateSignal _t2)
{
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))),
const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t2))) };
// 魔术数字 2 是 qt_meta_data_QObject[] 中函数索引值
QMetaObject::activate(this, &staticMetaObject, 2, _a);
}

之后无符号整型指针 d.data (即 qt_meta_data_QObject[]) 又是按照 QMetaObjectPrivate 解析的,后者的定义在哪?

找到这个约定我们就能理解 moc_qobject.cppconst uint qt_meta_data_CObject[] 一堆的数字是什么意思了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 5.12.12\Src\qtbase\src\corelib\kernel\qmetaobject_p.h
// 无符号整型数值强转有符号整型数值,存在风险。qt 如何避免的?
struct QMetaObjectPrivate
{
// revision 7 is Qt 5.0 everything lower is not supported
// revision 8 is Qt 5.12: It adds the enum name to QMetaEnum
enum { OutputRevision = 8 }; // Used by moc, qmetaobjectbuilder and qdbus

int revision;
int className;
int classInfoCount, classInfoData;
int methodCount, methodData;
int propertyCount, propertyData;
int enumeratorCount, enumeratorData;
int constructorCount, constructorData;
int flags;
int signalCount;
// ...
}

关于 methodCount() 在帮助手册中的描述前后有差异(推断后一段存在笔误):

method() and methodCount() provide information about a class’s meta-methods (signals, slots and other invokable member functions).

Returns the number of methods in this class, including the number of methods provided by each base class. These include signals and slots as well as normal member functions.

帮助手册针对 Q_INVOKABLE 的解释,很明显 invokableMethod()normalMethod() 是不同的!前者表示:

Apply this macro to declarations of member functions to allow them to be invoked via the meta-object system.

在尝试追踪 connect 如何工作之前,我们先看一下怎么使用 QMetaObject 执行 QObject 中的函数?

通过元对象执行函数

元对象系统并不能调用常规的成员函数。

QMetaObject::invokeMethod() 静态函数,以及 enum Qt::ConnectionType 枚举定义。

it determines whether a particular signal is delivered to a slot immediately or queued for delivery at a later time.

默认参数 Qt::AutoConnection 根据信号槽的接收者是否和 发送者 emit 在相同线程:

  • 如果在同一线程,就直接调用(等同于回调函数)
  • 如果不在同一线程,等进入接收者所在线程的事件循环时再执行槽函数(等同于发送异步信号)。

为了更清晰地理解后者,我把有关的描述列在下面:

Qt::QueuedConnection The slot is invoked when control returns to the event loop of the receiver’s thread. The slot is executed in the receiver’s thread.

摘自 QMetaObject::invokeMethod()

If type is Qt::QueuedConnection, a QEvent will be sent and the member is invoked as soon as the application enters the main event loop.

所以,如何理解每个线程都有独立的事件循环?或者放弃多线程只用单一线程。

源文件内部类

我们可以在 .cpp 文件中定义只用于当前文件的 C++ 类,但 QObject 子类不支持这种做法。

因为链接时无法看到 moc 扩展的定义,报错 LNK2001: unresolved external symbol

推测可能需要某种技巧才能满足。

Define a QObject derived class inside an anonymous namespace?