Chinaunix首页 | 论坛 | 博客
  • 博客访问: 65149
  • 博文数量: 15
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 160
  • 用 户 组: 普通用户
  • 注册时间: 2014-11-04 17:12
个人简介

It

文章分类

全部博文(15)

文章存档

2015年(13)

2014年(2)

我的朋友

分类: C/C++

2015-07-22 16:38:33

1. 引子

    有一个同事C,在一个原来的C++动态库的基础上,为一个现场添加一个新的功能,需要在原来的class接口提供一个新的虚函数,以实现该功能。功能很简单,很快开发完毕并提交代码到主分支,该现场运行也没问题,该需求顺利关闭。当QA把该最新的库发布到其他现场,嗯哼,问题出现了。QA在发布该最新库时,问同事C,是否可以发布该最新库到其他现场。同事C想,我的修改是添加一个功能,并没有修改其他功能和接口,其他现场还是用原来的功能,肯定没有问题,就同意了发布最新库,这样可以在一个分支上进行维护。
    那么,为什么一个现场没有问题,而另一个现场有问题呢?
    这就是动态库的兼容性(ABI)问题。
    没有出现问题的现场,是因为该现场为使用最新的功能,必然会重新编译链接使用该库的程序,所以不会出现问题;出现问题的现场,是因为该现场没有新的需求增加,使用该库的程序没有重新编译链接,导致使用该最新的库出现问题。
    如果使用动态库,那么动态库发生变化,都需要重新编译链接一遍吗?如果答案为真,那么使用动态库优势少了一半,使用动态库仅仅是节约一点内存,在内存价格低廉的今天,好像该优势不大。其实不然,如果我们能够编写二进制兼容的库,我们可以不用重新编译链接一遍。

2. 分析

使用C++语言做开发,有许多'坑'需要我们时刻注意,ABI就是一个'坑'.
C++为什么会出现ABI问题呢?
看例子:

点击(此处)折叠或打开

  1. // 我们新添加并实现一个虚函数 new_f(),其他没有修改。
  2. // foo.h
  3. class Foo {
  4. public:
  5.     Foo(int x, int y);
  6.     virtual void f();
  7.     virtual void new_f(); // 新添加的功能
  8.     virtual void g();
  9.     
  10.     // ...
  11.     
  12. private:
  13.     // ...
  14. };
  15. // foo.cpp
  16. Foo::Foo(int x, int y) {
  17.     this.x = x;
  18.     this.y = y;
  19. }
  20. void Foo::f() {
  21.     // ...
  22. }
  23. void Foo::new_f() {
  24.     // ...
  25. }
  26. void Foo::g() {
  27.     // ...
  28. }
  29. // end



  30. // main.cpp
  31. int main() {
  32.     // ...
  33.     
  34.     Foo foo(1,2);
  35.     
  36.     foo.f();
  37.     foo.g();
  38.     
  39.     // ...
  40.     
  41.     return 0;
  42. }

    使用foo.so库的程序a.out,在foo.so为添加 void new_f() 前编译。现在foo.so中添加了new_f(),但程序a.out不使用foo.so库的new_f(),从表面上看好像不用重新编译链接。我们看一下,如果不重新编译链接,会出现什么问题。
    我们知道,C++虚函数一般使用虚函数表vtbl来实现,如下图:
Foo.vtbl-->---------
          0|       |
           ---------
          1|  f()  |
           ---------
          2|  g()   |
           ---------
          3|  ...   |
    我们假设f()在虚函数表vtbl的第二项(从0开始), 所以调用foo.f(),实际上是foo.vtbl[1]();而调用foo.g(),实际上是foo.vtbl[2]()。
    但是,当添加虚函数void new_f(), foo.so库中的Foo的虚函数表发生了变化,
Foo.vtbl-->---------
            0|       |
             ---------
            1|  f()  |
             ---------
            2|new_f()|
             ---------
            3|  g()  |
             ---------
            4|  ...  |
    a.out在使用新的foo.so库时,foo.g()实际调用了foo.new_f().
    所以出现问题。

    其实,在例中,把新添加的 virtual void new_f()放到所有的virtual之后,在大多数情况下(该类未被继承),应该就没有问题了。

3. 总结

    知道了二进制兼容性后,应该如何避免问题?或者对动态库代码的修改,什么的修改会保持二进制兼容,什么样的修改会破坏二进制兼容呢?

3.1 可能破坏二进制兼容的情况

1. 增加/减少虚函数
    修改虚函数表内的排列顺序,即使把新增加的虚函数放到最后一个,也可能会引起问题,如该类作为父类被其他类继承等;
2. 修改函数的参数列表,无论是全局函数,类成员函数等,没有加extern "C"
    由于C++支持同名函数重载,C++编译时,会对函数名字进行name mangling,如果修改了函数的参数列表,经过C++编译器编译后,函数的名称就变了; 
3. 虚函数从有变无,或者从无改为有
    改变该类的对象的大小
4. 增加减少成员变量,
    这会改变该类的对象的大小,当在使用该库的程序中有如下代码:
    pfoo = new Foo;
    由于sizeof(Foo)发生了变化,分配的内存可能不够。另外当使用pfo->member_variable访问成员变量时,也可能会出错;当使用 inline setxxx(x)时,也可能会出错,因为inline函数可能已经编译进使用该库的程序代码中。

5. 应该还有,目前没有想到。

    有那么多可能破坏二进制兼容性,我们是不是不要使用动态库了呢?其实这要看具体情况.

3.2 静态库/动态库

    无论是动态库还是静态库,其根本目的是复用已有的、经过测试的成熟代码。我们看一下,静态库和动态库的优缺点.

静态库

    优点:
    使用静态库,主程序会把该库编译链接进程序本身,以后就与该库没有关系了,部署简单。

    缺点:
    1. 如果一台机器上有多个程序使用该库,会浪费一部分空间资源。
    2. 修改了库,使用该库的程序必须重新编译连接。

动态库

    优点:
    1. 介意一部分空间资源,如果一台机器上只有一个程序使用该库,也谈不上节约空间资源了。
    2. 动态库修改后,是二进制兼容的,可以不用重新编译。这样当发布升级新的功能时,只更新部分动态库。
    缺点:
    1. 部署相对复杂;
    2. 要注意二进制的兼容性。

3.3 结论

    如果使用静态库,就不会有二进制兼容性了。如果可以,使用静态库吧,呵呵。
    如果必须使用动态库,那么最好做到如下几点:
        1. 尽可能的不要使用虚函数作为接口;
        2. 增加一个间接层,使用pimpl。如下:

点击(此处)折叠或打开

  1. // foo.h
  2. class Foo {
  3. public:
  4.     Foo();
  5.     ~Foo();
  6.     void f1();
  7.     void f2();
  8.     void f3(); // new memeber func
  9.     // ...
  10.     
  11. private:
  12.     class FooImpl;
  13.     FooImpl *impl;
  14. }

  15. // foo.cpp
  16. #include "foo.h"
  17. class Foo::FooImpl {
  18. public:
  19.     virtual void f1();
  20.     virtual void f2();
  21.     virtual void f3();
  22.     // ...
  23.     
  24. private:
  25.     int x;
  26.     int y;
  27.     int z; // new member data
  28. };
  29. void Foo::FooImpl::f1() {
  30.     // ...
  31. }
  32. void Foo::FooImpl::f2() {
  33.     // ...
  34. }
  35. void Foo::FooImpl::f3() {
  36.     // ...
  37. }

  38. // 可以放到头文件中,作为 inline func
  39. Foo::Foo() : impl(new FooImpl) {
  40. }
  41. Foo::~Foo() {
  42.     delete impl;
  43. }

  44. void Foo::f1() {
  45.     impl->f1();
  46. }
  47. void Foo::f2() {
  48.     impl->f2();
  49. }
  50. void Foo::f3() {
  51.     impl->f3();
  52. }




阅读(3191) | 评论(0) | 转发(0) |
0

上一篇:浅谈JavaScript 与 Python对比

下一篇:没有了

给主人留下些什么吧!~~