本文首发公众号 猩猩程序员 欢迎关注
自 NAPI-RS V2 发布已有四年。在这段时间里,NAPI-RS 社区发展迅速。我们发现了许多 API 设计上的问题。例如,ThreadsafeFunction
一直很难使用。主要原因是 Node-API 的 ThreadsafeFunction
设计得过于复杂,导致 Rust 的封装暴露了底层复杂性。然而,通过与 Rolldown 和 Rspack 团队的合作,我们最终找到了一个可以平衡 API 复杂度和正确性的设计。
WebAssembly 是此次更新的最大亮点。在 V3 中,您几乎无需更改代码即可将项目编译为 WebAssembly。如果编译目标是 wasm32-wasip1-threads
或更高版本,您可以直接在浏览器中运行使用 Rust 特性如 std::thread
和 tokio
的代码,无需额外修改。
跨编译也是一个重大的更新。在之前的版本中,您需要使用 nodejs-rust:lts-debian
或 nodejs-rust:lts-debian-aarch64
Docker 镜像来构建项目。这些镜像体积庞大,导致 CI 构建时间变慢,并且很难与社区同步工具和基础设施。
接下来,让我们深入了解 V3 中的新特性。
WebAssembly
支持 WebAssembly 对 NAPI-RS 社区意义重大。WebAssembly 可以处理以下几种场景:
- 提供浏览器中的游乐场和可重现环境,如 Rolldown repl 和 Oxc playground。
- 为没有预构建二进制文件的平台提供回退包。对于某些项目来说,很难为所有可能的平台维护预构建的二进制文件。
- 使项目能够在 StackBlitz 上使用。
为什么不使用 wasm-bindgen?
其中一个主要原因是,您不需要为同一个项目编写两种不同的绑定。
例如,Oxc 项目之前维护了一个 wasm-bindgen
绑定。但是,随着项目规模的扩大,API 增加,维护成本也变得越来越高,通常需要将相同的逻辑从 Node.js 绑定移植到 wasm-bindgen
绑定。
此外,使用 wasm-bindgen
还有许多限制,例如无法使用 std::thread
和依赖 std::thread
的第三方库。例如,您可能需要编写如下代码:
rust
#[cfg(not(target_arch = "wasm32"))]
use rayon::prelude::*;
...
#[cfg(not(target_arch = "wasm32"))]
const hash = entries.par_iter().map(|chunk| chunk.compute_hash()).collect::<Vec<_>>();
#[cfg(target_arch = "wasm32")]
const hash = entries.iter().map(|chunk| chunk.compute_hash()).collect::<Vec<_>>();
...
使用 NAPI-RS,您可以直接编译代码并运行,无需额外的麻烦。
另一个痛点是,如果您使用的是 C 或 C++ 编写的 crate,设置 wasm-bindgen
的构建过程非常复杂。详情请见:
NAPI-RS WebAssembly 示例应用
这是一个使用 NAPI-RS WebAssembly 的示例应用。您可以将图像转换为 WebP、JPEG 或 AVIF 格式,并调整不同的质量设置。
WebP 功能来自 libwebp-sys
crate,它底层使用了 libwebp
。libwebp
是一个 C 库,但您可以自由地在 NAPI-RS 项目中使用它,并将其编译为 WebAssembly,而无需做任何额外修改。
AVIF 功能来自 libavif
,它是一个 C/C++ 混合库,您同样可以自由地在 NAPI-RS 项目中使用它。
原始图像:
- 原始大小:13.3 MB
WebP 转换:
- 质量:75
- 转换后的 WebP 图像大小:0 B
API 改进
此次更新在 API 设计上做了许多改进,增强了可用性和安全性。
生命周期
NAPI-RS V3 引入了生命周期(lifetime)的概念,详情请参见 理解生命周期。
在 V2 中,由于代码生成和 API 设计的复杂性,我们没有时间为 API 添加生命周期,导致出现了一些问题。
一些类型存在安全问题,例如之前的 JsObject
类型,它可能在 #[napi] fn
调用之外被使用,实际其底层的 napi_value
已经变得无效。现在,我们可以通过 Rust 的生命周期来约束这种行为。
例如:
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;
#[napi]
pub fn promise_finally_callback(mut promise: PromiseRaw<()>, config: Object) -> Result<()> {
// ❌ 编译错误
// 借用的数据逃逸了函数作用域
// `config` 在此处逃逸了函数体
promise.finally(|env| {
let on_finally = config.get_named_property::<Function<(), ()>>("on_finally")?;
Ok(())
})?;
Ok(())
}
在 V3 中,提供了相关的参考 API,更多信息请参见 JavaScript 值引用。
ThreadsafeFunction
ThreadsafeFunction
在 V3 中重新设计。在之前的版本中,ThreadsafeFunction
的 API 过于底层,且不安全。在新的 API 中,我们隐藏了 Node-API 中的 ref
、unref
、acquire
、release
等概念,使用所有权来封装这些 API,并禁止使用底层的引用计数模型进行生命周期管理。
如果您需要将 ThreadsafeFunction
传递到不同的线程,现在我们允许使用 std::sync::Arc
来实现。
rust
use std::sync::Arc;
use napi::{
bindgen_prelude::*,
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
};
use napi_derive::napi;
#[napi]
pub fn pass_threadsafe_function(tsf: Arc<ThreadsafeFunction<u32, u32>>) -> Result<()> {
for i in 0..100 {
let tsf = tsf.clone();
std::thread::spawn(move || {
tsf.call(Ok(i), ThreadsafeFunctionCallMode::NonBlocking);
});
}
Ok(())
}
TypeScript 类型生成也有所改进。在 V2 中,您只能为 ThreadsafeFunction
生成 (...args: any[]) => any
类型,但在 V3 中,由于在泛型中定义了 FnArgs
和 Return
类型,您现在可以为 ThreadsafeFunction
生成正确的类型。
跨编译
跨编译一直是 Rust 社区的一大痛点。尽管有 rust-cross
项目,但使用起来并不容易:
- GNU Linux 发行版的 GLIBC 版本太新,例如
arm-unknown-linux-gnueabihf
仅支持 GLIBC 2.31。 - 配置旧版 GLIBC 2.17(许多企业仍需支持的版本)非常复杂。
- 在 QEMU 和 Docker 上运行非常慢且有限。
NAPI-RS V3 引入了新的跨编译功能,您可以通过 napi build --use-napi-cross
标志来支持以下跨编译矩阵:
Target/Host | x86_64 | arm64 |
---|---|---|
x86_64-unknown-linux-gnu | ✅ | ✅ |
aarch64-unknown-linux-gnu | ✅ | ✅ |
armv7-unknown-linux-gnueabihf | ✅ | ✅ |
powerpc64le-unknown-linux-gnu | ✅ | ✅ |
s390x-unknown-linux-gnu | ✅ | ✅ |
所有这些目标都支持 GLIBC 2.17。
这是跨工具链的 GitHub 仓库。我们基本上从 manylinux-cross 项目中提取了必要的工具和文件,然后上传到 npm 注册表中。@napi-rs/cli 将选择正确的工具链并将环境变量注入构建过程中。
我们还将 cargo-zigbuild
和 cargo-xwin
集成到 @napi-rs/cli 中,因此您可以在一台机器上构建多个不同的目标。详情请见 跨编译。
全新的 pnpm 包模板
自 V2 起,我们已支持在 package-template
项目中使用 yarn 作为包管理器,因为 yarn 的 supportedArchitectures
特性非常适合跨平台编译和测试,并且有很好的 Docker 支持。
随着 pnpm 的流行,我们也在 V3 中支持了 package-template-pnpm
。
为什么没有 npm 包模板?
因为存在以下问题:
BUG\] 在重新安装 时,平台特定的可选依赖未包含在 `package-lock.json` 中 @JustinChristensen 详细信息见 npm/issues。 ### NAPI-RS API 您现在可以轻松地将 NAPI-RS 工具集成到您的 JavaScript 基础设施中: ```php // 程序化调用 import { NapiCli } from '@napi-rs/cli' const cli = new NapiCli() const { task, abort } = await cli.build({ release: true, features: ['allocator-api'], esm: true, platform: true, }) const outputs = await task ``` 所有 NAPI 命令都有相应的 API,您可以访问 [cli 文档](https://link.juejin.cn?target=https%3A%2F%2Fchatgpt.com%2Fc%2F68801f74-52ac-8002-b9e7-d092a1f97e9c%23 "https://chatgpt.com/c/68801f74-52ac-8002-b9e7-d092a1f97e9c#") 了解更多信息。 ### 社区发展迅速! 当 V2 发布时,只有 Next.js、Parcel 和 SWC 在使用 NAPI-RS。 如今,NAPI-RS 已广泛应用于各种类型应用程序的开发中。 本文首发公众号 **猩猩程序员** 欢迎关注