Skip to main content

Announcing NAPI-RS v2

· 8 min read

🦀 NAPI-RS v2 - Faster 🚀 , Easier to use, and compatible improvements.

📅 2021/12/17

We are proudly announcing the release of NAPI-RS v2. This is the biggest release of NAPI-RS ever.

- A minimal library for building compiled Node.js add-ons in Rust via Node-API
+ A framework for building compiled Node.js add-ons in Rust via Node-API

The v2 was started at Aug 10, 2021. v2 aims to provide easier use API and better compatible with the Node.js ecosystem.

The core of the v2 release is the new macro API for defining JavaScript values in Rust. Let's see the differences between v1 and v2 by implementing a minimal runnable sum function:

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)
}

The v2 API is clearly more clean and elegant. The complexity of the value cast between Node.js value and Rust value is hidden by the new #[napi] macro. You will not be confused by how to get a value via Node-API and how to cast a Rust value into JsValue anymore.

What's new in NAPI-RS v2​

NAPI-RS v2 is totally rewrite top on the v1 codebase. But most of the v1 API is still available for compatibility. Which means you can smoothly upgrade to v2 in most cases.

Besides the small refactor and breaking changes in on the v1 API, there are also some new exciting features in v2.

TypeScript and JavaScript binding files generation​

NAPI-RS now will generate TypeScript definition and JavaScript binding files for you. In previous version, you need @node-rs/helper to help you load the right native addon. But this package has many problem with the existed JavaScript toolchain. Like #316 and #491.

In the NAPI-RS v2, we are totally rewrite the JavaScript load logic and there will be no more need to use @node-rs/helper anymore, and you can use the packages which are built by NAPI-RS with webpack, vercel and the others JavaScript toolchain.

Support async fn​

With the powerful #[napi] macro, you can define async functions in Rust. And the async fn will be converted into JavaScript async function.

lib.rs
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
}

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

index.d.ts
export function readFileAsync(path: string): Promise<Buffer>

Await Promise in the Rust​

This sounds crazy, but you can do it in NAPI-RS!

lib.rs
use napi::bindgen_prelude::*;

#[napi]
pub async fn async_plus_100(p: Promise<u32>) -> Result<u32> {
let v = p.await?;
Ok(v + 100)
}
test.mjs
import { asyncPlus100 } from './index.js'

const fx = 20
const result = await asyncPlus100(
new Promise((resolve) => {
setTimeout(() => resolve(fx), 50)
}),
)

console.log(result) // 120

The JavaScript Promise will be converted into Promise<T> struct in Rust, and std::future::Future trait will be implemented for it. So you can use await keyword in Rust on it.

Define Class with struct​

Like PyO3 and node-bindgen, you can define a class in Rust with struct and #[napi] macro.

lib.rs
// A complexity struct which can not be exposed into JavaScript directly.
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;
}
}

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

index.d.ts
export class QueryEngine {
static withInitialCount(count: number): QueryEngine
constructor()
query(query: string): Promise<string>
get status(): number
set count(count: number)
}

See class for more details.

Rust enum into JavaScript Object​

lib.rs
#[napi]
enum Kind {
Duck,
Dog,
Cat,
}

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

index.d.ts
export const enum Kind {
Duck,
Dog,
Cat,
}

exports Rust const​

lib.rs
#[napi]
pub const DEFAULT_COST: u32 = 12;
index.d.ts
export const DEFAULT_COST: number

Abortable AsyncTask​

lib.rs
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)
}

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

index.d.ts
export function asyncFib(input: number, signal?: AbortSignal | null) => Promise<number>

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

test.mjs
import { asyncFib } from './index.js'

const controller = new AbortController()

asyncFib(20, controller.signal).catch((e) => {
console.error(e) // Error: AbortError
})

controller.abort()

See AsyncTask for more details.

Support export Rust mod as JavaScript Object​

lib.rs
#[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
}
}

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

index.d.ts
export namespace xxh3 {
export const ALIGNMENT: number
export function xxh3_64(input: Buffer): BigInt
export function xxh128(input: Buffer): BigInt
}

Breaking changes​

Besides the new features, v2 also brings some breaking changes.

Rust version​

The minimal version of Rust required to use napi is 1.57.0 because of the new #[napi] macro requires 60fe8b3.

Task trait​

The fn resolve and fn reject methods of Task trait now accepted &mut self rather thant self. Because we introduced a new fn finally method on it.

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 now accept single name: &str:

- Property::new(&env, "name)
+ Property::new("name")

Can I upgrade now?​

Yes, v2 beta has been tested in many projects. Including SWC Prisma and @parcel/source-map, and many other projects in the NAPI-RS ecosystem.

What's the next step​

NAPI-RS has grown to be a vast ecosystem. We are planning to add more platform support to make Rust codes easier to deploy to the Developers and the end Users.

The first priority feature in the future is the WebAssembly support. We want to make the existed projects with NAPI-RS v2 being able to compile into WebAssembly with no extra effort. (If the crates they are using supported WebAssembly). And after that, it's more easier for developers to share codes between Node.js and Browser.

And we want to investigate the Deno FFI support too. See #12577 for the context.

Thanks​

yiliuliuyi for the initializing the v2 alpha version. And most of the #[napi] macro was implemented by him.

Jared Palmer for reviewing the full documentation and the blog.

node-bindgen neon and wasm-bindgen inspiring many of API designs in the v2.

info

Special thanks to my wife. Without the weekends she sacrificed, I probably wouldn't even know how to Rust!

Contributors ✨​

Thanks goes to these wonderful people ✨:


Ouyang Yadong

💻

Nathaniel Daniel

💻

messense

💻 🚇

Chris Nixon

💻 🔧

Daniel Henry-Mantilla

💻

Michael

💻

Tim Fish

💻 📖

一丝

💻 🔧

Daniel Maslowski

💻

Franz Heinzmann

💻

Idan Attias

💻

Jasper De Moor

💻

João Moreno

💻

Julius de Bruijn

💻

Kirill Fomichev

💻

Sam Holmes

💻

Hana

💻