作者:蔡钧
相信很多同学都知道或者听说过N-API,也相信很多同学都使用过开发过用C/rust开发的N-API或者是比较火的napi-rs,但大多是一知半解的状态,每次在学习的时候都会看到一个名词ABI,他们究竟是啥玩意儿呢,那本文就稍微深入一点N-API和ABI。
知识点
ABI(应用二进制接口)
ABI是软件接口的一部分,它定义了二进制级别的调用约定和数据结构布局,用于程序或模块之间(通常是不同的语言或库)如何交换数据和调用函数。
简单来说,ABI 描述了程序如何:
- 调用函数:包括函数参数的传递方式、返回值的处理、调用者和被调用者之间如何协作。
- 数据布局:指定数据(如结构体、类、数组等)如何在内存中布局,确保不同的程序能够以相同的方式解释内存中的数据。
- 栈帧管理:描述了函数调用过程中栈空间的分配和销毁方式。
- 二进制兼容性:ABI 保障不同编译器和平台之间的二进制兼容性。
ABI 是 编译器、操作系统和硬件平台之间的桥梁,它确保了不同组件、语言或库能够无缝地协作。它与 API(应用程序接口)不同,API 是源代码级的接口,而 ABI 是二进制级别的接口。
N-API (Node.js API)
N-API(Node.js API)是 Node.js 提供的一套 API,专门用于开发 原生插件(native addons),即用 C/C++ 或其他语言编写的库,以便与 Node.js 进行交互。N-API 的目标是提供一个稳定的接口,允许开发者编写高性能的原生模块,并能够跨 Node.js 版本兼容运行。
N-API 的功能包括:
- 定义和管理 JavaScript 类型(如字符串、对象、数组等)。
- 调用 JavaScript 函数。
- 处理异步操作。
- 管理原生内存。
通过 N-API,开发者可以在 JavaScript 和 C/C++ 之间传递数据、调用函数,进而构建高性能的原生模块。例如,Node.js 使用 N-API 来调用 C/C++ 编写的扩展,这些扩展可以在 require() 中直接加载使用。
N-API 和 ABI 对比
假设我们想在 Node.js 中调用一个用 C++ 编写的库函数,C++ 编写的函数使用了一定的 ABI(例如 x86_64 ABI,或者 ARM ABI)来管理函数调用和内存布局。为了让 Node.js 调用这个函数,有两个方法:
- 使用 N-API 编写一个 Node.js 扩展,包装 C++ 函数,以便 Node.js 能调用。
- 通过 ABI,确保你编写的 C++ 代码能正确地与 Node.js 和操作系统之间进行交互(如参数传递、内存分配、返回值处理等)。
总结:N-API是Node.js的针对于ABI的抽象层,N-API避免了直接处理底层 ABI 的复杂性,提供了跨平台的兼容性,减少了开发的复杂度和维护成本。
实现
通过rust编写两个方法,输出“hello world”和add方法,对比ABI和N-API实现上的区别
ABI
- 编写代码
lib.rs
#[no_mangle]
pub extern "C" fn hello_world() -> *const u8 {
"Hello, world!".as_ptr()
}
#[no_mangle]
pub extern "C" fn add(a: f64, b: f64) -> f64 {
a + b
}
- 设置
Cargo.toml
[package]
name = "rust_node_module"
version = "0.1.0"
edition = "2024"
[dependencies]
[lib]
crate-type = ["cdylib"]
- 通过Node.js 的 FFI(
ffi-napi)或手动创建 C++ 模块来加载和调用。
如果使用的是C语言,那我们需要使用我们的好兄弟“ node-gyp ”编译成 .node 文件,同样通过 FFI 或其他手段将 C 函数导入到 Node.js 中。
N-API
- 编写代码
lib.rs
use napi::{bindgen_prelude::*, Result};
#[js_function(0)]
fn hello_world(ctx: CallContext) -> Result<String> {
Ok("Hello, world!".into())
}
#[js_function(2)]
fn add(ctx: CallContext) -> Result<f64> {
let a = ctx.get::<f64>(0)?;
let b = ctx.get::<f64>(1)?;
Ok(a + b)
}
#[module_exports]
fn init(mut exports: NodeExports) -> Result<()> {
exports.create_function("helloWorld", hello_world)?;
exports.create_function("add", add)?;
Ok(())
}
- 设置
Cargo.toml
[package]
name = "rust_node_module"
version = "0.1.0"
edition = "2024"
[dependencies]
napi = "2.0"
[lib]
name = "my_rust_module"
crate-type = ["cdylib"]
- 编译成共享库“.node”即可直接通过require使用
总结
| 特性 | 通过 ABI 实现 | 通过 N-API 实现 |
|---|---|---|
| 抽象层次 | 直接处理底层 ABI,要求开发者自己管理内存和数据转换 | 高层次的抽象,通过 N-API 自动处理内存管理、类型转换等 |
| 跨平台兼容性 | 需要手动适配不同操作系统和平台的 ABI | 自动跨平台兼容,N-API 处理不同平台和 Node.js 版本之间的差异 |
| 内存管理 | 开发者需要手动管理内存分配、数据转换和释放 | 自动内存管理,N-API 处理 JavaScript 和 Rust 类型之间的转换 |
| 灵活性 | 极高,能够精细控制内存布局、函数调用约定等底层细节 | 适度灵活,适合大多数应用,但会限制某些低层的控制或优化 |
| 开发复杂度 | 较高,需要理解并处理 ABI、内存管理和类型转换的低级细节 | 较低,简化了大部分复杂性,使开发者专注于功能实现 |
| 性能 | 性能最优,可以精确控制数据交换和调用过程 | 可能有轻微的性能开销,但通常足够高效,适用于大多数应用 |