cover_image

深入一点N-API和ABI

蔡钧 Goodme前端团队
2024年12月16日 00:30

相信很多同学都知道或者听说过N-API,也相信很多同学都使用过开发过用C/rust开发的N-API或者是比较火的napi-rs,但大多是一知半解的状态,每次在学习的时候都会看到一个名词ABI,他们究竟是啥玩意儿呢,那本文就稍微深入一点N-APIABI

知识点

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 调用这个函数,有两个方法:

  1. 使用N-API 编写一个 Node.js 扩展,包装 C++ 函数,以便 Node.js 能调用。
  2. 通过ABI,确保你编写的 C++ 代码能正确地与 Node.js 和操作系统之间进行交互(如参数传递、内存分配、返回值处理等)。

总结:N-API是Node.js的针对于ABI的抽象层,N-API避免了直接处理底层 ABI 的复杂性,提供了跨平台的兼容性,减少了开发的复杂度和维护成本。

实现

通过rust编写两个方法,输出“hello world”和add方法,对比ABI和N-API实现上的区别

ABI

  1. 编写代码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
}
  1. 设置Cargo.toml
[package]
name = "rust_node_module"
version = "0.1.0"
edition = "2024"

[dependencies]

[lib]
crate-type = ["cdylib"]
  1. 通过Node.js 的 FFI(ffi-napi)或手动创建 C++ 模块来加载和调用。

如果使用的是C语言,那我们需要使用我们的好兄弟“node-gyp ”编译成.node 文件,同样通过 FFI 或其他手段将 C 函数导入到 Node.js 中。

N-API

  1. 编写代码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(())
}

  1. 设置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"]
  1. 编译成共享库“.node”即可直接通过require使用

总结

特性通过 ABI 实现通过 N-API 实现
抽象层次
直接处理底层 ABI,要求开发者自己管理内存和数据转换
高层次的抽象,通过 N-API 自动处理内存管理、类型转换等
跨平台兼容性
需要手动适配不同操作系统和平台的 ABI
自动跨平台兼容,N-API 处理不同平台和 Node.js 版本之间的差异
内存管理
开发者需要手动管理内存分配、数据转换和释放
自动内存管理,N-API 处理 JavaScript 和 Rust 类型之间的转换
灵活性
极高,能够精细控制内存布局、函数调用约定等底层细节
适度灵活,适合大多数应用,但会限制某些低层的控制或优化
开发复杂度
较高,需要理解并处理 ABI、内存管理和类型转换的低级细节
较低,简化了大部分复杂性,使开发者专注于功能实现
性能
性能最优,可以精确控制数据交换和调用过程
可能有轻微的性能开销,但通常足够高效,适用于大多数应用

继续滑动看下一个
Goodme前端团队
向上滑动看下一个