本文共 5481 字,大约阅读时间需要 18 分钟。
二进制兼容ABI(application binary interface)主要指动态库文件单独升级,现有用到老动态库的应用程序是否受到影响。
1.升级库文件,不影响使用库文件的程序。
2.新库必然有新头文件,但是旧的二进制可执行文件还是按照旧的头文件中的“使用说明”来调用库。
意思就是你应用程序A调用库B1.0,现在库B升级了,变成B1.1,应用程序A调用库B1.1还是能够正常使用,这种就叫二进制兼容,反之就是不兼容。
C/C++ 通过头文件暴露出动态库的使用方法,这个“使用方法”主要是给编译器看的,编译器会据此生成二进制代码,然后在运行的时候通过装载器(loader)把可执行文件和动态库绑到一起。如何判断一个改动是不是二进制兼容,主要就是看头文件暴露的这份“使用说明”能否与新版本的动态库的实际使用方法兼容。因为新的库必然有新的头文件,但是现有的二进制可执行文件还是按旧的头文件来调用动态库。
1 类的普通成员函数 void f( int ) 改成了 void f( double ) 。老EXE会传int进来,新库会用double的长度取数据。从而发生undefined symbol
2 基类增加虚函数会导致基类虚表发生变化。老EXE调用虚表的时候给出的slot是老的,但是新库里面的这个slot已经是另一个函数了。
3 给函数增加默认参数
4 增加默认模板类型
5 改变enum的值
6 给class Bar增加数据成员导致sizeof(Bar)的值变大。这种增加成员变量的情况通常是不安全的,但也有例外:
● 如果客户代码里有 new Bar,那么肯定不安全,因为 new 的字节数不够装下新 Bar。相反,如果 library 通过 factory 返回 Bar* (并通过 factory 来销毁对象)或者直接返回 shared_ptr<Bar>,客户端不需要用到 sizeof(Bar),那么可能是安全的。同样的道 理,直接定义 Bar bar; 对象(无论是函数局部对象还是作为其他 class 的成员)也有二进制兼容问题。
● 如果客户代码里有 Bar* pBar; pBar->memberA = xx;,那么肯定不安全,因为 memberA 的新 Bar 的偏移可能会变。相反,如果 只通过成员函数来访问对象的数据成员,客户端不需要用到 data member 的 offsets,那么可能是安全的。
● 如果客户调用 pBar->setMemberA(xx); 而 Bar::setMemberA() 是个 inline function,那么肯定不安全,因为偏移量已经被 inline 到客户的二进制代码里了。如果 setMemberA() 是 outline function,其实现位于 shared library 中,会随着 Bar 的更新而更新, 那么可能是安全的。
7 如果EXE里调用new Bar,导致new出来的内存盛不下新的Bar对象(构造函数会使用新DLL中的构造函数来填充数据),从而:
● 如果新的库实现访问了新的数据成员肯定会访问到一个无法预知的地方;
● 如果EXE得到的是shared_ptr<Bar> 由DLL来管理内存,那么此时是安全的。
● 如果EXE调用的是p->member 那么肯定不对,因为偏移量可能因为member前面插入了新的成员而被新DLL中构造函数填充了新的成员,从而访问的并不是老的member。
● 如果EXE是使用p->get_member()来获取数据,那么是正常的。
● 如果p->get_member()是inline的,那么是不安全的,因为偏移量已经在EXE中了。
8 虚函数做接口的基本上都是二进制不兼容的。具体地说,以只包含虚函数的 class (称为 interface class)作为程序库的接口,这样的接口是僵硬的,一旦发布,无法修改。
这里没有列出的一些情况也可能二进制不兼容。
1 增加新的class(定义在新DLL中,老的EXE里没有)
2 增加非virtual函数(定义在新DLL中,老的EXE里没有)
3 增加static成员函数(定义在新DLL中,老的EXE里没有)
还有很多,就不一一列举了
1 采用静态链接
这个是王道。在分布式系统这,采用静态链接也带来部署上的好处,只要把可执行文件放到机器上就行运行,不用考虑它依赖的 libraries。目前 muduo 就是采用静态链接。
2 通过动态库的版本管理来控制兼容性
这需要非常小心检查每次改动的二进制兼容性并做好发布计划,比如 1.0.x 系列做到二进制兼容,1.1.x 系列做到二进制兼容,而 1.0.x 和 1.1.x 二进制不兼容。《程序员的自我修养》里边讲过 .so 文件的命名与二进制兼容性相关的话题,值得一读。
3 用pimpl技法
在头文件中只暴露 non-virtual 接口,并且 class 的大小固定为 sizeof(Impl*),这样可以随意更新库文件而不影响可执行文件。当然,这么做有多了一道间接性,可能有一定的性能损失。见 Exceptional C++ 有关条款和 C++ Coding Standards 101。
Qt作为一个跨平台的开发框架,应用很广,本身肯定也是实现二进制兼容的。那Qt是怎么实现的呢?其实Qt使用的就是pimpl技法,就是Qt的d指针和q指针的使用。
首先定义一个宏定义的类:
dqglobal.h
#ifndef DQGLOBAL_H#define DQGLOBAL_H#include#include #define DQ_DECLARE_PRIVATE(Class) \ Q_DECLARE_PRIVATE(Class) \ QScopedPointer d_ptr;#define DQ_DECLARE_PUBLIC(Class) \ Q_DECLARE_PUBLIC(Class) \ Class* q_ptr;#define DQ_SAFE_DELETE(p) do { if(p) { delete (p); (p) = 0; } } while(0)#endif // DQGLOBAL_H
定义一个接口类Widget:
widget.h
#ifndef WIDGET_H#define WIDGET_H#include#include "dqglobal.h"class WidgetPrivate;class Widget : public QObject{ Q_OBJECT DQ_DECLARE_PRIVATE(Widget)public: explicit Widget(QObject *parent = 0); void print(); void showMsg(const QString &text); void sendText(const QString &text);signals: void sigSendText(const QString &text);};#endif // WIDGET_H
widget.cpp
#include "widget.h"#include "widgetprivate.h"#includeWidget::Widget(QObject *parent): QObject(parent), d_ptr(new WidgetPrivate(this)){}void Widget::print(){ Q_D(Widget); d->testFun();}void Widget::showMsg(const QString &text){ qDebug() << __FUNCTION__ << "song" << text;}void Widget::sendText(const QString &text){ emit sigSendText(text);}
定义实现类WidgetPrivate,代码的实现放在这个类中:
widgetprivate.h
#ifndef WIDGETPRIVATE_H#define WIDGETPRIVATE_H#include "dqglobal.h"class Widget;class WidgetPrivate{ DQ_DECLARE_PUBLIC(Widget)public: WidgetPrivate(Widget *q); void testFun(); void sendText();};#endif // WIDGETPRIVATE_H
widgetprivate.cpp
#include "widgetprivate.h"#include "widget.h"WidgetPrivate::WidgetPrivate(Widget *q): q_ptr(q){}void WidgetPrivate::testFun(){ Q_Q(Widget);// q->showMsg("555555555"); q->sendText("666666666");}void WidgetPrivate::sendText(){ Q_Q(Widget); q->sendText("666666666");}
为实现d指针和q指针,qt已经在qtglobal里面使用宏的方式定义相关的辅助函数和友元类。
其中d_ptr指针指向私有实现类,使用如下宏定义:
#define Q_DECLARE_PRIVATE(Class) \ inline Class##Private* d_func() { return reinterpret_cast(qGetPtrHelper(d_ptr)); } \ inline const Class##Private* d_func() const { return reinterpret_cast (qGetPtrHelper(d_ptr)); } \ friend class Class##Private;
里面定义了两个函数,一个是返回d_ptr指针对象,一个是d_ptr常量指针对象,并生命为友元函数。##为拼接符,这里传Widget,就变成WidgetPrivate。
我们定义d_ptr的使用使用智能指针QScopedPointer,这样就不用关注D指针的释放。这就是为什么要另外封装一层的原因。
#define DQ_DECLARE_PRIVATE(Class) \ Q_DECLARE_PRIVATE(Class) \ QScopedPointerd_ptr;
其中q_ptr指针指向父类,使用如下定义:
#define Q_DECLARE_PUBLIC(Class) \ inline Class* q_func() { return static_cast(q_ptr); } \ inline const Class* q_func() const { return static_cast (q_ptr); } \ friend class Class;
使用d指针和q指针的宏定义:
#define Q_D(Class) Class##Private * const d = d_func()#define Q_Q(Class) Class * const q = q_func()
使用Q_D返回d指针对象,变量名为d,这样就可以直接使用d对象进行操作。Q_Q同理。
我们可以看到类Widget中只有non-virtual函数及信号等,变量只有d_ptr的指针,这样sizeo()Widget的对象大小都不会改变,因为实现都在WidgetPrivate中,里面无论如何变,Widget对应的头文件都不影响,前面说了增加non-virtual函数不会导致二进制兼容,所以这样就能够实现二进制兼容了。
d指针和q指针的使用,除了能保证代码的二进制兼容外,还能够隐藏实现细节,就是我们在使用qt craeter变成跳到qt自带的类里面的时候,只能看到接口,是看不到实现细节的,qt是开源的,当然可以另外下载源码来看,但实际使用的时候是看不到。当然还能提高编译的速度,比如我们修改了某个函数的实现,就智能编译widgetprivate及相关的类,假如不使用这种模式,不是另外提供接口,那外部调用这个函数相关的文件也要跟着编译。
所以编写模块化的程序的时候推荐使用这种模式,结构会更清晰一些。
转载地址:http://niflz.baihongyu.com/