ABI常识
有相当一部分程序员弄不清楚API和ABI的差别,甚至根本就没说过ABI。这并不是ABI不重要,而是在小型开发团队中ABI问题不容易遇到,当团队扩大,开发程序使用的组件增多,这个问题就会变得严重。我们倾向于从头说起,就当是科普了。
API和ABI
API,Application Programming Interface
ABI,Application Binary Interface
从名字上就可以看出,API是面向编程人员的,是面向人的;而ABI,编程人员是不容易直接看到的,但编程人员的行为可以直接影响到ABI,简单来说,ABI是面向编译器的。举个简单的例子
void print(int tag1, int tag2, int tag3)
{
printf("%d\n", tag1 + tag2 + tag3);
}
这段程序提供一个API,函数原型为
void print(int, int, int)
对于开发人员而言,到这里已经不需要再深入了。对于编译器而言,却有很多工作要考虑,例如
- 参数计算的顺序
- 参数入栈的顺序
- 寄存器传参如何安排
这些工作发生在二进制层面,因此就称作ABI,意味着二进制层面的接口规范。编译器如何处理这些问题和处理器密切相关,一般不足以引起什么问题,更严重的问题发生在语言和编译器之间,例如:
- 异常的处理
- STL代码的例化
- 虚函数的实现
- 结构体的对齐
ABI不兼容
既然知道了什么是ABI,那么就可以考虑ABI的兼容问题了。
假设有一个程序A,依赖一个库libA中的某个函数,假设为
void __cdecl A_calc(int s1, int s2);
A由程序员John维护,libA由程序员CHIV维护,某一天CHIV就突然升级了一下
void __cdecl A_calc(int s1, int s2, int s3);
John习惯性的更新了libA,这就出现了问题,这既是典型的ABI不兼容问题。有人可能会说了,john重新编译一下不就能发现问题吗?显然,是的。如果随随便便更新某个lib,john就得重新编译程序,那么请问把软件分模块的意义是不是打了个大折扣。一个很大的程序A,居然因为一个小小的组件libA,就大动干戈重新编译一下,这不就是因小失大吗?
当使用的开发语言成为C++之后,ABI的问题就更严重了。主要体现在STL和虚函数上,试想一下,你的开发伙伴给你的头文件里一大堆的STD,你是不是有点发疯,你们的编译器都不一样好吗?过来一大堆C++11的代码,你连编译过去都不可能,VS2013支持的C++11连继承构造也没实现你信不?
既然如此,那大家都是用支持C++11的编译器行吗?还是不行,因为C++11没有规定ABI,编译器如何实现谁也不知道?于是你接口里的什么容器之类的就只能等着崩溃,VC自家的编译器都不能纵向兼容,不同厂家的还能指望吗?
是不是没有办法了?也不是,你只要给你的合作伙伴提供源代码就行,让他直接从源代码编译,无限期增加他的Build工作量,让他996,然后某一天他上班的时候也许会带把枪(这个梗能get到吗)什么的?
说完了STL,然后再说一下虚函数。
class U2B {
public:
U2B() {}
void Start();
virtual void Stop();
virtual void OnCompleted();
void Shutdown();
};
这是CHIV给你的接口,看着是不是很感人?但是没过多久,CHIV就增加了一个方法
class U2B {
public:
U2B() {}
virtual void Log();
void Start();
virtual void Stop();
virtual void OnCompleted();
void Shutdown();
};
就当所有编译器都一样吧,把所有的虚函数安排在类实例的起始位置,那里就放了一张虚函数表,表中的函数是个什么顺序?当然是虚函数出现的顺序。升级后,很明显表中的第一项变了,那么对Stop的调用,就指向了Log,这不就错了吗?关键是这个函数也能正常执行,于是John根本就找不到问题。更为要命的是,CHIV提供的这个lib是以组件形式管理的,你就算重新编译一下也找不到问题,john的妈妈每天都得喊他回去吃饭。这个ABI不兼容不崩溃、不报错,程序就是执行不正常。或者这个功能干脆就不常用,客户偶尔碰到一次,干脆一票否决了。
ABI兼容的措施
首先说结论,没有根本的办法防止ABI不兼容的问题。只能通过规范管理、增强安全编程的意识、提高对与程序的运行原理的认识才能降低这种概率,从编程技巧上,前辈们已经总结出了一些,这一节主要就介绍Pimpl方法。
在开发中不使用STL或者不使用虚函数确实能避免不少ABI问题,如果这样的话,还是用C++干啥。所以前辈们就想出了Pimpl方法,它的中心思想就是,给合作伙伴的接口里面没有STL,也没有虚函数,所有能引起ABI兼容问题的特征都被封死在库的本身范围之内。这需要用到C++的前置声明,还是举例说明。
class U2BImpl;
class U2B {
public:
U2B() {}
void Start();
void Stop();
void OnCompleted();
void Shutdown();
private:
U2BImpl* mImpl;
};
这就是给合作伙伴的接口,U2BImpl是真正的实现,U2B是这个实现的封装,U2BImpl并不需要用户直接访问,从而隔离了ABI不兼容的问题。
谁才是兼容界的一哥
舍微软其谁。你N年前编写的程序,可以跑遍Windows家族。
补充阅读内容
[1] https://community.kde.org/Policies/Binary_Compatibility_Issues_With_C%2B%2B