二进制兼容的那些事

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)
相关文章
相关标签/搜索