SD-二进制兼容

0.目录

  1. 定义
  2. ABI和API
  3. 二进制兼容的相关问题
  4. C++抽象类和Java的接口
  5. 总结
  6. 参考

1.定义

所谓二进制兼容就是在做版本升级(也可能是Bug fix)库文件的时候,不必要做重新编译使用这个库的可执行文件或使用这个库的其他库文件,同时能保证程序功能不被破坏。

先明确两个概念:二进制兼容源码兼容

  • 二进制兼容:升级库文件时,不必重新编译使用此库的可执行文件或其他库文件,且程序的功能不被破坏
  • 源代码兼容:升级库文件时,不必修改使用此库的可执行文件或其他库文件的源代码只需重新编译应用程序,即可使程序的功能不被破坏

2.ABI和API

应用二进制接口(application binary interface,缩写为ABI)描述了应用程序(或者其他类型)和操作系统之间或其他应用程序的低级接口。ABI涵盖了各种细节,如:数据类型的大小、布局和对齐;调用约定等。

在了解二进制兼容和源码兼容两个定义以后,我们再看与其类似且对应的两个概念:ABIAPIABI不同于API(应用程序接口),API定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译,然而ABI允许编译好的目标代码在使用兼容ABI的系统中无需改动就能运行。

举个例子,在Qt和Java两种跨平台程序中,API像是Qt的接口,Qt有着通用接口,源代码只需要在支持Qt的环境下编译即可。ABI更像是Jvm,只要支持Jvm的系统上,都可以运行已有的Java程序。

C++的ABI

ABI更像是一个产品的使用说明书,同理C++的ABI就是如何使用C++生成可执行程序的一张说明书。编译器会根据这个说明书,生成二进制代码。C++的ABI在不同的编译器下会略有不同。

c++ ABI的部分内容举例:

  • 函数参数传递的方式,比如 x86-64 用寄存器来传函数的前 4 个整数参数
  • 虚函数的调用方式,通常是vptr/vtbl然后用vtbl[offset]来调用
  • structclass的内存布局,通过偏移量来访问数据成员

综上所述,如果可执行程序通过以上说明书访问动态链接库A,以及此库的升级版本A+,若按此说明书上的方法,可以无痛的使用A和A+,那么我们就称库A的这次升级是二进制兼容的。

3.二进制兼容的相关问题

3.1 破坏二进制兼容的几种常见方式

  • 添加新的虚函数
    修改虚函数表内的排列顺序,即使把新增加的虚函数放到最后一个,也可能会引起问题,如该类作为父类被其他类继承等;

  • 修改函数的参数列表
    由于C++支持同名函数重载,C++编译时,会对函数名字进行name mangling,如果修改了函数的参数列表,经过C++编译器编译后,函数的名称就变了,现有的可执行文件无法传这个额外的参数;

  • 不导出或者移除一个导出类

  • 改变类的继承

  • 改变虚函数声明时的顺序
    偏移量改变,导致调用失败

  • 添加/删除非静态成员变量
    改变该类的对象的大小,类的内存布局改变,偏移量也发生变化,如:

pfoo = new Foo(); // 由于sizeof(Foo)发生了变化,分配的内存可能不够
pfo->member_variable; // 可能会出错,偏移量变化
// 当使用 inline setxxx(x)时,也可能会出错,因为inline函数可能已经编译进使用该库的程序代码中。
  • 改变非静态成员变量的声明顺序
    偏移量改变

  • 增加默认模板类型参数

// 如:
template <typename T> class Grid {}; // old
template <typename t, typenameContainer=vector> class Grid{}; // new
  • 改变enum的值
enum Color { Red = 3}; // old
enum Color { Red = 4}; // new
// 这会造成错位。当然,由于enum自动排列取值,添加enum项也是不安全的,除非是在末尾添加。

3.2 不会破坏二进制兼容的几种常见方式

  • 添加非虚函数(包括构造函数)
  • 添加新的类
  • 在已存在的枚举类型中添加一个枚举值
  • 添加新的静态成员变量
  • 修改成员变量名称(偏移量未改变)
  • 增减类的友元声明

只要我们知道了程序是以什么方式访问动态库的(C++的ABI),那么我们就很好判断,哪些操作会破坏二进制兼容。更多方式请参见Policies/Binary Compatibility Issues With C++

3.3 解决二进制兼容问题的相关方法

编写库时最大的问题是,不能安全地添加数据成员,因为这会改变每个包含类对象(包括子类)的类,结构或数组的大小和布局。

1. 使用Bitflags即位域

//old
uint m1 : 1;
uint m2 : 3;
uint m3 : 1;
//new
uint m1 : 1;
uint m2 : 3;
uint m3 : 1;
uint m4 : 2; // new member without breaking binary compatibility.

不会破坏二进制兼容性。 但需要根据字节对其取整,否则可能会因改变了整个类的大小导致sizeof()之类的方法出问题,使用最后一位可能会在某些编译器上引起问题。

2. 使用静态库(当然也随之带来一系列弊端)

3. D指针设计模式(PImpl机制)

4. COM理论
COM (Component object model) 组件对象模型是微软提出的一个想法,它其实是一个规范,并且是二进制规范,也就是说只要遵循这个规范,任何语言、任何平台都可以相互调用相应组件。

COM涉及到几个概念:

  1. class ID,可以是CLSID - class的GUID 或者 IID - interface的GUID。COM通过这个ID来保证快语言,因为基本上所有语言都可以处理GUID字符串;另外COM开发者可以通过GUID来获取到准确的对象结构。
  2. coclass - component object class,简单来说就是COM组件提供给使用者的接口类,这些类其实都是都继承 IUnkown接口的抽象类,里面都是纯虚函数。这个IUnknown包含三个方法:
    • AddRef - 增加对象引用计数
    • Release - 减少引用计数,如果计数为0,则销毁
    • QueryInterface - 根据GUID来查到对象

COM组件还涉及到注册表,它可以注册到操作系统的注册表中,这样就算当前这个组件DLL物理位置与运行文件不在同一个目录,也可以加载并获取DLL的导出对象或者函数。更多了解可以看 CodeProject - Introduction to COM - What It Is and How to Use It

那为什么可以说COM能保证二进制兼容呢?

其实通过上面两个概念可以有点思绪,所谓二进制兼容对于C++ 来说就是要保证第三方使用DLL提供的接口对象时,保证内存布局不会改变,或者说不会影响。对于C++来说,对象内存布局的主要包括:

变量
虚函数 - 每个实例都会有一个虚函数列表(包括基类的)
对于COM实现来说,因为是通过GUID来获取对象,并且这些对象都是由接口来提供的实例化(抽象类不能创建实例,这些实例都是继承的子类实现),就像 caller ----> coclass (interface) --create--> instance 这样调用。
由于 instance 是在COM组件类(DLL)实例化以及释放,所以其内存布局对于 caller 来说是没有影响的。

4.C++抽象类和Java的接口

之前我一直认为C++的抽象类就类似于Java的接口,现在发现,如果把一个C++的抽象类作为动态库的接口发布,那将是毁灭的。因为你无法增加虚函数,无法增加成员变量,这使得这个接口变得非常的不友好。这也就是Java接口的优势所在。Java 实际上把 C/C++ 的 linking 这一步骤推迟到 class loading 的时候来做,便不存在上述二进制兼容的问题。

理解Java二进制兼容的关键是要理解延迟绑定(Late Binding)。延迟绑定是指Java直到运行时才检查类、域、方法的名称,而不象C/C++的编译器那样在编译期间就清除了类、域、方法的名称,代之以偏移量数值——这是Java二进制兼容得以发挥作用的关键。
由于采用了延迟绑定技术, 方法、域、类的名称直到运行时才解析,意味着只要域、方法等的名称(以及类型)一样,类的主体可以任意替换。

5.总结

  • 尽可能的不要使用虚函数作为接口
  • 使用pimpl

6.参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341