二進制兼容的那些事

DLL的二進制兼容

詳解

什麼是二進制兼容?

所謂二進制兼容就是在作版本升級(也多是Bug fix)庫文件的時候,沒必要要作從新編譯使用這個庫的可執行文件或使用這個庫的其餘庫文件,同時能保證程序功能不被破壞。
固然,這只是一個現象級描述,其實在一些簡單的例子裏,假設咱們導出一個C++類,在調用時,第三方仍然不須要從新編譯能夠運行。以下面例子:html

  • FastString.dll - FastString.h文件
//導出類
class __declspec(dllexport) FastString
{
public:
    FastString();
    ~FastString();

    size_t length() { return 0; }

private:
    unsigned char *m_bytes;
}
  • test.exe - main.cpp文件
int main()
{
    FastString fStr;
    size_t len = fStr.length();

    printf("fast string length %d\n", len);
    _getch();
    return 0;
}

若是咱們給導出類加上一個虛函數設計模式

virtual boole isEmpty();  // 位於 length 方法以前

從新編譯FastString.dll,而後直接運行test.exe,發現仍然能打印出fast string length 0,而且沒有運行錯誤。
因此按照上面所說,FastString.dll是二進制兼容的。然而不是的!由於它增長了一個虛函數,致使FastString實例增長了一個虛函數表(是一個void **指針),那爲何運行的時候沒有錯誤呢?參考這個問題:SO- why new virtual function will not break binary compatibility per phenomenon? 安全

因此嚴格來說,二進制兼容是保證在版本升級的狀況下,對象實例的內存佈局沒有發生變化。app

爲何須要二進制兼容?

打個比方,若是庫A升級沒有作到二進制兼容,那麼全部依賴它的程序(或庫)都須要要從新編譯才能應用A庫的新版本,不然會出現各類未知異常,其直接現象就是程序莫名其妙的掛掉。 函數

譬如像Qt這種使用率很廣的程序庫,若是每次版本升級都須要第三方使用者從新編譯源程序,我想確定是不少人不肯意的。佈局

哪些常見作法會破壞二進制兼容?

  1. 給函數增長參數,現有的可執行文件沒法傳這個額外的參數。
  2. 增長虛函數,會形成虛函數表vtbl裏的排列變化。(不要考慮「只在末尾增長」這種取巧行爲,由於你的class可能已被繼承)
  3. 增長默認模板類型參數
    例如:template <typename T> class Grid {} 變動爲 template <typename t, typenameContainer=vector> class Grid{}
  4. 改變enum的值。把enum Color { Red = 3};改成Red = 4,這會形成錯位。固然,因爲enum自動排列取值,添加enum項也是不安全的,除非是在末尾添加。

哪些作法多半不會破壞二進制兼容?

  1. 增長新的class
  2. 增長非虛成員函數
關於更多的 Do's and Don'ts,能夠閱讀KDE的兩篇wiki: Policies/Binary Compatibility Issues With C++Policies/Binary Compatibility Examples

如何實現二進制兼容?

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能保證二進制兼容呢? code

其實經過上面兩個概念能夠有點思緒,所謂二進制兼容對於C++ 來講就是要保證第三方使用DLL提供的接口對象時,保證內存佈局不會改變,或者說不會影響。對於C++來講,對象內存佈局的主要包括:

  1. 變量
  2. 虛函數 - 每一個實例都會有一個虛函數列表(包括基類的)

對於COM實現來講,由於是經過GUID來獲取對象,而且這些對象都是由接口來提供的實例化(抽象類不能建立實例,這些實例都是繼承的子類實現),就像 caller ----> coclass (interface) --create--> instance 這樣調用。
因爲 instance 是在COM組件類(DLL)實例化以及釋放,因此其內存佈局對於 caller 來講是沒有影響的。

D指針設計模式

D指針模式其實和上面COM的方式有點相似,可是它沒有COM那麼複雜。咱們用一個例子來講明爲何D指針模式能作到二進制兼容。

假設你的class Foo 裏定義了一個前置聲明類FooPrivate

class FooPrivate;

而且把D指針放在private區

private:
    FooPrivate *d;

FooPrivate 類能夠徹底在class實現的地方定義(通常是 *.cpp),例如:

class FooPrivate {
public:
    FooPrivate() : m1(0), m2(0) {}
    int m1;
    int m2;
    QString s;
}

而後你所要作的就是在Foo的構造函數或者 init 方法裏建立 private 數據

d = new FooPrivate();

而且在析構函數裏 delete 掉

delete d;

固然,在不少時候,咱們可能不想讓D指針被修改,或者被複制致使咱們失去了它的控制權,最後致使內存泄漏。因此不少時候咱們會把D指針聲明爲 const,即

private:
    FooPrivate* const d;

這樣就能夠容許第三方去修改D指針指向的內容,可是不能修改這個指針的指向目標。

當這樣實現後,咱們全部的數據操做都是經過class Foo 的成員方法來作,例如:

QString Foo::string() const
{
    return d->s;
}

void Foo::setString( const QString& s )
{
    d->s = s;
}

從上面能夠看到,D指針的實現形式其實也是把數據區域隱藏,只經過方法的調用形式來操做。這樣當咱們須要修改 Foo 成員變量,對於第三方來講是沒有影響的,由於這個成員變量是在 FooPrivate 實例裏。

引用

  1. KDE - Policies/Binary Compatibility Issues With C++
  2. stackoverflow - When do we break binary compatibility
  3. Wikipedia - Application binary interface (ABI)
  4. stackoverflow - What is an application binary interface
  5. HowTo: Export C++ classes from a DLL

和COM相關:

  1. MSDN - COM Objects and Interfaces
  2. SO - COM(C++) programming tutorials?[closed]
  3. 博客園 - COM 入門(1)
相關文章
相關標籤/搜索