DTK背后的技术: D-Pointer与二进制兼容

D-Pointer是在Qt源码中广泛使用的一项技术。D-Pointer是不透明指针(Opaque Pointer)在Qt中的一种实现,不透明指针主要用于解决二进制库变更时产生的兼容性问题。

在Windows平台上,也提供了一种类似的解决方案,即COM技术中IUnknown/IDispath对象等,当然COM需要解决的问题比不透明指针复杂太多,这里不再深入叙述。

内存布局

Opaque Pointer的主要思路在于隐藏接口中的私有成员,这样做的好处在于可以使得接口看上去简洁明了。同时通过隐藏实现,可以实现在内部接口变动时,外部接口不会发生变化,另外对一个全部由D-Pointer组成的类来说,其大小是固定的,即只包含一个指针大小。

// dtkobject.h
#pragma once

#include <QObject>
#include <QScopedPointer>

class DtkObjectPrivate;
class DtkObject : public QObject
{
    Q_OBJECT
public:
    explicit DtkObject(QObject *parent = Q_NULLPTR);
    ~DtkObject();

private:
    QScopedPointer<DtkObjectPrivate> dd_ptr;
    Q_DECLARE_PRIVATE_D(qGetPtrHelper(dd_ptr), DtkObject)
};

class DtkObjectMorePrivate;
class DtkObjectMore : public QObject
{
    Q_OBJECT
public:
    explicit DtkObjectMore(QObject *parent = Q_NULLPTR);
    ~DtkObjectMore();

    void more();

private:
    QScopedPointer<DtkObjectMorePrivate> dd_ptr;
    Q_DECLARE_PRIVATE_D(qGetPtrHelper(dd_ptr), DtkObjectMore)
};

// dtkmore.cpp
#include "dtkobject.h"
#include <QDebug>

class DtkObjectPrivate
{[<a href="https://docs.deepin.io/wp-content/uploads/2018/06/dtk-template.tar.gz">dtk-template.tar</a>](http://)
public:
    DtkObjectPrivate(DtkObject *parent) : q_ptr(parent) {}

    DtkObject *q_ptr;
    Q_DECLARE_PUBLIC(DtkObject)
};

DtkObject::DtkObject(QObject *parent) :
    QObject(parent), dd_ptr(new DtkObjectPrivate(this))
{
    qDebug() << "size of DtkObjectPrivate pointer" << sizeof(dd_ptr);
}

DtkObject::~DtkObject()
{

}

class DtkObjectMorePrivate
{
public:
    DtkObjectMorePrivate(DtkObjectMore *parent) : q_ptr(parent) {}

    QString more = “More data”;

    DtkObjectMore *q_ptr;
    Q_DECLARE_PUBLIC(DtkObjectMore)
};

DtkObjectMore::DtkObjectMore(QObject *parent) :
    QObject(parent), dd_ptr(new DtkObjectMorePrivate(this))
{
    qDebug() << "size of DtkObjectMorePrivate pointer" << sizeof(dd_ptr);
}

DtkObjectMore::~DtkObjectMore()
{

}

void DtkObjectMore::more()
{
    Q_D(DtkObjectMore);
    qDebug() << d->more;
}
#include <QCoreApplication>
#include <QDebug>
#include "dtkobject.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QObject qObj;
    qDebug() << "size of QObject" << sizeof(qObj);

    DtkObject dtkObj;
    qDebug() << "size of DtkObject" << sizeof(dtkObj);

    DtkObjectMore dtkMoreObj;
    qDebug() << "size of DtkObjectMore" << sizeof(dtkMoreObj);

    return 0;
}

输出结果如下:

size of QObject 16
size of DtkObjectPrivate pointer 8
size of DtkObject 24
size of DtkObjectMorePrivate pointer 8
size of DtkObjectMore 24

对于一个基础的QObject对象,其大小永远时16Byte,而对由QObject继承的对象,则其大小为24Byte,即QObject大小加上一个QScopedPointer大小组成。

通过使用这种方法,可以使得对外发布的对象接口永远保持一致的大小,不会随着内部实现的变化导致对象的内存占用大小发生变化。

Qt中,通过Q_DECLARE_PRIVATE_D/Q_D/Q_Q等几个辅助宏来协助使用D-Pointer:

#define Q_DECLARE_PRIVATE_D(Dptr, Class) \
    inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(Dptr); } \
    inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(Dptr); } \
    friend class Class##Private;

#define Q_DECLARE_PUBLIC(Class)                                    \
    inline Class* q_func() { return static_cast<Class *>(q_ptr); } \
    inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
    friend class Class;

#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()

Q_D主要在导出的公共类中使用, 通过Q_D可以定义一个指向私有类的指针,可以直接获取私有类的数据实际上是指向dd_ptr这个变量。而Q_Q则可以在私有类的方法中获取一个指向公共类的指针q,通过q来访问公共类的方法。

接口导出

除了获得在内存布局中的好处外,D-Pointer还能起到隐藏私有接口中的左右。所有的私有方法全部会定制在私有类中,如果需要修改一个方法的实现,只需要修改私有类的实现即可。

由于库的导出的头文件中并不包含私有类的申明,这就避免类库的使用者随意的使用私有对象的方法。但是,由于私有对象在二进制中还是实际存在,我们可以使用objdump/nm/c++fit来查看库中实际的导出符号表:

nm -gCD libdtkcore.so.2.0.8 |c++filt
objdum -TC libdtkcore.so.2.0.8 |c++filt

libdtkcore.so.2.0.8的导出符号表部分如下:

0000000000019600 g    DF .text  0000000000000012  Base        Dtk::Core::DObjectPrivate::~DObjectPrivate()
0000000000033340 g    DF .text  000000000000054f  Base        Dtk::Core::DFileSystemWatcherPrivate::removePaths(QStringList const&, QStringList*, QStringList*)
000000000002bbd0 g    DF .text  0000000000000c44  Base        Dtk::Core::DFileWatcherPrivate::start()
00000000002ac080 g    DO .bss   0000000000000008  Base        Dtk::Core::DBaseFileWatcherPrivate::watcherList

可以看到,实际上导出的符号中还是包含了DFileWatcherPrivate等私有的对象。这时候,通过完全构造出一个和DFileWatcherPrivate相同的申明,就可以正常的使用DifleWatcherPrivate这个对象了。

对于Qt,debian中甚至为此提供类一个单独的的包qtbase5-private-dev,来导出这些私有的头文件,方便开发者对Qt进行进一步的定制开发。

所以说,通过D-Pointer不能从技术上完全杜绝上层应用来使用私有方法,但是在正常使用的情况下,能够在导出头文件上区分出共有方法,因而也是项目开发中的一种推荐的实践。

PS. 对于使用QtCreator的同学,可以将附件中的文件放到 /esr/share/qtcreator/templates/wizards/classes/cpp/中,可以支持QtCreator的C++向导中生成D-Pointer风格的类。

dtk-template.tar.gz

Refs:

http://wiki.qt.io/D-Pointer

https://en.wikipedia.org/wiki/Opaque_pointer

发表评论

电子邮件地址不会被公开。 必填项已用*标注