二进制兼容和 abi 兼容
二进制兼容和 abi 兼容
1. 什么是二进制兼容问题?
在升级库文件的时候,不必重新编译使用此库的可执行文件或其他库文件,并且程序的功能不被破坏。
2. 二进制兼容有什么好处?
保持二进制兼容性是为了确保软件的更新和升级过程更加平滑和无缝,以减少对用户和开发者的影响。
- 减少用户升级的难度: 如果新版本的程序能够与旧版本的程序兼容,用户在升级时无需重新编译或修改配置。这降低了用户升级的难度和成本。
- 避免破坏性变更: 破坏性变更可能会导致旧版本的程序无法与新版本的程序一起工作,从而造成功能中断或数据丢失。通过保持二进制兼容性,可以避免这种情况的发生。
- 提高用户信任度: 如果用户知道他们可以在不受影响的情况下升级软件,他们更有可能愿意使用最新版本的程序,从而提高了用户对软件的信任度。
- 简化发布流程: 保持二进制兼容性可以使软件发布变得更加简单,因为你不需要为每个新版本都重新构建所有的旧版本二进制文件。
- 降低维护成本: 如果软件能够保持二进制兼容性,你可以更轻松地维护多个版本,因为不需要重新构建和测试旧版本。
3. 实现二进制兼容常见方式-pimpl(pointer to implementation)
指针指向实现。简单来说就是将实现和接口分类的一种设计思路。
也就是说将一个类分为接口类(外观类/导出类)Foo
,以及实现类。
外部使用接口类时,接口类的函数的内存偏移是固定的;而更新内容放在实现类就好了。而接口类通过一个指针,指向实现类,调用实现类的方法。这个指针就称作 D 指针。
3.1 pimpl 还有什么好处?
-
数据隐藏 如果您正在开发一个库,尤其是专有库,则可能不希望公开用于实现库公共接口的其他库/实现技术。 要么是由于知识产权问题,要么是因为您认为用户可能会被诱使对实现进行危险的假设,或者只是通过使用可怕的转换技巧来破坏封装。 PIMPL 解决/缓解了这一难题。
-
编译时间 编译时间减少了,因为当您向 XImpl 类添加/删除字段和/或方法时(仅映射到标准技术中添加私有字段/方法的情况),仅需要重建 X 的源(实现)文件。 实际上,这是一种常见的操作。
使用标准的标头/实现技术(没有 PIMPL),当您向 X 添加新字段时,曾经重新分配 X(在堆栈或堆上)的每个客户端都需要重新编译,因为它必须调整分配的大小 . 好吧,每个从未分配 X 的客户端也都需要重新编译,但这只是开销(客户端上的结果代码是相同的).
3.2 pimpl 通用实现范式:D/Q 指针
D/Q 指针则是实现 pimpl 的一种方式。
// 接口类
class Foo {
public:
Foo();
~Foo();
void someMethod();
private:
class FooImpl; // 前向声明
FooImpl* d; // D 指针
};
// 实现类
class Foo::FooImpl {
public:
FooImpl(Foo* q) : q(q) {}
void someMethodImpl();
private:
Foo* q; // Q 指针
};
Foo::Foo() : d(new FooImpl(this)) {}
Foo::~Foo() { delete d; }
void Foo::someMethod() { d->someMethodImpl(); }
void Foo::FooImpl::someMethodImpl() {
// 可以通过 Q 指针访问接口类的方法和成员变量
q->someMethod();
}
4. abi 兼容
ABI(Application Binary Interface,应用二进制接口)兼容性和二进制兼容性密切相关,但它们并不是完全相同的概念。
ABI 兼容性指的是在二进制级别上,程序或库的不同版本之间能够互操作的能力。具体来说,ABI 兼容性确保了以下方面的一致性:
- 函数和方法的签名:参数类型、数量和顺序必须保持一致。
- 数据结构的布局:结构体和类的成员变量的顺序和对齐方式必须保持一致。
- 类的继承关系:基类和派生类的关系不能改变。
- 虚函数表(vtable):虚函数的顺序和数量不能改变。
5. 总结
简单而言,实现二进制兼容就是要分离接口和实现。接口 api 的 abi 是稳定的,以及接口类的内存布局是稳定的。
-
ABI 兼容性 ABI 兼容性指的是在二进制级别上,程序或库的不同版本之间能够互操作的能力。具体来说,ABI 兼容性确保了以下方面的一致性:
- 函数和方法的签名:参数类型、数量和顺序必须保持一致。
- 数据结构的布局:结构体和类的成员变量的顺序和对齐方式必须保持一致。
- 类的继承关系:基类和派生类的关系不能改变。
- 虚函数表(vtable):虚函数的顺序和数量不能改变。
-
二进制兼容性
二进制兼容性是一个更广泛的概念,它指的是一个程序的新版本可以使用旧版本的二进制接口(如共享库或对象文件)而不需要重新编译。二进制兼容性依赖于 ABI 兼容性,但还包括其他方面:
- 操作系统和硬件平台:二进制兼容性还涉及操作系统和硬件平台的兼容性。
- 编译器和链接器:不同编译器和链接器生成的二进制文件可能不兼容。
- 运行时环境:运行时库和环境的变化也可能影响二进制兼容性。
- ABI 兼容性:主要关注程序或库在二进制级别上的接口一致性,确保不同版本之间能够互操作。
- 二进制兼容性:是一个更广泛的概念,除了 ABI 兼容性,还包括操作系统、硬件平台、编译器、链接器和运行时环境的兼容性。
在实际开发中,保持 ABI 兼容性是实现二进制兼容性的关键步骤之一,但要实现完全的二进制兼容性,还需要考虑其他因素。
99. quiz
1. 怎样才可以保证二进制兼容?二进制兼容的关键是什么?
二进制兼容性是指一个程序的新版本可以使用旧版本的二进制接口(如共享库或对象文件)而不需要重新编译。保证二进制兼容性的关键是保持 ABI(Application Binary Interface)的稳定性。
以下是一些保证二进制兼容性的方法:
-
不改变公共函数和方法的签名:如果你改变了函数或方法的参数类型、数量或顺序,那么二进制接口将会改变,导致二进制不兼容。
-
不改变数据结构的布局:如果你添加、删除或重新排序结构的成员,那么结构的布局将会改变,导致二进制不兼容。
-
不改变类的继承关系:如果你改变了类的基类或派生类,那么类的布局和虚函数表将会改变,导致二进制不兼容。
-
保持虚函数表的稳定性:如果你添加、删除或重新排序虚函数,那么虚函数表将会改变,导致二进制不兼容。
注意,以上的规则只适用于公共接口。你可以自由地改变私有函数、方法和数据成员,而不影响二进制兼容性。
2. 一般什么情况下特别要求二进制兼容?
-
操作系统: 操作系统的升级通常会涉及到底层的系统组件,保持二进制兼容性可以确保应用程序继续在新版本的操作系统上正常运行。
-
大型应用软件: 大型应用软件通常会有许多用户,升级可能涉及到大量的用户数据和配置。保持二进制兼容性有助于减少升级的风险。
-
软件库和框架: 开发人员在开发应用程序时可能会使用各种第三方库和框架,保持这些库和框架的二进制兼容性有助于减少对应用程序的影响。
-
游戏客户端: 游戏客户端经常需要进行更新和修复,保持二进制兼容性可以确保玩家在更新后继续享受游戏体验。
如果是二进制兼容的,弄一个更新包可能就好了,如果不是的话,可能整个软件都要重新安装。
3. stl 的 abi 兼容问题
STL(标准模板库)的实现确实会针对不同的编译器和编译选项进行优化,这导致了不同编译器和编译器版本之间的实现细节可能有所不同。这些差异可能包括内存布局、对齐方式、函数内联、异常处理等方面。
不同的编译选项(如优化级别、调试符号、C++标准等)可能导致不同的二进制布局。 这意味着,如果你在库中使用 STL 容器作为参数,并且这个库需要在不同的编译环境下使用,可能会导致 ABI 不兼容,进而引发内存崩溃或未定义行为。
使用接口隔离:通过定义稳定的接口和抽象层来隔离不同编译器实现的差异。 避免跨 DLL 边界使用 STL 容器:尽量避免在 DLL 或共享库的边界上传递 STL 容器,或者确保所有相关组件都使用相同的编译器和编译选项。
我在网上看到给出的例子是:VS2010 编的库在 VS2013 上使用就经常会出问题。然后 VS2015 开始进入了长期 ABI 兼容周期,到现在 VS2022 还是与 VS2015 保持 ABI 兼容的。这反而又导致很多优化会被拖延到下一个打破 ABI 的版本。
4. 静态库和动态库场景下对二进制的兼容
在使用静态库的时候,保持二进制兼容的意义在于减少编译时间和简化维护过程。 因为使用了静态库,所有代码在编译时就被链接到可执行文件中。
而在使用动态库的时候,保持二进制兼容尤为重要,因为它可以简化库的升级过程。 这个时候只需要替换动态库文件(如 .dll、.so)即可,无需重新编译和重新部署应用程序
Enjoy Reading This Article?
Here are some more articles you might like to read next: