0.目錄
- 定義
- ABI和API
- 二進(jìn)制兼容的相關(guān)問題
- C++抽象類和Java的接口
- 總結(jié)
- 參考
1.定義
所謂二進(jìn)制兼容就是在做版本升級(jí)(也可能是Bug fix)庫(kù)文件的時(shí)候蒋川,不必要做重新編譯使用這個(gè)庫(kù)的可執(zhí)行文件或使用這個(gè)庫(kù)的其他庫(kù)文件,同時(shí)能保證程序功能不被破壞邮破。
先明確兩個(gè)概念:二進(jìn)制兼容和源碼兼容:
- 二進(jìn)制兼容:升級(jí)庫(kù)文件時(shí)揍堕,不必重新編譯使用此庫(kù)的可執(zhí)行文件或其他庫(kù)文件,且程序的功能不被破壞
- 源代碼兼容:升級(jí)庫(kù)文件時(shí)蜒滩,不必修改使用此庫(kù)的可執(zhí)行文件或其他庫(kù)文件的源代碼,只需重新編譯應(yīng)用程序,即可使程序的功能不被破壞
2.ABI和API
應(yīng)用二進(jìn)制接口(application binary interface玫氢,縮寫為
ABI
)描述了應(yīng)用程序(或者其他類型)和操作系統(tǒng)之間或其他應(yīng)用程序的低級(jí)接口。ABI
涵蓋了各種細(xì)節(jié)谜诫,如:數(shù)據(jù)類型的大小漾峡、布局和對(duì)齊;調(diào)用約定等喻旷。
在了解二進(jìn)制兼容和源碼兼容兩個(gè)定義以后生逸,我們?cè)倏磁c其類似且對(duì)應(yīng)的兩個(gè)概念:ABI
和API
。ABI
不同于API
(應(yīng)用程序接口)且预,API
定義了源代碼和庫(kù)之間的接口槽袄,因此同樣的代碼可以在支持這個(gè)API的任何系統(tǒng)中編譯,然而ABI允許編譯好的目標(biāo)代碼在使用兼容ABI的系統(tǒng)中無(wú)需改動(dòng)就能運(yùn)行锋谐。
舉個(gè)例子掰伸,在Qt和Java兩種跨平臺(tái)程序中,API
像是Qt的接口怀估,Qt有著通用接口狮鸭,源代碼只需要在支持Qt的環(huán)境下編譯即可。ABI
更像是Jvm多搀,只要支持Jvm的系統(tǒng)上歧蕉,都可以運(yùn)行已有的Java程序。
C++的ABI
ABI
更像是一個(gè)產(chǎn)品的使用說明書康铭,同理C++的ABI
就是如何使用C++生成可執(zhí)行程序的一張說明書惯退。編譯器會(huì)根據(jù)這個(gè)說明書,生成二進(jìn)制代碼从藤。C++的ABI
在不同的編譯器下會(huì)略有不同催跪。
c++ ABI
的部分內(nèi)容舉例:
- 函數(shù)參數(shù)傳遞的方式,比如 x86-64 用寄存器來傳函數(shù)的前 4 個(gè)整數(shù)參數(shù)
- 虛函數(shù)的調(diào)用方式夷野,通常是
vptr/vtbl
然后用vtbl[offset]
來調(diào)用 -
struct
和class
的內(nèi)存布局懊蒸,通過偏移量來訪問數(shù)據(jù)成員
綜上所述,如果可執(zhí)行程序通過以上說明書訪問動(dòng)態(tài)鏈接庫(kù)A悯搔,以及此庫(kù)的升級(jí)版本A+骑丸,若按此說明書上的方法,可以無(wú)痛的使用A和A+,那么我們就稱庫(kù)A的這次升級(jí)是二進(jìn)制兼容的通危。
3.二進(jìn)制兼容的相關(guān)問題
3.1 破壞二進(jìn)制兼容的幾種常見方式
添加新的虛函數(shù)
修改虛函數(shù)表內(nèi)的排列順序铸豁,即使把新增加的虛函數(shù)放到最后一個(gè),也可能會(huì)引起問題菊碟,如該類作為父類被其他類繼承等节芥;修改函數(shù)的參數(shù)列表
由于C++支持同名函數(shù)重載,C++編譯時(shí)逆害,會(huì)對(duì)函數(shù)名字進(jìn)行name mangling藏古,如果修改了函數(shù)的參數(shù)列表,經(jīng)過C++編譯器編譯后忍燥,函數(shù)的名稱就變了拧晕,現(xiàn)有的可執(zhí)行文件無(wú)法傳這個(gè)額外的參數(shù);不導(dǎo)出或者移除一個(gè)導(dǎo)出類
改變類的繼承
改變虛函數(shù)聲明時(shí)的順序
偏移量改變梅垄,導(dǎo)致調(diào)用失敗添加/刪除非靜態(tài)成員變量
改變?cè)擃惖膶?duì)象的大小厂捞,類的內(nèi)存布局改變,偏移量也發(fā)生變化队丝,如:
pfoo = new Foo(); // 由于sizeof(Foo)發(fā)生了變化靡馁,分配的內(nèi)存可能不夠
pfo->member_variable; // 可能會(huì)出錯(cuò),偏移量變化
// 當(dāng)使用 inline setxxx(x)時(shí)机久,也可能會(huì)出錯(cuò)臭墨,因?yàn)閕nline函數(shù)可能已經(jīng)編譯進(jìn)使用該庫(kù)的程序代碼中。
改變非靜態(tài)成員變量的聲明順序
偏移量改變增加默認(rèn)模板類型參數(shù)
// 如:
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
// 這會(huì)造成錯(cuò)位膘盖。當(dāng)然胧弛,由于enum自動(dòng)排列取值,添加enum項(xiàng)也是不安全的侠畔,除非是在末尾添加结缚。
3.2 不會(huì)破壞二進(jìn)制兼容的幾種常見方式
- 添加非虛函數(shù)(包括構(gòu)造函數(shù))
- 添加新的類
- 在已存在的枚舉類型中添加一個(gè)枚舉值
- 添加新的靜態(tài)成員變量
- 修改成員變量名稱(偏移量未改變)
- 增減類的友元聲明
只要我們知道了程序是以什么方式訪問動(dòng)態(tài)庫(kù)的(C++的ABI
),那么我們就很好判斷软棺,哪些操作會(huì)破壞二進(jìn)制兼容红竭。更多方式請(qǐng)參見Policies/Binary Compatibility Issues With C++
3.3 解決二進(jìn)制兼容問題的相關(guān)方法
編寫庫(kù)時(shí)最大的問題是,不能安全地添加數(shù)據(jù)成員喘落,因?yàn)檫@會(huì)改變每個(gè)包含類對(duì)象(包括子類)的類茵宪,結(jié)構(gòu)或數(shù)組的大小和布局。
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.
不會(huì)破壞二進(jìn)制兼容性瘦棋。 但需要根據(jù)字節(jié)對(duì)其取整稀火,否則可能會(huì)因改變了整個(gè)類的大小導(dǎo)致sizeof()之類的方法出問題,使用最后一位可能會(huì)在某些編譯器上引起問題兽狭。
2. 使用靜態(tài)庫(kù)(當(dāng)然也隨之帶來一系列弊端)
3. D指針設(shè)計(jì)模式(PImpl
機(jī)制)
4. COM理論
COM (Component object model) 組件對(duì)象模型是微軟提出的一個(gè)想法憾股,它其實(shí)是一個(gè)規(guī)范鹿蜀,并且是二進(jìn)制規(guī)范箕慧,也就是說只要遵循這個(gè)規(guī)范服球,任何語(yǔ)言、任何平臺(tái)都可以相互調(diào)用相應(yīng)組件颠焦。
COM涉及到幾個(gè)概念:
- class ID斩熊,可以是CLSID - class的GUID 或者 IID - interface的GUID。COM通過這個(gè)ID來保證快語(yǔ)言伐庭,因?yàn)榛旧纤姓Z(yǔ)言都可以處理GUID字符串粉渠;另外COM開發(fā)者可以通過GUID來獲取到準(zhǔn)確的對(duì)象結(jié)構(gòu)。
-
coclass - component object class圾另,簡(jiǎn)單來說就是COM組件提供給使用者的接口類霸株,這些類其實(shí)都是都繼承
IUnkown
接口的抽象類,里面都是純虛函數(shù)集乔。這個(gè)IUnknown
包含三個(gè)方法:-
AddRef
- 增加對(duì)象引用計(jì)數(shù) -
Release
- 減少引用計(jì)數(shù)去件,如果計(jì)數(shù)為0,則銷毀 -
QueryInterface
- 根據(jù)GUID來查到對(duì)象
-
COM組件還涉及到注冊(cè)表扰路,它可以注冊(cè)到操作系統(tǒng)的注冊(cè)表中尤溜,這樣就算當(dāng)前這個(gè)組件DLL物理位置與運(yùn)行文件不在同一個(gè)目錄,也可以加載并獲取DLL的導(dǎo)出對(duì)象或者函數(shù)汗唱。更多了解可以看 CodeProject - Introduction to COM - What It Is and How to Use It宫莱。
那為什么可以說COM能保證二進(jìn)制兼容呢?
其實(shí)通過上面兩個(gè)概念可以有點(diǎn)思緒哩罪,所謂二進(jìn)制兼容對(duì)于C++ 來說就是要保證第三方使用DLL提供的接口對(duì)象時(shí)授霸,保證內(nèi)存布局不會(huì)改變,或者說不會(huì)影響际插。對(duì)于C++來說绝葡,對(duì)象內(nèi)存布局的主要包括:
變量
虛函數(shù) - 每個(gè)實(shí)例都會(huì)有一個(gè)虛函數(shù)列表(包括基類的)
對(duì)于COM實(shí)現(xiàn)來說,因?yàn)槭峭ㄟ^GUID來獲取對(duì)象腹鹉,并且這些對(duì)象都是由接口來提供的實(shí)例化(抽象類不能創(chuàng)建實(shí)例藏畅,這些實(shí)例都是繼承的子類實(shí)現(xiàn)),就像 caller ----> coclass (interface) --create--> instance 這樣調(diào)用功咒。
由于 instance 是在COM組件類(DLL)實(shí)例化以及釋放愉阎,所以其內(nèi)存布局對(duì)于 caller 來說是沒有影響的。
4.C++抽象類和Java的接口
之前我一直認(rèn)為C++的抽象類就類似于Java的接口力奋,現(xiàn)在發(fā)現(xiàn)榜旦,如果把一個(gè)C++的抽象類作為動(dòng)態(tài)庫(kù)的接口發(fā)布,那將是毀滅的景殷。因?yàn)槟銦o(wú)法增加虛函數(shù)溅呢,無(wú)法增加成員變量澡屡,這使得這個(gè)接口變得非常的不友好。這也就是Java接口的優(yōu)勢(shì)所在咐旧。Java 實(shí)際上把 C/C++ 的 linking 這一步驟推遲到 class loading 的時(shí)候來做驶鹉,便不存在上述二進(jìn)制兼容的問題。
理解Java二進(jìn)制兼容的關(guān)鍵是要理解延遲綁定(Late Binding)铣墨。延遲綁定是指Java直到運(yùn)行時(shí)才檢查類室埋、域、方法的名稱伊约,而不象C/C++的編譯器那樣在編譯期間就清除了類姚淆、域、方法的名稱屡律,代之以偏移量數(shù)值——這是Java二進(jìn)制兼容得以發(fā)揮作用的關(guān)鍵腌逢。
由于采用了延遲綁定技術(shù), 方法超埋、域搏讶、類的名稱直到運(yùn)行時(shí)才解析,意味著只要域纳本、方法等的名稱(以及類型)一樣窍蓝,類的主體可以任意替換。
5.總結(jié)
- 盡可能的不要使用虛函數(shù)作為接口
- 使用
pimpl