Announcing NAPI-RS v2
🦀 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
Work for v2
started on Aug 10, 2021 and it aims to provide easier to use API’s and better compatibility 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 cleaner and more 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
any more.
What’s new in NAPI-RS v2
NAPI-RS v2 is totally rewrite on top of 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 existing JavaScript toolchain. Like #316 and #491.
In the NAPI-RS v2
, we totally rewrote the JavaScript load logic and there will be no more need to use @node-rs/helper
. You can now use packages built by NAPI-RS with webpack
, vercel
and the others JavaScript toolchains.
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
.
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>
Await Promise in the Rust
This sounds crazy, but you can do it in 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) // 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.
// A complex 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;
}
}
⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
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
#[napi]
enum Kind {
Duck,
Dog,
Cat,
}
⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
export const enum Kind {
Duck,
Dog,
Cat,
}
exports Rust const
#[napi]
pub const DEFAULT_COST: u32 = 12;
export const DEFAULT_COST: number
Abortable 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()
See AsyncTask
for more details.
Support export Rust mod
as 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
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 easier for Developers and end Users to deploy Rust
.
The first priority feature in the future is the WebAssembly
support. We want to allow existing projects with NAPI-RS v2 able to compile into WebAssembly
with no extra effort. (If the crates they are using supported WebAssembly
). After that, it’s easier for developers to share code between Node.js and the Browser.
And we want to investigate the Deno FFI
support too. See #12577 for the context.
Thanks
yiliuliuyi for initiating 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
.
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 ✨: