相信很多同学都知道或者听说过N-API,也相信很多同学都使用过开发过用C/rust开发的N-API或者是比较火的napi-rs,但大多是一知半解的状态,每次在学习的时候都会看到一个名词ABI,他们究竟是啥玩意儿呢,那本文就稍微深入一点N-API和ABI。
ABI是软件接口的一部分,它定义了二进制级别的调用约定和数据结构布局,用于程序或模块之间(通常是不同的语言或库)如何交换数据和调用函数。
简单来说,ABI 描述了程序如何:
ABI 是编译器、操作系统和硬件平台之间的桥梁,它确保了不同组件、语言或库能够无缝地协作。它与API(应用程序接口)不同,API 是源代码级的接口,而 ABI 是二进制级别的接口。
N-API(Node.js API)是 Node.js 提供的一套 API,专门用于开发原生插件(native addons),即用 C/C++ 或其他语言编写的库,以便与 Node.js 进行交互。N-API 的目标是提供一个稳定的接口,允许开发者编写高性能的原生模块,并能够跨 Node.js 版本兼容运行。
N-API 的功能包括:
通过 N-API,开发者可以在 JavaScript 和 C/C++ 之间传递数据、调用函数,进而构建高性能的原生模块。例如,Node.js 使用 N-API 来调用 C/C++ 编写的扩展,这些扩展可以在require()
中直接加载使用。
假设我们想在 Node.js 中调用一个用 C++ 编写的库函数,C++ 编写的函数使用了一定的 ABI(例如 x86_64 ABI,或者 ARM ABI)来管理函数调用和内存布局。为了让 Node.js 调用这个函数,有两个方法:
总结:N-API是Node.js的针对于ABI的抽象层,N-API避免了直接处理底层 ABI 的复杂性,提供了跨平台的兼容性,减少了开发的复杂度和维护成本。
通过rust编写两个方法,输出“hello world”和add方法,对比ABI和N-API实现上的区别
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"]
ffi-napi
)或手动创建 C++ 模块来加载和调用。如果使用的是C语言,那我们需要使用我们的好兄弟“node-gyp
”编译成.node
文件,同样通过 FFI 或其他手段将 C 函数导入到 Node.js 中。
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"]
特性 | 通过 ABI 实现 | 通过 N-API 实现 |
---|---|---|
抽象层次 | ||
跨平台兼容性 | ||
内存管理 | ||
灵活性 | ||
开发复杂度 | ||
性能 |