二进制兼容
SDK的开发中,最需要注意的就是接口的设计,因为SDK一经投入使用,其中的任何一个函数都可能被某个应用程序调用,如果接口设计不合理,导致频繁地更改,则会影响上游的应用程序,要修改上游的应用程序的代码来适应新的接口。
其次就是要保证二进制兼容。本篇文章主要学习QT中对实现二进制兼容做的一些设计。
什么是二进制兼容呢?
假设有一个库Library,当前版本为v1.0,有一个应用程序App,App项目以动态链接的方式调用了v1.0版本的Library(App项目中应该包含Library中某些用到的头文件和动态链接库)。此时库Library进行了版本升级,从v1.0升级到V1.1,且已经部署到App所在的系统环境中。此时App项目中仍用到的是v1.0的Library的头文件,而系统中的Library的动态库已经升级成V1.1。在这种情况下,如果App无需重新编译,而能继续顺利运行,则称库Library是二进制兼容的。
什么样的操作会破坏二进制兼容呢?
引用Qt wiki上的例子:
//v1.0
class Widget
{
// ...
private:
Rect m_geometry;
};
class Label : public Widget
{
public:
// ...
String text() const
{
return m_text;
}
private:
String m_text;
};
假设V1.0库中的一个头文件中定义了两个类Widget和Label,其中Label继承自Widget,Widget中包含一个成员变量m_geometry,Label中包含一个成员变量m_text以及一个成员函数text()。之后,问题来了!假设之后的维护中,需要对Widget增加一个样式的调整,此时增加了成员变量m_stylesheet。
//v1.1
class Widget
{
// ...
private:
Rect m_geometry;
String m_stylesheet; // 新增成员变量
};
class Label : public Widget
{
public:
// ...
String text() const
{
return m_text;
}
private:
String m_text;
};
此时,如果系统中将lib从v1.0升级成v1.1,并且不重新编译应用程序App的话,直接运行App会发现:程序崩溃!
那为什么程序会崩溃呢?这就要引入C++对象模型的一些基础知识。
前提知识:
1.C++空对象占1个字节,而不是0个字节;
2. 类中的成员函数是不占用类对象的内存空间的;
3.非静态成员变量是占用对象的内容空间,同时会进行字节对齐;
4.静态成员变量是跟着类走的,所有类对象公用它,和成员函数一样,所以是不占用对象内存空间的;
5.如果类中有一个或多个虚函数时,则对象内存空间会增加4个字节(32位系统下)用来存放虚函数表指针(virtual table pointer);
好了,有了前提知识后,来看一下我们的例子中的内存模型示意图。假设在v1.0时,一个Label的实例化对象可能是下表左边的样子,m_geometry变量在offset 0的位置,m_text在offset 1的位置。而在v1.1中,由于添加了变量m_stylesheet,导致m_stylesheet出现在了offset 1的位置,而m_text移到了offset2的位置。这样的话,如果不重新编译App项目,则程序运行时,仍然去offset 1的位置去寻找m_text,然而offset 1的位置存放的已经不再是m_text,而是m_stylesheet,所以程序会崩溃。
WidgetLib 1.0 | WidgetLib 1.1 |
m_geometry <offset 0> | m_geometry <offset 0> |
- - - | m_stylesheet <offset 1> |
m_text <offset 1> | - - - |
- - - | m_text <offset 2> |
搬运整理了一下大佬整理的常见的会破坏二进制兼容的操作:
1.添加新的虚函数
2.改变类的继承
3.改变虚函数声明时的顺序
4.添加新的非静态成员变量
5.改变非静态成员变量的声明顺序
不会破坏二进制兼容的常见操作有:
1.添加非虚的成员函数
2.添加新的类 添加Qt中的信号槽
3.在已存在的枚举类型中添加一个枚举值
4.添加新的静态成员变量
5.修改成员变量名称(偏移量未改变,但是可能会破坏源码兼容!更严重..)
6.添加Q_OBJECT,Q_PROPERTY, Q_ENUMS ,Q_FLAGS宏,添加这些宏都是修改了moc生成的文件,而不是类本身
总结来讲就是:
任何改变外部类的对象模型大小或布局的操作都会破坏二进制兼容!
SDK为什么要保证二进制兼容呢?
上面啰嗦了一大堆,大家应该也能知道了。如果SDK不是二进制兼容的,那么每次升级SDK时,都需要重新编译用到该SDK的应用程序。当SDK被其他应用程序广泛应用时,会带来非常大的影响。
私有数据保护
像QT这种成熟的大型框架,其实现二进制兼容的方法很值得重点学习一下,其中主要的手段就是进行私有数据保护。
在C++程序开发中,一个项目通常由多个类组成,每个类由一个.h头文件和一个.cpp文件组成。头文件中通常包含成员函数的声明和成员变量的定义,而在.cpp文件编写函数的实现以及成员变量的初始化和逻辑上的操作。
这样做,就标准C++及面向对象编程来说,没有什么问题。但是如果我们开发的是一个库,需要将头文件交给使用者,虽然用了private修饰,用户不能操作私有成员,但是仍然能够看到私有成员。此外,更重要的是,如果将来需要对已发布的V1版本的库进行性能优化,对接口函数没有调整,仅对内部的实现方式进行优化,此时可能需要新增成员变量或者移除旧的成员变量,这个时候就必须更改头文件中的内容,而头文件中发生了变化,就需要重新编译所有的应用项目。
而在Qt中,设计者把与内部实现紧密联系的私有的数据以及相关函数放在一个专门的数据类里面,只在公有类中向外暴露稳定的,不易变的接口,在公共类里面声明一个数据类指针指向数据类。当需要对内部实现进行优化时,仅仅需要在私有数据类中进行修改,而公共类里面的数据类指针不会有任何影响,也就保证了提供给用户使用的头文件没有任何影响,不仅减少了头文件的依赖性,而且更大程度上是保证了程序的二进制兼容。
形如:
//widget.h
class WidgetPrivate;
class Widget
{
// ...
Rect geometry() const;
// ...
private:
WidgetPrivate *d_ptr;
};
//widget_p.h
struct WidgetPrivate
{
Rect geometry;
String stylesheet;
};
//widget.cpp
#include "widget_p.h"
Widget::Widget() : d_ptr(new WidgetPrivate)
{
// Creation of private data
}
Rect Widget::geometry() const
{
// The d-ptr is only accessed in the library code
return d_ptr->geometry;
}
在外部类的头文件中只暴露接口函数geometry( ),而将和实现相关的函数以及成员变量封装在private类中,在外部类中定义一个指针(也就是d指针)指向private类。
Q_Q和Q_D
翻看Qt源码时,会看到各种各样的宏定义,其中最常看到的就有Q_Q和Q_D两个宏,这两个宏就是用来进行私有数据封装的。源码中的宏定义:
#define Q_D(Class) Class##Private * const d = d_func( )
#define Q_Q(Class) Class * const q = q_func( )
实际上就是定义了一个d指针和q指针。那么问题又来了,刚才提到了d指针,那q指针又是干什么的呢?
q指针就是在创建私有类时,传入的指向对应的公有类的指针。当在私有类的函数中需要调用公有类的方法时,就可以使用q指针进行调用。上例子:
//label.cpp
//私有类的定义
class LabelPrivate{
LabelPrivate(Label *q) : q_ptr(q) { }
Label *q_ptr; //q指针
String text;
};
//公有类的构造函数,构造函数中创建私有类对象
Label::Label() : d_ptr(new LabelPrivate(this)){
}
//公有类函数中通过d指针获取私有类中的成员变量
String Label::text(){
Q_D(Label);
return d_ptr->text;
}
//私有类函数中通过q指针调用公有类的方法
void LabelPrivate::someHelperFunction()
{
Q_Q(Label);
q->selectAll();
}
至此,原理上应该大概也许啰嗦清楚了吧,实际应用中,还需要用到两个辅助的宏定义:
#define Q_DECLARE_PRIVATE(Class) \
inline Class##Private* d_func() \
{ Q_CAST_IGNORE_ALIGN(return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr));) } \
inline const Class##Private* d_func() const \
{ Q_CAST_IGNORE_ALIGN(return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr));) } \
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;
最终我的实操代码(简化样子)大概是这样的:
//kwidget.h
class KWidgetPrivate;
class GUI_EXPORT KWidget : public QWidget
{
Q_OBJECT
public:
explicit KWidget(QWidget *parent = nullptr);
~KWidget();
private:
Q_DECLARE_PRIVATE(KWidget)
KWidgetPrivate*const d_ptr;
};
//kwidget.cpp
#include "kwidget.h"
class KWidgetPrivate:public QObject
{
Q_OBJECT
Q_DECLARE_PUBLIC(KWidget)
public:
KWidgetPrivate(KWidget*parent);
private:
KWidget *q_ptr;
};
KWidget::KWidget(QWidget *parent)
:QWidget(parent),
d_ptr(new KWidgetPrivate(this))
{
Q_D(KWidget);
// ...
}
KWidget::~KWidget()
{
}
KWidgetPrivate::KWidgetPrivate(KWidget *parent)
:q_ptr(parent)
{
Q_Q(KWidget);
// ...
}
#include "kwidget.moc" //注意这里
#include "moc_kwidget.cpp" //注意这里
最后,由于对qt的元对象编译不熟悉而遇到的两个坑:
1.在.cpp文件中定义类,qt是不识别的,需在.cpp中最后添加#include "moc_kwidget.cpp"
2.如果.cpp中的私有类中需要添加Q_OBJECT宏,还需要在.cpp中最后添加#include "kwidget.moc"
个中原因,以及Qt元对象编译、Q_OBJECT之后再仔细研究。