目录
- 1 版本兼容问题
- 1.1 兼容性级别
- 1.1.1 向后兼容性
- 1.1.1.1 功能性兼容性
- 1.1.1.2 源代码兼容性
- 1.1.1.3 二进制兼容性
- 1.1.2 向前兼容性
- 1.2 怎么维护向后兼容性
- 1.2.1 添加功能
- 1.2.2 修改功能
- 1.2.3 弃用功能
- 1.2.4 移除功能
- 2 跨平台问题
1 版本兼容问题
1.1 兼容性级别
通常应该为 API 的“主、次和补丁”版本提供不同级别的兼容性承诺。例如,可以承诺补丁版本同时满足向后和向前兼容,或者承诺只有主版本才会破坏二进制兼容性。
1.1.1 向后兼容性
向后兼容,可以简单的描述为:二开人员基于1.0SDK 写的代码,在不改动情况下能在 2.0SDK 上使用。
这里暗示着新版本 API 是旧版本 API 的超集。可以添加新功能,但是不能对旧的 API 定义的已有的功能做出不兼容的修改。API 维护的基本原则是绝不从接口中移除任何内容。
1.1.1.1 功能性兼容性
功能性兼容意味着第 N+1 版本 API 的行为和第 N 版本一致。
1.1.1.2 源代码兼容性
源代码兼容性是对向后兼容性较为宽松的定义。它主要是指用户可以使用新版本的API重新编译程序,而不用对代码做任何修改。这个概念不涉及编译出的程序的行为,只要能够成功编译并链接即可。源代码兼容性有时也称为 API 兼容性。
例如,虽然下列两个函数的函数入参不同,但它们是源代码兼容的:
// v1.0
void SetImage(Image* img);
// v1.1
void SetImage(Image* img, bool keep_aspect = true);
这是因为之前编写的所有调用 1.0 版本函数的用户代码也可以使用 1.1 版本进行编译(新参数是可选的)。
1.1.1.3 二进制兼容性
二进制兼容性意味着API的任何修改一定不能影响任何类、函数在库文件中的表示。API中所有元素的二进制表示,包括类型、大小、结构体对齐和所有的函数签名必须维持原样。这通常也称为应用程序二进制接口( ABI)兼容性。
二进制兼容性也意味着使用第 N 版本 API 编写的应用程序可以仅通过替换或重新链接 API 的新动态链接库,就升级到第 N+1 版本。
二进制不兼容的API修改
- 移除类、方法或函数。
- 增加、移除类中的成员变量,或者重新排序成员变量。
- 增加或移除-一个 类的基类。
- 修改任何成员变量的类型。
- 以任何方式修改已有方法的签名。
- 增加、移除模板参数,或者重新排序模板参数。
- 把非内联方法改为内联方法。
- 把非虚方法改为虚方法,反之亦然。
- 改变虚方法的顺序。
- 给没有虚方法的类增加虚方法。
- 增加新的虚方法(对于有些编译器,如果只是在已有的虚方法后面增加新的虚方法,则有可能保持二进制兼容性)。
- 覆盖已有的虚方法(这在某些情况下可能是可行的,但最好避免这样做)。
二进制兼容的API修改
- 增加新的类、非虚方法或者函数。
- 给类增加新的静态变量。
- 移除私有静态变量(前提是它们从来没有在内联方法中引用)。
- 移除非虚私有方法(前提是它们从来没有在内联方法中调用)。
- 修改内联方法的实现(要使用新的实现就必须重新编译)。
- 把内联方法修改为非内联方法(如果实现也被修改,那么必须重新编译)。
- 修改方法的默认参数(要使用新的默认参数就必须重新编译)。
- 给类增加或移除友元声明。
- 给类增加新的枚举。
- 给已存在的枚举增加新的枚举量。
- 使用位域中未声明的余留位。
把API修改限制在第二个列表中列出的修改,就能够维持API发布版本之间的二进制兼容性。下面给出一些有助于实现二进制兼容性的进阶技巧。
- 不要给已有方法增加参数,可以定义该方法新的重载版本。这确保原有符号继续存在,同时也能提供新的调用约定。在.cpp文件内部,老方法的实现可以直接调用新的重载方法。
// v1.0
void SetImage(Image* img);
// v1.1
void SetImage(Image* img);
void SetImage(Image* img, bool keep_aspect);
(注意,如果方法尚未重载,那么这项技巧可能会影响源代码兼容性,因为如果没有显式转换,客户代码将不再能够引用函数指针&SetImage
。)
- PimpI 模式可以用来帮助保持接口的二进制兼容性,因为它把那些将来很可能发生变化的实现细节移进了 .cpp 文件,使之不会影响公有的 .h 文件。
- 采用纯C风格的API可以更容易地获得二进制兼容性,原因很简单,C不提供诸如继承、可选参数、重载、异常及模板等特性。为了利用C和C++各自的优势,可以选择使用面向对象的C++风格开发API,然后用纯C风格封装C++ API。
- 如果确实需要做二进制不兼容的修改,那么可以考虑为新库换个不同的名字,这样就不会破坏已有的应用程序。libz库采用了这种方法,1.1 4版本之前的Windows构建版命名为
ZLIB.DLL
。而该库的后续版本采用了一项二进制不兼容的编译器设置,所以库被重命名为ZLIB1.DLL
,其中“1"代表API的主版本号。
1.1.2 向前兼容性
向前兼容,可以简单的描述为:二开人员基于2.0SDK 写的代码,在不改动情况下能在 1.0SDK 上使用。
为API增加新的功能会破坏向前兼容性,因为利用这些新特性编写的客户代码将不能编译不包含这些特性的老版本API。
例如,SetImage( )
函数的下面两个版本是向前兼容的:
// v1.0
void SetImage(Image* img, bool unused = true);
// v1.1
void SetImage(Image* img, bool keep_aspect);
因为使用函数的1.1版本( 第二个参数必备)编写的代码,能够使用函数的1.0版本(第二个参数可选)成功编译。但是,下列两个版本不是向前兼容的:
// v1.0
void SetImage(Image* img);
// v1.1
void SetImage(Image* img, bool keep_aspect = false);
因为使用1.1版本编写的代码能够提供可选的第二个参数, 如果提供了这个参数, 那么使用该函数的1.0版本就不能通过编译。
1.2 怎么维护向后兼容性
1.2.1 添加功能
在源代码兼容性方面,给API添加新功能通常是安全的。添加新类、新方法或新的自由函数不会改变已有API元素的接口,所以不会破坏已有代码。
但这条经验法则也有例外,给抽象基类添加新的纯虚成员函数就不是向后兼容的,如下所示:
class ABC {
public:
virtual ~ABC();
virtual void ExistingCall() = 0;
virtual void NewCall() = 0; // 在API新发布的版本中添加
};
这是因为现存的所有客户代码此时必须定义这个新方法的实现,否则它们的派生类就不能被具体化,代码也无法通过编译。变通方案是为添加到抽象基类中的每个新方法提供默认实现,也就是说把它们定义为虚方法而非纯虚方法,如下所示:
class ABC {
public:
virtual ~ABC();
virtual void ExistingCall() = 0;
virtual void NewCall(); // 在API新发布的版本中添加
};
1.2.2 修改功能
修改功能而不破坏已有客户代码是一项技巧性更强的工作。如果只关心源代码兼容性,那么可以给方法添加新参数,它们放在所有现存的参数之后,并声明为可选的。这意味着不强制要求用户为添加新参数,更新所有已有的调用。
// v1.0
void SetImage(Image* img);
// v1.1
void SetImage(Image* img, bool keep_aspect = false);
修改那些先前返回void类型的已有方法的返回类型也是源代码兼容的,因为已有的代码不会检查返回类型。
// v1.0
void SetImage(Image* img);
// v1.1
bool SetImage(Image* img);
在维护二进制兼容性方面,对已有函数签名作出的任何修改都会破坏二进制兼容性,包括改变参数的顺序、类型、个数和常量性以及修改返回值。如果在修改已有方法签名的同时还需要保持二进制兼容性,那就必须创建具有相同功能的新方法,潜在地重载已有函数的名字。本文前面提到过这种技巧。
// v1.0
void SetImage(Image* img);
// v1.1
void SetImage(Image* img);
void SetImage(Image* img, bool keep_aspect);
1.2.3 弃用功能
弃用的功能是指强烈建议客户不要使用的某个特性,通常是因为特性更新,或有新的替代特性。由于弃用的特性仍然存在于API中,所以用户仍然可以调用,但这样做可能会产生某种类型的警告。理想的状态是把弃用的特性从API的未来版本中完全移除。
弃用功能是启动移除特性的过程,为的是给客户留以时间,以便使用推荐的新语法更新代码。
弃用已有函数时,应当在该函数的文档中标明这一事实, 同时说明可以取代它的新功能。除了这项文档任务,还可以采取一些方法,使函数使用时产生警告消息。大多数编译器提供了将类、方法或变量标记为“已弃用"的方法,只要程序访问了带有这种标记的符号,就会输出编译时警告。对于Visual Studio C++,可在方法声明前添加 __declspec(deprecated )
前缀,而对于 GNUC++ 编译器,可使用__attribute__((deprecated))
。 下列代码定义了DEPRECATED
宏,它对两种编译器都适用。
// deprecated.h
#ifdef __GNUC__
#define DEPRECATED __attribute__ ((deprecated))
#elif defined(_MSC_VER)
#define DEPRECATED __declspec(deprecated)
#else
#define DEPRECATED
#pragma message("DEPRECATED is not defined for this compiler")
#endif
使用这种定义,可以通过下列方式把特定方法标记为已弃用:
#include "deprecated.h"
#include <string>
class MyClass {
public:
DEPRECATED std::string GetName();
std::string GetFullName();
};
如果用户调用GetName()
方法,编译器就会输出警告消息说明方法已经弃用。例如,下列警告是 GNU C++ 4.3 编译器输出的:
In function 'int main(int, char**)':
warning: 'GetName' is deprecated (declared at myclass.h 21)
除了提供编译时警告,还可以编写在运行时给出弃用警告的代码。这样做的理由之一是能够在警告消息中提供更多的信息,比如说明可替代使用的方法。例如,可以声明下面这个函数,把它作为每个需要弃用的函数的第一-条语句。
void Deprecated(const std::string oldfunc. const std::string newfunc="");
...
std::string MyClass::GetName()
{
Deprecated( "MyC1ass::GetName", "MyClass::GetFul1Name");
...
}
Deprecated( )
的实现可以维护一个std::set
, 其中包含所有已经输出过警告的函数名。它支持只在第-次调用弃用的函数时才输出警告,从而避免方法被调用很多次时污染终端。Noel Llopis在他的写的Game Gem中描述了类似的技巧,只不过他的解决方案还记录了不重复的调用点数量,并在程序执行结束时把警告批量输出到独立的报告文件中(DeLoura,2001)。
1.2.4 移除功能
有些功能在至少一个发布版本中弃用后,可能最终会从API中移除。移除特性会破坏所有依赖该特性的现有客户程序。因此,应该首先把特性标记为已弃用,并通过警告向用户说明你要移除该功能的意图。
从API中移除功能是一项极端的操作。但如果某个功能不再继续维护,或者限制了API的改进能力,出于安全原因希望它不再被调用,那么就需要将其移除。
移除功能的同时仍然允许老用户访问旧功能的一种办法是提升主版本号,并声明新版本不是向后兼容的。然后从API的最新版本中彻底移除该功能,但仍然提供API的旧版本下载,让用户认识到这些功能已被弃用、不再支持,应当只在遗留应用中使用。甚至可以考虑把API的头文件存储在不同的目录,并重命名库,使得两个API互不冲突。这是一个大动作,不要经常这样做。没有什么事比让API的生命周期处于最佳状态更重要了。
2 跨平台问题
一般在软件设计时,是一套SDK文件可以应对所有平台的。
在做好操作系统相关宏隔离以及编译选项的不同,在接口层面是不用关注跨平台性问题的。
2.1 WINDOWS平台下关于宏隔离是用如下处理的
#if defined(_MSC_VER) || defined(_ADESK_MAC_)
#ifndef ACCORE_PORT
#ifdef ACCORE_API
#include "adesk.h"
#define ACCORE_PORT ADESK_EXPORT
#define ACCORE_DATA_PORT _declspec(dllexport)
#define ACCORE_STATIC_DATA_PORT _declspec(dllexport) static
#else
#define ACCORE_PORT
#define ACCORE_DATA_PORT _declspec(dllimport)
#define ACCORE_STATIC_DATA_PORT _declspec(dllimport) static
#endif
#endif
#else
#define ACCORE_PORT
#define ACCORE_DATA_PORT
#define ACCORE_STATIC_DATA_PORT static
#endif