ABI 稳定性

介绍

应用程序二进制接口(Application Binary Interface,ABI)是一种让程序能够调用其他已编译程序的函数和使用其数据结构的方式。它是应用程序编程接口(API)的编译版本。换句话说,描述类、函数、数据结构、枚举和常量的头文件,使应用程序能够执行所需任务,通过编译对应于一组地址、预期的参数值以及内存结构的大小和布局,这些都是 ABI 提供者编译时所使用的。

使用 ABI 的应用程序必须以这样的方式编译,即其可用的地址、预期的参数值以及内存结构的大小和布局,必须与 ABI 提供者编译时所用的一致。这通常通过针对 ABI 提供者提供的头文件进行编译来实现。

由于 ABI 的提供者和使用者可能在不同时间使用不同版本的编译器进行编译,因此确保 ABI 兼容性的部分责任在于编译器。不同版本的编译器,可能来自不同的供应商,必须从具有特定内容的头文件中生成相同的 ABI,并且必须为使用 ABI 的应用程序生成代码,该代码根据头文件中描述所产生的 ABI 的约定来访问 API。现代编译器在不破坏其编译的应用程序的 ABI 兼容性方面有相当好的记录。

确保 ABI 兼容性的其余责任在于维护头文件的团队,这些头文件提供了 API,经编译后产生需要保持稳定的 ABI。可以对头文件进行更改,但必须密切跟踪更改的性质,以确保编译后,ABI 不会以一种会使现有 ABI 用户与新版本不兼容的方式发生变化。

Node.js 中的 ABI 稳定性

Node.js 提供了由几个独立团队维护的头文件。例如,像 node.hnode_buffer.h 这样的头文件由 Node.js 团队维护。v8.h 由 V8 团队维护,该团队虽然与 Node.js 团队密切合作,但却是独立的,并且有自己的时间表和优先事项。因此,Node.js 团队只能部分控制项目提供的头文件中的更改。因此,Node.js 项目采用了语义化版本控制。这确保了项目提供的 API 在一个主版本内发布的所有次要版本和补丁版本中都会产生稳定的 ABI。实际上,这意味着 Node.js 项目承诺确保,针对特定主版本的 Node.js 编译的 Node.js 原生插件,在由该主版本内的任何 Node.js 次要或补丁版本加载时,都能成功加载。

N-API

现在出现了为 Node.js 配备一个 API 的需求,该 API 产生的 ABI 能够在多个 Node.js 主版本之间保持稳定。创建这样一个 API 的动机如下:

  • JavaScript 语言自早期以来一直保持着自身的兼容性,而执行 JavaScript 代码的引擎的 ABI 却随着 Node.js 的每个主版本的更新而改变。这意味着,当新的 Node.js 主版本被部署到生产环境中时,完全用 JavaScript 编写的 Node.js 包组成的应用程序不需要重新编译、重新安装或重新部署。相比之下,如果一个应用程序依赖于包含原生插件的包,那么每当新的 Node.js 主版本被引入生产环境时,该应用程序就必须重新编译、重新安装和重新部署。这种包含原生插件的 Node.js 包与完全用 JavaScript 编写的包之间的差异,增加了依赖原生插件的生产系统的维护负担。

  • 其他项目已经开始产生一些 JavaScript 接口,这些接口本质上是 Node.js 的替代实现。由于这些项目通常基于不同于 V8 的 JavaScript 引擎构建,它们的原生插件必然具有不同的结构并使用不同的 API。然而,在 Node.js JavaScript API 的不同实现中使用单一的 API 来开发原生插件,将使这些项目能够利用围绕 Node.js 积累起来的 JavaScript 包生态系统。

  • Node.js 将来可能会包含一个不同的 JavaScript 引擎。这意味着,从外部看,所有的 Node.js 接口都将保持不变,但 V8 头文件将不复存在。如果 Node.js 不首先提供一个与 JavaScript 引擎无关的 API 并被原生插件所采用,那么这一步将对 Node.js 生态系统,特别是原生插件生态系统造成破坏。

为此,Node.js 在 8.6.0 版本中引入了 N-API,并在 Node.js 8.12.0 版本中将其标记为项目的稳定组件。该 API 在头文件 node_api.hnode_api_types.h 中定义,并提供了一个跨越 Node.js 主版本边界的向前兼容性保证。该保证可以表述如下:

N-API 的特定版本 n 将在其发布的 Node.js 主版本以及所有后续的 Node.js 版本中可用,包括后续的主版本。

原生插件的作者可以通过确保插件只使用 node_api.h 中定义的 API 以及 node_api_types.h 中定义的数据结构和常量,来利用 N-API 的向前兼容性保证。通过这样做,作者向生产用户表明,将该原生插件添加到他们的项目中,其应用程序的维护负担不会比添加一个纯 JavaScript 编写的包增加得更多,从而促进了其插件的采用。

N-API 是有版本的,因为新的 API 会不时地被添加。与语义化版本控制不同,N-API 的版本控制是累积的。也就是说,N-API 的每个版本都传达了与 semver 系统中次要版本相同的含义,这意味着对 N-API 所做的所有更改都将是向后兼容的。此外,新的 N-API 是在实验性标志下添加的,以便社区有机会在生产环境中对其进行审查。实验性状态意味着,尽管已经采取了措施确保新的 API 将来不必以 ABI 不兼容的方式进行修改,但它尚未在生产中被充分证明其设计是正确和有用的,因此,在最终被纳入即将到来的 N-API 版本之前,可能会经历 ABI 不兼容的更改。也就是说,实验性的 N-API 尚未被向前兼容性保证所覆盖。