NAPI-RS v2 发布
🦀 NAPI-RS v2 - 更快 🚀 (opens in a new tab) , 更易用,与 Node.js 生态更好兼容.
📅 2021/12/17
很高兴能在此宣布 NAPI-RS v2 的发布。 这是 NAPI-RS 有史以来最大的一次更新。在这次更新以后,NAPI-RS 从一个轻量级 Rust 库,变成了一个强大的框架。
v2 的开发从 2021/08/10 (opens in a new tab) 开始. v2 旨在提供更易用的 API 和与 Node.js 生态更好的兼容性.
v2 版本的核心是新的 Rust 宏 API, 通过新的 #[napi] 宏,你可以更轻松的在 Rust 中定义 JavaScript 值。让我们看一下 v1 和 v2 版本同样定一个最小可运行的 sum 函数使两数相加的例子:
v2
use napi_derive::napi;
#[napi]
fn sum(a: u32, b: u32) -> u32 {
a + b
}v1
use napi::{CallContext, JsNumber, JsObject, Result};
use napi_derive::{module_exports, js_function};
#[module_exports]
fn init(mut exports: JsObject) -> Result<()> {
exports.create_named_method("sum", sum)?;
Ok(())
}
#[js_function(1)]
fn sum(ctx: CallContext) -> Result<JsNumber> {
let a = ctx.get::<JsNumber>(0)?.get_uint32()?;
let b = ctx.get::<JsNumber>(0)?.get_uint32()?;
ctx.env.create_uint32(a + b)
}可以看到 v2 提供的 API 明显更加简洁优雅。从 Node.js 到 Rust 值的相互转换过程被新提供的 #[napi] 宏隐藏了起来。你再也不用为如何通过底层的 Node-API 从 Node.js 将某个 JsValue 转换到 Rust 值,或者如何反过来转换而感到困惑。
NAPI-RS v2 有哪些新特性
NAPI-RS v2 是基于 v1 完全重写而来的。但大部分 v1 提供的 API 都在新版本保留,以便基于 v1 的库可以兼容性升级。所以大部分基于 v1 开发的库在大部分情况下可以非常顺畅的直接将版本号升级到 v2。
自动生成 TypeScript 和 JavaScript 绑定文件
NAPI-RS 现在会自动为你的项目生成 JavaScript 和 TypeScript 绑定文件。在上一个版本,你需要使用 @node-rs/helper (opens in a new tab) 这个库帮你加载正确的 native addon 文件/包。但是这个库对于 Node.js 生态的一些工具不太友好,因为它的加载逻辑过于动态。比如 #316 (opens in a new tab) 和 #491 (opens in a new tab)。
NAPI-RS v2 完全重新设计了基于 optionalDependencies 分发二进制包的 native addon 加载逻辑。现在我们不需要 @node-rs/helper 了,在生成的 JavaScript 绑定文件中它会自动帮你找到正确的 native addon 的位置并且加载。所以现在你可以非常顺畅的在 webpack vercel 等工具和平台中使用 NAPI-RS 构建的包了。
支持 Rust async fn
使用功能强大的 #[napi] 宏, 你可以在 Rust 里定义 async fn. 然后这个 async fn 会被转换成 JavaScript 的 async function。
use futures::prelude::*;
use napi::bindgen_prelude::*;
use tokio::fs;
#[napi]
async fn read_file_async(path: String) -> Result<Buffer> {
fs::read(path)
.map(|r| match r {
Ok(content) => Ok(content.into()),
Err(e) => Err(Error::new(
Status::GenericFailure,
format!("failed to read file, {}", e),
)),
})
.await
}⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
export function readFileAsync(path: string): Promise<Buffer>在 Rust 中 await Promise
这个功能看起来很疯狂,但是在 NAPI-RS 里你可以这样做!
use napi::bindgen_prelude::*;
#[napi]
pub async fn async_plus_100(p: Promise<u32>) -> Result<u32> {
let v = p.await?;
Ok(v + 100)
}import { asyncPlus100 } from './index.js'
const fx = 20
const result = await asyncPlus100(
new Promise((resolve) => {
setTimeout(() => resolve(fx), 50)
}),
)
console.log(result) // 120JavaScript Promise 会被转化成 Rust 里的 Promise<T> struct, 并且会实现 std::future::Future trait. 所以你可以直接在上面使用 Rust 的 await 关键字.
使用 struct 定义 Class
与 PyO3 (opens in a new tab) 和 node-bindgen (opens in a new tab) 类似, 你可以使用 Rust struct 和 #[napi] 宏定义一个 JavaScript Class。
// 一个无法直接暴露给 JavaScript 的复杂结构.
struct QueryEngine {}
#[napi(js_name = "QueryEngine")]
struct JsQueryEngine {
engine: QueryEngine,
}
#[napi]
impl JsQueryEngine {
#[napi(factory)]
pub fn with_initial_count(count: u32) -> Self {
JsQueryEngine { engine: QueryEngine::with_initial_count(count) }
}
#[napi(constructor)]
pub fn new() -> Self {
JsQueryEngine { engine: QueryEngine::new() }
}
/// Class method
#[napi]
pub async fn query(&self, query: String) -> napi::Result<String> {
self.engine.query(query).await
}
#[napi(getter)]
pub fn status(&self) -> napi::Result<u32> {
self.engine.status()
}
#[napi(setter)]
pub fn count(&mut self, count: u32) {
self.engine.count = count;
}
}⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
export class QueryEngine {
static withInitialCount(count: number): QueryEngine
constructor()
query(query: string): Promise<string>
get status(): number
set count(count: number)
}移步 class 来查阅更多细节.
Rust enum 到 JavaScript Object
#[napi]
enum Kind {
Duck,
Dog,
Cat,
}⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
export const enum Kind {
Duck,
Dog,
Cat,
}导出 Rust const
#[napi]
pub const DEFAULT_COST: u32 = 12;export const DEFAULT_COST: number可中断的 AsyncTask
use napi::{Task, Env, Result, JsNumber, bindgen_prelude::AbortSignal};
struct AsyncFib {
input: u32,
}
impl Task for AsyncFib {
type Output = u32;
type JsValue = JsNumber;
fn compute(&mut self) -> Result<Self::Output> {
Ok(fib(self.input))
}
fn resolve(&mut self, env: Env, output: u32) -> Result<Self::JsValue> {
enc.create_uint32(output)
}
}
#[napi]
fn async_fib(input: u32, signal: Option<AbortSignal>) -> AsyncTask<AsyncFib> {
AsyncTask::with_optional_signal(AsyncFib { input }, signal)
}⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
export function asyncFib(input: number, signal?: AbortSignal | null) => Promise<number>⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
import { asyncFib } from './index.js'
const controller = new AbortController()
asyncFib(20, controller.signal).catch((e) => {
console.error(e) // Error: AbortError
})
controller.abort()移步 AsyncTask 查看更多细节.
支持 export Rust mod 到 JavaScript Object
#[napi]
mod xxh3 {
use napi::bindgen_prelude::{BigInt, Buffer};
#[napi]
pub const ALIGNMENT: u32 = 16;
#[napi(js_name = "xxh3_64")]
pub fn xxh64(input: Buffer) -> u64 {
let mut h: u64 = 0;
for i in input.as_ref() {
h = h.wrapping_add(*i as u64);
}
h
}
}⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
export namespace xxh3 {
export const ALIGNMENT: number
export function xxh3_64(input: Buffer): BigInt
export function xxh128(input: Buffer): BigInt
}Breaking changes
除了新功能以外, v2 也带来了一些不兼容更新.
最小支持的 Rust 版本
使用 napi 现在至少需要 Rust 1.57.0, 因为新的 #[napi] 宏需要 Rust 的这个功能: 60fe8b3 (opens in a new tab).
Task trait
Task trait 中的 fn resolve 和 fn reject 方法现在接受 &mut self 而不是 self。因为我们引入了一个新的 fn finally 方法,会在它们之后调用。
struct BufferLength(Ref<JsBufferValue>);
impl Task for BufferLength {
type Output = usize;
type JsValue = JsNumber;
fn compute(&mut self) -> Result<Self::Output> {
Ok(self.0.len() + 1)
}
- fn resolve(self, env: Env, output: Self::Output) -> Result<Self::JsValue> {
- self.0.unref(env)?;
+ fn resolve(&mut self, env: Env, output: Self::Output) -> Result<Self::JsValue> {
env.create_uint32(output as u32)
}
- fn reject(self, err: Error) -> Result<Self::JsValue> {
- self.0.unref(env)?;
- Err(err)
- }
+ fn finally(&mut self, env: Env) -> Result<()> {
+ self.0.unref(env)?;
+ Ok(())
+ }
}
Property::new
Property::new 现在只接受单个 name: &str 参数:
- Property::new(&env, "name)
+ Property::new("name")现在可以升级了吗?
是的!v2 beta 版本已经在很多项目中通过测试了。包括 SWC Prisma @parcel/source-map 和 NAPI-RS 生态中的许多其它项目。
下一步计划
NAPI-RS 生态最近一年扩张的非常快。 我们计划在新的 #[napi] 宏的基础上支持更多的平台来让 Rust 代码更容易编译部署到不同平台,能让更多不同平台的开发者和用户享受到 Rust 带来的各种强大功能。
在未来 WebAssembly 支持是最高优先级。 我们希望能让基于 NAPI-RS v2 开发的项目能无痛编译到 WebAssembly。 (在它使用到的 crate 都支持 WebAssembly 的前提下)。有了这个功能, 开发者可以更方便的在 Node.js 和浏览器之间共享代码。
我们也希望能开始调研如何支持 Deno FFI。 可以到这个 Issue #12577 (opens in a new tab) 了解更多上下文。
致谢
感谢 yiliuliuyi (opens in a new tab) 发起 v2 版本,他完成了大部分 #[napi] 宏的功能。
感谢 Jared Palmer (opens in a new tab) 审阅了所有文档和博客。
v2 的 API 设计和实现部分借鉴于 node-bindgen (opens in a new tab) neon (opens in a new tab) 和 wasm-bindgen (opens in a new tab).
特别感谢我的妻子。如果没有她牺牲掉的那些周末,我现在连 Rust 都不会写。
贡献者 ✨
感谢这些了不起的开发者的贡献 ✨: