# Getting started The quickest way to start a napi-rs v3 package is `napi new`. It copies the maintained package template, applies your package name and target selection, and optionally creates a GitHub Actions workflow. ## Prerequisites - **Node.js for the `@napi-rs/cli` toolchain.** The minimum supported versions are `^20.17.0 || ^22.13.0 || >=23.5.0` (the `engines` field of the CLI). Node.js 22.13+ on the Node 22 LTS line, or Node.js 24+, is recommended. This build-time requirement is separate from the runtime requirement of the addon you produce. See [Support and compatibility](/docs/more/support-compatibility#cli-and-rust-requirements). - **Rust 1.88 or newer**, including Cargo. Installing Rust through [rustup](https://rustup.rs/) is recommended. - **Git**, because `napi new` downloads and updates its template with Git. - A working linker for your development platform: Xcode Command Line Tools on macOS, MSVC Build Tools on Windows, or the usual C build tools on Linux. Node-API makes a native binary ABI-compatible with later Node.js releases that provide the Node-API level it was compiled against. That is different from the Node versions and target triples exercised by napi-rs CI. Read [Support and compatibility](/docs/more/support-compatibility) before choosing a runtime or shipping matrix. ## Create a project You do not need a global CLI installation. Run the package directly with your preferred package runner: ::: pm ```sh npm npx @napi-rs/cli new cool ``` ```sh yarn yarn dlx @napi-rs/cli new cool ``` ```sh pnpm pnpm dlx @napi-rs/cli new cool --package-manager pnpm ``` ::: The command is interactive by default. It asks for: 1. The package name written to `package.json`. 2. The minimum Node-API level used for the generated Cargo feature and package Node.js engine requirement. 3. The target triples to keep from the selected template. 4. The license. 5. Whether to generate TypeScript declarations. 6. Whether to keep the template's GitHub Actions workflow. Only the maintained **Yarn** and **pnpm** templates are supported. The template pins its own package-manager version, so use the matching commands after the project is created. To create a project without prompts, pass every value you want to change and add `--no-interactive`; see [`napi new`](/docs/cli/new). ## Install, build, and test Inside the new project, install dependencies, then build and test: ::: pm ```sh yarn cd cool yarn install yarn build yarn test ``` ```sh pnpm cd cool pnpm install pnpm build pnpm test ``` ::: The local build compiles one native target: your host unless you pass `--target`. It produces: - `..node`, the native addon. - `index.js`, the generated loader. - `index.d.ts`, the generated TypeScript declarations when type generation is enabled. The important source files in the generated project are: | Path | Purpose | | -------------------------- | ------------------------------------------------------------ | | `src/lib.rs` | Rust functions, structs, and classes exported with `#[napi]` | | `Cargo.toml` | Rust crate metadata and napi-rs dependencies | | `build.rs` | Required napi-rs build setup | | `package.json` | JavaScript scripts, package metadata, and the `napi` config | | `.github/workflows/CI.yml` | Multi-target build, test, artifact, and publish workflow | The templates do not check in `npm/`. The publish job creates its per-target package directories with `napi create-npm-dirs` after the platform builds. Continue with [A simple package](./simple-package) to edit the Rust API and call it from Node.js. ## Deep dive ### How the generated package is distributed napi-rs normally publishes a small root package plus one optional package per platform. For example, `@cool/core` might depend on: **package.json** ```json { "optionalDependencies": { "@cool/core-darwin-x64": "1.0.0", "@cool/core-win32-x64-msvc": "1.0.0", "@cool/core-linux-arm64-gnu": "1.0.0" } } ``` The generated `index.js` first looks for a local addon produced during development. In an installed package, it loads the optional package matching the current operating system, CPU, and Linux libc. The package manager uses the platform package's `os`, `cpu`, and, where applicable, `libc` fields to avoid installing incompatible binaries. Using an [npm scope](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/) is recommended because every supported target needs a distinct package name. The `napi.targets` array defines what the project packages; it does **not** make one `napi build` invocation compile every target. The scaffold can only retain build jobs already present in its template. For additional accepted targets, add the config entry, npm directory, and CI build explicitly. See [Support and compatibility](/docs/more/support-compatibility) and [Cross build](/docs/cross-build). ## Start directly from a template ![package-template](/assets/package-template.png) If you prefer GitHub's **Use this template** flow, choose the matching project: - [Yarn package template](https://github.com/napi-rs/package-template) - [pnpm package template](https://github.com/napi-rs/package-template-pnpm) After cloning, install dependencies and run `napi rename` through the selected package manager before publishing under your own package name. ## Next steps - [`napi new`](/docs/cli/new) for every scaffold option. - [Build](/docs/cli/build) and [Cross build](/docs/cross-build) for additional targets. - [Release native packages](/docs/deep-dive/release) before publishing anything to npm. --- # Build your first package This tutorial creates a napi-rs v3 addon, calls it from Node.js, and prepares the project for CI. Complete the [prerequisites](./getting-started#prerequisites) first. ## Create the project Choose a package name you control. A scope is strongly recommended because the release workflow creates one npm package per target. The following command uses the Yarn template and supplies the non-interactive defaults for everything else: ```sh npx @napi-rs/cli new cool \ --name @your-scope/cool \ --no-interactive ``` To use the pnpm template instead: ```sh pnpm dlx @napi-rs/cli new cool \ --name @your-scope/cool \ --package-manager pnpm \ --no-interactive ``` Replace `@your-scope` before running the command. If you want to choose the Node-API level, targets, license, and CI workflow interactively, omit `--no-interactive`. ::: info `napi new` supports the maintained Yarn and pnpm templates. It does not generate a generic template that can be switched to an arbitrary package manager afterward. ::: ## Understand the generated project Enter the project and install its dependencies: ```sh cd cool yarn install ``` Use `pnpm install` for the pnpm template. The files you will work with first are: ```text . ├── .github/workflows/CI.yml ├── Cargo.toml ├── build.rs ├── package.json ├── src/lib.rs └── __test__/index.spec.ts ``` - `src/lib.rs` is the Rust addon source. - `Cargo.toml` declares a `cdylib` and the napi-rs v3 crates. - `build.rs` calls the napi-rs build setup and must remain at the crate root. - `package.json` contains the CLI scripts and `napi` packaging config. - `.github/workflows/CI.yml` builds and tests the target rows retained from the template. `npm/` is not present yet. The publish job creates its per-platform package directories with `napi create-npm-dirs` after the platform builds finish. The generated Rust source exports a small function: **src/lib.rs** ```rust #![deny(clippy::all)] use napi_derive::napi; #[napi] pub fn plus_100(input: u32) -> u32 { input + 100 } ``` The macro exposes the Rust function as the JavaScript function `plus100` and writes the corresponding TypeScript declaration during the build. ## Build the addon Run the template's release build for your current platform: ::: pm ```sh yarn yarn build ``` ```sh pnpm pnpm build ``` ::: The script invokes `napi build --platform --release` and produces files like: ```text cool.darwin-arm64.node index.js index.d.ts ``` The exact `.node` suffix follows your current OS, architecture, and ABI. A Linux glibc build, for example, uses `linux-x64-gnu`. Use `yarn build:debug` when you want a debug build. The generated declaration contains: **index.d.ts** ```ts export declare function plus100(input: number): number ``` Call the native function from Node.js: ```sh node -e "const { plus100 } = require('./index.js'); console.log(plus100(42))" ``` The output is: ```text 142 ``` ## Change and test the Rust API Add another exported function to `src/lib.rs`: **src/lib.rs** ```rust #[napi] pub fn multiply(left: i32, right: i32) -> i32 { left * right } ``` Rebuild, then verify both the runtime export and generated types: ```sh yarn build node -e "const { multiply } = require('./index.js'); console.log(multiply(6, 7))" ``` Add a matching AVA assertion to `__test__/index.spec.ts`: \***\*test**/index.spec.ts\*\* ```ts import test from 'ava' import { multiply } from '../index' test('multiply in native code', (t) => { t.is(multiply(6, 7), 42) }) ``` Run the tests: ```sh yarn test ``` ## Prepare the repository Before pushing the generated workflow, update `package.json`: - Set `name` to a package and scope you can publish. - Set `repository` to the final GitHub repository. npm provenance checks this metadata, so do not leave the template repository URL in place. - Review `license`, `description`, `keywords`, `homepage`, and `bugs`. - Review `napi.targets`. It controls package creation and publishing, but each target still needs an actual CI build job. If the name or binary name changes later, use the CLI so Cargo, package config, CI, and generated binding names stay aligned: ```sh yarn napi rename \ --name @your-scope/cool \ --binary-name cool \ --repository https://github.com/your-name/cool.git ``` Then create and push the repository: ```sh git init git add . git commit -m "Create napi-rs package" git branch -M main git remote add origin git@github.com:your-name/cool.git git push -u origin main ``` ## Prepare npm and GitHub Actions The generated workflow publishes through npm and creates a GitHub release. For its token-based setup: 1. Create the npm scope and package access you intend to use. 2. Create an npm automation token that can publish the root and every per-platform package. 3. Add it as the `NPM_TOKEN` Actions secret. 4. Keep the workflow's `contents: write` permission for GitHub releases and `id-token: write` permission for npm provenance. 5. Run the ordinary CI path successfully before attempting a release. ::: warning Publishing is not atomic. `napi pre-publish` updates package metadata, publishes platform packages, and can create or update a GitHub release before npm publishes the root package. Do not use it as a trial command with real credentials. ::: Read [Release native packages](/docs/deep-dive/release) for the complete preflight, release, and recovery procedure. The exact side effects and flags are documented under [`napi pre-publish`](/docs/cli/pre-publish). ## Where to go next - [Release native packages](/docs/deep-dive/release) to publish your package to npm when you are ready. - [Testing and debugging](/docs/more/testing-debugging) to grow the test suite and debug the native side. - [Values](/docs/concepts/values) for Rust-to-JavaScript conversions. - [Async functions](/docs/concepts/async-fn) for asynchronous exports. - [Support and compatibility](/docs/more/support-compatibility) before expanding the target matrix. - [Cross build](/docs/cross-build) for non-host builds. --- # Manual setup Use this guide when you already have a Rust crate or JavaScript package, need a minimal project, or want to place the Rust and JavaScript packages in different parts of a monorepo. If you are starting a standalone package and want the full release workflow, [`napi new`](/docs/cli/new) is usually faster. The CLI is a build and packaging tool. Your addon remains an ordinary Cargo crate, so you can use the workspace layout and package manager you already have. ## Prerequisites Install a current Rust toolchain, Node.js 22.13+ (or Node.js 24+) for the current CLI, and the NAPI-RS CLI in the JavaScript package that owns the addon: ```sh rustc --version node --version npm install --save-dev @napi-rs/cli@^3 ``` Keeping the CLI local makes local builds and CI use the version recorded by the project. Run it through a package script or `npx napi`; a global installation is not required. ## Minimal project The smallest useful layout is: ```text my-addon/ ├── Cargo.toml ├── build.rs ├── package.json ├── src/ │ └── lib.rs └── test.cjs ``` ### Configure Cargo The library must be a `cdylib`: Node loads the resulting shared library rather than linking it into another Rust executable. `napi-build` configures the output for the host platform, and the default `napi-derive` features enable strict macro validation and TypeScript definition generation. **Cargo.toml** ```toml [package] name = "my-addon-native" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] napi = "3" napi-derive = "3" [build-dependencies] napi-build = "2" ``` Create the build script: **build.rs** ```rust fn main() { napi_build::setup(); } ``` ### Export a Rust function **src/lib.rs** ```rust use napi_derive::napi; #[napi] pub fn add(left: i32, right: i32) -> i32 { left + right } ``` ### Configure the JavaScript package `binaryName` controls the generated file name. `--platform` adds the current platform suffix and generates a loader that selects either the local binary or the corresponding optional platform package. **package.json** ```json { "name": "my-addon", "version": "0.1.0", "main": "index.js", "types": "index.d.ts", "scripts": { "build": "napi build --platform", "build:release": "napi build --platform --release", "test": "node --test test.cjs" }, "napi": { "binaryName": "my-addon" }, "devDependencies": { "@napi-rs/cli": "^3" } } ``` Build and call the addon: **test.cjs** ```js const assert = require('node:assert/strict') const test = require('node:test') const { add } = require('./index.js') test('adds two numbers', () => { assert.equal(add(2, 3), 5) }) ``` ```sh npm run build npm test ``` A debug build produces these files in the crate directory by default: ```text index.d.ts index.js my-addon..node ``` The `.node` file is the native library. Import `index.js`, not a hard-coded platform file: the generated loader also handles libc selection, separately published platform packages, and an optional WASI fallback. ::: info Without `--platform`, the CLI copies a single `my-addon.node` file but does not generate the JavaScript loader. That is useful for low-level experiments; published packages should normally use `--platform`. ::: ## Common variations ### Async functions Enable the `async` feature when an exported Rust `async fn` should become a JavaScript `Promise`: **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["async"] } napi-derive = "3" tokio = { version = "1", features = ["fs"] } ``` See [Async and concurrency](/docs/more/async-concurrency) before choosing between Tokio, `AsyncTask`, ThreadsafeFunction, and streams. ### A custom output directory All paths are relative to `--cwd`. Place the generated JavaScript, TypeScript, and native files in a JavaScript package with: ```sh napi build --platform --output-dir ./dist ``` Keep the loader and its local `.node` file together. Moving only `index.js` breaks its relative lookup. ### A separate config file By default, the CLI reads the `napi` object from `package.json`. You can move that object to JSON and pass `--config-path`; when both exist, the separate file wins. **napi.config.json** ```json { "binaryName": "my-addon", "packageName": "@scope/my-addon", "targets": [ "x86_64-unknown-linux-gnu", "aarch64-apple-darwin", "x86_64-pc-windows-msvc" ] } ``` ```sh napi build --platform --config-path napi.config.json ``` `targets` describes the artifacts you intend to package. A local build still builds one target at a time; pass `--target ` explicitly in CI. ## Cargo and JavaScript workspaces The Rust crate and the JavaScript package do not have to share a directory. For example: ```text workspace/ ├── Cargo.toml # [workspace] members = ["crates/native"] ├── crates/ │ └── native/ │ ├── Cargo.toml # package.name = "my-addon-native" │ ├── build.rs │ └── src/lib.rs └── packages/ └── addon/ └── package.json # owns the napi config and generated output ``` Run the CLI from the workspace root while making every path explicit: ```sh napi build \ --cwd packages/addon \ --manifest-path ../../Cargo.toml \ --package my-addon-native \ --package-json-path package.json \ --output-dir . \ --platform ``` The important distinction is: | Option | Selects | | --------------------- | -------------------------------------------------------- | | `--cwd` | Base directory for all other relative paths | | `--manifest-path` | Crate or workspace `Cargo.toml` used by `cargo metadata` | | `--package` | Exact Cargo package name to build inside a workspace | | `--package-json-path` | JavaScript package and NAPI-RS configuration | | `--output-dir` | Destination for `.node`, loader, and `.d.ts` files | If the manifest points at a virtual Cargo workspace, `--package` is required. The CLI otherwise cannot know which `cdylib` member owns the addon. ## Prepare for distribution For one local machine, the generated loader and `.node` file are enough. A published cross-platform package normally uses a separate optional npm package for every target: 1. Add all release triples to `napi.targets`. 2. Build one `--platform --release --target ` artifact per CI job. 3. Run [`napi create-npm-dirs`](/docs/cli/create-npm-dirs). 4. Download the CI artifacts and run [`napi artifacts`](/docs/cli/artifacts). 5. Follow the [release guide](/docs/deep-dive/release) and read every side effect of [`napi pre-publish`](/docs/cli/pre-publish) before publishing. Do not publish a binary built on your development machine as if it supported other operating systems. Use the [cross-build guide](/docs/cross-build) and test the final package on each runtime you claim to support. ## What to read next - [Testing and debugging](/docs/more/testing-debugging) - [Integrating with applications and bundlers](/docs/more/integrations) - [Troubleshooting](/docs/more/troubleshooting) - [NAPI-RS configuration](/docs/cli/napi-config) --- # Exports ::: info Unlike defining modules in Node.js, we don't need to explicitly register exports like `module.exports.xxx = xxx`. The `#[napi]` macro will automatically generate module registering code for you. This auto registering idea was inspired by [node-bindgen](https://github.com/infinyon/node-bindgen). ::: ## `Function` Exporting a function is incredibly simple. Just decorate a normal rust function with `#[napi]`: **lib.rs** ```rust #[napi] pub fn sum(a: f64, b: f64) -> f64 { a + b } ``` ## `Const` **lib.rs** ```rust #[napi] pub const DEFAULT_COST: u32 = 12; ``` **index.d.ts** ```ts export const DEFAULT_COST: number ``` ## `Class` See [`class section`](./class) for more details. **lib.rs** ```rust #[napi(constructor)] pub struct Animal { pub name: String, pub kind: u32, } #[napi] impl Animal { #[napi] pub fn change_name(&mut self, new_name: String) { self.name = new_name; } } ``` ## `Enum` See [`enum section`](./enum) for more details. **lib.rs** ```rust #[napi] pub enum Kind { Dog, Cat, Duck, } ``` ## `exports` object You can use the `#[napi(module_exports)]` attribute to access the `exports` object. **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi(module_exports)] pub fn exports(mut export: Object) -> Result<()> { let symbol = Symbol::new("NAPI_RS_SYMBOL"); export.set_named_property("NAPI_RS_SYMBOL", symbol)?; Ok(()) } ``` ## Namespaces For larger addons you can group related exports into nested JavaScript module objects instead of flattening everything onto the root `exports`. There are two ways: - `#[napi] mod name { ... }` — export an inline Rust module as a namespace. Every child item that also carries `#[napi]` is exported inside it (nested napi modules are not supported). Add `#[napi(js_name = "...")]` on the `mod` to rename the namespace object. - `#[napi(namespace = "...")]` on individual functions, classes, impl blocks, enums, consts, and type aliases — registers that item under `exports.`; apply the same namespace to a class and its `impl` blocks. See [`namespace` in the attributes reference](/docs/concepts/napi-attributes#naming-and-exports). The canonical example is [`examples/napi/src/js_mod.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/js_mod.rs): **lib.rs** ```rust #[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 } #[napi] pub struct Xxh3 { inner: BigInt, } #[napi] impl Xxh3 { #[napi(constructor)] pub fn new() -> Xxh3 { // ... } } } #[napi] mod xxh2 { use napi::bindgen_prelude::*; #[napi] pub fn xxh2_plus(a: u32, b: u32) -> u32 { a + b } } ``` The members are reached through the namespace objects on the package exports: **index.ts** ```ts import { xxh2, xxh3 } from './index.js' xxh3.xxh3_64(Buffer.from('hello')) // function renamed with js_name console.log(xxh3.ALIGNMENT) // 16 const hasher = new xxh3.Xxh3() // classes live inside the namespace too xxh2.xxh2Plus(1, 2) // 3 ``` And the generated `.d.ts` mirrors the nesting with `export declare namespace`: **index.d.ts** ```ts export declare namespace xxh2 { export function xxh2Plus(a: number, b: number): number } export declare namespace xxh3 { export class Xxh3 { constructor() } export const ALIGNMENT: number export function xxh3_64(input: Buffer): bigint } ``` --- # Function Defining a JavaScript `function` is very simple in **NAPI-RS**. Just a plain Rust `fn`: **lib.rs** ```rust #[napi] pub fn sum(a: u32, b: u32) -> u32 { a + b } ``` The most important thing you should keep in mind is **_NAPI-RS fn does not support every Rust type_**. Each argument type must implement `FromNapiValue`, and each return type must implement `ToNapiValue`. The canonical conversion matrix — argument and return types, direction, ownership, and required Cargo features — lives in [Type conversions](/docs/concepts/type-conversions). The short version for functions: - Numbers (`u32`, `i32`, `i64`, `f64`), `bool`, and `String` map to their JavaScript equivalents in both directions. - `Option` as an argument accepts `T`, `null`, or `undefined` (`T | null | undefined`); as a return type, `None` becomes `null` (`T | null`). - `Vec`, tuples, `HashMap`, and `#[napi(object)]` structs map to JavaScript arrays and plain objects. - `Buffer` and the typed-array wrappers map to `Buffer` and `TypedArray`. - `Function` and `ThreadsafeFunction` accept JavaScript callbacks with fully typed signatures (see below). - An `async fn` or an `AsyncTask` return maps to `Promise`. ## Return Type The return type of a `#[napi] fn` is converted with `ToNapiValue` and appears directly in the generated `.d.ts`. A `Result` return throws on `Err` instead of producing a value. See the [Type conversions](/docs/concepts/type-conversions) reference for the complete mapping, including BigInt output types (`i64n`, `i128`, `u128`) and async returns. ## `Function` as parameter You can pass a `Function` as a parameter to a `fn`: **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn call_function(callback: Function) -> Result { callback.call(1) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export declare function callFunction(callback: (arg: number) => number): number ``` ::: info You can also create a `Function` at the Rust side, see [`Env::create_function`](/docs/concepts/env#create_function) ::: ## `FnArgs` When the number of parameters exceeds 1, you can use `FnArgs` to define the parameters. ::: info The `tuple` type can be converted to `FnArgs` by calling `.into()`. ::: **lib.rs** ```rust {6} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn call_function_with_args(callback: Function, u32>) -> Result { callback.call((1, 2).into()) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export declare function callFunctionWithArgs( callback: (arg1: number, arg2: number) => number, ): number ``` ## `apply` Like JavaScript, you can also use `apply` to call a `Function` with the `this` value. **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub struct RustClass { pub name: String, } #[napi] impl RustClass { #[napi(constructor)] pub fn new(name: String) -> Self { Self { name } } } #[napi] pub fn call_function_with_apply( this: ClassInstance, callback: Function<(), ()>, ) -> Result<()> { callback.apply(this, ()) } ``` **index.ts** ```ts import { callFunctionWithApply, RustClass } from './index.js' const rustClass = new RustClass('foo') callFunctionWithApply(rustClass, function () { console.log(this.name) // foo }) ``` ## `create_ref` See [**Function Reference**](/docs/concepts/reference#functionref) for more details. ## `build_threadsafe_function` You can build a `ThreadsafeFunction` from a `Function` by calling `build_threadsafe_function`. The return type of the `build_threadsafe_function` is a `ThreadsafeFunctionBuilder`. By default, the `ThreadsafeFunctionBuilder` will create a `ThreadsafeFunction` with the default options: ::: info See [**ThreadsafeFunction**](/docs/concepts/threadsafe-function) for the details of options ::: ::: tip Since you can pass `ThreadsafeFunction` and `Arc` directly to the `#[napi] fn`, only use the `build_threadsafe_function` when you are need to create a `ThreadsafeFunction` dynamically. ::: - `max_queue_size` is `0` - `weak` is `false` - `callee_handled` is `true` - `error_status` is `napi::Status` **lib.rs** ```rust use napi::{bindgen_prelude::*, threadsafe_function::ThreadsafeFunctionCallMode}; use napi_derive::napi; #[napi] pub fn build_threadsafe_function_from_function( callback: Function, u32>, ) -> Result<()> { let tsfn = callback.build_threadsafe_function().build()?; let jh = std::thread::spawn(move || { tsfn.call((1, 2).into(), ThreadsafeFunctionCallMode::NonBlocking); }); Ok(()) } ``` --- # Values Conversions between Rust and JavaScript types. This page introduces common values. For the complete source-backed matrix—including conversion direction, ownership, Cargo features, `Option`, `Either`, collections, paths, functions, Promises, streams, and Node-API levels—see [Type conversions](/docs/concepts/type-conversions). ### Undefined Represent `undefined` in JavaScript. **lib.rs** ```rust {3} #[napi] pub fn get_undefined() -> Undefined { () } // default return or empty tuple `()` are `undefined` after converted into JS value. #[napi] pub fn log(n: u32) { println!("{}", n); } ``` **index.d.ts** ```ts export function getUndefined(): void export function log(n: number): void ``` ### Null Represents `null` value in JavaScript. **lib.rs** ```rust {3} #[napi] pub fn get_null() -> Null { Null } #[napi] pub fn get_env(env: String) -> Option { match std::env::var(env) { Ok(val) => Some(val), Err(_) => None, } } ``` **index.d.ts** ```ts export function getNull(): null export function getEnv(env: string): string | null ``` `Option` accepts `T`, `null`, or `undefined` as an argument, but returns `null` for `None`. In an `#[napi(object)]` field, the default representation is an optional property and `None` is omitted on output; `#[napi(use_nullable)]` instead makes it a required `T | null` property. See [`Option`, `null`, and `undefined`](/docs/concepts/type-conversions#option-null-and-undefined) for the full position-dependent mapping. ### Numbers JavaScript `Number` type with Rust Int/Float types: `u32`, `i32`, `i64`, `f64`. For Rust types like `u64`, `u128`, `i128`, checkout [`BigInt`](#bigint) section. **lib.rs** ```rust #[napi] pub fn sum(a: u32, b: i32) -> i64 { i64::from(a) + i64::from(b) } ``` **index.d.ts** ```ts export function sum(a: number, b: number): number ``` ### String Represents JavaScript `String` type. **lib.rs** ```rust {3} #[napi] pub fn greet(name: String) -> String { format!("greeting, {}", name) } ``` **index.d.ts** ```ts export function greet(name: string): string ``` ### Boolean Represents JavaScript `Boolean` type. **lib.rs** ```rust #[napi] pub fn is_good() -> bool { true } ``` **index.d.ts** ```ts export function isGood(): boolean ``` ### Buffer **lib.rs** ```rust #[napi] pub fn with_buffer(buf: Buffer) { let buf: Vec = buf.into(); // do something } #[napi] pub fn read_buffer(file: String) -> Result { Ok(std::fs::read(file)?.into()) } ``` **index.d.ts** ```ts export function withBuffer(buf: Buffer): void export function readBuffer(file: string): Buffer ``` ### Object Represents JavaScript anonymous object values. ::: warning **Performance** The costs of `Object` conversions between JavaScript and Rust are higher than other primitive types. Every call of `Object.get("key")` is actually dispatched to node side including two steps: fetch value, convert JS to rust value, and so is `Object.set("key", v)`. ::: **lib.rs** ```rust #[napi] pub fn keys(obj: Object) -> Result> { Object::keys(&obj) } #[napi] pub fn log_string_field(obj: Object, field: String) -> Result<()> { println!("{}: {:?}", &field, obj.get::(&field)?); Ok(()) } #[napi] pub fn create_obj(env: &Env) -> Result { let mut obj = Object::new(env)?; obj.set("test", 1)?; Ok(obj) } ``` **index.d.ts** ```ts export function keys(obj: object): Array export function logStringField(obj: object, field: string): void export function createObj(): object ``` If you want **NAPI-RS** to convert objects from JavaScript with the same shape defined in Rust, you can use the `#[napi]` macro with the `object` attribute. **lib.rs** ```rust use std::collections::HashMap; /// #[napi(object)] requires all struct fields to be public #[napi(object)] pub struct PackageJson { pub name: String, pub version: String, pub dependencies: Option>, pub dev_dependencies: Option>, } #[napi] pub fn log_package_name(package_json: PackageJson) { println!("name: {}", package_json.name); } #[napi] pub fn example_package_json() -> PackageJson { PackageJson { name: "example".to_owned(), version: "1.0.0".to_owned(), dependencies: None, dev_dependencies: None, } } ``` **index.d.ts** ```ts export interface PackageJson { name: string version: string dependencies?: Record devDependencies?: Record } export function logPackageName(packageJson: PackageJson): void export function examplePackageJson(): PackageJson ``` ::: warning **Clone over Reference** The `#[napi(object)]` struct passed to a Rust `fn` is cloned from the **_JavaScript Object_**. Any mutation on it will not be reflected in the original **_JavaScript_** object. ::: `#[napi(object)]` is an owned plain-object shape, not a class. Use `#[napi] struct` for native class identity and methods, `#[napi(transparent)]` for a Rust newtype with the inner JavaScript representation, or `#[napi(array)]` for a tuple-shaped array. See [Type conversions](/docs/concepts/type-conversions#objects-classes-and-custom-shapes). **lib.rs** ```rust /// #[napi(object)] requires all struct fields to be public #[napi(object)] pub struct Animal { pub name: String, } #[napi] pub fn change_animal_name(mut animal: Animal) { animal.name = "cat".to_string(); } ``` ```js const animal = { name: 'dog' } changeAnimalName(animal) console.log(animal.name) // "dog" ``` ### Array Because `Array` values in JavaScript can hold elements with different types, but Rust `Vec` can only contain elements of the same type, there are two different ways to handle array types. ::: warning **Performance** Because JavaScript `Array` type is actually backed by `Object`, the performance of manipulating `Array`s is the same as `Object`s. The conversion between `Array` and `Vec` is even heavier, which is in `O(n)` complexity. ::: **lib.rs** ```rust #[napi] pub fn arr_len(arr: Array) -> u32 { arr.len() } #[napi] pub fn get_tuple_array(env: &Env) -> Result { let mut arr = env.create_array(2)?; arr.insert(1)?; arr.insert("test")?; Ok(arr) } #[napi] pub fn vec_len(nums: Vec) -> Result { u32::try_from(nums.len()) .map_err(|_| Error::new(Status::InvalidArg, "Array is too large")) } #[napi] pub fn get_nums() -> Vec { vec![1, 1, 2, 3, 5, 8] } ``` **index.d.ts** ```ts export function arrLen(arr: unknown[]): number export function getTupleArray(): unknown[] export function vecLen(nums: Array): number export function getNums(): Array ``` ### BigInt This requires the `napi6` feature. ::: warning The only way to pass `BigInt` in `Rust` is using `BigInt` type. But you can return `BigInt`, `i64n`, `u64`, `i128`, `u128`. Return `i64` will be treated as `JavaScript` number, not `BigInt`. ::: ::: tip The reason why Rust functions can't receive `i128` `u128` `u64` `i64n` as arguments is that they may lose precision when converting JavaScript `BigInt` into them. You can use `BigInt::get_u128`, `BigInt::get_i128`, etc. to get the value in `BigInt`. The return value of these methods also indicates whether precision is lost. ::: **lib.rs** ```rust /// the return value of `get_u128` is (signed: bool, value: u128, lossless: bool) #[napi] pub fn bigint_add(a: BigInt, b: BigInt) -> Result { let (a_signed, a_value, a_lossless) = a.get_u128(); let (b_signed, b_value, b_lossless) = b.get_u128(); if a_signed || b_signed || !a_lossless || !b_lossless { return Err(Error::new( Status::InvalidArg, "both values must be lossless, non-negative u128 integers", )); } a_value.checked_add(b_value).ok_or_else(|| { Error::new(Status::InvalidArg, "u128 addition overflowed") }) } #[napi] pub fn create_big_int_i128() -> i128 { 100 } ``` **index.d.ts** ```ts export function bigintAdd(a: bigint, b: bigint): bigint export function createBigIntI128(): bigint ``` ### TypedArray ::: tip Unlike JavaScript Object, the `TypedArray` passed into Rust fn is a **Reference**. No data `Copy` or `Clone` will be performed. Every mutation on the `TypedArray` will be reflected to the original JavaScript `TypedArray`. ::: **lib.rs** ```rust #[napi] pub fn convert_u32_array(input: Uint32Array) -> Vec { input.to_vec() } #[napi] pub fn create_external_typed_array() -> Uint32Array { Uint32Array::new(vec![1, 2, 3, 4, 5]) } #[napi] pub fn mutate_typed_array(mut input: Float32Array) { for item in unsafe { input.as_mut() } { *item *= 2.0; } } ``` **index.d.ts** ```ts export function convertU32Array(input: Uint32Array): Array export function createExternalTypedArray(): Uint32Array export function mutateTypedArray(input: Float32Array): void ``` **test.mjs** ```js import { convertU32Array, mutateTypedArray } from './index.js' convertU32Array(new Uint32Array([1, 2, 3, 4, 5])) // [1, 2, 3, 4, 5] const values = new Float32Array([1, 2, 3, 4, 5]) mutateTypedArray(values) console.log(values) // Float32Array(5) [ 2, 4, 6, 8, 10 ] ``` --- # Type conversions Every exported argument must implement `FromNapiValue` (or one of the reference conversion traits), and every returned value must implement `ToNapiValue`. The generated TypeScript type is useful documentation, but the Rust trait implementation is what determines whether a conversion is actually available. This reference describes napi-rs v3's bindgen runtime. For the lower-level handles such as `JsString` and `JsObject`, see [Env and low-level values](/docs/concepts/env). ## Direction legend | Mark | Meaning | | --------- | ---------------------------------------------------------------------------------------------------------------- | | JS → Rust | The type can be used as an exported function argument. | | Rust → JS | The type can be returned or assigned to a JavaScript value. | | Scoped | The Rust value borrows a Node-API environment or JavaScript callback scope and must not escape it. | | Owned | Conversion creates or retains Rust-owned data that can outlive the callback, subject to the type's `Send` rules. | ::: warning A TypeScript mapping does not imply both conversion directions. For example, `u64` generates `bigint` but is output-only; use `BigInt` when accepting an arbitrary JavaScript `bigint` so you can check whether narrowing is lossless. ::: ## Primitive values | Rust type | JavaScript / TypeScript | Direction | Ownership and caveats | Feature / minimum Node-API | | ----------------------------------------------- | --------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------- | | `()` / `Undefined` | `undefined`; a function return becomes `void` | Both | Zero-sized marker. Under `strict`, input must be `undefined`. | Base API | | `Null` | `null` | Both | Explicit null marker. Plain input conversion accepts and discards any value; under `strict`, input must be `null`. | Base API | | `bool` | `boolean` | Both | Copied. | Base API | | `i8`, `u8`, `i16`, `u16`, `i32`, `u32` | `number` | Both | Integer conversion; JavaScript still stores a Number. | Base API | | `f32` | `number` | Rust → JS | Widened to a JavaScript double; there is no `FromNapiValue` implementation. Use `f64` for input. | Base API | | `f64` | `number` | Both | JavaScript Number is IEEE-754 double precision. | Base API | | `i64` | `number` | Both | Uses Node-API's signed 64-bit Number conversion. Values outside JavaScript's safe-integer range can lose precision. | Base API | | `BigInt` | `bigint` | Both | Keeps a sign bit and little-endian `u64` words. Its getters report whether narrowing was lossless. | `napi6` | | `u64`, `u128`, `i128`, `usize`, `isize`, `i64n` | `bigint` | Rust → JS | Output-only to avoid silently narrowing arbitrary JavaScript BigInts. | `napi6` | | `String` | `string` | Both | Owned UTF-8 string. | Base API | | `&str` | `string` | Rust → JS | Borrowed Rust output only; JavaScript strings cannot be accepted as `&str`. Use `String` for input. | Base API | | `Utf16String` | `string` | Both | Owned UTF-16 code units; useful when exact UTF-16 representation matters. | Base API | | `Latin1String` | `string` | Both | Owns Latin-1 bytes. Formatting it as UTF-8 requires `latin1`. | Base API; `latin1` for decoding/display | | `OsString`, `PathBuf` | `string` | Both | Owned. Windows uses UTF-16 and preserves unpaired surrogates. Unix output rejects a non-Unicode path rather than replacing bytes. | Base API | | `&OsStr`, `&Path` | `string` | Rust → JS | Borrowed output. Same platform caveats as the owned forms. | Base API | | `Symbol` | `symbol` | Both | Plain input conversion discards the value without retaining identity or description. `#[napi(strict)]` first validates that it is a symbol, but still does not retain it. Returning `Symbol` creates one from Rust descriptor state. Use scoped `JsSymbol` to preserve an existing value. `Symbol::for_desc` needs Node-API 9. | Base API; `napi9` for global symbols | `i64` deliberately maps to `number`, while `i64n` maps to `bigint`. Prefer the wrapper only when the JavaScript API is intended to expose a BigInt. **lib.rs** ```rust #[napi] pub fn inspect_bigint(value: BigInt) -> Result { let (negative, narrowed, lossless) = value.get_u64(); if negative || !lossless { return Err(Error::from_reason("value does not fit in u64")); } Ok(narrowed) } ``` The return type above is `bigint` because `u64` is a BigInt output type. ## `Option`, `null`, and `undefined` {#option-null-and-undefined} `Option` has an intentionally asymmetric mapping: | Position | JavaScript accepted or produced | Generated TypeScript | | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | Function argument | `T`, `null`, or `undefined`; both nullish values become `None` | `T \| null \| undefined` and normally an optional trailing parameter | | Function return | `Some(T)` becomes `T`; `None` becomes `null` | `T \| null` | | `#[napi(object)]` or structured-shape field with default `use_nullable = false` | Missing or `undefined` becomes `None`; explicit `null` is passed to the inner `T` conversion and normally fails; `None` is omitted on output | `field?: T` | | `#[napi(object)]` or structured-shape field with `use_nullable = true` | Missing or `undefined` is an error; `null` becomes `None`; `None` is emitted as `null` | `field: T \| null` | | Public class field | The accessor always exists. An `Option` getter emits `null` for `None`, and a writable setter accepts the normal nullish `Option` inputs. `use_nullable` changes the generated property/constructor shape, not whether the accessor exists. | Default: `field?: T`; with `use_nullable`: `field: T \| null` | Use `Null` or `Undefined` when the distinction itself is part of the API. Use `Either` or `Either` when exactly one nullish value is accepted. **lib.rs** ```rust #[napi] pub fn optional_name(value: Option) -> Option { value.filter(|name| !name.is_empty()) } #[napi] pub fn null_but_not_undefined(value: Either) -> bool { matches!(value, Either::B(Null)) } ``` ::: info Non-trailing optional parameters may be emitted as required unions so that a later required parameter remains callable in TypeScript. The union still accepts `undefined` and `null`. ::: ## Unions with `Either` `Either` through `Either26` map to TypeScript unions. On input, napi-rs tests variants from left to right with each type's `ValidateNapiValue` implementation, then converts the first match. **lib.rs** ```rust #[napi] pub fn normalize_id(value: Either) -> String { match value { Either::A(number) => number.to_string(), Either::B(text) => text, } } ``` **index.d.ts** ```ts export function normalizeId(value: number | string): string ``` Wider unions work the same way: `Either3` and `Either4` are generated alongside `Either` (and so on up to `Either26`), with variants named `A`, `B`, `C`, `D`, …: **lib.rs** ```rust #[napi] pub fn either3(input: Either3) -> u32 { match input { Either3::A(s) => s.len() as u32, Either3::B(n) => n, Either3::C(b) => u32::from(b), } } ``` **index.d.ts** ```ts export function either3(input: string | number | boolean): number ``` Order overlapping alternatives from most specific to least specific. Validation for a plain `Object`, for example, cannot prove a complete object schema. `Either` is a runtime union, not a serde-style untagged enum with backtracking after arbitrary user code. ## Arrays, tuples, maps, and sets | Rust type | JavaScript / TypeScript | Direction | Conversion behavior | Feature | | -------------------------------------------- | ----------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `Vec` | `Array` | Both | Copies/converts every element. Input requires each element to implement `FromNapiValue`. | Base API | | `[T; N]` | `Array` | Rust → JS | Creates a JavaScript array. | Base API | | Rust tuples, up to supported generated arity | TypeScript tuple / JS Array | Both | Input must have at least the tuple's length; each indexed element is converted. | Base API | | `Array<'env>` | `unknown[]` | JS → Rust and scoped pass-through | Scoped handle with `get`, `get_ref`, `set`, and `insert`; avoids converting the whole array up front. | Base API | | `HashMap`, `BTreeMap` | `Record` / plain object | Both | Uses own enumerable string-keyed properties. This is **not** JavaScript `Map`. Keys must convert to/from strings. | Base API | | `IndexMap` | `Record` / plain object | Both | Uses the same own enumerable string-keyed property shape while preserving Rust insertion order where JavaScript's property rules allow it. | `object_indexmap` | | `HashSet`, `BTreeSet` | `Set` | Both | Constructs or iterates an actual JavaScript `Set`. | Base API | | `IndexSet` | `Set` | Both | Insertion-ordered Rust set. | `object_indexmap` | Conversion of `Vec` and collections is O(n). Use scoped `Array`, `Object`, typed-array views, or a stream when you need incremental access rather than an owned copy. ## Objects, classes, and custom shapes {#objects-classes-and-custom-shapes} | Rust type or declaration | JavaScript / TypeScript | Direction | Ownership / identity | | ---------------------------------------- | ----------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `Object<'env>` | `object` | Both within scope | Direct scoped handle. Property access crosses the Node-API boundary on each operation. | | `ObjectRef` | `object` | Both | Holds a Node-API reference so the object can outlive the callback. | | `Unknown<'env>` | `unknown` | Both within scope | Unchecked scoped handle; inspect/coerce it explicitly. | | `#[napi(object)] struct` | Plain object / interface | Controlled by `object_from_js` and `object_to_js`, both on by default | JavaScript input is converted into a new owned Rust struct. Mutating it does not mutate the source object. | | `#[napi] struct` | JavaScript class | Through class references and instances | Preserves native class identity. Methods receive `&self`/`&mut self`; public fields become accessors. | | `ClassInstance<'env, T>` | An instance of class `T` | JS → Rust / scoped output | Use inside object fields or collections when the JavaScript class instance itself is needed. | | `#[napi(transparent)] struct Wrapper(T)` | Same representation as `T` | Controlled per direction | Rust newtype without a JavaScript wrapper object. | | `#[napi(array)]` tuple struct | JavaScript array / TypeScript tuple | Controlled per direction | Named Rust type with positional JavaScript representation. | | Structured `#[napi] enum` | Discriminated object union | Controlled per direction | Owned conversion; discriminator defaults to `type`. | Do not treat a class as an object shape. A normal read-write public class field needs both output conversion for its getter and input conversion for its setter; a `readonly` field needs only output conversion, while a skipped class field has no accessor. For nested class values, accept `&T`, `ClassInstance`, or use `Array::get_ref`; `Vec` requires an owned `FromNapiValue` implementation and is therefore not the way to accept a list of class instances. See [Classes](/docs/concepts/class), [Objects](/docs/concepts/object), [Enums](/docs/concepts/enum), and [`#[napi]` attributes](/docs/concepts/napi-attributes) for shape-specific examples. ## Buffers, ArrayBuffers, and typed arrays | Rust type | JavaScript / TypeScript | Direction | Lifetime and data behavior | Feature / minimum Node-API | | -------------------------------------------------- | ------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------- | | `BufferSlice<'env>` | Node.js `Buffer` | Both within a synchronous scope | Mutable borrowed view for synchronous code. Do not hold it across `await`. | Base API | | `Buffer` | Node.js `Buffer` | Both | Keeps a reference to JavaScript-owned data and is designed for async use. Cloning references the same underlying buffer. | Best lifecycle handling with `napi4` | | `ArrayBuffer<'env>` | `ArrayBuffer` | JS → Rust and scoped pass-through | Borrowed bytes tied to the environment. | Base API | | `Int8Array`, `Uint8Array`, … | Corresponding typed array | Both | Owned/reference-retaining wrappers suitable for async use. | BigInt array variants require `napi6` | | `Int8ArraySlice<'env>`, `Uint8ArraySlice<'env>`, … | Corresponding typed array | Both within scope | Borrowed views for synchronous code. | BigInt array variants require `napi6` | | `&[i8]`, `&[u8]`, `&[i16]`, … | Corresponding typed array | JS → Rust in a synchronous callback | Borrowed slice; cannot outlive the callback. | BigInt slices require `napi6` | External buffers and ArrayBuffers can be zero-copy when the runtime accepts external backing stores. A runtime may reject external buffers; constructors such as `BufferSlice::from_data` then fall back to a copy. Do not promise zero-copy behavior across every Node-compatible runtime. See [Typed arrays](/docs/concepts/typed-array) and [Understanding lifetime](/docs/concepts/understanding-lifetime). ## Dates and serde JSON | Rust type | JavaScript / TypeScript | Direction | Feature / caveat | | --------------------------------------- | --------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------ | | `Date` (`JsDate`) | `Date` | Scoped low-level value | `napi5` | | `chrono::DateTime`, `NaiveDateTime` | `Date` | Both | `chrono_date`, which enables `chrono` and `napi5`; milliseconds since epoch determine precision. | | `serde_json::Value` | JSON-compatible JavaScript value | Both | `serde-json`; rejects functions, `undefined`, symbols, and external values. | | `serde_json::Map` | Plain object | Both | `serde-json` | | `serde_json::Number` | Number, BigInt, or string depending value and enabled API | Both | `serde-json`; with `napi6`, out-of-safe-range integers are emitted as BigInt. | `serde_json::Value` is not a lossless representation of arbitrary JavaScript. In particular, a large input BigInt may become a JSON number when it fits or a decimal string when it does not. Use `BigInt` when BigInt identity and exact narrowing rules matter. `serde-json-ordered` additionally enables serde_json's `preserve_order` behavior. ## Functions, promises, and streams | Rust type | JavaScript / TypeScript | Direction | Lifetime / feature | | ------------------------------ | ------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Function<'env, Args, Return>` | Typed JavaScript function | JS → Rust in scope; can be passed through in scope | Calls JavaScript only on its owning thread. Use `FnArgs<(...)>` for multiple positional arguments. | | `FunctionRef` | Typed JavaScript function | JS → Rust / retained reference | Owns a Node-API reference but does not implement `ToNapiValue`. To pass it back, call `borrow_back(env)` to obtain a scoped `Function`; still use it only in the owning environment/thread. | | `ThreadsafeFunction<...>` | Typed callback | JS → Rust, then callable from other threads | `napi4`; see [ThreadsafeFunction](/docs/concepts/threadsafe-function). | | `Promise` | `Promise` | JS → Rust only | Awaitable Rust future. Requires the async runtime for normal exported async use. | | `PromiseRaw<'env, T>` | `Promise` | Scoped JS promise handle | Supports `then`, `catch`, and `finally` without moving the promise to another thread. | | Rust `async fn` return | `Promise` | Rust → JS | `async` or `tokio_rt`; `Result::Err` rejects. | | `AsyncTask` | `Promise` | Rust → JS | Runs `compute` on libuv's worker pool. | | `ReadableStream<'env, T>` | Web `ReadableStream` | Both | `web_stream`; construction needs `T: Send + 'static` and a `Send + 'static` Rust stream. | | `WriteableStream<'env>` | Web `WritableStream` | JS → Rust and scoped pass-through | `web_stream`; the Rust API is currently spelled `WriteableStream`, and the type generator does not canonicalize that spelling, so use `ts_arg_type = "WritableStream"` for a public parameter. | `ReadableStream::new` checks that the runtime provides a global `ReadableStream`. Node-API 4 alone does not guarantee the Web Streams global; `with_readable_stream_class` accepts a compatible constructor explicitly. ## External native data `External` exposes an opaque, type-tagged native allocation to JavaScript. It is not serialized and its generated type is `ExternalObject`. - Return an owned `External` to transfer it into a JavaScript external value. - Accept `&External` or `&mut External` to borrow and type-check the wrapped value. - Use `ExternalRef` when Rust must keep a JavaScript reference to the external. - `External::new_with_size_hint` reports native allocation size to the JavaScript garbage collector; the number is a GC accounting hint, not a memory limit. See [External](/docs/concepts/external) for lifecycle details. ## Validation is not coercion Most generated functions convert according to their `FromNapiValue` implementation. Adding `#[napi(strict)]` first invokes `ValidateNapiValue` and rejects a mismatched top-level JavaScript type. It does not coerce strings to numbers, and it does not recursively validate every property before conversion. `#[napi(return_if_invalid)]` performs the same validation but returns `undefined` on invalid input instead of throwing. See [`#[napi]` attributes](/docs/concepts/napi-attributes) for its constraints. ## Choosing an ownership model Use this order of preference: 1. Use owned Rust values (`String`, `Vec`, `#[napi(object)]`) when a copy is acceptable and the value must cross threads or await points. 2. Use scoped handles and slices (`Object<'env>`, `Array<'env>`, `BufferSlice<'env>`, typed-array slices) for synchronous, zero- or low-copy access. 3. Use reference-retaining wrappers (`Buffer`, `ObjectRef`, `FunctionRef`, `Reference`) when JavaScript-owned data must outlive the callback. 4. Use `ThreadsafeFunction` to invoke JavaScript from another thread; never move a scoped JavaScript handle there. 5. Use a stream when data should be produced incrementally instead of copied into one collection. The compiler enforces many of these boundaries through lifetimes and `Send`, but an `unsafe` method or raw Node-API handle can bypass them. Read [Understanding lifetime](/docs/concepts/understanding-lifetime) before doing so. --- # Object `Object` is very easy to confuse with the use of `Class`. Unlike `Class` you can't assign `function` or `method` to `Object`. **lib.rs** ```rust #[napi(object)] pub struct Pet { pub name: String, pub kind: u32, } ``` Any `impl` block of this `struct` will not affect the JavaScript `Object`. ::: warning If you want to convert a Rust `struct` into JavaScript `Object` using `#[napi(object)]` attribute, you need to mark all of its fields as `pub`. ::: Once `struct` is marked as `#[napi(object)]`, you can use it as a function argument type or return type. **lib.rs** ```rust #[napi(object)] pub struct Pet { pub name: String, pub kind: u32, } #[napi] pub fn print_pet(pet: Pet) { println!("{}", pet.name); } #[napi] pub fn create_cat() -> Pet { Pet { name: "cat".to_string(), kind: 1, } } ``` ::: warning The JavaScript Object passed in or returned from Rust is cloned. This means any mutation on JavaScript `Object` will not affect the original Rust `struct`. And any mutation on Rust `struct` will not affect the JavaScript `Object` either. ::: --- # Class ::: tip There is no concept of a class in Rust. We use `struct` to represent a JavaScript `Class`. ::: ## Choose the right JavaScript shape `#[napi]` on a struct creates a JavaScript class with native identity and methods. Other struct attributes create value shapes instead: | Rust declaration | JavaScript representation | Use it for | | ---------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------- | | `#[napi] struct` | Class instance backed by one Rust value | Stateful native objects, methods, identity, and references | | `#[napi(object)] struct` | Plain object copied to/from an owned Rust struct | Records, options, and configuration shapes | | `#[napi(transparent)] struct Wrapper(T)` | The inner value `T` | Rust newtypes that should not add a JavaScript wrapper | | `#[napi(array)]` tuple struct | JavaScript Array / TypeScript tuple | Fixed positional data | See [Type conversions](/docs/concepts/type-conversions) for direction and ownership rules and [`#[napi]` attributes](/docs/concepts/napi-attributes) for the complete shape controls. ## `Constructor` ### Default `constructor` If all fields in a `Rust` struct are `pub`, then you can use `#[napi(constructor)]` to make the `struct` have a default `constructor`. **lib.rs** ```rust #[napi(constructor)] pub struct AnimalWithDefaultConstructor { pub name: String, pub kind: u32, } ``` **index.d.ts** ```ts export class AnimalWithDefaultConstructor { name: string kind: number constructor(name: string, kind: number) } ``` Every public field is part of the JavaScript API: napi-rs generates a getter and, unless the field is `#[napi(readonly)]`, a setter. Its Rust type must therefore support the generated JavaScript conversion direction. Keep native-only state private, as `count` is in the custom-constructor example below. ### Custom `constructor` If you want to define a custom `constructor`, you can use `#[napi(constructor)]` on your constructor `fn` in the struct `impl` block. **lib.rs** ```rust #[napi(js_name = "QueryEngine")] pub struct JsQueryEngine { count: u32, } #[napi] impl JsQueryEngine { #[napi(constructor)] pub fn new() -> Self { JsQueryEngine { count: 0 } } } ``` **index.d.ts** ```ts export class QueryEngine { constructor() } ``` ::: warning **NAPI-RS** does not currently support `private constructor`. Your custom constructor must be `pub` in Rust. ::: ## Factory Besides `constructor`, you can also define factory methods on `Class` by using `#[napi(factory)]`. **lib.rs** ```rust #[napi(js_name = "QueryEngine")] pub struct JsQueryEngine { count: u32, } #[napi] impl JsQueryEngine { #[napi(factory)] pub fn with_initial_count(count: u32) -> Self { JsQueryEngine { count } } } ``` **index.d.ts** ```ts export class QueryEngine { static withInitialCount(count: number): QueryEngine constructor() } ``` ::: warning If no `#[napi(constructor)]` is defined in the `struct`, and you attempt to create an instance (`new`) of the `Class` in JavaScript, an error will be thrown. ::: **test.mjs** ```js {3} import { QueryEngine } from './index.js' new QueryEngine() // Error: Class contains no `constructor`, cannot create it! ``` ## `class method` You can define a JavaScript class method with `#[napi]` on a struct method in **Rust**. **lib.rs** ```rust #[napi(js_name = "QueryEngine")] pub struct JsQueryEngine { count: u32, } #[napi] impl JsQueryEngine { #[napi(factory)] pub fn with_initial_count(count: u32) -> Self { JsQueryEngine { count } } /// Class method #[napi] pub async fn query(&self, query: String) -> napi::Result { Ok(format!("{query}: {}", self.count)) } #[napi] pub fn status(&self) -> napi::Result { Ok(self.count) } } ``` **index.d.ts** ```ts export class QueryEngine { static withInitialCount(count: number): QueryEngine constructor() query(query: string): Promise status(): number } ``` ::: warning `async fn` needs the `napi4` and `tokio_rt` features to be enabled. ::: ::: tip Any `fn` in `Rust` that returns `Result` will be treated as `T` in JavaScript/TypeScript. If the `Result` is `Err`, a JavaScript Error will be thrown. ::: ## `Getter` Define [JavaScript class `getter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) using `#[napi(getter)]`. The Rust `fn` must be a struct method, not an associated function. **lib.rs** ```rust #[napi(js_name = "QueryEngine")] pub struct JsQueryEngine { count: u32, } #[napi] impl JsQueryEngine { #[napi(factory)] pub fn with_initial_count(count: u32) -> Self { JsQueryEngine { count } } /// Class method #[napi] pub async fn query(&self, query: String) -> napi::Result { Ok(format!("{query}: {}", self.count)) } #[napi(getter)] pub fn status(&self) -> napi::Result { Ok(self.count) } } ``` **index.d.ts** ```ts {4} export class QueryEngine { static withInitialCount(count: number): QueryEngine constructor() get status(): number } ``` ## `Setter` Define [JavaScript class `setter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) using `#[napi(setter)]`. The Rust `fn` must be a struct method, not an associated function. **lib.rs** ```rust #[napi(js_name = "QueryEngine")] pub struct JsQueryEngine { count: u32, } #[napi] impl JsQueryEngine { #[napi(factory)] pub fn with_initial_count(count: u32) -> Self { JsQueryEngine { count } } /// Class method #[napi] pub async fn query(&self, query: String) -> napi::Result { Ok(format!("{query}: {}", self.count)) } #[napi(getter)] pub fn status(&self) -> napi::Result { Ok(self.count) } #[napi(setter)] pub fn count(&mut self, count: u32) { self.count = count; } } ``` **index.d.ts** ```ts {5} export class QueryEngine { static withInitialCount(count: number): QueryEngine constructor() get status(): number set count(count: number) } ``` ## Class as argument `Class` is different from [`Object`](./object). The Rust value is wrapped by a JavaScript instance and managed by that environment's garbage collector. Pass the instance back to Rust as `&T` for shared access or `&mut T` for mutable access; the value is not cloned from a plain object. Only public struct fields become JavaScript properties. They are writable by default because napi-rs generates both accessors; `#[napi(readonly)]` suppresses the setter, and `#[napi(skip)]` suppresses both accessors. Private fields remain native implementation details. A writable field needs both `ToNapiValue` and `FromNapiValue`; a readonly field needs only `ToNapiValue`. See the [field attribute reference](/docs/concepts/napi-attributes#fields), including the `#[napi(constructor)]` shorthand limitation. **lib.rs** ```rust {1,5} #[napi] pub fn accept_class(engine: &QueryEngine) { // ... } #[napi] pub fn accept_class_mut(engine: &mut QueryEngine) { // ... } ``` **index.d.ts** ```ts export function acceptClass(engine: QueryEngine): void export function acceptClassMut(engine: QueryEngine): void ``` For nested class instances, arrays of class instances, and `ClassInstance`, see the [class section of the conversion reference](/docs/concepts/type-conversions#objects-classes-and-custom-shapes). ## Property attributes The default Property attributes are `writable = true`, `enumerable = true` and `configurable = true`. You can control the Property attributes over the `#[napi]` macro: **lib.rs** ```rust {20} use napi::bindgen_prelude::*; use napi_derive::napi; // A complex struct that cannot be exposed to JavaScript directly. #[napi] pub struct QueryEngine { num: i32, } #[napi] impl QueryEngine { #[napi(constructor)] pub fn new() -> Result { Ok(Self { num: 42, }) } // writable / enumerable / configurable #[napi(writable = false)] pub fn get_num(&self) -> i32 { self.num } } ``` In this case, the `getNum` method of `QueryEngine` is not writable: **main.mjs** ```js {4} import { QueryEngine } from './index.js' const qe = new QueryEngine() qe.getNum = function () {} // TypeError: Cannot assign to read only property 'getNum' of object '#' ``` ## Custom Finalize logic **NAPI-RS** will drop the Rust struct wrapped in the JavaScript object when the JavaScript object is garbage collected. You can also specify custom finalize logic for the Rust struct. **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi(custom_finalize)] pub struct CustomFinalize { width: u32, height: u32, inner: Vec, } #[napi] impl CustomFinalize { #[napi(constructor)] pub fn new(mut env: Env, width: u32, height: u32) -> Result { let inner_size = u64::from(width) .checked_mul(u64::from(height)) .and_then(|pixels| pixels.checked_mul(4)) .and_then(|bytes| usize::try_from(bytes).ok()) .ok_or_else(|| Error::new(Status::InvalidArg, "image dimensions are too large"))?; let external_size = i64::try_from(inner_size) .map_err(|_| Error::new(Status::InvalidArg, "image dimensions are too large"))?; let mut inner = Vec::new(); inner.try_reserve_exact(inner_size).map_err(|err| { Error::new( Status::GenericFailure, format!("failed to allocate image buffer: {err}"), ) })?; inner.resize(inner_size, 0); env.adjust_external_memory(external_size)?; Ok(Self { width, height, inner, }) } } impl ObjectFinalize for CustomFinalize { fn finalize(self, mut env: Env) -> Result<()> { let external_size = i64::try_from(self.inner.len()) .map_err(|_| Error::new(Status::InvalidArg, "image buffer is too large"))?; env.adjust_external_memory(-external_size)?; Ok(()) } } ``` First, you can set `custom_finalize` attribute in `#[napi]` macro, and NAPI-RS will not generate the default `ObjectFinalize` for the Rust struct. Then, you can implement `ObjectFinalize` yourself for the Rust struct. In this case, the `CustomFinalize` struct increases external memory in the **constructor** and decreases it in `fn finalize`. ## `instance of` There is `fn instance_of` on all `#[napi]` class: **lib.rs** ```rust {9} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi(constructor)] pub struct NativeClass {} #[napi] pub fn is_native_class_instance(env: &Env, value: Unknown) -> Result { NativeClass::instance_of(env, &value) } ``` **main.mjs** ```js import { NativeClass, isNativeClassInstance } from './index.js' const nc = new NativeClass() console.log(isNativeClassInstance(nc)) // true console.log(isNativeClassInstance(1)) // false ``` --- # Enum ::: warning There is no `enum` in JavaScript, and Rust `enum` is very different from TypeScript `enum`. You need to read this section carefully before you use Rust `enum` in JavaScript. ::: In **NAPI-RS**, Rust `enum` is basically transformed into a plain JavaScript Object. **lib.rs** ```rust #[napi] pub enum Kind { Duck, Dog, Cat, } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export const enum Kind { Duck, Dog, Cat, } ``` In `TypeScript`, numeric `enum` members also get a reverse mapping from enum values to enum names. However, in Rust, we don't have this reverse mapping behavior. It is just a plain JavaScript Object. Numeric variants default to consecutive `i32` values starting at zero. Explicit Rust integer discriminants are preserved, and following implicit variants continue from the previous value. ## String enum **lib.rs** ```rust #[napi(string_enum)] pub enum Kind { Duck, Dog, Cat, } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export const enum Kind { Duck = 'Duck', Dog = 'Dog', Cat = 'Cat', } ``` Use `string_enum = "case"` to transform every variant name, or `#[napi(value = "...")]` on one variant to choose its exact JavaScript value. Supported cases are listed in the [`#[napi]` attribute reference](/docs/concepts/napi-attributes#enums). **lib.rs** ```rust #[napi(string_enum = "kebab-case")] pub enum AccessMode { ReadOnly, #[napi(value = "read-write")] Writable, } ``` ## Structured enum An enum with a data-carrying variant becomes a discriminated object union rather than a JavaScript enum object. **lib.rs** ```rust #[napi] pub enum Event { Ready, FileChanged { path: String }, Progress(u32, u32), } ``` **index.d.ts** ```ts export type Event = | { type: 'Ready' } | { type: 'FileChanged'; path: string } | { type: 'Progress'; field0: number; field1: number } ``` The discriminator is `type` by default. Change it with `discriminant = "kind"`, and transform variant values with `discriminant_case = "camelCase"` or another supported case. Named variant fields keep their names; tuple fields become `field0`, `field1`, and so on. A field cannot have the same JavaScript name as the discriminator. Structured enum conversion is owned: accepting one reads and converts its fields into a Rust enum value, while returning one creates a new JavaScript object. `object_from_js = false` or `object_to_js = false` can make the type one-directional. See [Type conversions](/docs/concepts/type-conversions#objects-classes-and-custom-shapes). **NAPI-RS** doesn't support generating Rust `enum` `impl` into JavaScript. --- # Errors and panics Expected failures should cross the native boundary as `napi::Result`, an alias for `std::result::Result`. napi-rs turns the `Err` into a synchronous exception or a Promise rejection according to the exported API. **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn divide(left: f64, right: f64) -> Result { if right == 0.0 { return Err(Error::new(Status::InvalidArg, "right must not be zero")); } Ok(left / right) } ``` **index.mjs** ```js try { divide(1, 0) } catch (error) { console.error(error.code) // "InvalidArg" console.error(error.message) // "right must not be zero" } ``` TypeScript does not encode thrown exceptions or rejected Promises. Document domain errors in JSDoc and test their JavaScript shape. ## The core types ```rust pub type Result = std::result::Result>; pub struct Error { pub status: S, pub reason: String, pub cause: Option>, // private reference to an original JavaScript exception when available } ``` | Field | JavaScript meaning | Notes | | ----------------- | ------------------ | --------------------------------------------------------------------------- | | `reason` | `error.message` | Human-readable description. | | `status.as_ref()` | `error.code` | `Status` is primarily a Node-API status, not an application error taxonomy. | | `cause` | `error.cause` | Set with `set_cause`; nested causes are converted recursively. | Use `Error::from_reason(message)` for a `GenericFailure`, or `Error::new(status, message)` when a Node-API status conveys useful information. **lib.rs** ```rust #[napi] pub fn load_config() -> Result<()> { std::fs::read_to_string("config.json") .map(|_| ()) .map_err(|source| { let mut error = Error::new(Status::GenericFailure, "could not load config"); error.set_cause(Error::from(source)); error }) } ``` `Error` implements conversions for common failures including `std::io::Error` and `std::ffi::NulError`. With `serde-json`, it also converts `serde_json::Error` to `Status::InvalidArg`. ## Synchronous functions When a synchronous exported function returns `Err`, the generated callback throws a JavaScript `Error` before returning to JavaScript. | Rust return | JavaScript behavior | | ----------------------------- | ------------------------------------------------- | | `T` | Returns a value. Conversion failures still throw. | | `Result` with `Ok(value)` | Converts and returns `value`. | | `Result` with `Err(error)` | Throws an `Error`. | Argument conversion happens before the Rust function is called. A wrong input type therefore throws a conversion error even if the function's Rust return type is not `Result`. ## Async functions After its arguments have converted successfully, an exported Rust `async fn` returns a JavaScript Promise: | Future outcome | JavaScript behavior | | ------------------------------- | --------------------------------------------- | | `T` | Fulfills the Promise after converting `T`. | | `Result::Ok(value)` | Fulfills the Promise with `value`. | | `Result::Err(error)` | Rejects the Promise with the converted error. | | Return-value conversion failure | Rejects the Promise. | Argument validation and conversion still run synchronously before that Promise is created. Invalid input can therefore throw synchronously. With `#[napi(return_if_invalid)]`, invalid input returns `undefined` synchronously instead, even though the generated declaration still describes the successful path as returning `Promise`. **lib.rs** ```rust #[napi] pub async fn read_text(path: String) -> Result { napi::tokio::fs::read_to_string(&path) .await .map_err(|source| { let mut error = Error::new(Status::GenericFailure, format!("could not read {path}")); error.set_cause(source.into()); error }) } ``` This example requires `napi`'s `async` (or `tokio_rt`) and `tokio_fs` features. See [async fn](/docs/concepts/async-fn) for runtime and lifetime rules. ### Async stack traces Errors constructed after work moves to another thread normally have a stack beginning at the rejection point, not the original JavaScript call. The optional `deferred_trace` feature captures a JavaScript error when the deferred Promise is created and reuses that stack when rejecting a napi-rs deferred. **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["async", "deferred_trace"] } ``` This adds an error object/reference to each affected deferred operation. Enable it when the diagnostic value is worth that allocation and reference-management cost. ## `AsyncTask` `AsyncTask` runs `Task::compute` in libuv's worker pool and completes the Promise on the JavaScript thread. 1. `compute` returns `Result` off the JavaScript thread. 2. `Ok(output)` is passed to `resolve` on the JavaScript thread. 3. `Err(error)` is passed to `reject` on the JavaScript thread. 4. The resulting `JsValue` resolves the Promise; an error from `resolve` or `reject` rejects it. 5. `finally` runs after either path for cleanup. The default `Task::reject` simply returns the same `Err`, so the Promise rejects. A custom `reject` may instead return `Ok(fallback)`, which **recovers** and fulfills the Promise. **lib.rs** ```rust impl Task for Lookup { type Output = String; type JsValue = String; fn compute(&mut self) -> Result { self.lookup().map_err(Error::from) } fn resolve(&mut self, _: Env, output: Self::Output) -> Result { Ok(output) } fn reject(&mut self, _: Env, error: Error) -> Result { if error.status == Status::GenericFailure { Ok("default".to_owned()) // Promise fulfillment, not rejection } else { Err(error) } } } ``` Cancellation before libuv starts the task rejects with an error whose name is `AbortError`. Once the task has started, cancellation is not guaranteed to stop the computation. See [AsyncTask](/docs/concepts/async-task). ## ThreadsafeFunction errors ThreadsafeFunction has two error strategies: - With `CalleeHandled = true` (the default), the JavaScript callback is error-first: `(error, value) => ...`. Call it with `Ok(value)` or `Err(error)`. - With `CalleeHandled = false`, the generated callback has no error parameter and the Rust call accepts the value directly. Handle native failures before calling it. `call_with_return_value` reports the callback result to its Rust completion callback. With `CalleeHandled = true`, `call_async` also returns a JavaScript throw as `Err`. With `CalleeHandled = false`, use `call_async_catch`; plain `call_async` routes a synchronous throw through `napi_fatal_exception` instead. Fire-and-forget calls cannot turn a later JavaScript throw into the return value of the originating Rust call. ThreadsafeFunction queue and lifecycle failures use Node-API statuses such as `QueueFull` or `Closing`; always inspect the return value of non-blocking or async call methods when the API provides one. See [ThreadsafeFunction](/docs/concepts/threadsafe-function) for its complete generic parameters and call modes. ## Custom error codes `Error` accepts any status type implementing `AsRef`. This sets `error.code` without changing the JavaScript error subclass. **lib.rs** ```rust #[derive(Debug)] pub enum ConfigError { Missing, Invalid, } impl AsRef for ConfigError { fn as_ref(&self) -> &str { match self { Self::Missing => "ERR_CONFIG_MISSING", Self::Invalid => "ERR_CONFIG_INVALID", } } } #[napi] pub fn validate_config(present: bool) -> Result<(), ConfigError> { if present { Ok(()) } else { Err(Error::new(ConfigError::Missing, "configuration is required")) } } ``` The generated wrapper accepts the custom status because it only needs `AsRef`. If lower-level napi-rs APIs must convert their `Status` into the custom type, also implement `From`. ## Error subclasses and arbitrary thrown values Returning an ordinary `Error` from an exported function produces a JavaScript `Error`. To throw a more specific built-in subclass directly, use `Env`: **lib.rs** ```rust #[napi] pub fn set_percentage(env: Env, value: f64) -> Result<()> { if !(0.0..=100.0).contains(&value) { return env.throw_range_error("percentage must be between 0 and 100", Some("ERR_RANGE")); } Ok(()) } ``` Available helpers include `throw_error`, `throw_type_error`, and `throw_range_error`. `throw_syntax_error` requires `napi9`. `Env::throw(value)` can throw any `ToNapiValue`, including a custom JavaScript error object. The lower-level wrappers `JsError`, `JsTypeError`, `JsRangeError`, and, with `napi9`, `JsSyntaxError` can construct or throw those subclasses when working with raw environments. ::: warning After calling an `Env::throw_*` method, return immediately. A JavaScript exception is pending in that environment; continuing to call unrelated Node-API operations can replace or obscure the original failure. ::: ## Preserving a JavaScript exception Converting an `Unknown` JavaScript value into `Error` records its message and cause. On native builds it also attempts to retain a reference to the original value. When that retained value is a JavaScript `Error` and the Rust error is converted back in its owning JavaScript environment, napi-rs can reuse the object, preserving its subclass, stack, and custom properties. A retained non-`Error` value is not passed through by `Result` error conversion; napi-rs rebuilds a generic `Error` from the owned error data instead. **lib.rs** ```rust #[napi] pub fn pass_error_through(value: Unknown) -> Result<()> { Err(value.into()) } ``` Important boundaries: - `Error::try_clone` always preserves owned status, reason, and cause information. - With Node-API 4 lifecycle support, a clone can share the retained reference safely across threads, but the original object is only dereferenced on its owning JavaScript thread. - When an error is surfaced in another environment/thread, napi-rs rebuilds a fresh generic `Error` from status, reason, and cause rather than touching a foreign environment. - WASI builds do not retain a native `napi_ref`; they rebuild from the available data. Do not use `try_clone` as a guarantee of JavaScript object identity across workers or isolates. ## `anyhow` Enable `error_anyhow` to add conversion from `anyhow::Error` and re-export the dependency through napi-rs: **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["error_anyhow"] } ``` **lib.rs** ```rust #[napi] pub fn parse_document(source: String) -> Result { parse(&source).map_err(Error::from) } ``` The conversion uses `Status::GenericFailure` and formats the anyhow error chain into the reason. If callers need stable machine-readable codes or a structured `cause`, map the domain error into `Error` explicitly instead. ## Panics are not ordinary errors A Rust panic is not a supported substitute for `Result` at the FFI boundary. An uncaught panic in a synchronous generated callback can terminate the process. `#[napi(catch_unwind)]` wraps a function or method call in `std::panic::catch_unwind` and converts an unwinding payload into a `GenericFailure` error: **lib.rs** ```rust #[napi(catch_unwind)] pub fn call_untrusted_rust() { library_that_may_panic(); } ``` Its limits are fundamental: - It only works when the crate is built with an unwind-capable panic strategy. `panic = "abort"` cannot be caught. - Some Rust operations abort without unwinding. - It catches the Rust call at that generated boundary, not panics on arbitrary detached threads. - Catching a panic does not prove that external state remains consistent. Panics while polling napi-rs's Tokio tasks are observed by the runtime and normally reject the deferred Promise, but the available panic payload and stack are limited. Keep recoverable failures in `Result` and reserve panics for violated internal invariants. ## Design checklist - Use stable custom codes for failures callers are expected to branch on. - Preserve the original failure with `cause` rather than concatenating unrelated messages. - Throw or reject; do not log-and-return a plausible value unless recovery is part of the API contract. - In `AsyncTask::reject`, remember that `Ok` fulfills the Promise. - Do not access `Env`, scoped JavaScript values, or raw `napi_value`s from worker threads. - Treat error object identity as local to one JavaScript environment. - Test the JavaScript `name`, `code`, `message`, `cause`, and sync-versus-async behavior—not only the Rust result. --- # async fn ::: tip You must enable the **_async_** or **_tokio_rt_** feature in `napi` to use `async fn`: **Cargo.toml** ```toml {2} [dependencies] napi = { version = "3", features = ["async", "tokio_fs", "tokio_time"] } napi-derive = "3" ``` The examples below use the `tokio_fs` and `tokio_time` subfeatures. Enable only the Tokio APIs your addon uses. ::: ## Tokio integration You can do a lot of async/multi-threaded work with `AsyncTask` and `ThreadsafeFunction`, but sometimes you may want to use the crates from the Rust async ecosystem directly. With `async` or `tokio_rt` enabled, **NAPI-RS** provides a Tokio runtime. If you `await` a Tokio future in an exported `async fn`, **NAPI-RS** executes it on that runtime and converts the result into a JavaScript `Promise`. **lib.rs** ```rust {6} use napi::bindgen_prelude::*; use napi_derive::napi; use napi::tokio::fs; #[napi] pub async fn read_file_async(path: String) -> Result { let content = fs::read(path).await?; Ok(content.into()) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export function readFileAsync(path: string): Promise ``` ## Unsafe `&mut self` In some cases, you may need to use `&mut self` in an `async fn`. However, this is `unsafe` in **NAPI-RS**, because the `self` is also _owned_ by the Node.js runtime. You cannot ensure that the `self` is only owned by Rust. **lib.rs** ```rust {9} use napi_derive::napi; #[napi] pub struct Engine {} #[napi] impl Engine { #[napi] pub async fn run(&mut self) {} } ``` ```rust error: &mut self in async napi methods should be marked as unsafe --> src/lib.rs:9:18 | 9 | pub async fn run(&mut self) {} | ^^^ ``` You need to mark the `fn` as `unsafe` to use `&mut self` in an `async fn`. **lib.rs** ```rust {9} use napi_derive::napi; #[napi] pub struct Engine {} #[napi] impl Engine { #[napi] pub async unsafe fn run(&mut self) {} } ``` ## Auto reference Usually, JavaScript values are only valid within a function call. `async fn` is not the case, the JavaScript values may be garbage collected in any `await` point. ::: info See [Understanding Lifetime](/docs/concepts/understanding-lifetime) for more details. ::: There are 3 kinds of parameters are automatically turned into `Reference` types: - `&self` - `&mut self` - `This` Considering the following example: **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub struct NativeClass { name: String, } #[napi] impl NativeClass { #[napi(constructor)] pub fn new(name: String) -> Self { Self { name } } #[napi] pub async fn sleep(&self, delay: u32) -> Result<&str> { napi::tokio::time::sleep(std::time::Duration::new(delay as u64, 0)).await; Ok(&self.name) } } ``` **index.ts** ```ts const nativeClass = new NativeClass('Brooklyn') const name = await nativeClass.sleep(1) console.log(name) // Brooklyn ``` There is a implicit [`napi_create_reference`](https://nodejs.org/api/n-api.html#napi_create_reference) call for the JavaScript `Object` value which holds the `NativeClass` before the `async fn` call; and a implicit [`napi_delete_reference`](https://nodejs.org/api/n-api.html#napi_delete_reference) call after the `async fn` call. This strategy makes sure the `NativeClass` is alive during the `async fn` call. ## Beyond `async fn`: `AsyncBlock` An exported `async fn` covers the common case, but it always resolves its promise with the function's return value, converted after the future completes. When you need more control — resolving with a value that can only be created on the JavaScript thread (for example a zero-copy `BufferSlice<'static>` or a Web `Response`), or running cleanup through a dispose hook when the future settles — return an `AsyncBlock` from a synchronous `#[napi]` function instead. The future starts eagerly on the NAPI-RS runtime and converts into a JavaScript `Promise`, just like an `async fn`. See [Web Streams: `AsyncBlock`](/docs/concepts/streams#asyncblock-a-promise-with-a-dispose-hook) for the full `AsyncBlockBuilder` API and examples. ## Custom runtimes By default the future runs on the NAPI-RS-managed Tokio runtime. With the `async-runtime` Cargo feature you can instead register your own executor — including tokio-free runtimes for threadless WASI or workerd — and every generated `async fn` will run on it. See [Custom async runtime](/docs/concepts/async-runtime). --- # Promise ## `Promise` Awaiting a JavaScript `Promise` in Rust sounds crazy, but it's feasible in **NAPI-RS**. The `Promise` in **NAPI-RS** implements the `std::future::Future` trait, so you can use the `await` keyword to await it. ::: tip Awaiting a JavaScript `Promise` needs the `async` or `tokio_rt` feature. `tokio_rt` enables `napi4` for you. ::: ::: info `Promise` is `Send` when `T` is `Send`, so the compiler prevents a non-`Send` resolved value from crossing Tokio worker threads. ::: **lib.rs** ```rust {5} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub async fn async_plus_100(p: Promise) -> Result { let v = p.await?; v.checked_add(100) .ok_or_else(|| Error::new(Status::InvalidArg, "result exceeds u32")) } ``` **test.mjs** ```js {4} import { asyncPlus100 } from './index.js' const fx = 20 const result = await asyncPlus100( new Promise((resolve) => { setTimeout(() => resolve(fx), 50) }), ) console.log(result) // 120 ``` ## `PromiseRaw<'env, T>` `PromiseRaw<'env, T>` represent the raw `Promise` value in the `JavaScript`, it contains the lifetime so it can only be used in the sync context. But conveniently, it can call methods on the JavaScript Promise, such as `then`, `catch`, and `finally`. **lib.rs** ```rust {6} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn promise_callback(promise: PromiseRaw) -> Result> { promise.then(|ctx| Ok(ctx.value + 100)) } ``` **index.ts** ```js import { promiseCallback } from './index.js' const value = await promiseCallback(Promise.resolve(100)) console.log(value) // 200 ``` ## `AsyncBlock` `AsyncBlock` is the other way to return a `Promise` from Rust. Where an exported [`async fn`](/docs/concepts/async-fn) starts when JavaScript calls it and resolves with the function's return value, an `AsyncBlock` wraps a manually built future via `AsyncBlockBuilder` — which additionally lets you attach a **dispose hook** (`.with_dispose`) or a **map closure** (`build_with_map`) that runs on the JavaScript thread at resolution time, so the promise can resolve with values that can only be created there (a zero-copy `BufferSlice<'static>`, an instance of a JavaScript class, …). The future starts eagerly when the Rust function is called, not when the promise is awaited. **lib.rs** ```rust #[napi] pub fn process_buffer(env: &Env, buffer: Buffer) -> Result> { AsyncBlockBuilder::new(async move { Ok(buffer) }).build(env) } ``` **index.d.ts** ```ts export declare function processBuffer(buffer: Buffer): Promise ``` See [Web Streams](/docs/concepts/streams#asyncblock-a-promise-with-a-dispose-hook) for the full `AsyncBlockBuilder` API and worked examples, and [TypedArray](/docs/concepts/typed-array) for more `AsyncBlock` usage with buffers. --- # AsyncTask We need to talk about `Task` before talking about `AsyncTask`. ## `Task` Addon modules often need to leverage async helpers from libuv as part of their implementation. This allows them to schedule work to be executed asynchronously so that their methods can return in advance of the work being completed. This allows them to avoid blocking the overall execution of the Node.js application. The `Task` trait provides a way to define such an asynchronous task that needs to run in the libuv thread. You can implement the `compute` method, which will be called in the libuv thread. **lib.rs** ```rust {20-22} use napi::bindgen_prelude::*; use napi_derive::napi; fn fib(n: u32) -> u32 { if n <= 1 { return n; } fib(n - 1) + fib(n - 2) } pub struct AsyncFib { input: u32, } #[napi] impl Task for AsyncFib { type Output = u32; type JsValue = u32; fn compute(&mut self) -> Result { Ok(fib(self.input)) } fn resolve(&mut self, _: Env, output: u32) -> Result { Ok(output) } } #[napi] pub fn async_fib(input: u32) -> AsyncTask { AsyncTask::new(AsyncFib { input }) } ``` `fn compute` runs on the libuv thread, so you can run heavy computation here without blocking the main JavaScript thread. You may notice there are two associated types on the `Task` trait. The `type Output` and the `type JsValue`. `Output` is the return type of the `compute` method. `JsValue` is the return type of the `resolve` method. ::: tip We need separate `type Output` and `type JsValue` because we cannot call the JavaScript function back in `fn compute`, as it is not executed on the main thread. So we need `fn resolve`, which runs on the main thread, to create the `JsValue` from `Output` and `Env` and call it back in JavaScript. ::: You can use the low-level API `Env::spawn` to spawn a defined `Task` in the libuv thread pool. See example in [Reference](/docs/concepts/reference). In addition to `compute` and `resolve`, you can also provide a `reject` method to do some cleanup when `Task` runs into an error, like `unref`ing some object: **lib.rs** ```rust {32} use napi::bindgen_prelude::*; use napi_derive::napi; pub struct CountBufferLength { data: Buffer, } impl CountBufferLength { pub fn new(data: Buffer) -> Self { Self { data } } } impl Task for CountBufferLength { type Output = usize; type JsValue = u32; fn compute(&mut self) -> Result { if self.data.len() == 10 { return Err(Error::new( Status::GenericFailure, "Random fatal error".to_string(), )); } Ok((&self.data).len()) } fn resolve(&mut self, _: Env, output: Self::Output) -> Result { u32::try_from(output) .map_err(|_| Error::new(Status::InvalidArg, "buffer length exceeds u32")) } fn reject(&mut self, _: Env, err: Error) -> Result { // catch the error if err.status == Status::GenericFailure { Ok(0) } else { Ok(1) } } } #[napi] pub fn async_count_buffer_length(data: Buffer) -> AsyncTask { AsyncTask::new(CountBufferLength { data }) } ``` You can also provide a `finally` method to do something after the `Task` is `resolved` or `rejected`: **lib.rs** ```rust {41} use napi::bindgen_prelude::*; use napi_derive::napi; pub struct CountBufferLength { data: Buffer, } impl CountBufferLength { pub fn new(data: Buffer) -> Self { Self { data } } } impl Task for CountBufferLength { type Output = usize; type JsValue = u32; fn compute(&mut self) -> Result { if self.data.len() == 10 { return Err(Error::new( Status::GenericFailure, "Random fatal error".to_string(), )); } Ok((&self.data).len()) } fn resolve(&mut self, _: Env, output: Self::Output) -> Result { u32::try_from(output) .map_err(|_| Error::new(Status::InvalidArg, "buffer length exceeds u32")) } fn reject(&mut self, _: Env, err: Error) -> Result { // catch the error if err.status == Status::GenericFailure { Ok(0) } else { Ok(1) } } fn finally(self, _: Env) -> Result<()> { println!("finally"); drop(self.data); Ok(()) } } #[napi] pub fn async_count_buffer_length(data: Buffer) -> AsyncTask { AsyncTask::new(CountBufferLength { data }) } ``` ::: tip The `#[napi]` macro above the `impl Task for AsyncFib` is just for `.d.ts` generation. If no `#[napi]` is defined here, the generated TypeScript type of returned `AsyncTask` will be `Promise`. ::: ## `AsyncTask` The `Task` you define cannot be returned to JavaScript directly—the JavaScript engine has no idea how to run and resolve the value from your `struct`. `AsyncTask` is a wrapper of `Task` that can be returned to the JavaScript engine. It can be created with a `Task` and an optional [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). **lib.rs** ```rust #[napi] fn async_fib(input: u32) -> AsyncTask { AsyncTask::new(AsyncFib { input }) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export function asyncFib(input: number): Promise ``` ### Create `AsyncTask` With `AbortSignal` In some scenarios, you may want to abort the queued `AsyncTask`, for example, using `debounce` on some compute tasks. You can provide `AbortSignal` to `AsyncTask`, so that you can abort the `AsyncTask` if it has not been started. **lib.rs** ```rust {4} use napi::bindgen_prelude::AbortSignal; #[napi] fn async_fib(input: u32, signal: AbortSignal) -> AsyncTask { AsyncTask::with_signal(AsyncFib { input }, signal) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export function asyncFib(input: number, signal: AbortSignal): Promise ``` If you invoke `AbortController.abort` before libuv starts the `AsyncTask`, Node-API can cancel the queued work and the Promise rejects with an error whose `name` is `AbortError`. **test.mjs** ```js {6} import { asyncFib } from './index.js' const controller = new AbortController() asyncFib(20, controller.signal).catch((e) => { console.error(e) // Error: AbortError }) controller.abort() ``` You can also provide `Option` to `AsyncTask` if you don't know if the `AsyncTask` needs to be aborted: **lib.rs** ```rust use napi::bindgen_prelude::AbortSignal; #[napi] fn async_fib(input: u32, signal: Option) -> AsyncTask { AsyncTask::with_optional_signal(AsyncFib { input }, signal) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export function asyncFib( input: number, signal?: AbortSignal | undefined | null, ): Promise ``` ::: tip If `AsyncTask` has already started, Node-API cannot cancel its `compute` callback; if it has completed, its Promise result is already settled. `AbortSignal::on_abort` callbacks registered in Rust still run when the JavaScript signal fires, even when cancellation is too late. The current adapter installs its handler while converting the argument and does not inspect `signal.aborted`, so pass a signal that has not already been aborted. It assigns `signal.onabort`, replacing any handler stored in that property; use `signal.addEventListener('abort', ...)` for independent JavaScript listeners. ::: ## `ScopedTask` `ScopedTask` is mostly equal to `Task`, but it pass `&'env Env` to the `resolve` and `reject` method, so that you can create a `JsValue` with lifetime from the `&'env Env`. For example: **lib.rs** ```rust {14,16} use napi::{JsString, ScopedTask, bindgen_prelude::*}; use napi_derive::napi; pub struct CountBufferLength { data: Buffer, } impl CountBufferLength { pub fn new(data: Buffer) -> Self { Self { data } } } impl<'env> ScopedTask<'env> for CountBufferLength { type Output = usize; type JsValue = JsString<'env>; fn compute(&mut self) -> Result { if self.data.len() == 10 { return Err(Error::new( Status::GenericFailure, "Random fatal error".to_string(), )); } Ok((&self.data).len()) } fn resolve(&mut self, env: &'env Env, output: Self::Output) -> Result { env.create_string(format!("{output}")) } fn reject(&mut self, env: &'env Env, err: Error) -> Result { // catch the error if err.status == Status::GenericFailure { env.create_string("Random fatal error".to_string()) } else { env.create_string("Random error".to_string()) } } fn finally(self, _: Env) -> Result<()> { drop(self.data); Ok(()) } } #[napi] pub fn async_count_buffer_length(data: Buffer) -> AsyncTask { AsyncTask::new(CountBufferLength { data }) } ``` --- # ThreadsafeFunction [`Threadsafe Function`](https://nodejs.org/api/n-api.html#asynchronous-thread-safe-function-calls) is a complex concept in Node.js. As we all know, Node.js is single-threaded, so you can't access [`napi_env`](https://nodejs.org/api/n-api.html#napi_env), [`napi_value`](https://nodejs.org/api/n-api.html#napi_value), and [`napi_ref`](https://nodejs.org/api/n-api.html#napi_ref) on another thread. ::: tip [`napi_env`](https://nodejs.org/api/n-api.html#napi_env), [`napi_value`](https://nodejs.org/api/n-api.html#napi_value), and [`napi_ref`](https://nodejs.org/api/n-api.html#napi_ref) are low level concepts in `Node-API`, which the `#[napi]` macro of **NAPI-RS** is built on top of. **NAPI-RS** also provides a [low level API](/docs/concepts/env) to access the original `Node-API`. ::: `Node-API` provides complex `Threadsafe Function` APIs to call JavaScript functions on other threads. It's very complex, so many developers don't understand how to use it correctly. **NAPI-RS** wraps these APIs in a safe, ownership-based `ThreadsafeFunction` type that is much easier to use: **lib.rs** ```rust {10} use std::{sync::Arc, thread}; use napi::{ bindgen_prelude::*, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, }; use napi_derive::napi; #[napi] pub fn call_threadsafe_function(callback: ThreadsafeFunction) -> Result<()> { let tsfn = Arc::new(callback); for n in 0..100 { let tsfn = tsfn.clone(); thread::spawn(move || { tsfn.call(Ok(n), ThreadsafeFunctionCallMode::Blocking); }); } Ok(()) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export function callThreadsafeFunction( callback: (err: null | Error, result: number) => void, ): void ``` ## Return type The return type of the `ThreadsafeFunction` is the same as the return type of the JavaScript callback, you can define the return type in the second generic parameter of `ThreadsafeFunction`: **lib.rs** ```rust {10} use std::thread; use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; #[napi] pub fn call_threadsafe_function(callback: ThreadsafeFunction) { thread::spawn(move || { callback.call_with_return_value(Ok(1), ThreadsafeFunctionCallMode::Blocking, |ret, _| { println!("ret: {:?}", ret); // Ok(101) Ok(()) }); }); } ``` **index.ts** ```ts import { callThreadsafeFunction } from './index.js' callThreadsafeFunction((err, result) => { return result + 100 }) ``` ## CallJsBackArgs Sometimes the args passed to the `ThreadsafeFunction` are not the same as the args passed to the JavaScript callback. You can build the `ThreadsafeFunction` from `Function` with the `CallJsBackArgs` to achieve this: **lib.rs** ```rust {17} use std::thread; use napi::{ bindgen_prelude::*, threadsafe_function::{ThreadsafeCallContext, ThreadsafeFunctionCallMode}, }; use napi_derive::napi; struct Data { name: String, } #[napi] pub fn call_threadsafe_function(callback: Function) -> Result<()> { let tsfn = callback .build_threadsafe_function() .build_callback(|ctx: ThreadsafeCallContext| Ok(format!("Hello {}", ctx.value.name)))?; thread::spawn(move || { tsfn.call( Data { name: "John".to_string(), }, ThreadsafeFunctionCallMode::NonBlocking, ); }); Ok(()) } ``` ::: warning The callback argument and return types stored by a ThreadsafeFunction must be `'static`, because the callback can run after the exported Rust function has returned. Do not use scoped values such as `Unknown<'env>`, `Object<'env>`, or `Function<'env, ...>` as `CallJsBackArgs`. Convert to owned Rust data such as `String`, `Buffer`, or a plain owned struct before crossing the thread boundary. An explicit scoped lifetime here produces `E0521` because the borrowed JavaScript value would escape its callback scope; see [napi-rs#3383](https://github.com/napi-rs/napi-rs/issues/3383). ::: ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.ts** ```ts {4} import { callThreadsafeFunction } from './index.js' callThreadsafeFunction((data) => { console.log(data) // Hello John }) ``` ## Error Status The error status of the `ThreadsafeFunction` is the same as the error status of the JavaScript callback. You can define the error status in the fourth generic parameter of `ThreadsafeFunction`: **lib.rs** ```rust {25} use std::{sync::Arc, thread}; use napi::{ bindgen_prelude::*, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, }; use napi_derive::napi; pub struct CustomErrorStatus(String); impl AsRef for CustomErrorStatus { fn as_ref(&self) -> &str { &self.0 } } impl From for CustomErrorStatus { fn from(value: Status) -> Self { CustomErrorStatus(value.to_string()) } } #[napi] pub fn call_threadsafe_function( tsfn: Arc>, ) -> Result<()> { for n in 0..100 { let tsfn = tsfn.clone(); thread::spawn(move || { tsfn.call( Err(Error::new( CustomErrorStatus("Custom".to_owned()), format!("Custom error: {}", n), )), ThreadsafeFunctionCallMode::Blocking, ); }); } Ok(()) } ``` ## `CalleeHandled` error behavior There are two different error-handling strategies for `Threadsafe Function`. The strategy can be defined in the fifth generic parameter of `ThreadsafeFunction`: **lib.rs** ```rust let tsfn: ThreadsafeFunction = ... ``` ### `CalleeHandled: true` (default behavior) `Err` from Rust code will be passed into the first argument of the JavaScript callback. This behavior follows the async callback conventions from Node.js: https://nodejs.org/en/learn/asynchronous-work/javascript-asynchronous-programming-and-callbacks#handling-errors-in-callbacks. Many async APIs in Node.js are designed this way, like `fs.read`. With `CalleeHandled: true`, you must call the `ThreadsafeFunction` with the `Result` type, so that the `Error` will be handled and passed back to the JavaScript callback: **lib.rs** ```rust {11,16-22} use std::{sync::Arc, thread}; use napi::{ bindgen_prelude::*, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, }; use napi_derive::napi; #[napi] pub fn call_threadsafe_function( tsfn: Arc>, ) -> Result<()> { for n in 0..100 { let tsfn = tsfn.clone(); thread::spawn(move || { tsfn.call( Err(Error::new( Status::GenericFailure, format!("Error with: {n}"), )), ThreadsafeFunctionCallMode::Blocking, ); }); } Ok(()) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.ts** ```ts {5} import { callThreadsafeFunction } from './index.js' callThreadsafeFunction((err, result) => { if (err) { console.error(err) // [Error: Error with: 0] { code: 'GenericFailure' } } console.log(result) }) ``` ### `CalleeHandled: false` No `Error` will be passed back to the JavaScript side. You can use this strategy to avoid the `Ok` wrapping on the Rust side if your code will never return `Err`. With this strategy, `ThreadsafeFunction` doesn't need to be called with `Result`, and the first argument of JavaScript callback is the value from the Rust, not `Error | null`. ::: warning With the `CalleeHandled: false` strategy, the `ThreadsafeFunction` will not be able to handle the error in the Rust threads, so you can't pass the `Error` back to the JavaScript side. The plain `call` method has no error channel back to Rust. A synchronous throw in the JavaScript callback is routed through `napi_fatal_exception`, and a returned `Promise` is not awaited automatically. If Rust needs the callback's result, set a concrete `Return` type and use `call_async_catch`, or use `call_with_return_value` and handle the `Result` passed to its completion callback. Use this mode only when native failures are handled before `call` and the JavaScript callback cannot throw. ::: **lib.rs** ```rust {11} use std::{sync::Arc, thread}; use napi::{ bindgen_prelude::*, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, }; use napi_derive::napi; #[napi] pub fn call_threadsafe_function( tsfn: Arc>, ) -> Result<()> { for n in 0..100 { let tsfn = tsfn.clone(); thread::spawn(move || { tsfn.call(n, ThreadsafeFunctionCallMode::Blocking); }); } Ok(()) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts {2} export declare function callThreadsafeFunction( tsfn: (arg: number) => void, ): void ``` ## `Weak` ThreadsafeFunction By default, the `ThreadsafeFunction` will cause the event loop on the thread on which it is created to remain alive until the `ThreadsafeFunction` is destroyed. See [**Deciding whether to keep the process running**](https://nodejs.org/api/n-api.html#deciding-whether-to-keep-the-process-running). If you don't want to keep the Node.js process/event loop alive, you can define the `Weak` parameter of `ThreadsafeFunction` to `true`: **lib.rs** ```rust {11} use std::{sync::Arc, thread}; use napi::{ bindgen_prelude::*, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, }; use napi_derive::napi; #[napi] pub fn call_threadsafe_function( tsfn: Arc>, ) -> Result<()> { for n in 0..100 { let tsfn = tsfn.clone(); thread::spawn(move || { tsfn.call(n, ThreadsafeFunctionCallMode::Blocking); }); } Ok(()) } ``` If you call this function like this: **index.ts** ```ts import { callThreadsafeFunction } from './index.js' // Weak mode does not keep the event loop alive by itself. callThreadsafeFunction((n) => console.log(n)) ``` If nothing else keeps the event loop alive, Node.js may exit before some or all queued callbacks run. Other active handles or work can keep the process alive long enough to deliver them. Weak mode does not guarantee callback delivery or suppress callbacks; it only removes this `ThreadsafeFunction` as a reason to keep the loop alive. ## `MaxQueueSize` You can set the `MaxQueueSize` parameter of `ThreadsafeFunction` to limit the number of messages in the queue. ::: info `MaxQueueSize` sets the queue capacity in both call modes. When that capacity is reached, `Blocking` waits for space; `NonBlocking` returns immediately with `Status::QueueFull`. See [`napi_call_threadsafe_function`](https://nodejs.org/api/n-api.html#napi_call_threadsafe_function) for more details. ::: **lib.rs** ```rust {11,16} use std::{sync::Arc, thread}; use napi::{ bindgen_prelude::*, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, }; use napi_derive::napi; #[napi] pub fn call_threadsafe_function( tsfn: Arc>, ) -> Result<()> { thread::spawn(move || { for n in 0..100 { let tsfn = tsfn.clone(); let status = tsfn.call(n, ThreadsafeFunctionCallMode::NonBlocking); println!("{}", status) } }); Ok(()) } ``` When you call this function, and add heavy work in the callback, you will see the `QueueFull` status return from the `tsfn.call`: **index.ts** ```ts import { callThreadsafeFunction } from './index.js' function fib(n: number): number { if (n <= 1) return n return fib(n - 1) + fib(n - 2) } callThreadsafeFunction(() => { fib(40) }) ``` An illustrative run might produce output like this: ``` Ok Ok QueueFull QueueFull QueueFull QueueFull QueueFull QueueFull QueueFull QueueFull ... ``` The exact number and order of `Ok` and `QueueFull` results depends on when the JavaScript thread drains the queue relative to the producer thread. A capacity of one guarantees the backpressure behavior, not a fixed output sequence. --- # Iterators napi-rs can make a native class implement JavaScript's synchronous or asynchronous iteration protocol. These APIs are currently marked **experimental** in the Rust source: test the exact napi-rs and runtime versions you publish, and expect refinements to trait or lifecycle behavior. | Rust marker and trait | JavaScript protocol | Cargo feature | | ------------------------------------------------------ | ------------------------------------------------------------------- | ----------------------- | | `#[napi(iterator)]` + `Generator` or `ScopedGenerator` | `Symbol.iterator`, `next`, `return`, `throw` | Base `napi` API | | `#[napi(async_iterator)]` + `AsyncGenerator` | `Symbol.asyncIterator`, Promise-returning `next`, `return`, `throw` | `tokio_rt` (or `async`) | The two marker attributes are mutually exclusive on one class. A marked class also cannot have public fields named `next`, `return`, or `throw`, because napi-rs installs those protocol methods. ## Synchronous iterator Implement `Generator` when yielded values are owned Rust values: **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi(iterator)] pub struct Counter { current: u32, end: u32, } #[napi] impl Generator for Counter { type Yield = u32; type Next = u32; type Return = (); fn next(&mut self, value: Option) -> Option { if let Some(next) = value { self.current = next; } if self.current >= self.end { return None; } let value = self.current; self.current += 1; Some(value) } } #[napi] impl Counter { #[napi(constructor)] pub fn new(end: u32) -> Self { Self { current: 0, end } } } ``` **index.mjs** ```js const counter = new Counter(3) console.log(counter.next()) // { value: 0, done: false } console.log(counter.next(2)) // { value: 2, done: false } console.log(counter.next()) // { done: true } console.log([...new Counter(3)]) // [0, 1, 2] ``` The generated declaration extends `Iterator`. ### Associated types | Associated type | Required trait | Used by | | --------------- | --------------- | ------------------------------------------------------------------ | | `Yield` | `ToNapiValue` | Converts `Some(value)` from `next` or `catch` to JavaScript. | | `Next` | `FromNapiValue` | Converts the optional argument passed to `iterator.next(value)`. | | `Return` | `FromNapiValue` | Converts the optional argument passed to `iterator.return(value)`. | The method receives `Option` because JavaScript may call `next()` with no argument. `Some(yielded)` produces `{ value: yielded, done: false }`; `None` produces `{ done: true }` for that call. The synchronous adapter does not persist natural completion: a later `next()` invokes Rust again. If later calls must remain done, record that state in your struct and keep returning `None`. ### `return()` and `complete` Override `complete` for cleanup when JavaScript closes iteration early—for example, when a `for...of` loop executes `break`. **lib.rs** ```rust fn complete(&mut self, _value: Option) -> Option { self.release_native_cursor(); None } ``` The synchronous adapter currently invokes `complete`, marks the generator done, and uses the argument supplied by JavaScript as the returned iterator-result value. The `Option` returned by `complete` is not currently exposed. Treat it as a cleanup hook and do not rely on its return value until this experimental API is stabilized. ### `throw()` and `catch` The default `catch` returns the original JavaScript value as `Err`, so `iterator.throw(error)` throws that value and completes the iterator. Override it to recover: **lib.rs** ```rust fn catch<'env>( &'env mut self, _env: Env, value: Unknown<'env>, ) -> std::result::Result, Unknown<'env>> { if self.can_recover() { Ok(Some(self.fallback())) } else { Err(value) } } ``` - `Err(value)` throws `value` and completes iteration. - `Ok(Some(value))` yields it with `done: false`. - `Ok(None)` completes without throwing. Use `std::result::Result` in the signature above because the error side is the original `Unknown`, not `napi::Error`. ## Scoped synchronous yields `Generator::Yield` must be an owned or otherwise directly convertible value. Implement `ScopedGenerator<'env>` when a yielded value borrows the current JavaScript environment: **lib.rs** ```rust use napi::iterator::ScopedGenerator; #[napi(iterator)] pub struct ObjectCounter { current: u32, end: u32, } #[napi] impl<'env> ScopedGenerator<'env> for ObjectCounter { type Yield = Object<'env>; type Next = (); type Return = (); fn next( &mut self, env: &'env Env, _value: Option, ) -> Option { if self.current >= self.end { return None; } let mut object = Object::new(env).ok()?; object.set("value", self.current).ok()?; self.current += 1; Some(object) } } ``` The scoped trait receives `&Env` in `next` and `catch`. The yielded value is converted immediately on the JavaScript thread; it must not be stored in the class or moved to another thread. ## Iterator helpers and prototypes `Symbol.iterator` returns the class instance itself. On runtimes that expose the global `Iterator` constructor, napi-rs adjusts the generated class prototype to inherit from `Iterator.prototype`, which makes iterator-helper methods such as `map`, `filter`, `take`, and `drop` available. On runtimes without that global, the basic iteration protocol still works but those helpers are absent. Because prototype integration is part of this experimental API, test subclassing and any code that freezes or replaces class prototypes. ## Async iterator Enable the async runtime: **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["async", "tokio_time"] } napi-derive = "3" ``` Then mark the class and implement `AsyncGenerator`: **lib.rs** ```rust use std::future::Future; use napi::bindgen_prelude::*; use napi_derive::napi; #[napi(async_iterator)] pub struct DelayedCounter { current: u32, end: u32, delay_ms: u64, } #[napi] impl AsyncGenerator for DelayedCounter { type Yield = u32; type Next = (); type Return = (); fn next( &mut self, _value: Option, ) -> impl Future>> + Send + 'static { let value = self.current; let end = self.end; let delay_ms = self.delay_ms; self.current += 1; async move { napi::tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await; Ok((value < end).then_some(value)) } } } #[napi] impl DelayedCounter { #[napi(constructor)] pub fn new(end: u32, delay_ms: u32) -> Self { Self { current: 0, end, delay_ms: delay_ms as u64 } } } ``` **index.mjs** ```js for await (const value of new DelayedCounter(3, 10)) { console.log(value) // 0, 1, 2 } ``` The generated class implements: ```ts [Symbol.asyncIterator](): AsyncGenerator ``` ### Async bounds `AsyncGenerator` deliberately prevents JavaScript-scoped values from crossing an await point: ```rust type Yield: ToNapiValue + Send + 'static; fn next( &mut self, value: Option, ) -> impl Future>> + Send + 'static; ``` The future cannot borrow `self`. Update synchronous state and copy or clone everything the future needs before creating the `async move` block, as the example does. A scoped `Object<'env>`, `Function<'env, ...>`, or `BufferSlice<'env>` cannot be the yielded type. Return owned values, or create a JavaScript value later in a different API that explicitly supplies `Env` on the JavaScript thread. ### Async `next()` Each call returns a Promise: - `Ok(Some(value))` fulfills with `{ value, done: false }`. - `Ok(None)` fulfills with `{ value: undefined, done: true }`. - `Err(error)` rejects the Promise. The current experimental async adapter does not keep a separate terminal-state flag after `Ok(None)`. If a subsequent call must remain completed, keep that state in your Rust struct and continue returning `Ok(None)`. Do not assume overlapping `next()` calls are serialized for you. State mutation before the future is returned happens immediately on the JavaScript thread, while the resulting futures can remain in flight together. Design the state machine for concurrent in-flight operations or document that callers must await one result before requesting the next. ### Async `return()` Override `complete` to perform asynchronous cleanup: **lib.rs** ```rust fn complete( &mut self, _value: Option, ) -> impl Future>> + Send + 'static { let handle = self.take_handle(); async move { handle.close().await.map_err(Error::from)?; Ok(None) } } ``` The returned Promise always resolves with `done: true`; `Some(value)` becomes its final value and `None` becomes `undefined`. An error rejects. As with `next`, the adapter does not itself persist a terminal flag for later operations, so record completion in your class if callers can retain and reuse the iterator object. There is an experimental typing mismatch here: `complete` returns `Option` at runtime, while the generated `AsyncGenerator` declaration types the final value as `Return`. If `complete` can return `Some(value)`, keep `Yield` and `Return` the same type; otherwise return `None`. With different `Yield` and `Return` types, the generated declaration can disagree with the runtime value. JavaScript normally calls `return()` when a `for await...of` loop exits early, but cleanup should still tolerate the iterator being garbage-collected without an orderly return. ### Async `throw()` The default `catch` turns the thrown JavaScript value into `napi::Error`, so the returned Promise rejects. **lib.rs** ```rust fn catch( &mut self, _env: Env, value: Unknown, ) -> impl Future>> + Send + 'static { let error: Error = value.into(); async move { Err(error) } } ``` A custom `catch` can recover with `Ok(Some(value))`. In the current experimental adapter, a recovered `Ok(None)` is represented as a non-terminal result with a null value; use `Err` to rethrow or `Ok(Some(...))` to recover, and use `return()`/explicit class state to model completion. See [Error handling](/docs/concepts/error-handling) for JavaScript error preservation across async work. ## Lifetime and garbage collection For async iteration, `[Symbol.asyncIterator]()` creates an iterator object that keeps a hidden, non-enumerable, non-writable reference to the native class instance. This prevents the class from being collected while the iterator is retained. The reference is released when the iterator object is finalized. That reference does not cancel an in-flight Rust future. Futures and any external resources they own need their own cancellation and shutdown design. Keep cleanup idempotent so it is safe from `complete`, explicit class methods, and `Drop`/finalization paths. The synchronous iterator is the class instance itself, so normal class-instance reachability keeps the Rust value alive. ## Choosing another abstraction Use an iterator when each request naturally produces one item and the consumer controls pacing. - Use a normal array or `Vec` for small, already-materialized results. - Use `ReadableStream` for streaming with Web Streams backpressure and cancellation semantics. - Use `AsyncTask` for one CPU-heavy result from libuv's worker pool. - Use an async function for one Tokio future and one Promise. - Use ThreadsafeFunction for repeated callbacks originating on a native thread. Because iterator support is experimental, prefer these established abstractions when interoperability or long-term API stability matters more than the iterator syntax. ## Test checklist - `next()` with and without its argument. - Natural completion and calls after completion. - Early `break`, explicit `return(value)`, and cleanup failure. - Default and recovered `throw(error)` behavior. - Two overlapping async `next()` calls if the API permits them. - Dropping the original async class while retaining only its iterator. - Forced garbage collection and worker-environment shutdown. - Runtimes both with and without the global `Iterator` helper API. --- # Web Streams NAPI-RS can accept a JavaScript [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) or [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream) as a function argument, and can create a `ReadableStream` from any Rust [`Stream`](https://docs.rs/futures-core/latest/futures_core/stream/trait.Stream.html) and hand it back to JavaScript. Support lives behind the **`web_stream`** Cargo feature, which also enables `tokio_rt` (see [Cargo features](/docs/concepts/cargo-features)): **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["async", "web_stream"] } napi-derive = "3" ``` The host runtime must provide compatible Web Streams globals — Node.js 18+ exposes `ReadableStream` globally (also via `node:stream/web`). When the global `ReadableStream` constructor is missing, stream creation fails with a "ReadableStream is not supported in this Node.js version" error. ## Accepting a `ReadableStream` A `ReadableStream` argument is validated with `instanceof` against the global `ReadableStream` constructor. Calling `.read()` locks the stream with `getReader()` and returns a `Reader` — a Rust `Stream` of `Result` items you can consume with `tokio_stream::StreamExt` (re-exported as `napi::tokio_stream`) inside any async block: **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; use napi::tokio_stream::StreamExt; /// Counts the chunks read from a stream, swallowing (dropping) any read error. #[napi] pub fn drain_stream_count( env: &Env, stream: ReadableStream, ) -> Result> { let mut reader = stream.read()?; AsyncBlockBuilder::new(async move { let mut count = 0u32; while let Some(item) = reader.next().await { match item { Ok(_) => count += 1, // Drop the error on the Tokio thread instead of returning it to JS. Err(_err) => break, } } Ok(count) }) .build(env) } ``` **index.d.ts** ```ts export declare function drainStreamCount( stream: ReadableStream, ): Promise ``` **index.mjs** ```js import { createReadStream } from 'node:fs' import { Readable } from 'node:stream' import { drainStreamCount } from './index.js' // Any Node.js stream can be converted with Readable.toWeb const count = await drainStreamCount( Readable.toWeb(createReadStream('./file.txt')), ) ``` ## Returning a `ReadableStream` To hand a stream **to** JavaScript, build one from a Rust `Stream>` that is `Unpin + Send + 'static`: - `ReadableStream::new(env, stream)` — for any item type that implements `ToNapiValue + Send + 'static` (numbers, strings, `#[napi(object)]` structs, …). - `ReadableStream::create_with_stream_bytes(env, stream)` — for byte streams; the items only need `Into>`, and the underlying source is created with `type: 'bytes'`. - `ReadableStream::with_stream_bytes_and_readable_stream_class(env, class, stream)` — the same byte stream, but constructed from a caller-supplied `ReadableStream` class, for runtimes whose stream implementation is not the global constructor. **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; use napi::tokio_stream::wrappers::ReceiverStream; #[napi] pub fn create_readable_stream(env: &Env) -> Result>> { let (tx, rx) = napi::tokio::sync::mpsc::channel(100); std::thread::spawn(move || { for _ in 0..100 { tx.try_send(Ok(b"hello".to_vec())).expect("stream queue is full or closed"); } }); ReadableStream::create_with_stream_bytes(env, ReceiverStream::new(rx)) } #[napi(object)] #[derive(Default)] pub struct StreamItem { pub name: String, pub size: i32, } /// Creates a ReadableStream that emits StreamItem objects. #[napi] pub fn create_readable_stream_with_object( env: &Env, ) -> Result> { let (tx, rx) = napi::tokio::sync::mpsc::channel(100); std::thread::spawn(move || { for i in 0..100 { let item = StreamItem { name: format!("item-{i}"), size: i, }; tx.try_send(Ok(item)).expect("stream queue is full or closed"); } }); ReadableStream::new(env, ReceiverStream::new(rx)) } ``` **index.d.ts** ```ts export declare function createReadableStream(): ReadableStream export declare function createReadableStreamWithObject(): ReadableStream ``` On the JavaScript side a returned stream is a real Web `ReadableStream` — consume it with a reader or `for await`: **index.mjs** ```js import { createReadableStreamWithObject } from './index.js' for await (const item of createReadableStreamWithObject()) { console.log(item.name, item.size) } ``` `ReadableStream` also exposes the `locked()` and `cancel(reason)` Web API methods from Rust. ## `AsyncBlock`: a promise with a dispose hook The examples above return `AsyncBlock`, the primitive behind "return a promise" when you are not using an `async fn`. An `AsyncBlock` starts its future on the NAPI-RS runtime **eagerly** — when the Rust function is called, not when JavaScript awaits it — and the returned value converts into a JavaScript `Promise` (see [Promise](/docs/concepts/promise) and [async fn](/docs/concepts/async-fn)). Build one with `AsyncBlockBuilder`: - `AsyncBlockBuilder::new(future)` — the future is `Future> + Send + 'static`. - `.with_dispose(|env| ...)` — a hook that runs on the JavaScript thread when the future resolves **successfully**, before the value is converted. Note it only covers the `Ok` path: when the future returns `Err`, the promise rejects without running the hook. If a resource must be released on failure too, release it inside the future itself (or map the `Err` before building the block) and keep `with_dispose` for the success path. - `.build(env)` — spawns the future and produces the `AsyncBlock`. - `AsyncBlockBuilder::build_with_map(env, future, map)` — a static constructor whose `map` closure runs on the JavaScript thread at resolution time, turning the future's output into the final JavaScript value. This is the escape hatch for values that can only be created on the JS thread. `build_with_map` is how you resolve a promise with a zero-copy buffer: the future produces owned bytes on the runtime thread, and the map closure wraps them in a `BufferSlice<'static>` — without copying — on the JavaScript thread: **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; use napi::tokio_stream::StreamExt; #[napi] pub fn accept_stream( env: &Env, stream: ReadableStream, ) -> Result>> { let mut reader = stream.read()?; AsyncBlockBuilder::build_with_map( env, async move { let mut bytes = Vec::new(); while let Some(chunk) = reader.next().await { bytes.extend_from_slice(&chunk?); } Ok(bytes) }, |env, mut value| { let value_ptr = value.as_mut_ptr(); unsafe { BufferSlice::from_external(&env, value_ptr, value.len(), value, move |_, bytes| { drop(bytes); }) } }, ) } ``` (The [full example](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/stream.rs) adapts the chunk stream into an `AsyncRead` with `tokio_util::io::StreamReader` instead of collecting chunk by chunk; both shapes end in the same `from_external` map closure.) **index.d.ts** ```ts export declare function acceptStream( stream: ReadableStream, ): Promise ``` Because the map closure runs on the JavaScript thread with a live `Env`, it can also call back into JavaScript constructors. The full [`fetch` example](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/fetch.rs) uses this to build a Web `Response` around a NAPI-RS byte stream, and overrides the generated declaration with [`ts_return_type`](/docs/concepts/napi-attributes) so the return type references the `undici-types` package: **lib.rs** ```rust #[napi(ts_return_type = "Promise")] pub fn fetch( env: &Env, url: String, request_init: Option, ) -> Result>> { AsyncBlockBuilder::build_with_map( env, async move { /* ... reqwest request ... */ }, |env, response| { let global = env.get_global()?; let response_constructor: Function, ()> = global.get_named_property("Response")?; let js_stream = ReadableStream::create_with_stream_bytes(&env, /* response body stream */)?; response_constructor.new_instance(js_stream) }, ) } ``` **index.d.ts** ```ts export declare function fetch( url: string, requestInit?: RequestInit | undefined | null, ): Promise ``` ## Accepting a `WritableStream` A `WriteableStream` argument (note the NAPI-RS spelling) wraps a JavaScript `WritableStream`. It is a thin handle over the Web API: `ready()`, `abort(reason)`, and `close()` each call the corresponding JavaScript method and return a `PromiseRaw<()>` you can convert into an awaitable [`Promise`](/docs/concepts/promise) inside an async context. The writable side of a stream cannot be created from Rust today — accept it from JavaScript, or pipe a returned `ReadableStream` into it on the JavaScript side. ::: tip The runnable versions of every snippet on this page live in [`examples/napi/src/stream.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/stream.rs) and [`examples/napi/src/fetch.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/fetch.rs), with their tests in [`examples/napi/__tests__/values.spec.ts`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/__tests__/values.spec.ts). ::: --- # Typed Array `TypedArray` describes an array-like view of an underlying [binary data buffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). NAPI-RS can expose a view of that storage to Rust without copying it, subject to the lifetime and synchronization rules below. ## Buffer [`Buffer`](https://nodejs.org/api/buffer.html) is a subclass of JavaScript's [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array). It is often used to share data between Node.js and Rust. A `Buffer` can be created from `Vec`. Where the runtime permits external buffers, NAPI-RS transfers the allocation to the JavaScript `Buffer` without a copy, and its finalizer releases the `Vec` after JavaScript collects the buffer. If the runtime rejects external buffers, NAPI-RS falls back to copying the bytes into a runtime-owned buffer. **lib.rs** ```rust {6} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn create_buffer() -> Buffer { vec![0, 1, 2].into() } ``` ::: info On runtimes that support external buffers, the underlying `Vec` is not copied in this path. ::: ::: warning The `Electron` will not be able to create `Buffer` in zero copy way. See [V8 Memory Cage](https://www.electronjs.org/blog/v8-memory-cage) for more details. **NAPI-RS** will copy the data of the `Vec` into the underlying `Buffer` in this case. ::: ## Buffer and TypedArray Types **NAPI-RS** provides two categories of buffer types for different use cases. For more details on how lifetimes work for these types, see [Understanding Lifetime](/docs/concepts/understanding-lifetime#lifetime-of-buffer-and-typedarray). ### Owned Types These types can outlive the current native call and cross async boundaries: - `Buffer` - Reference-backed Node.js Buffer wrapper - `Uint8Array`, `Int32Array`, `Float64Array`, etc. - Owned typed-array wrappers For a value received from JavaScript, NAPI-RS creates a [`napi_ref`](https://nodejs.org/api/n-api.html#napi_create_reference). It keeps the JavaScript object and backing store alive until the Rust wrapper is dropped. Dropping the wrapper only releases Rust's reference; JavaScript may retain the same object independently. **lib.rs** ```rust {5} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn process_buffer(env: &Env, buffer: Buffer) -> Result> { // Copy while the synchronous JavaScript callback still has control. let mut data = buffer.to_vec(); AsyncBlockBuilder::new(async move { data.reverse(); Ok(data.into()) }) .build(env) } ``` ::: info `AsyncBlock` and `AsyncBlockBuilder` are re-exported under napi's `async` feature, so this example does not compile without it. Enable the feature on the `napi` dependency in your `Cargo.toml`: `napi = { version = "3", features = ["async"] }`. The `tokio_time` feature is only required for the `napi::tokio::time::sleep` helper shown later. ::: ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export declare function processBuffer(buffer: Buffer): Promise ``` ::: warning `Buffer` and the owned typed-array wrappers implement `Send` and `Sync` so their lifetime and cleanup can cross threads. Those traits do not synchronize the shared bytes. JavaScript can retain and mutate the same backing store while Rust holds a wrapper. Accessing it on a Rust worker while JavaScript or another Rust thread may mutate it is a data race and can cause undefined behavior—even if Rust only reads. Copy the bytes before dispatching work, as above, or enforce an ownership protocol that rules out unsynchronized access. ::: ### Borrowed Types (`BufferSlice`, `Uint8ArraySlice`, etc.) These types borrow data and are lifetime-bound to the function scope: - `BufferSlice<'env>` - Zero-copy Buffer slice - `Uint8ArraySlice<'env>`, `Int32ArraySlice<'env>`, etc. - Zero-copy TypedArray slices - `ArrayBuffer<'env>` - Zero-copy ArrayBuffer view - `&[u8]/&[i8]/&[f32]/&[f64]...` - Zero-copy slice **lib.rs** ```rust {4} use napi_derive::napi; #[napi] pub fn sum_array_slice(input: &[u32]) -> u32 { // Zero-copy access to the underlying data input.iter().sum() } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export declare function sumArraySlice(input: Uint32Array): number ``` **index.ts** ```ts {5} import { sumArraySlice } from './index.js' const input = new Uint32Array([1, 2, 3, 4, 5]) const result = sumArraySlice(input) console.log(result) // 15 ``` ### When to Use Each Type **Use `&[u8]/&[i8]/&[f32]/&[f64]...` when**: - You need zero-copy performance - Working in synchronous context only - Data lifetime is bounded to the function call **Use `BufferSlice<'env>` or `Uint8ArraySlice<'env>/Int32ArraySlice<'env>/...` when**: - You need zero-copy performance - You need to convert them into owned types in some scenarios - You need to convert them into `Object` or `Unknown` **Use `Buffer` when**: - You need to store the buffer beyond the function call - Working with async functions ## Common Usage Patterns ### Converting Between Types **lib.rs** ```rust {7,10} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn buffer_slice_to_buffer(env: &Env, slice: BufferSlice) -> Result> { // Convert BufferSlice to owned Buffer for async usage let buffer = slice.into_buffer(env)?; // Copy before the async work can run concurrently with JavaScript. let data = buffer.to_vec(); AsyncBlockBuilder::new(async move { Ok(data.iter().sum()) }) .build(env) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export declare function bufferSliceToBuffer(slice: Buffer): Promise ``` **index.ts** ```ts {5} import { bufferSliceToBuffer } from './index.js' const slice = Buffer.from([1, 2, 3, 4, 5]) const result = await bufferSliceToBuffer(slice) console.log(result) // 15 ``` ### Async vs Sync Patterns **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; // ✅ Correct: Using owned Buffer in async context #[napi] pub async fn process_async(buffer: Buffer) -> Result { // Buffer can cross await boundaries napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await; Ok(buffer) } // ❌ Won't compile: BufferSlice cannot cross await boundaries // #[napi] // pub async fn process_async_slice(slice: BufferSlice<'_>) -> Result> { // napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Ok(slice) // Error: slice doesn't live long enough // } #[napi] // ✅ Correct: Convert slice to owned for async usage pub fn process_slice_async(env: &Env, slice: BufferSlice<'_>) -> Result> { let buffer = slice.into_buffer(env)?; AsyncBlockBuilder::new(async move { Ok(buffer) }).build(env) } ``` All of the `AsyncBlock` examples above build their futures with the `napi` crate's async support, which is gated behind the `async` feature on the `napi` dependency (`napi = { version = "3", features = ["async"] }`). That feature is what re-exports `AsyncBlock`/`AsyncBlockBuilder` and the Tokio runtime. The `napi::tokio::time::sleep` helper used above additionally requires the `tokio_time` feature. ## Memory Management ### Copied Buffers In some cases, you cannot transfer ownership of the data to a `Buffer` or typed array. Use `copy_from` to create a copy instead. ::: warning If you create the `Buffer` or `TypedArray` in this way, the ownership of the data will not be transferred to the `Buffer` or `TypedArray`, but the underlying data will be copied, there should be performance overhead of the data copy. ::: **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn create_copied_buffer(env: &Env) -> Result> { let data = b"Hello, World!"; BufferSlice::copy_from(env, data) } ``` ### External Buffers Sometimes, you may want to create a `Buffer` or `TypedArray` from data types that can `deref` to `[u8]` or get the raw pointer like `*mut u8`. And you don't want to copy the whole data into a `Vec` which can be very expensive. We provide the `from_external` method to achieve this, but it's unsafe and you must ensure the data is valid until the `finalize` callback is called. ::: info The `finalize_hint` parameter is passed to the finalizer. In the first example below, the boxed slice is both the allocation owner and the hint, so it stays alive until the callback receives and drops it. If the runtime rejects external buffers, NAPI-RS first copies the bytes and then invokes that callback immediately during `from_external`; otherwise the callback runs when JavaScript finalizes the external buffer. Do not require the callback to be deferred until garbage collection. ::: **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn create_shared_buffer(env: &Env) -> Result> { let mut data = vec![1, 2, 3, 4, 5].into_boxed_slice(); let data_ptr = data.as_mut_ptr(); let len = data.len(); unsafe { BufferSlice::from_external(env, data_ptr, len, data, move |_, boxed_data| { drop(boxed_data); }) } } #[napi] pub fn create_external_buffer(env: &Env) -> Result> { let mut data = vec![1, 2, 3, 4, 5]; let data_ptr = data.as_mut_ptr(); let len = data.len(); let capacity = data.capacity(); // make sure the data is valid until the finalize callback is called std::mem::forget(data); unsafe { BufferSlice::from_external(env, data_ptr, len, data_ptr, move |_, ptr| { // Cleanup data when JavaScript GC runs std::mem::drop(Vec::from_raw_parts(ptr, len, capacity)); }) } } ``` ## Safety Considerations ### External Buffer Safety When using `from_external` methods, ensure: 1. **Pointer Validity**: The pointer must remain valid until the finalize callback 2. **Memory Layout**: The memory must be compatible with the expected type 3. **Proper Cleanup**: The finalize callback must properly deallocate memory **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn unsafe_external_example(env: &Env) -> Result> { let mut data = vec![1u8, 2, 3, 4, 5]; let ptr = data.as_mut_ptr(); let len = data.len(); let capacity = data.capacity(); // ⚠️ CRITICAL: Must forget the Vec to prevent double-free std::mem::forget(data); unsafe { BufferSlice::from_external(env, ptr, len, ptr, move |_, ptr| { // ✅ Properly reconstruct and drop the Vec std::mem::drop(Vec::from_raw_parts(ptr, len, capacity)); // Vec automatically deallocates when dropped }) } } ``` ### Unsafe mutable access The unsafe `as_mut` methods expose a mutable slice into storage that JavaScript may also access. Calling the method is only sound when you can guarantee that JavaScript and every other Rust alias will neither read nor write the backing store for the entire mutable borrow. Violating that contract can cause undefined behavior. In cross-thread code, prefer an owned copy unless you have an explicit synchronization and ownership protocol spanning both JavaScript and Rust. --- # External [`External`](https://nodejs.org/api/n-api.html#napi_create_external) is very similar to [`Object Wrap`](https://nodejs.org/api/n-api.html#object-wrap), which is used in [Class](./class) under the hood. `Object Wrap` attaches a native value to a JavaScript Object and can notify you when the attached JavaScript Object is recycled by GC. `External` creates an empty, blank JavaScript Object that holds the native value under the hood. It only works by passing the object back to Rust: **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn create_source_map(length: u32) -> External { External::new(vec![0; length as usize].into()) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export class ExternalObject { readonly '': { readonly '': unique symbol [K: symbol]: T } } export function createSourceMap(length: number): ExternalObject ``` `External` is very useful when you want to return a JavaScript Object with some methods that interact with the native Rust code. Here is a real-world example: https://github.com/h-a-n-a/magic-string-rs/blob/v0.3.0/node/src/lib.rs#L96-L103 https://github.com/h-a-n-a/magic-string-rs/blob/v0.3.0/node/index.js#L7-L23 **lib.rs** ```rust impl MagicString { #[napi(ts_return_type = "{ toString: () => string, toUrl: () => string }")] pub fn generate_map( &mut self, options: Option, ) -> Result> { let external = create_external(self.0.generate_map(options.unwrap_or_default())?); Ok(external) } /// @internal #[napi] pub fn to_sourcemap_string(&mut self, sourcemap: External) -> Result { Ok((*sourcemap.as_ref()).to_string()?) } /// @internal #[napi] pub fn to_sourcemap_url(&mut self, sourcemap: External) -> Result { Ok((*sourcemap.as_ref()).to_url()?) } } ``` First, the `generate_map` method returns an `External` object, and then the JavaScript function holds the `External` object in a closure: **index.js** ```ts module.exports.MagicString = class MagicString extends MagicStringNative { generateMap(options) { const sourcemap = super.generateMap({ file: null, source: null, sourceRoot: null, includeContent: false, ...options, }) const toString = () => super.toSourcemapString(sourcemap) const toUrl = () => super.toSourcemapUrl(sourcemap) return { toString, toUrl, } } } ``` --- # Reference In some scenarios, you may want to extend the lifetime of the `Object` to the `Rust` side. You can use `Reference` to hold a reference to this object. ::: warning Both the `Reference` and `WeakReference` are not `Send`, because of the `drop` of the `Reference` must be called in the same thread as the `Reference` is created. ::: ## `Reference` `Reference` is a wrapper of the [`napi_ref`](https://nodejs.org/api/n-api.html#napi_ref). ::: info NAPI-RS calls the [`napi_wrap`](https://nodejs.org/api/n-api.html#napi_wrap) function to wrap the Rust `struct` with the class instance object when creating the class instance. There is a [`napi_ref`](https://nodejs.org/api/n-api.html#napi_ref) that is created by the `napi_wrap`. `Reference` holds the `napi_ref` so you can always access the underlying `struct` reference before the underlying `napi_ref` is deleted. ::: For example: **lib.rs** ```rust {11} pub struct Repository { dir: String, } impl Repository { fn remote(&self) -> Remote { Remote { inner: self } } } pub struct Remote<'repo> { inner: &'repo Repository, } impl<'repo> Remote<'repo> { fn name(&self) -> String { "origin".to_owned() } } ``` The `Repository` struct below is easy to create a `#[napi]` Class around, because it doesn't contain any **lifetime** in the definition. However, the `Remote<'repo>` struct cannot have a `#[napi]` Class created around it, because it has a `'repo` lifetime. With the `Reference` API, you can create a `'static` lifetime struct, which means the created struct will live as long as you can access it in your `Rust` code. Like the [`Env`](./inject-env) and [`This`](./inject-this), the `Reference` is injected into parameters of `#[napi]` functions. **lib.rs** ```rust {37-42,45-48} use napi::bindgen_prelude::*; use napi_derive::napi; pub struct Repository { dir: String, } impl Repository { fn remote(&self) -> Remote { Remote { inner: self } } } pub struct Remote<'repo> { inner: &'repo Repository, } impl<'repo> Remote<'repo> { fn name(&self) -> String { "origin".to_owned() } } #[napi] pub struct JsRepo { inner: Repository, } #[napi] impl JsRepo { #[napi(constructor)] pub fn new(dir: String) -> Self { JsRepo { inner: Repository { dir }, } } #[napi] pub fn remote(&self, reference: Reference, env: Env) -> Result { Ok(JsRemote { inner: reference.share_with(env, |repo| Ok(repo.inner.remote()))?, }) } } #[napi] pub struct JsRemote { inner: SharedReference>, } #[napi] impl JsRemote { #[napi] pub fn name(&self) -> String { self.inner.name() } } ``` As you can see, the injected `Reference` has the `share_with` fn on it, which can be used to create a `'static` lifetime `JsRepo` struct in the closure. ![reference lifetime diagram](/assets/reference.svg) The created `Reference` will make Node.js hold the `JsRepo` instance until all the references are dropped. ## `WeakReference` `WeakReference` is very useful when you are creating circular references. **lib.rs** ```rust {13,24,71} use std::{cell::RefCell, rc::Rc}; use napi::bindgen_prelude::*; use napi_derive::napi; pub struct OwnedStyleSheet { rules: Vec, } #[napi] pub struct CSSRuleList { owned: Rc>, parent: WeakReference, } #[napi] impl CSSRuleList { #[napi] pub fn get_rules(&self) -> Vec { self.owned.borrow().rules.to_vec() } #[napi(getter)] pub fn parent_style_sheet(&self) -> WeakReference { self.parent.clone() } #[napi(getter)] pub fn name(&self, env: Env) -> Result> { Ok( self .parent .upgrade(env)? .map(|stylesheet| stylesheet.name.clone()), ) } } #[napi] pub struct CSSStyleSheet { name: String, inner: Rc>, rules: Option>, } #[napi] impl CSSStyleSheet { #[napi(constructor)] pub fn new(name: String, rules: Vec) -> Result { let inner = Rc::new(RefCell::new(OwnedStyleSheet { rules })); Ok(CSSStyleSheet { name, inner, rules: None, }) } #[napi(getter)] pub fn rules( &mut self, env: Env, reference: Reference, ) -> Result> { if let Some(rules) = &self.rules { return rules.clone(env); } let rules = CSSRuleList::into_reference( CSSRuleList { owned: self.inner.clone(), parent: reference.downgrade(), }, env, )?; self.rules = Some(rules.clone(env)?); Ok(rules) } } ``` In the example above, the `CSSRuleList` struct is created with a `WeakReference` as its `parent` field. Because the `CSSRuleList` is created by the `CSSStyleSheet` in this case, the `CSSStyleSheet` instance is a circular reference to the `CSSRuleList` instance it created. The `WeakReference` will not increase the reference count of the raw Object, so the `upgrade` function of `WeakReference` may return `None` if the raw Object is dropped. ## JavaScript Value Reference ### `ObjectRef` ::: warning An owned `ObjectRef` must either be returned to JavaScript or be consumed by `unref`. Dropping it only reports a leak; it cannot delete the Node-API reference without an `Env`, so the object remains strongly referenced. ::: In the example below, we create the `ObjectRef` in the constructor and use it later in the `getOptions` method. **lib.rs** ```rust {14} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub struct NativeClass { options: Option, } #[napi] impl NativeClass { #[napi(constructor)] pub fn new(options: Object) -> Result { Ok(Self { options: Some(options.create_ref()?), }) } #[napi] pub fn get_options<'env>(&self, env: &'env Env) -> Result> { self .options .as_ref() .ok_or_else(|| Error::from_reason("options were released"))? .get_value(env) } #[napi] pub fn release_options(&mut self, env: &Env) -> Result<()> { if let Some(options) = self.options.take() { options.unref(env)?; } Ok(()) } } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.ts** ```ts import { NativeClass } from './index.js' const nativeClass = new NativeClass({ name: 'John', age: 30, }) const options = nativeClass.getOptions() // { name: 'John', age: 30 } nativeClass.releaseOptions() ``` ### `SymbolRef` ::: warning An owned `SymbolRef` must either be returned to JavaScript or be consumed by `unref`. Dropping it only reports a leak; it cannot delete the Node-API reference without an `Env`, so the symbol remains strongly referenced. ::: The `SymbolRef` API is basically the same as the `ObjectRef`. **lib.rs** ```rust use napi::{SymbolRef, bindgen_prelude::*}; use napi_derive::napi; #[napi] pub fn create_symbol_ref(env: &Env) -> Result { Symbol::new("NAPI_RS_SYMBOL") .into_js_symbol(env)? .create_ref() } ``` ### `FunctionRef` ::: info `FunctionRef` is `Send + Sync`, but the `Function` borrowed from it is tied to the `Env` supplied to `borrow_back`. Moving the reference does not make it valid to call JavaScript from an arbitrary thread; borrow and call it only on a thread where that environment may be used. ::: `FunctionRef` can be created on the `Function` directly. In the example below, if you try to call `Function` in the `Promise.finally` callback, you will encounter a lifetime error: **lib.rs** ```rust {15} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn promise_finally_callback( mut promise: PromiseRaw<()>, on_finally: Function<()>, ) -> Result<()> { // ❌ compile Error // borrowed data escapes outside of function // `on_finally` escapes the function body here // lib.rs(7, 3): `on_finally` is a reference that is only valid in the function body // lib.rs(7, 3): has type `napi::bindgen_prelude::Function<'1, ()>` promise.finally(|env| { on_finally.call(()); Ok(()) })?; Ok(()) } ``` You can create the `FunctionRef` and borrow back the `Function` from it later: **lib.rs** ```rust {11} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn promise_finally_callback( mut promise: PromiseRaw<()>, on_finally: Function<(), ()>, ) -> Result<()> { let on_finally_ref = on_finally.create_ref()?; promise.finally(move |env| { let on_finally = on_finally_ref.borrow_back(&env)?; on_finally.call(())?; Ok(()) })?; Ok(()) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.ts** ```ts import { promiseFinallyCallback } from './index.js' promiseFinallyCallback(Promise.resolve(), () => { console.log('finally') }) ``` ### `ExternalRef` ::: info The `ExternalRef` is not `Send` because it needs to be dropped in the same thread as the `ExternalRef` is created. ::: `ExternalRef` holds the [`napi_ref`](https://nodejs.org/api/n-api.html#napi_ref) to the object thats created by the [`napi_create_external`](https://nodejs.org/api/n-api.html#napi_create_external) function. It's basically the same as the `ObjectRef` API: **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn create_external_ref(env: &Env, size: u32) -> Result> { let external = External::new(size).into_js_external(env)?; external.create_ref() } ``` --- # Understanding Lifetime Interoperability between the `Rust` lifetime system and `JavaScript` memory management is tricky. In most cases, you can't keep using a JavaScript handle after the Rust function returns. However, there are [a bunch of APIs in Node-API](https://nodejs.org/api/n-api.html#references-to-values-with-a-lifespan-longer-than-that-of-the-native-method) that can extend the lifetime of the `JavaScript` values. **NAPI-RS** uses these APIs to align the lifetime of the `JavaScript` values with the `Rust` lifetime system as much as possible. During a Node-API function call, JavaScript value handles are normally valid only until the call's handle scope closes; see [Object Lifetime Management](https://nodejs.org/api/n-api.html#object-lifetime-management). > As Node-API calls are made, handles to objects in the heap for the underlying VM may be returned as napi_values. These handles must hold the objects 'live' until they are no longer required by the native code, otherwise the objects could be collected before the native code was finished using them.

> As object handles are returned they are associated with a 'scope'. The lifespan for the default scope is tied to the lifespan of the native method call. The result is that, by default, handles remain valid and the objects associated with these handles will be held live for the lifespan of the native method call. ## Lifetime of owned primitive conversions When JavaScript primitives are accepted as owned Rust values such as `bool`, a Rust integer or float, or `String`, NAPI-RS copies their value into Rust-owned data. That Rust data is not tied to a Node-API handle scope. This is different from accepting a handle wrapper such as `JsString<'env>` or `JsNumber<'env>`. ## Lifetime of `JsValue` Handle wrappers such as `JsNumber<'env>` and `JsString<'env>` refer to a `napi_value` in the current environment's handle scope. You can read an owned Rust value from them—for example, a `JsNumber` can be read as `f64` or `u32`—but the wrapper itself remains scoped. **lib.rs** ```rust use napi::{bindgen_prelude::{Either, Result}, JsNumber}; use napi_derive::napi; #[napi] pub fn read_number(a: JsNumber) -> Result> { let input_u32 = a.get_uint32()?; let input_f64 = a.get_double()?; if input_u32 as f64 == input_f64 { Ok(Either::B(input_u32)) } else { Ok(Either::A(input_f64)) } } ``` The returned numbers in this example are owned Rust values. The `JsNumber` handle is not: its lifetime prevents it from being used after the native call's scope closes. The same distinction applies to strings: `String` contains a copy, while `JsString<'env>` is a scoped JavaScript handle. In most signatures, Rust infers the scope lifetime for you. ## Lifetime of class instances In `#[napi]` class, the instance is created by the Rust side and sent the ownership to the JavaScript side: **lib.rs** ```rust use std::sync::Arc; use napi_derive::napi; #[napi] pub struct Engine { inner: Arc<()>, } #[napi] impl Engine { #[napi(constructor)] pub fn new() -> Self { Self { inner: Arc::new(()) } } } ``` **index.ts** ```ts const engine = new Engine() ``` In this case, the `Engine` instance is created in the constructor and returned to JavaScript. Unlike `JsNumber` or `JsString`, the `Engine` holds the Rust struct under the hood, so if it's passed back from the JavaScript side, you can get the `&Engine` or `&mut Engine` directly. ### Class instances Lifetime Flowchart The following flowchart illustrates the lifetime of a NAPI-RS struct instance lifetime: ```mermaid flowchart A[JavaScript calls new Engine] B[Rust constructor creates Engine] C[Box Engine and attach it with napi_wrap] D[Return the JavaScript Engine instance] F[Pass back to Rust] G[napi_unwrap] H[Get &Engine or &mut Engine] I[JavaScript GC] J[napi_finalize_cb] K[Delete Engine struct] A --> B B --> C C --> D D --> F F --> G G --> H D --> I I --> J J --> K ``` ## Lifetime of `Buffer` and `TypedArray` `Buffer` and the concrete owned typed-array types (`Uint8Array`, `Int32Array`, and so on) can outlive a native call. Their wrappers keep the backing store alive while Rust holds them. The scoped `BufferSlice<'env>`, typed-array slice types, and `TypedArray<'env>` instead borrow a handle from the current environment scope. NAPI-RS provides two categories of buffer types with different lifetime characteristics: ### Owned Types - Cross-Thread Lifetime For a JavaScript-origin value, converting to an owned `Buffer`, `Uint8Array`, and similar type creates a [`napi_ref`](https://nodejs.org/api/n-api.html#napi_create_reference): - The reference keeps the JavaScript object and its backing data alive until the Rust wrapper is dropped - The wrapper can be moved across async boundaries and threads - Dropping the wrapper releases Rust's reference; JavaScript may still retain the same object independently **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn print_buffer(buffer: Buffer) { // Make a Rust-owned copy while this synchronous callback has control. let data = buffer.to_vec(); std::thread::spawn(move || { println!("data: {:?}", data); }); } ``` ::: warning `Send` and `Sync` make it possible to move the wrapper; they do not synchronize access to the bytes. JavaScript can retain and mutate the same backing store while Rust holds it. Reading or writing that memory on a Rust worker while JavaScript or another Rust thread can mutate it is a data race and can cause undefined behavior. Copy the data before dispatching work, or enforce an ownership protocol that rules out all unsynchronized access. ::: ::: info Cleanup is tied to the Rust wrapper's `Drop`, not to JavaScript GC. With the `napi4` feature, each Node-API environment/isolate has its own unreferenced custom-GC `ThreadsafeFunction`. A wrapper dropped on its owning JavaScript thread calls [`napi_reference_unref`](https://nodejs.org/api/n-api.html#napi_reference_unref) and [`napi_delete_reference`](https://nodejs.org/api/n-api.html#napi_delete_reference) directly. A wrapper dropped elsewhere sends its `napi_ref` to the `ThreadsafeFunction` captured from the value's owning environment, whose callback releases it on that environment's JavaScript thread. If that environment has already shut down, NAPI-RS detects the aborted handle and makes no further Node-API call because the runtime has already invalidated the reference. Releasing the Rust reference only makes the JavaScript value eligible for GC if JavaScript holds no other references. For Rust-created buffers, Rust owns the allocation until it is exported; then the JavaScript finalizer owns that allocation (or NAPI-RS copies it when the runtime rejects external buffers). ::: ### Borrowed Types - Function Scope Lifetime Borrowed types (`BufferSlice<'env>`, `Uint8ArraySlice<'env>`, etc.) have lifetimes bound to the function scope: - Zero-copy access to the underlying data - Cannot cross async boundaries due to lifetime constraints - Must be used within the same function call where they were created **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn process_buffer_slice<'env>(env: &'env Env, data: &'env [u8]) -> Result> { // BufferSlice lifetime is bound to this function scope BufferSlice::from_data(env, data.to_vec()) } ``` ### Buffer Lifetime Flowchart ```mermaid flowchart TD A[JavaScript calls Rust with a Buffer] B{Rust parameter type} C[BufferSlice<'env> or another scoped view] D[Use only while the native call scope is open] E[Native call returns] F[Scoped Rust handle expires; JavaScript lifetime is independent] G[Buffer or an owned typed array] H[napi_create_reference in FromNapiValue] I[Owned wrapper may cross await or thread boundaries] J[Rust wrapper Drop] K{Dropped on its owning environment thread?} L[Unref and delete the napi_ref directly] M[Queue the napi_ref on that environment's custom-GC TSFN] N[Owning JavaScript thread unrefs and deletes it] O[Rust's reference is released] P{Does JavaScript retain another reference?} Q[JavaScript value remains alive] R[Value is eligible for JavaScript GC] A --> B B -->|Borrowed| C C --> D D --> E E --> F B -->|Owned| G G --> H H --> I I --> J J --> K K -->|Yes| L K -->|No| M M --> N L --> O N --> O O --> P P -->|Yes| Q P -->|No| R ``` ### When Lifetimes Matter **Function-scoped lifetime (`BufferSlice<'env>`):** **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn sync_only(env: &Env) -> Result> { // ✅ Works: BufferSlice lifetime tied to function scope BufferSlice::from_data(env, vec![1, 2, 3]) } // ❌ Won't compile: Cannot cross async boundaries // #[napi] // async fn async_fail(env: &Env) -> Result> { // let slice = BufferSlice::from_data(env, vec![1, 2, 3])?; // napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await; // Ok(slice) // Error: slice doesn't live long enough // } ``` The sleep examples require the `async` and `tokio_time` features on the `napi` dependency. **Reference-backed lifetime (`Buffer`):** **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub async fn async_works(buffer: Buffer) -> Result { // ✅ Works: Buffer is Send + Sync napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await; Ok(buffer) } ``` For more details on Buffer and TypedArray usage patterns, see the [TypedArray documentation](/docs/concepts/typed-array). ## JavaScript Value Reference For other values, reference wrappers such as `ObjectRef`, `UnknownRef`, `SymbolRef`, `FunctionRef`, and `ExternalRef` use a `napi_ref` to keep a JavaScript value alive beyond the current callback. The wrapper itself has no scope lifetime, but that does not make JavaScript APIs environment-independent or safe to call from arbitrary threads. Borrow the scoped value back with the owning `Env`, and follow the type's release contract: some wrappers release on `Drop`, while `ObjectRef`, `UnknownRef`, and `SymbolRef` require an explicit `unref(env)` (or must be returned to JavaScript). See [Reference](/docs/concepts/reference#javascript-value-reference) for more details. --- # Env In most cases, the Node-API is encapsulated within various high-level abstractions and structures of **NAPI-RS**. However, in some cases, you still need to access the underlying Node-API. The `Env` struct provides access to the Node-API environment and allows you to create JavaScript values, handle errors, manage memory, and interact with the JavaScript runtime. ## String and Symbol Creation ### `create_string` Creates a JavaScript string from a Rust type that can be converted to `&str`. ```rust pub fn create_string>(&self, s: S) -> Result> ``` **Example:** ```rust let js_string = env.create_string("Hello, World!")?; ``` ### `create_string_from_std` Creates a JavaScript string from a Rust `String`. ```rust pub fn create_string_from_std<'env>(&self, s: String) -> Result> ``` ### `create_string_from_c_char` Creates a JavaScript string from a C-style string pointer. This is used for C FFI scenarios. ::: info You can pass `NAPI_AUTO_LENGTH` as the `len` parameter if the C string is null-terminated. ::: ```rust pub unsafe fn create_string_from_c_char<'env>( &self, data_ptr: *const c_char, len: isize, ) -> Result> ``` ### `create_string_utf16` Creates a JavaScript string from UTF-16 encoded data. ```rust pub fn create_string_utf16>(&self, chars: C) -> Result> ``` ### `create_string_latin1` Creates a JavaScript string from Latin-1 encoded data. ```rust pub fn create_string_latin1>(&self, chars: C) -> Result> ``` ### `create_symbol` Creates a JavaScript symbol with an optional description. ```rust pub fn create_symbol(&self, description: Option<&str>) -> Result> ``` ### `symbol_for` ::: info Requires `napi9` feature. ::: Creates or retrieves a symbol from the global symbol registry. ```rust pub fn symbol_for(&self, description: &str) -> Result> ``` ## Error Handling ### `get_last_error_info` Retrieves extended error information about the last error that occurred. ```rust pub fn get_last_error_info(&self) -> Result ``` ### `throw` Throws any JavaScript value as an exception. ```rust pub fn throw(&self, value: T) -> Result<()> ``` ### `throw_error` Throws a JavaScript Error with the provided message and optional error code. ```rust pub fn throw_error(&self, msg: &str, code: Option<&str>) -> Result<()> ``` ### `throw_range_error` Throws a JavaScript RangeError with the provided message and optional error code. ```rust pub fn throw_range_error(&self, msg: &str, code: Option<&str>) -> Result<()> ``` ### `throw_type_error` Throws a JavaScript TypeError with the provided message and optional error code. ```rust pub fn throw_type_error(&self, msg: &str, code: Option<&str>) -> Result<()> ``` ### `throw_syntax_error` _requires napi9_ Throws a JavaScript SyntaxError with the provided message and optional error code. ```rust pub fn throw_syntax_error, C: AsRef>(&self, msg: S, code: Option) ``` ### `fatal_error` Triggers a fatal error that immediately terminates the process. ```rust pub fn fatal_error(self, location: &str, message: &str) ``` ### `fatal_exception` ::: info Requires `napi3` feature. ::: Triggers an 'uncaughtException' in JavaScript. Useful for async callbacks that throw unrecoverable exceptions. ```rust pub fn fatal_exception(&self, err: Error) ``` ### `create_error` Creates a JavaScript error object from a Rust `Error`. ```rust pub fn create_error(&self, e: Error) -> Result> ``` ## Function and Class Creation ### `create_function` Creates a JavaScript function from a native callback. ```rust pub fn create_function( &self, name: &str, callback: Callback, ) -> Result> ``` **Example:** ::: info You can access the **`C`** Callback by adding the `_c_callback` suffix to the function name. In the example below, the `custom_function_c_callback` is the `C` callback for the `custom_function`. ::: **lib.rs** ```rust {6,10} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn create_function(env: &Env) -> Result> { env.create_function("customFunction", custom_function_c_callback) } #[napi(no_export)] fn custom_function(input: u32) -> u32 { input * 2 } ``` ::: info The `no_export` attribute is used to prevent the function from being exported to the JavaScript side. ::: The `custom_function` is not exported, so it's not visible in the JavaScript side. But the `C` callback is used for creating `Function` in `fn create_function`. You can use it like this: **index.ts** ```ts import { createFunction } from './index.js' const customFunction = createFunction() console.log(customFunction(2)) // 4 ``` ### `create_function_from_closure` ::: info Requires `napi5` feature. ::: Creates a JavaScript function from a Rust closure. ```rust pub fn create_function_from_closure( &self, name: &str, callback: F, ) -> Result> where Return: ToNapiValue, F: 'static + Fn(FunctionCallContext) -> Result, ``` **Example:** **lib.rs** ```rust {6,9} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn create_function(env: &Env) -> Result> { let var_moved_into_closure = 42; // this variable is moved into the closure env.create_function_from_closure("rustClosure", move |ctx| { // get the first argument from the JavaScript side let result = var_moved_into_closure + ctx.get::(0)?; Ok(result) }) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.ts** ```ts import { createFunction } from './index.js' const rustClosure = createFunction() console.log(rustClosure(2)) // 44 ``` ### `define_class` Creates a JavaScript class with the given constructor and properties. ```rust pub fn define_class( &self, name: &str, constructor_cb: Callback, properties: &[Property], ) -> Result>> ``` ## Memory Management ### `adjust_external_memory` Indicates to V8 the amount of externally allocated memory kept alive by JavaScript objects. ```rust pub fn adjust_external_memory(&self, size: i64) -> Result ``` ### `run_in_scope` Executes a function within a handle scope, which helps manage memory for temporary objects. ```rust pub fn run_in_scope(&self, executor: F) -> Result where F: FnOnce() -> Result, ``` ### `HandleScope` and `EscapableHandleScope` `run_in_scope` wraps a whole closure in one scope. When you need finer control — the classic case is a **loop that creates many short-lived JavaScript values** — use the standalone scope types from `napi::bindgen_prelude`. Without a per-iteration scope, every value created inside the loop stays alive (as a `napi_handle`) until the outer call returns, which can exhaust memory on large inputs. `HandleScope::create` opens a scope, and the unsafe `close` runs your closure and then closes the scope, invalidating every value created inside it: **lib.rs** ```rust {6} use napi::{bindgen_prelude::*, JsString}; #[napi] pub fn shorter_scope(env: &Env, arr: Array) -> Result> { let len = arr.len(); let mut result = Vec::with_capacity(len as usize); for i in 0..len { let scope = HandleScope::create(env)?; let value: Unknown = arr.get_element(i)?; let len = unsafe { scope.close(value, |v| match v.get_type()? { ValueType::String => { let string = v.cast::()?; Ok(string.utf8_len()? as u32) } ValueType::Object => Ok(1), _ => Ok(0), })? }; result.push(len); } Ok(result) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export declare function shorterScope(arr: unknown[]): Array ``` ::: warning `close` is `unsafe` because it invalidates every JavaScript value created within the scope — including the one passed to the closure. Convert whatever you need to owned Rust data (here, the `u32` length) before the scope closes, or escape the value explicitly with `EscapableHandleScope`. ::: `EscapableHandleScope` is for the case where one value must **outlive** the scope: `EscapableHandleScope::with(env, args, |scope, args| ...)` opens the scope, and `value.escape(scope)` (or `scope.escape(value)`) promotes a single value to the outer scope before the inner one closes when the `with` call returns: **lib.rs** ```rust {7,14} use napi::{bindgen_prelude::*, JsString}; #[napi] pub fn shorter_escapable_scope<'env>( env: &'env Env, create_string: Function<(), Option>, ) -> Result> { let mut longest_string = env.create_string("")?; let mut prev_len = 0; loop { if let Some(maybe_longest) = EscapableHandleScope::with( env, (create_string, longest_string), move |scope, (create_string, prev)| { let elem = create_string.call(())?; if let Some(string) = elem { let len = string.utf8_len()?; if len > prev.utf8_len()? { return Ok(Some(Either::A(string.escape::(scope)?))); } } else { return Ok(Some(Either::B(()))); } Ok(None) }, )? { match maybe_longest { Either::A(longest) => { if longest.utf8_len()? == prev_len { break; } prev_len = longest.utf8_len()?; longest_string = longest; } Either::B(_) => break, } } } Ok(longest_string) } ``` Each iteration calls back into JavaScript for a candidate string; only the longest candidate escapes its iteration's scope, so the rest are released immediately instead of accumulating until the function returns. The runnable versions of both functions live in [`examples/napi/src/scope.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/scope.rs). ## Environment Cleanup ### `add_env_cleanup_hook` ::: info Requires `napi3` feature. ::: Registers a cleanup hook to be called when the environment is being torn down. ```rust pub fn add_env_cleanup_hook( &self, cleanup_data: T, cleanup_fn: F, ) -> Result> where T: 'static, F: 'static + FnOnce(T), ``` ### `remove_env_cleanup_hook` ::: info Requires `napi3` feature. ::: Removes a previously registered cleanup hook. ```rust pub fn remove_env_cleanup_hook(&self, hook: CleanupEnvHook) -> Result<()> where T: 'static, ``` ### `add_async_cleanup_hook` ::: info Requires `napi8` feature. ::: Registers an asynchronous cleanup hook. ```rust pub fn add_async_cleanup_hook(&self, arg: Arg, cleanup_fn: F) -> Result<()> where F: FnOnce(Arg), Arg: 'static, ``` ### `add_removable_async_cleanup_hook` ::: info Requires `napi8` feature. ::: Registers a removable asynchronous cleanup hook. ```rust pub fn add_removable_async_cleanup_hook( &self, arg: Arg, cleanup_fn: F, ) -> Result where F: FnOnce(Arg), Arg: 'static, ``` ## Script Execution and Environment Information ### `run_script` Executes a JavaScript string and returns the result. ```rust pub fn run_script, V: FromNapiValue>(&self, script: S) -> Result ``` **Example:** ```rust let result: i32 = env.run_script("2 + 2")?; assert_eq!(result, 4); ``` ### `get_napi_version` Gets the N-API version (`process.versions.napi`). ```rust pub fn get_napi_version(&self) -> Result ``` ### `get_node_version` Gets the Node.js version information. ```rust pub fn get_node_version(&self) -> Result ``` ### `get_module_file_name` ::: info Requires `napi9` feature. ::: Retrieves the file path of the currently running JS module as a URL. ```rust pub fn get_module_file_name(&self) -> Result ``` ### `get_uv_event_loop` ::: info Requires `napi2` feature. ::: Gets a pointer to the underlying libuv event loop. ```rust pub fn get_uv_event_loop(&self) -> Result<*mut sys::uv_loop_s> ``` ## Instance Data Management ### `set_instance_data` ::: info Requires `napi6` feature. ::: Associates data with the currently running Agent. ```rust pub fn set_instance_data(&self, native: T, hint: Hint, finalize_cb: F) -> Result<()> where T: 'static, Hint: 'static, F: FnOnce(FinalizeContext), ``` ### `get_instance_data` ::: info Requires `napi6` feature. ::: Retrieves data previously associated with the currently running Agent. ```rust pub fn get_instance_data(&self) -> Result> where T: 'static, ``` ## Async and Future Support ### `spawn` Runs a task in the libuv thread pool and returns an `AsyncWorkPromise`. ```rust pub fn spawn(&self, task: T) -> Result> ``` ### `spawn_future` ::: info Requires `tokio_rt` and `napi4` feature. ::: Spawns a Rust future and returns a JavaScript Promise. ```rust pub fn spawn_future< T: 'static + Send + ToNapiValue, F: 'static + Send + Future>, >(&self, fut: F) -> Result> ``` ### `spawn_future_with_callback` ::: info Requires `tokio_rt` and `napi4` feature. ::: Spawns a future with a callback to process the result. ```rust pub fn spawn_future_with_callback< T: 'static + Send, V: ToNapiValue, F: 'static + Send + Future>, R: 'static + FnOnce(Env, T) -> Result, >(&self, fut: F, callback: R) -> Result> ``` ## Date Creation ### `create_date` ::: info Requires `napi5` feature. ::: Creates a JavaScript Date object from a timestamp. ```rust pub fn create_date(&self, time: f64) -> Result> ``` ## JSON Serialization ### `to_js_value` ::: info Requires `serde-json` feature. ::: Serializes a Rust struct into a JavaScript value using serde. ```rust pub fn to_js_value<'env, T>(&self, node: &T) -> Result> where T: Serialize, ``` ### `from_js_value` ::: info Requires `serde-json` feature. ::: Deserializes a JavaScript value into a Rust type using serde. ```rust pub fn from_js_value<'v, T, V>(&self, value: V) -> Result where T: DeserializeOwned, V: JsValue<'v>, ``` ## Value Comparison ### `strict_equals` Performs strict equality comparison between two JavaScript values (equivalent to `===`). ```rust pub fn strict_equals<'env, A: JsValue<'env>, B: JsValue<'env>>( &self, a: A, b: B, ) -> Result ``` --- # Inject Env The `#[napi]` macro is a very high level abstraction for the `Node-API`. Most of the time, you use the Rust native API and crates. But sometimes you still need to access the low-level `Node-API`, for example, to call [`napi_async_cleanup_hook`](https://nodejs.org/api/n-api.html#napi_async_cleanup_hook) or [`napi_adjust_external_memory`](https://nodejs.org/api/n-api.html#napi_adjust_external_memory). For this scenario, **NAPI-RS** allows you to inject `Env` into your `fn` which is decorated by the `#[napi]`. **lib.rs** ```rust {4} use napi::{Env, bindgen_prelude::*}; #[napi] pub fn call_env(env: Env, length: u32) -> Result>> { env.adjust_external_memory(length as i64)?; Ok(External::new(vec![0; length as usize])) } ``` And the `Env` will be auto injected by **NAPI-RS**, it does not affect the `arguments` types in the JavaScript side: **index.d.ts** ```ts export function callEnv(length: number) -> ExternalObject ``` You can also inject `Env` in `impl` block: **lib.rs** ```rust {20} use napi::bindgen_prelude::*; // A complex struct which can not be exposed into JavaScript directly. struct QueryEngine { initial_count: u32, } impl QueryEngine { fn with_initial_count(initial_count: u32) -> Self { Self { initial_count } } fn query(&self, query: String) -> Result { Ok(format!("{} results for `{query}`", self.initial_count)) } } #[napi(js_name = "QueryEngine")] pub struct JsQueryEngine { engine: QueryEngine, } #[napi] impl JsQueryEngine { #[napi(factory)] pub fn with_initial_count(count: u32) -> Self { JsQueryEngine { engine: QueryEngine::with_initial_count(count) } } /// Class method #[napi] pub fn query(&self, env: Env, query: String) -> napi::Result { self.engine.query(query).map_err(|err| Error::new(Status::GenericFailure, format!("Query failed {}", err))) } } ``` The behavior is just the same with the pure `fn`. --- # Inject This In class methods, you may want to access the raw `Object` value of the `Class` instance. **lib.rs** ```rust {15} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub struct QueryEngine {} #[napi] impl QueryEngine { #[napi(constructor)] pub fn new() -> Result { Ok(Self {}) } #[napi] pub fn get_ref_count(&self, this: This<'_>) -> Result> { this.get::("refCount") } } ``` **main.mjs** ```js {5} import { QueryEngine } from './index.js' const qe = new QueryEngine() qe.refCount = 3 console.log(qe.getRefCount()) // 3 ``` In functions, it may be bind with some objects in JavaScript: **lib.rs** ```rust {10} use napi::bindgen_prelude::*; use napi_derive::napi; #[napi(constructor)] pub struct Width { pub value: i32, } #[napi] pub fn plus_one(this: This<&Width>) -> i32 { this.object.value + 1 } ``` **main.mjs** ```js {4} import { Width, plusOne } from './index.js' const width = new Width(1) console.log(plusOne.call(width)) // 2 ``` --- # Naming conventions ## `snake_case` to `camelCase` The code styles are very different between Rust and JavaScript. The Rust community prefers the `snake_case` style while the JavaScript community prefers the `camelCase` style. **NAPI-RS** will change the case of the Rust code to the `camelCase` style automatically. **lib.rs** ```rust #[napi] pub fn a_function(a_arg: u32) -> u32 { a_arg + 1 } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export function aFunction(aArg: number): number ``` ## `js_name` You can use the `js_name` attribute in `#[napi]` to rename the JavaScript function. **lib.rs** ```rust {1} #[napi(js_name = "coolFunction")] pub fn a_function(a_arg: u32) -> u32 { a_arg + 1 } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export function coolFunction(aArg: number): number ``` The JavaScript function name will be `coolFunction`, both in the generated TypeScript definition and in the JavaScript runtime: **test.mjs** ```js {1} import { coolFunction } from './index.js' console.log(coolFunction(1)) // 2 ``` --- # #[napi] attributes The `#[napi]` macro exports Rust items and controls their JavaScript runtime behavior and generated TypeScript declarations. This page covers every public option accepted by `napi-derive` v3, including the two context-specific options parsed on parameters and enum variants. **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi(js_name = "addOne", strict)] pub fn add_one(value: u32) -> u32 { value + 1 } ``` ::: info Runtime conversion and TypeScript generation are separate. Options beginning with `ts_`, plus `skip_typescript`, change only the declaration emitted by `napi-derive`'s default `type-def` feature. They do not add runtime validation or conversion. ::: ## Supported targets In the tables below: - **Function** means an exported free function. - **Method** includes instance methods, static methods, factories, constructors, getters, and setters where the option makes sense. - **Class** means a struct exported with class identity. An `object`, `array`, or `transparent` struct is a value shape instead. - **Field** means a struct field or a field of a structured enum variant. With the default `napi-derive/strict` feature, an option accepted by the parser but unused on that kind of item is a compile error. Prefer the combinations documented here rather than relying on behavior when `strict` is disabled. ## Naming and exports | Option | Valid target | Runtime effect | TypeScript effect | Feature / status | | -------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------- | | `js_name = "name"` | Function, method, struct, enum, const, type alias, field, module | Replaces the default camelCase function/member name or PascalCase type name. On a `mod`, names the namespace object. A type alias has no runtime export. | Uses the same exported name; on a type alias, this only renames the declaration. | Supported | | `namespace = "name"` | Function, struct, impl, enum, const, type alias | Registers the item under `exports.name`. Apply the same namespace to a class and its `impl` blocks. A type alias has no runtime registration. | Places the declaration in the same generated namespace; on a type alias, this is the only effect. | Supported | | `module_exports` | Free function only | Runs the function during module initialization with the module `exports` object. | No function declaration is emitted. | Supported | | `no_export` | Free function only | Generates the Node-API callback wrapper without registering the function on `exports`. This is useful when passing the generated `*_c_callback` to a low-level API. | No declaration is emitted. | Supported | An inline Rust module can be turned into a JavaScript namespace. Only children that also carry `#[napi]` are exported, and nested napi modules are not supported. **lib.rs** ```rust #[napi(js_name = "math")] mod arithmetic { #[napi] pub fn add(a: u32, b: u32) -> u32 { a + b } } ``` **index.d.ts** ```ts export namespace math { export function add(a: number, b: number): number } ``` ### `module_exports` The callback must be a non-generic free function. It can accept only `Env`, `Object`, or references to them, and it can return only `()` or `Result<()>`. It cannot be combined with `constructor`, `factory`, `getter`, `setter`, `js_name`, `strict`, `return_if_invalid`, or `no_export`. **lib.rs** ```rust #[napi(module_exports)] pub fn initialize(mut exports: Object) -> Result<()> { exports.set("build", "release")?; Ok(()) } ``` For initialization that does not need the exports object, see [Module initialization](/docs/concepts/module-init). ## Functions and methods | Option | Valid target | Runtime effect | TypeScript effect | Feature / status | | --------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------- | | `constructor` | Method returning `Self`/`Result`; class struct shorthand | Exposes a JavaScript constructor. Constructors cannot be async. On a struct, public fields become constructor arguments. | Emits `constructor(...)`. | Supported | | `factory` | Associated method returning `Self`/`Result` | Exposes a static factory that constructs the class. It may be async. | Emits a static method returning the class or `Promise`. | Supported | | `getter` or `getter = name` | Method | Defines a JavaScript property getter. Without a name, `get_value` becomes `value`. | Emits a `get` accessor. | Supported | | `setter` or `setter = name` | Method | Defines a JavaScript property setter. Without a name, `set_value` becomes `value`. | Emits a `set` accessor. | Supported | | `strict` | Function or method | Calls `ValidateNapiValue` for every JavaScript argument before conversion and throws on a mismatch. | None. | Supported | | `return_if_invalid` | Function or method | Performs validation, but returns `undefined` instead of throwing for an invalid argument. | None. | Supported | | `catch_unwind` | Function or method | Catches an unwinding Rust panic at the generated callback boundary and converts its payload into a JavaScript `Error`. | None. | Requires an unwind-capable panic strategy; supported | | `async_runtime` | Synchronous function or method | Enters the napi-rs Tokio runtime while executing the function when that runtime is enabled. Without it, the wrapper is a no-op. | None. | Useful with `napi/tokio_rt`; supported | | `enumerable = false` | Method | Clears the enumerable descriptor flag. Omitting the value is equivalent to `true`. | None. | Supported | | `writable = false` | Method | Clears the writable descriptor flag. Omitting the value is equivalent to `true`. | None. | Supported | | `configurable = false` | Method | Clears the configurable descriptor flag. Omitting the value is equivalent to `true`. | None. | Supported | `strict` and `return_if_invalid` are mutually exclusive. They validate the `ValidateNapiValue` implementation for the Rust type; they do not perform arbitrary schema validation. Nested `Vec` elements are converted one by one, and conversion can still fail after the initial array check. Validation happens in the generated JavaScript callback before an async Rust future is created. On an async export, `strict` can therefore throw synchronously, while `return_if_invalid` returns synchronous `undefined` for invalid input rather than a Promise. These attributes do not change the generated async return type, so document that exceptional path explicitly. ::: warning `catch_unwind` is not a process-safety boundary. It cannot catch an aborting panic, and Rust explicitly does not guarantee that every panic is unwindable. Use `Result` for expected failures. See [Error handling](/docs/concepts/error-handling). ::: ## Classes and value shapes | Option | Valid target | Runtime effect | TypeScript effect | Feature / status | | --------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | | `object` | Struct | Converts a JavaScript object to/from an owned Rust value. All fields must be public. It has no JavaScript class identity. | Emits an interface. | Supported | | `array` | Tuple struct | Converts the tuple struct to/from a JavaScript array. | Emits a tuple type. | Supported | | `transparent` | Single-field tuple struct | Delegates conversion to the inner field instead of creating a wrapper object. | Emits an alias of the inner TypeScript type. | Supported | | `object_from_js = false` | Object, array, transparent struct; enum | Omits `FromNapiValue`; the type cannot be accepted from JavaScript through generated conversion. | None. | Supported | | `object_to_js = false` | Object, array, transparent struct; enum | Omits `ToNapiValue`; the type cannot be returned to JavaScript through generated conversion. | None. | Supported | | `use_nullable` or `use_nullable = true` | Class, object, array, structured enum | For object and structured-enum fields, emits `None` as `null` instead of omitting it and requires the input property. For arrays, writes/requires the tuple index instead of leaving/accepting a hole. Class accessor and constructor conversion is unchanged. | Emits a required `T \| null` property or tuple element. For a class, this is the option's only effect. | Supported; default is `false` | | `custom_finalize` | Class struct | Stops napi-derive from generating the default empty `ObjectFinalize` implementation, so the class must implement it itself. | None. | Supported | | `type_tag = "salt"` | Class struct | Replaces the `crate@version` component of the class's content-derived type tag with the given crate-unique salt. | None — runtime only, never emitted. | Stamping/checking requires `napi/napi8`; no-op on wasm. Supported | | `iterator` | Class struct | Makes each instance implement the synchronous iterator protocol. | Extends `Iterator`. | **Experimental** | | `async_iterator` | Class struct | Makes each instance implement the async iterator protocol. | Adds `[Symbol.asyncIterator](): AsyncGenerator<...>`. | `napi/tokio_rt`; **experimental** | Direction controls are compile-time controls: disabling a direction removes the corresponding conversion trait implementation. This is useful for input-only shapes containing callbacks or output-only shapes containing data that cannot be read from JavaScript. **lib.rs** ```rust #[napi(object, object_to_js = false)] pub struct Request { pub path: String, pub on_chunk: ThreadsafeFunction, } #[napi(transparent)] pub struct UserId(pub String); #[napi(array)] pub struct Point(pub f64, pub f64); ``` For an object or structured-enum field, the default mode accepts a missing property as `None` and omits `None` on output. A present value is converted as the inner `T`, so `null` and `undefined` are not universally accepted. With `use_nullable = true`, the property is required, `Option` conversion accepts `null` as `None`, and output uses `null`; a missing or `undefined` property is still rejected. Arrays apply the same distinction to a missing tuple index versus a required index containing `null`. On a class, accessors and shorthand-constructor arguments already use normal `Option` conversion and getters return `null` for `None`; `use_nullable` only changes the generated TypeScript shape. ### `type_tag` Every `#[napi]` class gets a 128-bit type tag derived from its content identity string `crate@version::module_path::ClassName`. On `napi8` native builds the tag is stamped onto each instance's JavaScript object right after `napi_wrap` and verified before every blind pointer cast — a method receiver, a `&T` / `&mut T` parameter, or a `ClassInstance`. A wrong-class, prototype-spoofed, or `method.call(wrongThis)` object is rejected with a catchable `Value is not an instance of class` error instead of causing a type-confused cast (see the [type-tag tests](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/__tests__/type-tag.spec.ts)). Because Rust forbids duplicate `module_path::ident`, two distinct classes always get distinct tags even when they share a `js_name` and namespace. Stamping and checking are no-ops without `napi8` and on all wasm targets. The default identity keys on `crate@version`, so two _unrelated_ addons that happen to share the same crate name, version, module path, and class name would derive the same tag. `#[napi(type_tag = "...")]` opts into a crate-unique salt that **replaces** the `crate@version` component — the tag becomes `salt::module_path::ClassName`, so the salt only needs to be unique per crate, and a UUID is recommended: **lib.rs** ```rust #[napi(type_tag = "6f9619ff-8b86-d011-b42d-00cf4fc964ff")] pub struct MyClass { pub value: i32, } ``` The salt is a compile-time constant like the default derivation, so the tag stays stable across process reloads and across two separately loaded copies of the same addon. The attribute is runtime-only and never appears in the generated TypeScript. Reach for it when your crate name is widely vendored (or otherwise likely to collide with an unrelated addon loaded into the same process); the default derivation is sufficient otherwise. ### Fields | Option | Valid target | Runtime effect | TypeScript effect | Feature / status | | ---------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | --------------------------------------------------------- | | `js_name = "name"` | Struct or structured-enum field | Uses a different JavaScript property name. | Uses the renamed property. | Supported | | `skip` | Class or value-shape field | On a class, omits the generated property accessors. Value-shape conversion still reads and writes the field. | Omits the field. | Supported; see the shorthand-constructor limitation below | | `readonly` | Class or value-shape field | On a class, generates a getter but no setter. It does not change value-shape conversion. | Adds `readonly`. | Supported | | `writable`, `enumerable`, `configurable` | Exposed field | Controls class property descriptor flags. Object and structured-enum output always uses writable, enumerable, configurable data properties. | None. | Supported | | `ts_type = "..."` | Exposed field | None. | Replaces the inferred field type. | `napi-derive/type-def` | | `skip_typescript` | Exposed field | The field is still present at runtime. | Omits only that field from the declaration. | `napi-derive/type-def` | For a normal class, `skip` removes the generated JavaScript accessor, while `skip_typescript` leaves the accessor at runtime and hides only its declaration. On an object, array, or structured enum, `skip` and `readonly` affect the generated declaration but the runtime conversion still processes the field. Avoid `skip` with the `#[napi(constructor)]` struct shorthand: the generated constructor still consumes every field even though the skipped field is absent from its TypeScript signature. ## Enums | Option | Valid target | Runtime effect | TypeScript effect | Feature / status | | -------------------------------- | -------------------------- | ------------------------------------------------------------ | -------------------------------------------------- | ---------------- | | `string_enum` | Fieldless enum | Converts variants to strings instead of integer values. | Emits string-valued enum members. | Supported | | `string_enum = "case"` | Fieldless enum | Converts variant names using the selected case. | Uses the converted string values. | Supported | | `value = "literal"` | Variant of a `string_enum` | Overrides the JavaScript string for one variant. | Uses the literal value. | Supported | | `discriminant = "key"` | Structured enum | Changes the discriminating property from the default `type`. | Uses the same property in the discriminated union. | Supported | | `discriminant_case = "case"` | Structured enum | Changes how variant names are encoded in the discriminator. | Uses the same encoded values. | Supported | | `use_nullable` | Structured enum | Applies nullable-field behavior to variant fields. | Controls optional versus `T \| null` fields. | Supported | | `object_from_js`, `object_to_js` | Any enum | Enables or disables generated conversion in one direction. | None. | Supported | Accepted case names are `lowercase`, `UPPERCASE`, `PascalCase`, `camelCase`, `snake_case`, `UPPER_SNAKE`, `kebab-case`, and `UPPER-KEBAB-CASE`. **lib.rs** ```rust #[napi(string_enum = "kebab-case")] pub enum Mode { ReadOnly, #[napi(value = "read-write")] Writable, } #[napi(discriminant = "kind", discriminant_case = "camelCase")] pub enum Event { Ready, FileChanged { path: String }, Progress(u32, u32), } ``` `string_enum` accepts only fieldless variants and cannot be combined with explicit Rust discriminants. An enum containing any data-carrying variant is a structured enum; each variant becomes an object with the discriminator plus its fields. A field whose JavaScript name equals the discriminator is rejected. ## TypeScript overrides | Option | Valid target | Declaration effect | Important constraints | | -------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ts_arg_type = "..."` | One function parameter | Replaces the inferred type of that parameter. | Context-specific parameter attribute. Mutually exclusive with function-level `ts_args_type`. | | `ts_args_type = "..."` | Function or method | Replaces the complete comma-separated parameter list. | Mutually exclusive with every parameter-level `ts_arg_type`. | | `ts_return_type = "..."` | Function or method | Replaces the inferred return type. | For an async function, include the complete intended type, normally `Promise`. | | `ts_generic_types = "..."` | Function or method | Adds the text between `<...>` before the arguments. | The string must be valid TypeScript generic parameter syntax. | | `ts_type = "..."` | Function/method or field | On a function, replaces the entire signature suffix after its exported name; on a field, replaces its type. | Function-level `ts_type` cannot be combined with `ts_args_type` or `ts_return_type`. It also replaces the generic section, so include any generics inside `ts_type` instead of combining it with `ts_generic_types`. | | `skip_typescript` | Function, method, field, enum, const, type alias | Omits the declaration while retaining the runtime export. A type alias has no runtime export, so the alias disappears entirely. | Not valid on a whole struct or `impl` block. | **lib.rs** ```rust #[napi( ts_generic_types = "T", ts_args_type = "value: T", ts_return_type = "T" )] pub fn identity<'env>(value: Unknown<'env>) -> Unknown<'env> { value } #[napi(ts_type = "(operation: 'add' | 'subtract', a: number, b: number): number")] pub fn calculate(operation: String, a: i32, b: i32) -> i32 { match operation.as_str() { "add" => a + b, "subtract" => a - b, _ => 0, } } ``` These strings are inserted into the generated declaration; napi-rs does not parse them as TypeScript or verify that they describe the runtime behavior. Keep runtime conversions authoritative and test the generated `.d.ts` file. To add your own types or imports at the top of the generated `.d.ts` file, configure the declaration file header (`dtsHeader` / `dtsHeaderFile`) in the [NAPI config](/docs/cli/napi-config#declaration-file-header). That is a file-level setting, independent of the per-export overrides above. ## Iterators `iterator` and `async_iterator` are mutually exclusive. A generator class cannot expose public fields named `next`, `return`, or `throw`, because napi-rs installs those protocol methods. See [Iterators and async iterators](/docs/concepts/iterators) for the required traits and lifecycle constraints. ## Option index The general parser accepts these options: `catch_unwind`, `async_runtime`, `module_exports`, `js_name`, `constructor`, `factory`, `getter`, `setter`, `readonly`, `enumerable`, `writable`, `configurable`, `skip`, `strict`, `return_if_invalid`, `object`, `object_from_js`, `object_to_js`, `custom_finalize`, `namespace`, `type_tag`, `iterator`, `async_iterator`, `ts_args_type`, `ts_return_type`, `ts_type`, `ts_generic_types`, `string_enum`, `use_nullable`, `discriminant`, `discriminant_case`, `transparent`, `array`, `no_export`, and `skip_typescript`. The context-specific parsers additionally accept `ts_arg_type` on a function parameter and `value` on a string-enum variant. --- # Types Overwrite In most cases, **NAPI-RS** generates the correct TypeScript types from the Rust signature. Override them only when the public TypeScript contract intentionally differs from the runtime conversion type, and keep the two behaviors aligned in tests. [ThreadsafeFunction](./threadsafe-function) is one example: a `build_callback` closure can transform owned Rust data into a different list of JavaScript callback arguments, so inference cannot always describe the final callback signature. ## `ts_args_type` Replace the complete comma-separated parameter list of the exported function. This changes only the generated declaration, not runtime conversion. **lib.rs** ```rust {10} use std::sync::Arc; use std::thread; use napi::{ bindgen_prelude::*, threadsafe_function::{ThreadsafeCallContext, ThreadsafeFunctionCallMode}, }; use napi_derive::napi; #[napi(ts_args_type = "callback: (err: null | Error, result: string) => void")] pub fn call_threadsafe_function(callback: Function) -> Result<()> { let tsfn_builder = callback.build_threadsafe_function(); let tsfn = Arc::new( tsfn_builder .callee_handled::() .build_callback( move |ctx: ThreadsafeCallContext| Ok(format!("n: {}", ctx.value)), )?, ); for n in 0..100 { let tsfn = tsfn.clone(); thread::spawn(move || { tsfn.call(Ok(n), ThreadsafeFunctionCallMode::Blocking); }); } Ok(()) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export function callThreadsafeFunction( callback: (err: null | Error, result: string) => void, ): void ``` ## `ts_arg_type` Replace one or more parameter types _individually_. NAPI-RS continues to infer the other parameters. **lib.rs** ```rust {1} #[napi] fn override_individual_arg_on_function( not_overridden: String, #[napi(ts_arg_type = "() => string")] f: Function<(), String>, not_overridden2: u32, ) -> Result { let value = f.call(())?; Ok(format!("{not_overridden}-{value}-{not_overridden2}")) } ``` **index.d.ts** ```ts export function overrideIndividualArgOnFunction( notOverridden: string, f: () => string, notOverridden2: number, ): string ``` ## `ts_return_type` Replace the generated return type. For an async export, provide the complete public type, normally `Promise`. **lib.rs** ```rust {1} #[napi(ts_return_type="number")] fn return_something_unknown<'env>(env: &'env Env) -> Result> { env.create_uint32(42).map(|v| v.to_unknown()) } ``` **index.d.ts** ```ts export function returnSomethingUnknown(): number ``` ## `ts_type` Overwrite the generated ts-type of a field in a struct. **lib.rs** ```rust {1} #[napi(object)] pub struct TsTypeChanged { #[napi(ts_type = "MySpecialString")] pub type_override: String, #[napi(ts_type = "object")] pub type_override_optional: Option, } ``` **index.d.ts** ```ts export interface TsTypeChanged { typeOverride: MySpecialString typeOverrideOptional?: object } ``` ## Custom Type Definitions in Header When NAPI-RS generates `index.d.ts`, it starts the file with a default header. You can replace that header with your own type aliases, imports, lint directives, or license comments. This is a file-level setting: unlike the `ts_*` attributes above, it is configured once for the whole package, not per export. How it works: - Set `dtsHeader` (inline string) or `dtsHeaderFile` (path to a `.d.ts` file) in the [`napi` config](/docs/cli/napi-config). `dtsHeaderFile` is the better choice for complex headers with imports. - The CLI flags `--dts-header` and `--no-dts-header` of [`napi build`](/docs/cli/build) override the inline value or disable the header entirely, which is useful in CI. - A custom header **replaces** the default header completely. Include the `/* auto-generated by NAPI-RS */` comment and the eslint directive in your own header if you want to keep them. Precedence summary: a header file always wins over inline header text; `--dts-header` beats the inline `dtsHeader` config value; the default header is used when nothing is set; `--no-dts-header` skips the header entirely. The full field reference, priority table, and examples live in [NAPI Config: Declaration file header](/docs/cli/napi-config#declaration-file-header). --- # Module Initialization NAPI-RS provides two APIs for module initialization: `#[napi_derive::module_init]` and `#[napi(module_exports)]`. While they may seem similar, they serve different purposes and execute at different times. ## Execution Timeline Understanding when each API executes is crucial for using them correctly:
  1. Node.js loads the .node file

  2. #[napi_derive::module_init] runs

    via ctor — at dynamic library load time · once per native library load

  3. napi_register_module_v1 called by Node.js

    Registers all #[napi] exports — functions, classes, and more

  4. #[napi(module_exports)] runs

    Receives the exports object and can customize it · once per Node.js thread/context

  5. Module is ready for use in JavaScript

## `#[napi_derive::module_init]` This macro marks a function to run when the native module is loaded. It uses the [`ctor`](https://crates.io/crates/ctor) crate internally to execute before Node-API registers the JavaScript exports. ### Timing - Runs at **dynamic library load time** (before `napi_register_module_v1`) - Executes once for a native library load, not once per Node-API environment; workers in the same process normally share that loaded library - No access to Node.js environment or exports object ### Signature ```rust #[napi_derive::module_init] fn init() { // initialization code } ``` The function must have no parameters and no return value. ### When to Use Use `#[napi_derive::module_init]` for: - **Setting up async runtimes** (e.g., tokio) - **Initializing global state** that should be shared across all threads - **One-time setup** that must happen before any exports are registered - **Configuring logging or tracing** ### Example: Custom Tokio Runtime **lib.rs** ```rust use napi::bindgen_prelude::create_custom_tokio_runtime; #[napi_derive::module_init] fn init() { let runtime = napi::tokio::runtime::Builder::new_multi_thread() .enable_all() .thread_name("my-native-module") .build(); match runtime { Ok(rt) => create_custom_tokio_runtime(rt), Err(err) => eprintln!("failed to create custom Tokio runtime: {err}"), } } ``` ::: warning The multi-thread Tokio runtime configured in the example above is target-dependent. Gate that runtime setup or choose a runtime configuration supported by your WebAssembly target. The `module_init` macro itself supports WebAssembly builds. ::: ## `#[napi(module_exports)]` This macro marks a function that receives the module's `exports` object, allowing you to customize it before the module is returned to JavaScript. ### Timing - Runs **after** all `#[napi]` exports are registered - Runs **during** `napi_register_module_v1` (Node.js module registration) - Executes **once per Node.js context** (main thread + each worker thread) ### Signature The function can return `()` or `Result<()>`. Each parameter, if present, must be `Env`, `Object`, or a reference to one of those types. `Object` receives the module's exports object, while `Env` receives the current Node-API environment. The usual signatures are: ```rust // With just the exports object #[napi(module_exports)] pub fn init(mut exports: Object) -> Result<()> { // customize exports Ok(()) } // With exports and Env #[napi(module_exports)] pub fn init(mut exports: Object, env: Env) -> Result<()> { // customize exports with access to Env Ok(()) } ``` ### When to Use Use `#[napi(module_exports)]` for: - **Adding custom properties** to the exports object - **Creating symbols** that should be exported - **Registering exports programmatically** (not via `#[napi]`) - **Per-thread initialization** that needs the Node.js environment ### Example: Adding a Symbol **lib.rs** ```rust use napi::bindgen_prelude::*; #[napi(module_exports)] pub fn init(mut exports: Object) -> Result<()> { // Add a unique symbol to exports let symbol = Symbol::new("MY_MODULE_SYMBOL"); exports.set_named_property("MY_SYMBOL", symbol)?; // Add a version string exports.set_named_property("VERSION", "1.0.0")?; Ok(()) } ``` **index.js** ```js const native = require('./index.node') console.log(native.MY_SYMBOL) // Symbol(MY_MODULE_SYMBOL) console.log(native.VERSION) // "1.0.0" ``` ## Key Differences | Aspect | `#[napi_derive::module_init]` | `#[napi(module_exports)]` | | ----------------------- | ----------------------------- | ---------------------------- | | **Execution time** | At `.node` file load | During module registration | | **Runs per** | Native library/module load | Node-API environment/context | | **Receives exports** | No | Yes | | **Can modify exports** | No | Yes | | **Access to Env** | No | Yes (Optional) | | **WebAssembly support** | Yes (Via our js binding) | Yes | ## Using Both Together These APIs are complementary and can be used together: **lib.rs** ```rust use napi::bindgen_prelude::*; // Runs once at module load - setup tokio runtime #[cfg(not(target_family = "wasm"))] #[napi_derive::module_init] fn setup_runtime() { let runtime = napi::tokio::runtime::Builder::new_multi_thread() .enable_all() .build(); match runtime { Ok(rt) => create_custom_tokio_runtime(rt), Err(err) => eprintln!("failed to create custom Tokio runtime: {err}"), } } // Runs per thread - customize exports #[napi(module_exports)] pub fn customize_exports(mut exports: Object) -> Result<()> { exports.set_named_property("THREAD_SAFE_SYMBOL", Symbol::new("THREAD_SAFE"))?; Ok(()) } // Regular export via #[napi] #[napi] pub async fn do_async_work() -> String { // This uses the tokio runtime set up in module_init napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await; "done".to_string() } ``` The custom-runtime examples require `napi`'s `async` (or `tokio_rt`) feature. The sleep example additionally requires `tokio_time`: **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["async", "tokio_time"] } napi-derive = "3" ``` ## Worker Thread Behavior When using worker threads in Node.js, the behavior differs between the two APIs: **main.js** ```js const { Worker } = require('worker_threads') // Main thread loads module const native = require('./index.node') // -> module_init runs (first time) // -> module_exports runs (main thread) // Worker thread loads same module new Worker( ` const native = require('./index.node') // -> module_init does NOT run again (already ran) // -> module_exports DOES run again (new thread context) `, { eval: true }, ) ``` This means: - Global resources (like tokio runtime) are initialized once and shared - Per-thread state can be set up in `module_exports` for each context ::: info The `#[napi_derive::module_init]` function runs via the `ctor` crate, which uses platform-specific mechanisms (`.init_array` on Unix, special constructor functions on Windows) to execute at dynamic library load time. ::: --- # Cargo features The `napi` feature set controls which Node-API symbols and high-level Rust APIs are compiled into an addon. Choose the lowest Node-API level that provides the APIs you use, then enable only the optional integrations your crate needs. **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["napi6", "async", "serde-json"] } napi-derive = "3" ``` ## Defaults `napi` enables these features by default: ```toml default = ["napi4", "dyn-symbols"] ``` This means adding `features = ["napi2"]` without disabling defaults still builds for Node-API 4. To target a level below 4, disable defaults explicitly and decide whether to retain dynamic symbol loading: **Cargo.toml** ```toml [dependencies] napi = { version = "3", default-features = false, features = ["napi3", "dyn-symbols"] } ``` ::: warning A Cargo feature is a compile-time capability, not a runtime polyfill. Calling a Node-API function that the host does not provide is unsupported even when `dyn-symbols` lets the native library itself load. ::: ## Node-API levels The `napi1` through `napi10` features are cumulative. For example, `napi8` enables `napi7`, which enables every lower level. The selected level is the minimum Node-API capability your addon may rely on. | Feature | Representative napi-rs APIs gated at this level | | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `napi1` | Base values, functions, objects, arrays, buffers, async work, Promises, and references. | | `napi2` | Access to the libuv event loop with `Env::get_uv_event_loop` on native targets. | | `napi3` | Environment cleanup hooks. | | `napi4` | ThreadsafeFunction, deferred/async runtime integration, and napi-rs's cross-thread reference cleanup machinery. This is the default. | | `napi5` | JavaScript `Date`, finalizers, and related object/property APIs. | | `napi6` | BigInt and BigInt typed arrays, per-environment instance data, and additional object/ArrayBuffer APIs. | | `napi7` | Detaching and testing detached ArrayBuffers. | | `napi8` | Async cleanup hooks, object freeze/seal, and type-tagging APIs. | | `napi9` | Global symbols, module file names, and JavaScript `SyntaxError` creation/throwing. | | `napi10` | External Latin-1/UTF-16 strings and dedicated property-key creation APIs. | The table lists representative high-level gates, not every raw function re-exported by `napi-sys`. Node.js has backported some Node-API levels to multiple release lines, so a single Node major version is not a precise compatibility test. Check the official [Node-API version matrix](https://nodejs.org/api/n-api.html#node-api-version-matrix) and the actual runtime value: ```js console.log(process.versions.napi) ``` Inside native code, `Env::get_napi_version()` reads the same value. Your package's supported-runtime claim should be no broader than both the selected Node-API level and the runtime versions you actually test. ### Choosing a level 1. Start with the template/default `napi4` unless a dependency or required API dictates otherwise. 2. Raise it when the compiler shows that a needed API is feature-gated. 3. Test the oldest runtime in the resulting compatibility range. 4. Keep the CLI's `minNodeApiVersion`, Cargo features, package `engines`, and CI matrix consistent. Raising the feature can make the resulting addon unloadable or unusable on older runtimes. Lowering it removes Rust APIs at compile time and is the safest way to discover accidental dependencies on a newer level. ## Async and Tokio | Feature | Enables | Important tradeoff | | ----------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `tokio_rt` | napi-rs's Tokio runtime integration and `napi4`; re-exports `tokio` from `napi`. | Adds Tokio runtime code and lifecycle management. Required by exported Rust `async fn`. | | `async` | Alias for `tokio_rt`. | Use either name; enabling both is redundant. | | `tokio_full` | Tokio's `full` feature set. | Large dependency and binary-size increase; it does **not** replace `tokio_rt` for napi-rs integration. | | `tokio_fs` | Tokio filesystem APIs. | Also enable `tokio_rt`/`async` for exported async functions. | | `tokio_io_std` | Tokio async stdin/stdout/stderr. | Same runtime requirement. | | `tokio_io_util` | Tokio I/O utility traits and adapters. | Same runtime requirement. | | `tokio_macros` | Tokio procedural macros. | Not needed merely to export an `async fn` with `#[napi]`. | | `tokio_net` | Tokio networking. | Same runtime requirement. | | `tokio_process` | Tokio child-process support. | Platform-specific behavior still applies. | | `tokio_signal` | Tokio signal handling. | Process-wide signal interactions still apply. | | `tokio_sync` | Tokio synchronization types. | Same runtime requirement. | | `tokio_test_util` | Tokio time/testing utilities. | Primarily for tests. | | `tokio_time` | Tokio timers and timeouts. | Same runtime requirement. | The component features enable features on the Tokio dependency. They do not all imply napi-rs's `tokio_rt`; list it explicitly unless another selected feature, such as `web_stream`, already enables it. **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["async", "tokio_fs", "tokio_time"] } ``` For CPU-bound work, prefer [AsyncTask](/docs/concepts/async-task), which uses libuv's worker pool, instead of blocking the Tokio runtime. ## Conversion features | Feature | Adds | Notes | | -------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `serde-json` | `serde_json::Value`, `Map`, and `Number` conversion; enables `serde` and `serde_json`. | JavaScript values outside JSON's data model are rejected or require an explicit representation. | | `serde-json-ordered` | `serde-json` plus `serde_json/preserve_order`. | Preserves map insertion order in serde_json's representation; JavaScript property-order rules still apply. | | `chrono_date` | `chrono::DateTime` and `NaiveDateTime` conversion; enables `chrono` and `napi5`. | JavaScript Date precision is milliseconds. | | `latin1` | Latin-1 to UTF-8 decoding/display through `encoding_rs`. | `Latin1String` conversion itself exists without this feature; formatting/decoding support is gated. | | `object_indexmap` | `IndexMap` and `IndexSet` conversions. | Adds the `indexmap` dependency. Maps still use plain JavaScript objects; sets use JavaScript `Set`. | | `web_stream` | `ReadableStream` and `WriteableStream`; enables `futures-core`, `tokio-stream`, `tokio_rt`, and `napi4`. | The runtime must also provide compatible Web Streams globals. | | `error_anyhow` | Conversion from `anyhow::Error` and the optional `anyhow` dependency. | Conversion uses `GenericFailure`; map errors manually for stable domain codes. | See [Type conversions](/docs/concepts/type-conversions) for directionality and data-copy behavior. ## Linking and runtime detection ### `dyn-symbols` On supported native targets, `dyn-symbols` resolves Node-API functions from the host process when the addon initializes instead of requiring every symbol to be resolved by the platform linker. It is enabled by default. This is especially useful across operating systems and Node-compatible hosts with different native linking behavior. Missing functions use generated stubs so symbol loading can continue, but calling a missing API still fails. `dyn-symbols` does not turn Node-API 10 into Node-API 4. Disable it only when the target's static/direct symbol-linking model is deliberate and tested: **Cargo.toml** ```toml napi = { version = "3", default-features = false, features = ["napi6"] } ``` ### `node_version_detect` `node_version_detect` reads and caches the host Node version during module registration. napi-rs uses it to select a few guarded optimized paths, including newer property-creation paths when the required symbols and companion features are enabled. It is not a general compatibility guard around every Node-API call. Your code must still respect the selected Node-API feature and runtime support matrix. ## Diagnostics and observability | Feature | Behavior | Cost / limitation | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | | `deferred_trace` | Captures a JavaScript error when creating a deferred so later cross-thread rejection retains the caller-side stack. Implies `napi4`. | Allocates and retains an error/reference for affected deferred operations. | | `tracing` | Re-exports `tracing` from `napi`. | To emit generated callback-entry events, also enable `napi-derive/tracing`. | **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["napi4", "tracing"] } napi-derive = { version = "3", features = ["tracing"] } ``` Generated callback events use the `napi` tracing target. Install and configure a tracing subscriber in the embedding application or addon initialization path if you want to observe them. ## Compatibility and development-only features | Feature | Purpose | Use in production? | | -------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `compat-mode` | Restores deprecated v2-era low-level types, traits, and macros for migration. | Only while migrating; prefer v3 bindgen APIs for new code. | | `experimental` | Enables experimental raw Node-API symbols and optimized paths guarded by napi-rs. | Only when you control and test the runtime; experimental Node APIs do not carry the stable Node-API ABI guarantee. | | `noop` | Replaces registration/conversion paths so Rust crates can be compiled or tested without loading Node. | No. Pair with `napi-derive/noop`; JavaScript conversion behavior is not exercised. | The iterator attributes are labeled experimental in the Rust API but are **not** controlled by the `experimental` Cargo feature. Synchronous iterators are in the base bindgen runtime; async iterators require `tokio_rt`. ### Cargo-only tests with `noop` **Cargo.toml** ```toml [features] noop = ["napi/noop", "napi-derive/noop"] [dependencies] napi = "3" napi-derive = "3" ``` Use this to test pure Rust logic that happens to live in an addon crate. Run JavaScript integration tests against a real built addon for conversions, exceptions, references, finalizers, workers, and environment cleanup. ## The `full` bundle `full` is a convenience bundle containing exactly: ```toml full = [ "latin1", "napi10", "async", "serde-json", "experimental", "chrono_date", ] ``` It does **not** mean every feature: for example, it does not include `web_stream`, `object_indexmap`, `deferred_trace`, `node_version_detect`, `tracing`, `error_anyhow`, or every Tokio component. Because it raises the Node-API level to 10 and enables experimental APIs, `full` is convenient for documentation builds and broad internal testing but is rarely the right default for a published addon. ## `napi-derive` features The procedural macro crate has its own feature set: | Feature | Default | Effect | | ------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `type-def` | Yes | Emits metadata consumed by `@napi-rs/cli` to generate `.d.ts` declarations. | | `strict` | Yes | Reports parsed `#[napi]` options that were not used for the selected item. This is compile-time macro validation, not JavaScript argument validation. | | `tracing` | No | Adds tracing events to generated callback wrappers. Pair with `napi/tracing`. | | `compat-mode` | No | Enables legacy derive macros used by the compatibility API. | | `noop` | No | Disables normal macro expansion for Cargo-only builds/tests. | | `full` | No | Enables `type-def`, `strict`, and `compat-mode`. | Do not confuse `napi-derive/strict` with the per-function `#[napi(strict)]` attribute: the feature checks macro usage at compile time; the attribute validates JavaScript values at runtime. ## Recommended published-addon baseline **Cargo.toml** ```toml [dependencies] napi = { version = "3", default-features = false, features = ["napi4", "dyn-symbols"] } napi-derive = "3" [build-dependencies] napi-build = "2" ``` Add integration features only as the public API requires them, document the resulting minimum Node-API level, and test that minimum runtime plus the current supported runtimes in CI. --- # Custom async runtime Exported Rust `async fn`s normally run on the Tokio runtime that NAPI-RS manages when the `async` / `tokio_rt` feature is enabled (see [async fn](/docs/concepts/async-fn)). The `async-runtime` Cargo feature exposes the layer underneath: a service-provider interface (SPI) that lets you back every generated `async fn`, `Env::spawn_future`, and `AsyncBlock` with **your own scheduler instead of Tokio**. You want this when: - your addon runs where Tokio does not fit — a **threadless `wasm32-wasip1`** build, **workerd** / edge isolates, or the browser; - you already own an executor (a game loop, an embedded runtime, a company-wide scheduler) and do not want a second runtime in the process; - you want a tokio-free binary: a pure `async-runtime` build links no Tokio at all. **Cargo.toml** ```toml [dependencies] napi = { version = "3", default-features = false, features = ["napi4", "async-runtime"] } napi-derive = "3" ``` The feature only enables `napi4`; it deliberately does **not** pull in Tokio. The three build shapes: | Features | Behavior | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `async-runtime` only | No Tokio is linked. Synchronous exports work without any registration; a runtime-backed operation (for example a generated `#[napi] async fn`) rejects its promise with a missing-backend error until a backend is registered. | | `async-runtime` + `tokio_rt` | Generated `async fn`s follow the _selection_: the custom backend if one was registered in time, otherwise Tokio. Selecting a custom backend never constructs Tokio. The compatibility helpers `spawn`, `spawn_blocking`, `block_on`, and `within_runtime_if_available` stay Tokio-backed with unchanged signatures. | | `noop` + `async-runtime` | The SPI types exist so code compiles, but no backend can be installed; registration retires the backend and reports `InvalidArg`. | ## The `AsyncRuntime` SPI A backend is a single `unsafe impl AsyncRuntime`, registered once per addon image: ```rust pub unsafe trait AsyncRuntime: Send + Sync + 'static { fn spawn( &self, task: AsyncRuntimeTask, ) -> std::result::Result<(), AsyncRuntimeRejection>; fn block_on(&self, future: Pin<&mut dyn Future>) -> Result<()>; fn enter(&self) -> Result> { ... } // default: no-op guard fn start(&self) -> Result<()> { ... } // default: no-op fn shutdown(&self) -> Result<()>; fn spawn_blocking( &self, work: Box, ) -> std::result::Result<(), AsyncRuntimeRejection>> { ... } // default: declines } ``` The pieces you interact with: - **`AsyncRuntimeTask`** — the opaque unit of work napi hands you (the future behind a generated `async fn` plus the promise-settling machinery). It is `Future + Send + 'static`. A conforming backend has exactly three options: **poll it to completion**, **drop it** (dropping before completion cancels the task and rejects the associated JavaScript promise), or **hand it back untouched** in an `AsyncRuntimeRejection` when declining the submission. Never `mem::forget` an accepted task, and never bypass its `Drop`. - **`AsyncRuntimeRejection`** — carries declined work back to napi together with a diagnostic `Error`, which surfaces as the promise rejection. - **`start` / `shutdown`** — the lifecycle hooks. Keep a freshly constructed backend _dormant_: create threads and queues in `start`, release them in `shutdown`. napi calls `start` when the first live Node environment begins (worker isolates and Electron renderer reloads can take the environment count from zero to one repeatedly, so it must be idempotent), and `shutdown` when its accounting observes the last environment exit. Embedders can also call [`shutdown_async_runtime`](#shutdown_async_runtime) explicitly. - **`enter`** — establishes an ambient runtime context for the calling thread (the Tokio equivalent of `Runtime::enter`). The default no-op guard is correct for backends that do not need one. - **`spawn_blocking`** — an optional blocking-capable lane. The default implementation declines; on threadless targets there is no blocking lane, so declining is the right behavior there. A minimal backend — one OS thread per task, with every worker joined before `shutdown` returns so nothing can outlive the native image — looks like this. It depends on `futures` (for `futures::executor::block_on`) in addition to `napi`: **lib.rs** ```rust use std::{ future::Future, pin::Pin, sync::Mutex, thread::JoinHandle, }; use napi::bindgen_prelude::{ register_async_runtime, AsyncRuntime, AsyncRuntimeRejection, AsyncRuntimeTask, Result, }; #[derive(Default)] struct SimpleRuntime { // Every spawned worker, so `shutdown` can join them all. workers: Mutex>>, } // SAFETY: `shutdown` joins every worker thread before returning, so no // backend-owned thread or task can execute addon code after the native image // starts unloading. unsafe impl AsyncRuntime for SimpleRuntime { fn spawn( &self, task: AsyncRuntimeTask, ) -> std::result::Result<(), AsyncRuntimeRejection> { let handle = std::thread::spawn(move || futures::executor::block_on(task)); self.workers.lock().unwrap().push(handle); Ok(()) } fn block_on(&self, future: Pin<&mut dyn Future>) -> Result<()> { futures::executor::block_on(future); Ok(()) } fn shutdown(&self) -> Result<()> { for handle in self.workers.lock().unwrap().drain(..) { let _ = handle.join(); } Ok(()) } } #[napi_derive::module_init] fn init() { register_async_runtime(SimpleRuntime::default()); } ``` A real backend also needs a `spawn_blocking` lane, bounded queues, and cancellation on drop. The runnable, fully-documented implementation is the [`examples/custom-async-runtime`](https://github.com/napi-rs/napi-rs/tree/main/examples/custom-async-runtime) crate — including the threadless-WASI, workerd, and browser wiring. ### Registering from `#[module_init]` Registration is **once per linked addon image, first-writer-wins**, and must happen before the registration window closes — when napi begins activating the first Node-API environment, or earlier when a runtime-backed operation commits a backend choice. That is why the natural place is a [`#[module_init]`](/docs/concepts/module-init) hook, which runs as a library constructor before napi owns any environment. Two entry points: - `register_async_runtime(runtime)` — the infallible form for `#[module_init]`. It never panics (a panic in a library constructor would abort the process); a duplicate or late registration is recorded, and every later runtime-backed operation surfaces it by rejecting its promise. - `try_register_async_runtime(runtime) -> Result<()>` — the fallible form that returns the error directly. A rejected backend is safely retired through its own `shutdown` hook either way. ### `shutdown_async_runtime` `shutdown_async_runtime()` invokes the registered backend's `shutdown` hook — its sole resource-release and quiescence point. A backend's `Drop` implementation is not guaranteed to run, so everything must be released in `shutdown`. On WebAssembly targets the runtime is **not** shut down automatically, so call `shutdown_async_runtime` explicitly there (for example between test runs). ::: warning After `shutdown` returns, Node may unload the addon's native image immediately. No backend-owned thread, task, closure, destructor, cancellation callback, or retained `Waker` may execute code from that image afterwards. A backend that cannot prove those references inert must keep the image loaded itself rather than return from `shutdown`. ::: ::: warning The panic containment napi performs around backend hooks and task polls requires a `panic = "unwind"` build. On `panic = "abort"` targets — including Rust's shipped `wasm32-wasip1` and `wasm32-wasip1-threads` targets — a panicking async function traps or aborts **before** its JavaScript promise is settled. ::: ## The `napi-async-runtime` crate You usually do not need to write a backend by hand. [`napi-async-runtime`](https://github.com/napi-rs/napi-rs/tree/main/crates/async-runtime) (crates.io, v0.2.x) is the published, batteries-included implementation: a tokio-free async/CPU/blocking scheduler plus an optional adapter that registers it as the addon's `AsyncRuntime` backend. **Cargo.toml** ```toml [dependencies] napi = { version = "3", default-features = false, features = ["napi4", "async-runtime"] } napi-derive = "3" napi-async-runtime = "0.2" ``` Install it from your own `#[module_init]` — the crate deliberately ships no `module_init` of its own, so the host stays in charge of configuration resolution order: **lib.rs** ```rust #[napi_derive::module_init] fn init() { // Resolve your own configuration (env vars, defaults) FIRST; the scheduler // never reads the environment itself. napi_async_runtime::install(napi_async_runtime::RuntimeOptions::default()) .expect("failed to install the shared async runtime"); } #[napi] pub async fn sleep_then_add(a: u32, b: u32, sleep_ms: u32) -> u32 { napi_async_runtime::sleep_until( std::time::Instant::now() + std::time::Duration::from_millis(u64::from(sleep_ms)), ) .await; a + b } ``` What the crate provides: - **Scheduler API at the crate root** (always compiled, napi-free): `spawn` / `try_spawn` / `spawn_detached`, `spawn_blocking`, `block_on`, `sleep_until`, `configure` / `configure_partial` / `configured_options`, `start` / `shutdown`, and `metrics` / `reset_metrics` returning a `RuntimeMetricsSnapshot`. - **Two flavors.** `MultiThread` (native only) runs futures on a Rayon-backed worker pool; `CurrentThread` — the only flavor on WebAssembly — never creates threads and instead publishes _host turns_ through registered `CurrentThreadTaskDriver` / `TimerDriver` host drivers. The blocking-lane cap is `worker_threads - 1`, so one execution lane always stays available for runnable futures and timers. - **The `install()` adapter** (default `napi` feature), which configures the scheduler and calls `register_async_runtime` for you, plus the JavaScript-facing host protocol exports (`registerCurrentThreadTaskHost`, `registerTimerHost`, `getAsyncRuntimeMetrics`, …). The matching JavaScript host installers for CurrentThread builds live in the [`@napi-rs/async-runtime`](https://www.npmjs.com/package/@napi-rs/async-runtime) npm package. - **Threadless-wasm safety**: on `wasm32-wasip1` / `wasm32-unknown-unknown` (no threads, no `Atomics.wait`), a `block_on` park that provably can never be woken fails loudly with a typed `BlockOnDeadlock` panic instead of hanging the JS event loop. The end-to-end consumer to copy from is [`examples/shared-async-runtime`](https://github.com/napi-rs/napi-rs/tree/main/examples/shared-async-runtime); the hand-rolled reference backend is [`examples/custom-async-runtime`](https://github.com/napi-rs/napi-rs/tree/main/examples/custom-async-runtime). See the [Examples](/docs/more/examples) page for the full list. --- # WebAssembly ::: info There is a amazing WebAssembly course developed by [@Dominic Elm](https://x.com/elmd_): Learn **WebAssembly** ::: NAPI-RS can compile an addon to [`wasm32-wasip1-threads`](https://doc.rust-lang.org/rustc/platform-support/wasm32-wasip1-threads.html) and generate loaders for Node.js and browsers. The primary use cases are: - a portable fallback when no prebuilt native addon matches the host; - a browser, StackBlitz, or WebContainer demo of the same Rust API; - an explicitly WASI-targeted package. `wasm32-wasip1-threads` is currently the supported default. Lower-level `wasm32-unknown-unknown` and non-threaded WASI targets require you to adapt threading and dependencies yourself and are not generated by this workflow. ::: warning A WASI build is not automatically equivalent to a native addon. Operating system APIs, native C/C++ dependencies, filesystem behavior, threads, memory limits, and host runtime support can differ. Test the WASI artifact as a separate release target. ::: ## Quick start The easiest starting point is `napi new` with the WASI target enabled. For an existing project, install the Rust target and add it to `napi.targets`: ```sh rustup target add wasm32-wasip1-threads ``` **package.json** ```json { "name": "@scope/my-addon", "main": "index.js", "types": "index.d.ts", "browser": "browser.js", "napi": { "binaryName": "my-addon", "targets": [ "x86_64-unknown-linux-gnu", "aarch64-apple-darwin", "x86_64-pc-windows-msvc", "wasm32-wasip1-threads" ], "wasm": { "initialMemory": 4000, "maximumMemory": 65536, "browser": { "fs": false, "asyncInit": false, "buffer": false, "errorEvent": true } } } } ``` The project must have compatible `@emnapi/core` and `@emnapi/runtime` dependencies. The scaffolds created by current `napi new` include them. If the versions resolved by the project differ from the `emnapi` version used by the CLI, the build stops with an explicit version-mismatch error; update them together rather than bypassing the check. Build the WASI target: ```sh napi build --platform --release --target wasm32-wasip1-threads ``` No cross-compilation flag is needed. Pure-Rust crates use Rust's WASI linker. `WASI_SDK_PATH` is only needed when C/C++ code in the dependency tree requires a WASI C toolchain. ## Generated artifacts For `binaryName: "my-addon"`, the build creates: | File | Purpose | | --- | --- | | `my-addon.wasm32-wasi.wasm` | Stripped release WASM module | | `my-addon.wasm32-wasi.debug.wasm` | Module retaining debug/name information when generation succeeds | | `my-addon.wasi.cjs` | Node.js WASI loader | | `my-addon.wasi-browser.js` | Browser ESM loader | | `wasi-worker.mjs` | Node worker used by emnapi threads | | `wasi-worker-browser.mjs` | Browser worker used by emnapi threads | | `browser.js` | Package browser entry that re-exports the WASI platform package | | `index.js` and `index.d.ts` | Normal platform-selecting loader and shared types | Keep each loader with its worker and WASM files. Renaming or moving one file without regenerating the loader breaks its relative URLs. ## The runtime package: `@napi-rs/wasm-runtime` The generated loaders and workers are thin glue on top of the published [`@napi-rs/wasm-runtime`](https://www.npmjs.com/package/@napi-rs/wasm-runtime) package, which the generated WASI platform package depends on (alongside `@emnapi/core` and `@emnapi/runtime`). Its main entry provides the WASI implementation and the emnapi glue: - `WASI` — a WASI preview1 implementation with pluggable `fs` and preopens; - `instantiateNapiModuleSync` — instantiate the WASM module and bind its napi exports; - `MessageHandler`, `createOnMessage`, `createFsProxy` — the worker/fs-proxy machinery used by emnapi threads; - `emnapiAsyncWorkPlugin`, `emnapiTSFNPlugin` — the emnapi plugins that back `AsyncTask` and `ThreadsafeFunction` on WASM. The `@napi-rs/wasm-runtime/fs` subpath provides the browser filesystem: `memfs()` returns a [memfs](https://github.com/streamich/memfs)-backed `{ fs, vol }` pair, plus `memfsExported` and a browser `Buffer`. This is the "memfs" the browser configuration below refers to — with `browser.fs: true`, the generated loader wires it into WASI and exports it: **my-addon.wasi-browser.js** ```ts import { WASI, instantiateNapiModuleSync, emnapiAsyncWorkPlugin, emnapiTSFNPlugin, } from '@napi-rs/wasm-runtime' import { memfs, Buffer } from '@napi-rs/wasm-runtime/fs' // the generated loader re-exports these when `browser.fs` is true const { fs: __fs, vol: __volume } = memfs() const __wasi = new WASI({ version: 'preview1', fs: __fs, preopens: { '/': '/', }, }) ``` The browser worker uses the same package from the other side — proxying fs calls back to the main thread: **wasi-worker-browser.mjs** ```js import { MessageHandler, WASI, createFsProxy, emnapiAsyncWorkPlugin, emnapiTSFNPlugin, } from '@napi-rs/wasm-runtime' import { memfsExported } from '@napi-rs/wasm-runtime/fs' const fs = createFsProxy(memfsExported) ``` Both snippets are condensed from the generated loaders in [`examples/napi`](https://github.com/napi-rs/napi-rs/tree/main/examples/napi) (`example.wasi-browser.js` and `wasi-worker-browser.mjs`); regenerate your own loaders with `napi build` rather than editing them by hand. ## How native-to-WASI fallback works The normal `index.js` loader first tries the native binary for the current platform. If native loading fails, it tries: 1. a local `my-addon.wasi.cjs`; 2. the separately published `@scope/my-addon-wasm32-wasi` package. Use `NAPI_RS_FORCE_WASI` to test the fallback even on a supported native host. For loaders generated by `@napi-rs/cli` 3.7 or newer: | Value | Behavior | | --- | --- | | unset or any other string | Prefer native; try WASI only after native fails | | `true` | Attempt and select WASI even if native loaded; no strict missing-WASI assertion | | `error` | Attempt WASI and throw if no local or packaged WASI binding exists | Values such as `1`, `0`, and `false` do not force WASI. Use `error` in tests so a missing artifact cannot silently use the native implementation: ```sh NAPI_RS_FORCE_WASI=error node ./test.cjs ``` In Node, the generated WASI loader: - uses `NAPI_RS_ASYNC_WORK_POOL_SIZE`, then `UV_THREADPOOL_SIZE`, then `4` for the emnapi async-work pool; - preopens the host filesystem root through Node's WASI implementation; - reuses and unreferences worker threads so idle workers do not keep Node alive; - prefers the `.debug.wasm` file when it is present beside the loader. ::: warning The Node WASI loader preopens the filesystem root. Treat the WASI addon as trusted native application code, not as a security sandbox for untrusted modules or input. ::: ## Browser demo This image transformer uses [`@napi-rs/image`](https://github.com/Brooooooklyn/Image) through its WASI browser entry: ```ts import { Transformer } from '@napi-rs/image' export async function transform() { const imageBytes = await fetch( 'https://images-assets.nasa.gov/image/carina_nebula/carina_nebula~orig.png', ).then((res) => res.arrayBuffer()) const transformer = new Transformer(new Uint8Array(imageBytes)) return transformer.webp() } ``` After installing the WASI package and configuring cross-origin isolation, it can be bundled with Vite or Webpack. For a local, unpublished build, import `./my-addon.wasi-browser.js` directly. For a published package, the root `browser` entry re-exports the separately published `-wasm32-wasi` package. ## Browser server configuration The threads target uses shared WebAssembly memory, `SharedArrayBuffer`, workers, and atomics. Browsers expose `SharedArrayBuffer` only in a cross-origin-isolated page because of side-channel security mitigations: Serve the document with both headers: ```text Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` For example, in Vite: **vite.config.ts** ```ts import { defineConfig } from 'vite' export default defineConfig({ server: { headers: { 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Embedder-Policy': 'require-corp', }, }, plugins: [ { name: 'configure-preview-response-headers', configurePreviewServer(server) { server.middlewares.use((_req, res, next) => { res.setHeader('Cross-Origin-Opener-Policy', 'same-origin') res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp') next() }) }, }, ], }) ``` Configure the production CDN/server as well; development headers do not carry over to a deployment. Cross-origin scripts, workers, WASM, images, and fonts must also satisfy the selected COEP policy. Verify the result in the browser: ```js console.log(globalThis.crossOriginIsolated) // true console.log(typeof SharedArrayBuffer) // 'function' ``` ## Browser runtime configuration The `napi.wasm` fields control generated browser glue: | Field | Default | Effect | | --- | --- | --- | | `initialMemory` | `4000` pages | Initial shared memory (a WebAssembly page is 64 KiB) | | `maximumMemory` | `65536` pages | Maximum shared memory, 4 GiB | | `browser.fs` | `false` | Create an in-memory filesystem, preopen `/`, and export `__fs` / `__volume` | | `browser.asyncInit` | `false` | Use emnapi's asynchronous instantiation API | | `browser.buffer` | `false` | Inject the `buffer` package's `Buffer` into the emnapi context | | `browser.errorEvent` | `false` | Forward worker failures as `napi-rs-worker-error` window events | The browser entry fetches the WASM file and therefore uses top-level `await` even when `asyncInit` is `false`. Make sure the bundler/output target supports ESM workers, `import.meta.url`, and top-level await. With `fs: true`, filesystem access is to memfs, not the user's host filesystem: ```ts import { __fs } from './my-addon.wasi-browser.js' __fs.writeFileSync('/input.txt', 'hello') ``` With `errorEvent: true`, observe worker errors before starting work: ```ts window.addEventListener('napi-rs-worker-error', (event) => { const { detail } = event as CustomEvent console.error('NAPI-RS WASI worker failed', detail) }) ``` ## Install the WebAssembly package To avoid increasing every native install, NAPI-RS marks the WASI platform package with `cpu: ["wasm32"]`. Package managers skip it unless wasm32 is an enabled installation architecture. ### Yarn For Yarn 4, add `wasm32` to `.yarnrc.yml`: **.yarnrc.yml** ```yaml supportedArchitectures: cpu: - current - wasm32 ``` Yarn 1 has no equivalent maintained architecture setting. Its `--ignore-engines` workaround is broad and bypasses other compatibility checks; prefer a current package manager for packages that rely on WASI fallback. ### pnpm **pnpm-workspace.yaml** ```yaml supportedArchitectures: cpu: - current - wasm32 ``` ### npm npm supports a target CPU flag in current releases: ```sh npm install --cpu=wasm32 ``` After installation, verify the package rather than assuming the setting was honored: ```sh npm ls @scope/my-addon-wasm32-wasi ``` ## Package and publish WASI WASI uses the same separate-package release flow as native targets: 1. Include `wasm32-wasip1-threads` in `napi.targets`. 2. Build and test it in its own CI job. 3. Run `napi create-npm-dirs`; the generated WASI package gets `cpu: ["wasm32"]`, a minimum Node engine compatible with the loader, and its emnapi/runtime dependencies. 4. Download all target artifacts and run `napi artifacts`. This copies the WASM module, Node/browser loaders, and both workers into the WASI package. 5. Run Node tests with `NAPI_RS_FORCE_WASI=error` and browser tests with cross-origin isolation. 6. Follow the normal [release guide](/docs/deep-dive/release). Inspect the final `npm pack --dry-run` output for the root package and the WASI platform package. The latter must contain the `.wasm`, `.wasi.cjs`, `.wasi-browser.js`, and worker files. ## Build C/C++ dependencies If the dependency tree compiles C or C++, install [`wasi-sdk`](https://github.com/WebAssembly/wasi-sdk/releases) and set `WASI_SDK_PATH` to the extracted SDK root: ```sh export WASI_SDK_PATH=/absolute/path/to/wasi-sdk test -x "$WASI_SDK_PATH/bin/clang" test -x "$WASI_SDK_PATH/bin/wasm-ld" napi build --platform --release --target wasm32-wasip1-threads ``` The CLI always points Cargo's WASI linker variables at `$WASI_SDK_PATH/bin/wasm-ld`. For the C/C++ toolchain variables (`TARGET_CC`, `TARGET_CXX`, `TARGET_AR`, `TARGET_RANLIB`, `TARGET_CFLAGS`, `TARGET_CXXFLAGS`, and `TARGET_LDFLAGS`), an existing environment value wins; the CLI fills only unset values. A dependency may still be incompatible when it assumes POSIX APIs that WASI does not provide; a successful native build is not evidence that the same dependency supports WASI. ## Runtime support matrix | Host | Status and constraints | | --- | --- | | Node.js | Generated `.wasi.cjs` path; platform package requires Node 14 or newer and uses Node WASI/worker APIs | | Cross-origin-isolated browser | Generated ESM + module worker path; requires shared memory, top-level await, and correct asset serving | | Browser without isolation | Unsupported for the threads target because shared memory is unavailable | | Bun and Deno | Do not claim support without a runtime test; Node-compatible WASI loading currently has an open incompatibility report | | Edge/serverless isolates | Host-specific; many do not expose Node WASI, filesystem, or worker APIs expected by the generated loader | The Bun/Deno limitation is tracked in [napi-rs#2965](https://github.com/napi-rs/napi-rs/issues/2965). It is a runtime compatibility gap, not something package installation instructions can fix. ## Test checklist Before publishing a WASI target, test: - one plain Node import with `NAPI_RS_FORCE_WASI=error`; - errors and rejected async operations, not only successful functions; - process exit after threads and async work complete; - the final separately packed/installed WASI npm package; - a production-style browser server with COOP/COEP headers; - worker startup, multiple concurrent calls, and worker error forwarding; - browser memory growth and out-of-memory behavior under representative input; - memfs behavior when filesystem APIs are part of the public contract; - every non-Node runtime you list as supported. For failure signatures and exact probes, see [Troubleshooting: WASI failures](/docs/more/troubleshooting#wasi-failures). --- # New project `napi new` creates a project from a maintained external template, renames the package and Rust crate, filters targets and CI jobs, writes a Node.js engine range derived from the selected Node-API level, and applies the license and type-generation settings. ::: warning Git must be installed and GitHub must be reachable. The destination must not be an existing file or a non-empty directory. ::: ## Usage The CLI is interactive by default: ```sh napi new ``` The path may be omitted; the first prompt then asks for it. Use `--no-interactive` for automation: ```sh napi new cool \ --name @scope/cool \ --min-node-api 8 \ --targets x86_64-unknown-linux-gnu \ --targets aarch64-apple-darwin \ --no-interactive ``` Programmatic calls never prompt. Supply `path` and any non-default values explicitly: ```ts import { NapiCli } from '@napi-rs/cli' await new NapiCli().new({ path: 'cool', name: '@scope/cool', packageManager: 'pnpm', targets: ['x86_64-unknown-linux-gnu', 'aarch64-apple-darwin'], }) ``` ## Interactive prompts When `--interactive` is enabled, the command asks for the package name, minimum Node-API level, targets, license, TypeScript declaration generation, and GitHub Actions. Pass `--package-manager pnpm` before the prompts if you want the pnpm template; package manager is not an interactive question. ## Options Boolean options accept the `--no-` prefix, such as `--no-interactive`, `--no-enable-type-def`, and `--no-enable-github-actions`. | Option | CLI syntax | Type | Required | Default | Description | | ---------------------- | ----------------------------------------- | -------------- | :--------------------------------------: | -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `path` | `` | `string` | Yes for non-interactive/programmatic use | | Empty destination directory, resolved from the current working directory. | | `interactive` | `--interactive,-i` | `boolean` | No | `true` | Ask for project information. This is a CLI-only option; use `--no-interactive` in automation. | | `name` | `--name,-n` | `string` | No | destination directory name | `package.json` package name. Scoped names are recommended for publishing. | | `minNodeApiVersion` | `--min-node-api,-v` | `number` | No | `4` | Minimum Node-API level used for both the generated package's Node.js engine requirement and the `napi` dependency feature. | | `packageManager` | `--package-manager` | `yarn \| pnpm` | No | `yarn` | Select the maintained Yarn or pnpm template. The selected template pins its own package-manager version. | | `license` | `--license,-l` | `string` | No | `MIT` | License written to `package.json`. | | `targets` | `--targets,-t` | `string[]` | No | `[]` | Target triples to retain. Repeat the flag for multiple targets. | | `enableDefaultTargets` | `--enable-default-targets` | `boolean` | No | `true` | When non-interactive `targets` is empty, use the default target set. | | `enableAllTargets` | `--enable-all-targets` | `boolean` | No | `false` | Select every target accepted by the CLI. This does not create missing template CI jobs. | | `enableTypeDef` | `--enable-type-def` | `boolean` | No | `true` | Keep the `type-def` feature and generated TypeScript declarations. | | `enableGithubActions` | `--enable-github-actions` | `boolean` | No | `true` | Keep and filter the template's GitHub Actions workflow. | | `testFramework` | `--test-framework` | `string` | No | `ava` | Test framework request. Only AVA is currently implemented by the templates. | | `dryRun` | `--dry-run` | `boolean` | No | `false` | Validate the options, Git availability, and destination without cloning or writing the project. | `napi new` writes the selected `napiN` feature into `Cargo.toml` and the corresponding Node.js range into `package.json#engines.node`. Other Cargo features can imply a higher Node-API level, so review the final feature set when you add async or other optional integrations. ## Templates and cache The only supported template selections are: | Value | Repository | | ------ | ----------------------------------------------------------------------------------- | | `yarn` | [`napi-rs/package-template`](https://github.com/napi-rs/package-template) | | `pnpm` | [`napi-rs/package-template-pnpm`](https://github.com/napi-rs/package-template-pnpm) | The CLI caches them under `~/.napi-rs/template//repo`. On a later run it fetches the repository and resets the cache to `origin/main` before copying it. The template's `.git` directory is not copied into your new project. ## Target selection is not a support guarantee The prompt lists every target triple understood by the CLI. The generated workflow can only retain matrix rows that already exist in the selected template, and the generated `napi.targets` list can only retain targets already present in that template's package config. An additional accepted target may therefore require manual config, npm-directory, CI, and runtime-test work. Read [Support and compatibility](/docs/more/support-compatibility) before using `--enable-all-targets`, and follow [Add a target to an existing project](/docs/cross-build#add-a-target-to-an-existing-project) for the complete workflow. --- # Rename project Rename the **NAPI-RS** project ## When you need this `napi rename` renames a project created by [`napi new`](./new) or cloned from a package template. It updates the package name, binary name, repository URL, and related fields across `package.json`, Cargo.toml, the GitHub Actions workflow, and the generated binding names, so they stay consistent. Run it once after scaffolding, before your first release; after that, the release pipeline (`napi build` → `napi artifacts` → `napi pre-publish`, described in [Release native packages](/docs/deep-dive/release)) uses the new names everywhere. ## Usage ```sh # CLI napi rename [--options] ``` ```typescript // Programmatically import { NapiCli } from '@napi-rs/cli' new NapiCli().rename({ // options }) ``` ## Examples Rename a freshly created project before publishing it under your own scope: ```sh napi rename \ --name @your-scope/cool \ --binary-name cool \ --repository https://github.com/your-name/cool.git ``` Change only the binary name (the `*.node` file names), leaving the package name untouched: ```sh napi rename --binary-name cool-core ``` Rename a project whose crate is not at the repository root: ```sh napi rename --name @your-scope/cool --manifest-path ./crates/cool/Cargo.toml ``` ## Options | Options | CLI Options | type | required | default | description | | --------------- | ------------------- | ------ | -------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | | --help,-h | | | | get help | | cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path | | configPath | --config-path,-c | string | false | | Path to napi config json file | | packageJsonPath | --package-json-path | string | false | package.json | Path to package.json | | npmDir | --npm-dir | string | false | npm | Path to the folder where the npm packages put | | name | --name,-n | string | false | | The new name of the project | | binaryName | --binary-name,-b | string | false | | The new binary name `*.node` files | | packageName | --package-name | string | false | | The new package name of the project | | manifestPath | --manifest-path | string | false | Cargo.toml | Path to Cargo.toml | | repository | --repository | string | false | | The new repository of the project | | description | --description | string | false | | The new description of the project | --- # Build Build the NAPI-RS project ## Usage ```sh # CLI napi build [--options] ``` ```typescript // Programmatically import { NapiCli } from '@napi-rs/cli' new NapiCli().build({ // options }) ``` ## Options | Options | CLI Options | type | required | default | description | | ----------------- | --------------------- | -------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | --help,-h | | | | get help | | target | --target,-t | string | false | | Build for the target triple, bypassed to cargo build --target | | cwd | --cwd | string | false | | The working directory of where napi command will be executed in, all other paths options are relative to this path | | manifestPath | --manifest-path | string | false | | Path to Cargo.toml | | configPath | --config-path,-c | string | false | | Path to napi config json file | | packageJsonPath | --package-json-path | string | false | | Path to package.json | | targetDir | --target-dir | string | false | | Directory for all crate generated artifacts, see cargo build --target-dir | | outputDir | --output-dir,-o | string | false | | Path to where all the built files would be put. Default to the crate folder | | platform | --platform | boolean | false | | Add platform triple to the generated nodejs binding file, eg: [name].linux-x64-gnu.node | | jsPackageName | --js-package-name | string | false | | Package name in generated js binding file. Only works with --platform flag | | constEnum | --const-enum | boolean | false | true | Generate TypeScript `const enum` declarations. Use --no-const-enum to emit regular/type-only forms. | | runtimeStringEnum | --runtime-string-enum | boolean | false | false | With --no-const-enum, emit #[napi(string_enum)] as runtime enums instead of type-only string unions. It has no effect while const enums are enabled. | | jsBinding | --js | string | false | | Path and filename of generated JS binding file. Only works with --platform flag. Relative to --output-dir. | | noJsBinding | --no-js | boolean | false | | Whether to disable the generation JS binding file. Only works with --platform flag. | | dts | --dts | string | false | | Path and filename of generated type def file. Relative to --output-dir | | dtsHeader | --dts-header | string | false | | Custom file header for generated type def file. Only works when typedef feature enabled. | | noDtsHeader | --no-dts-header | boolean | false | | Whether to disable the default file header for generated type def file. Only works when typedef feature enabled. | | dtsCache | --dts-cache | boolean | false | true | Whether to enable the dts cache, default to true | | esm | --esm | boolean | false | | Whether to emit an ESM JS binding file instead of CJS format. Only works with --platform flag. | | pipe | --pipe | string | false | | Pipe every generated output file to the given command, e.g. napi build --pipe "npx prettier --write" | | strip | --strip,-s | boolean | false | | Whether strip the library to achieve the minimum file size | | release | --release,-r | boolean | false | | Build in release mode | | verbose | --verbose,-v | boolean | false | | Verbosely log build command trace | | bin | --bin | string | false | | Build only the specified binary | | package | --package,-p | string | false | | Build the specified library or the one at cwd | | profile | --profile | string | false | | Build artifacts with the specified profile | | crossCompile | --cross-compile,-x | boolean | false | | [experimental] cross-compile by swapping the cargo subcommand: Windows MSVC targets from a non-Windows host use cargo-xwin; Windows GNU targets are rejected. Every non-Windows target uses cargo-zigbuild (requires zig on PATH). The subcommand is auto-installed on first use. Cannot combine with either other cross flag or --watch. | | useCross | --use-cross | boolean | false | | [experimental] legacy, not recommended: build inside a Docker/Podman container via cross (cross-rs); prefer --use-napi-cross or --cross-compile. Requires cross and a running container engine. Cannot combine with either other cross flag or --watch. | | useNapiCross | --use-napi-cross | boolean | false | | [experimental] download a gcc cross toolchain from npm (@napi-rs/cross-toolchain) and set linker/CC env vars. Linux glibc targets only: x64, arm64, armv7, ppc64le, s390x (glibc 2.17), on a Linux x64/arm64 host. Unsupported targets/hosts and setup failures are errors. Cannot combine with either other cross flag. | | watch | --watch,-w | boolean | false | | watch the crate changes and build continuously with cargo-watch crates | | features | --features,-F | string[] | false | | Space-separated list of features to activate | | allFeatures | --all-features | boolean | false | | Activate all available features | | noDefaultFeatures | --no-default-features | boolean | false | | Do not activate the default feature | ## Cross-compilation flags `napi build` has three cross-compilation flags: `--use-napi-cross`, `--cross-compile` (`-x`) and `--use-cross`. All three are experimental: behavior may change between minor releases. The recommended flags are `--use-napi-cross` for Linux glibc targets on a Linux x64/arm64 host, and `--cross-compile` (`-x`) for Windows MSVC targets from a non-Windows host and for musl targets. `-x` is also the fallback for glibc, macOS and FreeBSD targets when the preferred setup is not available on your host. Android, WASI and OpenHarmony targets need no cross flag at all: the CLI configures their toolchains from platform environment variables. `--use-cross` is legacy and not recommended, and the Docker-image based builds are deprecated. This page is a reference for what each flag does. To pick the right flag for your host and target, see [Cross build](../cross-build). For Alpine/musl specifics, see the [FAQ](../more/faq#build-for-linux-alpine). Each flag changes exactly one thing about the build: | Flag | What it changes | Resulting command | | ------------------------ | --------------------------------------------------- | -------------------------------------------------------------------------- | | _(none)_ | nothing | `cargo build --target ` | | `--use-cross` | the **binary** only | `cross build --target ` | | `--cross-compile` / `-x` | the **subcommand** only (plus two env side effects) | `cargo zigbuild --target ` or `cargo xwin build --target ` | | `--use-napi-cross` | **env vars** only (linker, CC, sysroot) | still `cargo build --target ` | ### Pick exactly one ::: warning These flags do not combine. Pick exactly one. The CLI rejects every pair before Cargo metadata, toolchain downloads, or cargo-subcommand installation. ::: | Combination | Result | | ------------------------------------------------------------------- | ----------------------------------------------------------- | | Any two of `--use-cross`, `--use-napi-cross`, and `--cross-compile` | Hard error before build-side effects. | | `--watch` + `--cross-compile` | Hard error; cargo-watch supports the plain Cargo flow only. | | `--watch` + `--use-cross` | Hard error; cargo-watch supports the plain Cargo flow only. | ### Prerequisites | Flag | Installed for you | You must provide | | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `-x`, non-Windows target (cargo-zigbuild) | cargo-zigbuild via `cargo install` on first use (which can be slow). | `zig` on `PATH`. The CLI never installs or checks zig; cargo-zigbuild errors if it is missing. | | `-x`, Windows target (cargo-xwin) | cargo-xwin via `cargo install`, on first use. It downloads the Microsoft CRT and Windows SDK itself (the Microsoft license applies). | `clang` (e.g. `apt install clang` / `brew install llvm`) — zig is **not** used on this path. For dependencies that compile assembly, also the LLVM tools (`rustup component add llvm-tools`). The CLI checks none of these. | | `--use-cross` | Nothing. | The `cross` binary (a missing binary fails with `spawn cross ENOENT`), plus a running Docker >= 20.10 or Podman >= 3.4. | | `--use-napi-cross` | The gcc toolchain, downloaded automatically from npm (@napi-rs/cross-toolchain) and cached under `~/.napi-rs/cross-toolchain`. | `npm` on `PATH`, and a Linux x64 or arm64 host. The CLI validates both host and target before build-side effects; download, extraction, and setup failures stop the build with an error. | ### Examples One copy-paste command per flag: ```sh # Linux glibc targets, from a Linux x64/arm64 host napi build --release --target aarch64-unknown-linux-gnu --use-napi-cross # Windows MSVC from a macOS/Linux host, musl, or the zigbuild fallback cases napi build --release --target x86_64-unknown-linux-musl --cross-compile # Legacy container build (not recommended) napi build --release --target x86_64-unknown-linux-gnu --use-cross ``` ## What `napi build` runs `napi build` is a wrapper around one spawned command plus a set of environment variables. This section spells both out. ### The command | Mode | Spawned command | | -------------------------------------------------------------- | ------------------------------------------------------------------------ | | No cross flag | `cargo build --target ` | | `--use-napi-cross` | `cargo build --target ` (only the env changes) | | `--use-cross` | `cross build --target ` (same args, same host-computed env) | | `--cross-compile`, target is Windows MSVC, host is not Windows | `cargo xwin build --target ` (`XWIN_ARCH=x86` is set for `i686`) | | `--cross-compile`, any other target | `cargo zigbuild --target ` | | `--cross-compile`, target is Windows, host is Windows | warns, then plain `cargo build --target ` | `--cross-compile` picks cargo-xwin by the target's **platform**, but accepts only Windows MSVC targets on a non-Windows host. Windows GNU and gnullvm targets are rejected before build-side effects because cargo-xwin cannot provide their toolchains. Build those without `-x`, with mingw-w64 or llvm-mingw respectively; see the windows-gnu note in [Recipes per target](../cross-build#recipes-per-target). Every non-Windows target goes through cargo-zigbuild—the CLI keeps no list of zigbuild-supported targets and uses it even when the target matches the host. If the `CARGO` environment variable is set, the CLI spawns that binary instead in every mode. With `--use-cross` or `--cross-compile`, it warns that the override replaces the binary required by the selected mechanism. ### RUSTFLAGS - Any `*musl*` target: the CLI appends `-C target-feature=-crt-static` to `RUSTFLAGS`. - `--strip`: the CLI appends `-C link-arg=-s`. Both are applied through the exported `RUSTFLAGS` environment variable. Cargo gives the env var precedence over `rustflags` in `.cargo/config.toml`, so once the CLI exports it, the rustflags from your `.cargo/config.toml` are ignored. If you need extra flags, add them to the `RUSTFLAGS` env var, not to `.cargo/config.toml`. ### C compiler When both `TARGET_CC` and `CC` are set, `TARGET_CC` wins (since `@napi-rs/cli` 3.0.0-alpha.92). ### Default linkers for less common targets Without `--cross-compile`, these targets get `CARGO_TARGET__LINKER` pointed at a cross gcc that **you must install yourself**. The CLI sets the env var without checking it: if the binary is missing, the build fails at link time. Your own `CARGO_TARGET__LINKER` env var always wins. With `--cross-compile` this table is skipped — linking is delegated to zig or xwin. | Target | Linker set by the CLI | | ------------------------------- | ------------------------------ | | `aarch64-unknown-linux-musl` | `aarch64-linux-musl-gcc` | | `loongarch64-unknown-linux-gnu` | `loongarch64-linux-gnu-gcc-13` | | `riscv64gc-unknown-linux-gnu` | `riscv64-linux-gnu-gcc` | | `powerpc64le-unknown-linux-gnu` | `powerpc64le-linux-gnu-gcc` | | `s390x-unknown-linux-gnu` | `s390x-linux-gnu-gcc` | ### Android, WASI and OpenHarmony These targets get their toolchain env from the CLI whenever the target platform matches — with or without any cross flag — but each platform has its own conditions: - **Android**: on a non-Android host, linker/CC/AR env is built from `ANDROID_NDK_LATEST_HOME`. If the variable is missing, the CLI stops before Cargo instead of exporting invalid tool paths. The whole setup, including the variable requirement, is skipped when the host itself is Android. - **WASI**: `EMNAPI_LINK_DIR` is always set to the bundled emnapi (the CLI errors if the `emnapi`, `@emnapi/core` and `@emnapi/runtime` versions mismatch). The wasi-sdk linker/CC env is set only when `WASI_SDK_PATH` is set **and** the directory exists — otherwise linking falls back to cargo's default, rustup's bundled `rust-lld`. - **OpenHarmony**: env is built from `$OHOS_SDK_PATH/native`, or from `OHOS_SDK_NATIVE` when `OHOS_SDK_PATH` is unset. If neither is set, the CLI warns and sets nothing. ## Passing flags to Cargo Flags after `--` will be passed through to the cargo build command. For example: ```sh napi build -- --locked ``` This will pass the `--locked` flag to `cargo build`, resulting in `cargo build --locked`. ## Build a Cargo executable `--bin ` selects a Cargo binary target, including in a package that also contains a `cdylib`. The CLI passes `--bin ` to Cargo and copies the resulting executable to `--output-dir` using its normal name (`.exe` on Windows): **Cargo.toml** ```toml [[bin]] name = "my-tool" path = "src/main.rs" ``` ```sh napi build --bin my-tool --release --output-dir dist ./dist/my-tool ``` This mode does not produce a `.node` addon, JavaScript loader, or TypeScript declarations; post-build binding generation runs only for a `cdylib`. Without `--bin`, a package's `cdylib` remains the preferred addon target. In a workspace, combine `--package ` and `--bin ` when the binary is not in the package selected by `--manifest-path`. ## Note for `--js-package-name` In the [Deep dive section](../introduction/getting-started#deep-dive), we recommended you publish your package under [`npm scope`](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/). But if you are migrating an existed package which is not under the [`npm scope`](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/) or you just don't want your package under an [`npm scope`](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/) , you may trigger the [_npm spam detection_](https://stackoverflow.com/questions/48668389/npm-publish-failed-with-package-name-triggered-spam-detection/54135900#54135900) while publishing the native platform packages. Like `snappy-darwin-x64` `snappy-darwin-arm64` etc... In this case, you can publish your platform packages under [`npm scope`](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/) to avoid the [_npm spam detection_](https://stackoverflow.com/questions/48668389/npm-publish-failed-with-package-name-triggered-spam-detection/54135900#54135900). And your users don't need to care about the platform native packages in `optionalDependencies`. Like [`snappy`](https://github.com/Brooooooklyn/snappy/), users only need to install it via `yarn add snappy`. But platform native packages are under `@napi-rs` scope: ```json { "name": "snappy", "version": "7.0.0", "optionalDependencies": { "@napi-rs/snappy-win32-x64-msvc": "7.0.0", "@napi-rs/snappy-darwin-x64": "7.0.0", "@napi-rs/snappy-linux-x64-gnu": "7.0.0", "@napi-rs/snappy-linux-x64-musl": "7.0.0", "@napi-rs/snappy-linux-arm64-gnu": "7.0.0", "@napi-rs/snappy-win32-ia32-msvc": "7.0.0", "@napi-rs/snappy-linux-arm-gnueabihf": "7.0.0", "@napi-rs/snappy-darwin-arm64": "7.0.0", "@napi-rs/snappy-android-arm64": "7.0.0", "@napi-rs/snappy-android-arm-eabi": "7.0.0", "@napi-rs/snappy-freebsd-x64": "7.0.0", "@napi-rs/snappy-linux-arm64-musl": "7.0.0", "@napi-rs/snappy-win32-arm64-msvc": "7.0.0" } } ``` For this case, `@napi-rs/cli` provides the `--js-package-name` to override generated package loading logic. For example in `snappy` we have package.json like this: ```json { "name": "snappy", "version": "7.0.0", "napi": { "binaryName": "snappy" } } ``` Without the `--js-package-name` flag, `@napi-rs/cli` will generate JavaScript binding to load platform native packages for you: **index.js** ```js {10,22} switch (platform) { case 'darwin': switch (arch) { case 'x64': localFileExisted = existsSync(join(__dirname, 'snappy.darwin-x64.node')) try { if (localFileExisted) { nativeBinding = require('./snappy.darwin-x64.node') } else { nativeBinding = require('snappy-darwin-x64') } } catch (e) { loadError = e } break case 'arm64': localFileExisted = existsSync(join(__dirname, 'snappy.darwin-arm64.node')) try { if (localFileExisted) { nativeBinding = require('./snappy.darwin-arm64.node') } else { nativeBinding = require('snappy-darwin-arm64') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on macOS: ${arch}`) } break ... } ``` This isn't what we want. So build it with `--js-package-name` to override the `package name` in generated JavaScript binding file: `napi build --release --platform --js-package-name @napi-rs/snappy`. Then the generated JavaScript file will become: **index.js** ```js {10,22} switch (platform) { case 'darwin': switch (arch) { case 'x64': localFileExisted = existsSync(join(__dirname, 'snappy.darwin-x64.node')) try { if (localFileExisted) { nativeBinding = require('./snappy.darwin-x64.node') } else { nativeBinding = require('@napi-rs/snappy-darwin-x64') } } catch (e) { loadError = e } break case 'arm64': localFileExisted = existsSync(join(__dirname, 'snappy.darwin-arm64.node')) try { if (localFileExisted) { nativeBinding = require('./snappy.darwin-arm64.node') } else { nativeBinding = require('@napi-rs/snappy-darwin-arm64') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on macOS: ${arch}`) } break ... } ``` --- # NAPI Config Put the configuration under the `napi` key in `package.json`: **package.json** ```json { "name": "@scope/addon", "napi": { "binaryName": "addon", "targets": ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"] } } ``` Commands that expose `--config-path` can instead read a standalone JSON file. When both sources are present, the standalone config takes precedence. All user-supplied fields are optional. ## Schema ```ts { napi?: { binaryName?: string targets?: string[] packageName?: string npmClient?: string constEnum?: boolean runtimeStringEnum?: boolean dtsHeader?: string dtsHeaderFile?: string wasm?: { initialMemory?: number maximumMemory?: number browser?: { fs?: boolean asyncInit?: boolean buffer?: boolean errorEvent?: boolean } } } } ``` ## Fields and effective defaults | Field | Default | Description | | ------------------------- | :--------------------------------------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `binaryName` | `index` | Base name of generated native and WASI files. A platform build produces a name such as `index.win32-x64-msvc.node`. | | `targets` | `[]` | Target triples the project packages and publishes. This is not a multi-target build command. | | `packageName` | root `package.json` `name` | Package name used by generated loaders and per-platform package names. Override it when the JavaScript package name differs from the root package metadata; see [Build: JS package name](./build#note-for---js-package-name). | | `npmClient` | `npm` | Command used for npm operations such as publishing each platform package. | | `constEnum` | `true` | Generate TypeScript `const enum` declarations. The effective type-generation default is `true` when neither config nor CLI overrides it. | | `runtimeStringEnum` | `false` | With `constEnum: false`, emit `#[napi(string_enum)]` as a runtime `enum` instead of a type-only string union. It has no effect while `constEnum` is `true`. | | `dtsHeader` | `undefined` | String used as the header of the generated declaration file. It replaces the default header entirely; see [Declaration file header](#declaration-file-header). | | `dtsHeaderFile` | `undefined` | Path, relative to the command's working directory, to a file whose content is used as the declaration file header. It takes precedence over `dtsHeader`. | | `wasm.initialMemory` | `4000` pages | Initial shared WebAssembly memory, approximately 250 MiB. | | `wasm.maximumMemory` | `65536` pages | Maximum shared WebAssembly memory, 4 GiB. | | `wasm.browser.fs` | `false` | Include the in-memory filesystem and filesystem proxy in browser WASI bindings. | | `wasm.browser.asyncInit` | `false` | Use emnapi's asynchronous module-instantiation path for the browser binding. | | `wasm.browser.buffer` | `false` | Import `Buffer` and inject it into the emnapi context used by the browser binding. | | `wasm.browser.errorEvent` | `false` | Forward worker failures to a browser `napi-rs-worker-error` `CustomEvent`, including captured worker error output. | One WebAssembly memory page is 64 KiB. The memory settings are written into the generated Node and browser WASI loaders; they are not Cargo memory limits. ::: info `runtimeStringEnum: true` requires `constEnum: false`. The equivalent build flags are `--runtime-string-enum --no-const-enum`. ::: ## Declaration file header When NAPI-RS generates `index.d.ts`, it starts the file with a default header: ```typescript /* auto-generated by NAPI-RS */ /* eslint-disable */ ``` You can replace this header with your own TypeScript types, imports, license comments, or lint directives. A custom header **replaces** the default header entirely; include the auto-generated comment and the eslint directive in your own header if you want to keep them. There are three ways to set the header, plus one way to disable it: | Method | Location | Best for | | ----------------- | ----------------------------------- | ---------------------------- | | `dtsHeaderFile` | napi config | Complex headers with imports | | `dtsHeader` | napi config | Simple single-line additions | | `--dts-header` | CLI flag of [`napi build`](./build) | CI/CD overrides | | `--no-dts-header` | CLI flag of [`napi build`](./build) | Disable the header entirely | When several of them are set, NAPI-RS resolves the header in this order: | Priority | Source | Description | | :------: | -------------------- | ---------------------------------------------------------------------------------------------------------------- | | 1 | Header file | `dtsHeaderFile` (or the programmatic `dtsHeaderFile` option). A header file always wins over inline header text. | | 2 | `--dts-header` (CLI) | Overrides the inline `dtsHeader` config value, but not a header file. | | 3 | `dtsHeader` (config) | Inline string in the `napi` config. | | 4 | Default header | Used when nothing else is specified. | `--no-dts-header` skips header resolution and generates the `.d.ts` without any header. For example, with a complex header in `dts-header.d.ts`: **dts-header.d.ts** ```typescript /* auto-generated by NAPI-RS */ /* eslint-disable */ import type { ReadableStream } from 'node:stream/web' type MaybePromise = T | Promise ``` **package.json** ```json { "napi": { "dtsHeaderFile": "./dts-header.d.ts" } } ``` See [Types overwrite](/docs/concepts/types-overwrite) for the attribute-level TypeScript overrides (`ts_args_type`, `ts_return_type`, and friends), which change individual declarations rather than the file header. ## What `targets` controls `targets` drives packaging: - [`napi create-npm-dirs`](./create-npm-dirs) creates one npm directory per target. - [`napi artifacts`](./artifacts) maps built files into those directories. - [`napi pre-publish`](./pre-publish) versions and publishes those packages. - A WASI target enables generation of `.wasi.cjs` and the related browser and worker files. Setting `targets` does **not** make `napi build` compile each entry. Every build invocation produces one target selected by `--target`, `CARGO_BUILD_TARGET`, or the host default. Likewise, the cross-compilation flags (`--use-napi-cross`, `--cross-compile`, and `--use-cross`) have no config equivalent. The target list also does not create arbitrary CI jobs. `napi new` filters the jobs already present in its selected template. If you add another accepted target, add its build job and verify its runtime separately. See [Support and compatibility](/docs/more/support-compatibility) and [Cross build](../cross-build). ## Deprecated v2 fields The CLI still reads these fields for compatibility, but new projects should not use them: | Deprecated | Replacement | | ----------------------------------------------------- | ----------------- | | `napi.name` | `napi.binaryName` | | `napi.triples.defaults` and `napi.triples.additional` | `napi.targets` | The old nested `napi.package.name` field is **not** read by the v3 config normalizer. Move that value explicitly to `napi.packageName`. ## What is a target triple? See [Rust platform support](https://doc.rust-lang.org/nightly/rustc/platform-support.html) and [LLVM cross-compilation](https://clang.llvm.org/docs/CrossCompilation.html#target-triple). A target triple describes the architecture, vendor, operating system, and ABI of the artifact, for example: ```text x86_64-unknown-linux-gnu └─ arch └ vendor └ system └ ABI ``` Once you know which triples you intend to ship, use [Cross build](../cross-build) to choose and verify the build mechanism for each one. --- # Programmatic API The `@napi-rs/cli` package exports programmatic APIs that allow you to customize your build workflow beyond what the CLI commands offer. This is useful when you need to: - Post-process build outputs (format, transform, or validate generated files) - Integrate with custom build systems like Bazel - Generate TypeScript definitions separately from the Rust compilation - Build automation scripts with full control over the build process ## Post-Processing Build Outputs The most common use case is running custom post-processing on the generated JavaScript and TypeScript files. Here's an example using oxfmt to format the output files: **build.ts** ```ts import { readFile, writeFile } from 'node:fs/promises' import { NapiCli, createBuildCommand } from '@napi-rs/cli' import { format, type FormatOptions } from 'oxfmt' import oxfmtConfig from './.oxfmtrc.json' with { type: 'json' } const buildCommand = createBuildCommand(process.argv.slice(2)) const cli = new NapiCli() const buildOptions = { ...buildCommand.getOptions(), cargoOptions: buildCommand.cargoOptions, } const { task } = await cli.build(buildOptions) const outputs = await task for (const output of outputs) { if (output.kind === 'js' || output.kind === 'dts') { const { code } = await format( output.path, await readFile(output.path, 'utf-8'), oxfmtConfig as FormatOptions, ) await writeFile(output.path, code) } } ``` Run this script with the same arguments you would pass to napi build, including Cargo arguments after `--`: ```sh oxnode ./build.ts --release --platform ``` ### How It Works 1. `createBuildCommand(args)` parses CLI arguments and returns a `BuildCommand` instance 2. `buildCommand.getOptions()` extracts the named options; `cargoOptions` carries the trailing arguments after `--` 3. `cli.build(options)` starts the build and returns `{ task, abort }` 4. `await task` waits for completion and returns an array of `Output` objects ### Output Types Each item in the outputs array has this structure: ```ts type OutputKind = 'js' | 'dts' | 'node' | 'exe' | 'wasm' type Output = { kind: OutputKind path: string // Absolute path to the output file } ``` | Kind | Description | | -------------------------------------- | ------------------------------------------------------------------ | | node | Native Node.js addon (.node file) | | js | JavaScript binding file | | dts | TypeScript definition file | | exe | Executable binary | | wasm | WebAssembly module | ## Standalone Types/JS Generation ::: info This is useful for build systems like Bazel that handle Rust compilation separately and only need the TypeScript type generation step. ::: If you compile Rust code outside of `@napi-rs/cli` (e.g., using Bazel's `rust_shared_library`), you can still generate TypeScript definitions using the `generateTypeDef` and `writeJsBinding` APIs: **generate-types.ts** ```ts import { spawn } from 'node:child_process' import { mkdir, writeFile, copyFile, rm } from 'node:fs/promises' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { generateTypeDef, writeJsBinding, parseTriple } from '@napi-rs/cli' import pkg from './package.json' with { type: 'json' } const currentTarget = 'x86_64-unknown-linux-gnu' const currentDir = dirname(fileURLToPath(import.meta.url)) const typeDefDir = join(currentDir, 'target', 'napi-rs', 'YOUR_PKG_NAME') const triple = parseTriple(currentTarget) const binaryName = pkg.napi.binaryName const bindingName = `${binaryName}.${triple.platformArchABI}.node` await mkdir(typeDefDir, { recursive: true }) const childProcess = spawn( 'cargo', ['build', '--release', '--target', currentTarget], { stdio: 'pipe', env: { ...process.env, NAPI_TYPE_DEF_TMP_FOLDER: typeDefDir, }, }, ) childProcess.stdout.on('data', (data) => { console.log(data.toString()) }) childProcess.stderr.on('data', (data) => { console.error(data.toString()) }) await new Promise((resolve, reject) => { childProcess.on('error', (error) => { reject(error) }) childProcess.on('close', (code) => { if (code === 0) { resolve(true) } else { reject(new Error(`cargo build --release failed with code ${code}`)) } }) }) // Remove an old loaded binding before replacing it. Overwriting it in place // can cause crashes on platforms such as macOS. await rm(join(currentDir, bindingName)).catch(() => { // ignore a missing old binding }) await copyFile( join(currentDir, 'target', currentTarget, 'release', 'libfoo.so'), join(currentDir, bindingName), ) const { dts, exports } = await generateTypeDef({ typeDefDir, cwd: process.cwd(), }) await writeFile(join(currentDir, 'customized.d.ts'), dts) await writeJsBinding({ jsBinding: 'customized.js', platform: true, binaryName, packageName: pkg.name, version: pkg.version, outputDir: currentDir, idents: exports, }) ``` ::: warning The `typeDefDir` must contain the intermediate type definition files generated by the `napi-derive` proc macro when the `type-def` feature is enabled. These files are normally created in a temporary directory during `napi build`. ::: ### Control Flow
  1. Phase 1 · Setup
  2. Read package.json for the napi config

  3. Pick the target triple

    Hardcoded to x86_64-unknown-linux-gnu in the sample above — a real build takes it from a CLI flag.

  4. parseTriple()platformArchABI

    Supplies the platform suffix for the binding filename.

  5. mkdir(typeDefDir)

    Creates the directory the intermediate type definitions land in.

  6. Phase 2 · Build
  7. spawn('cargo', ['build', '--release', '--target', currentTarget])

    With NAPI_TYPE_DEF_TMP_FOLDER: typeDefDir in env — that is what tells napi-derive where to write the type defs.

  8. Stream stdout / stderr from cargo

  9. await cargo completion

  10. rm(bindingName)

    Removes the old .node file before it is replaced.

  11. copyFile(libfoo.so → binaryName.platformArchABI.node)

  12. Phase 3 · Type generation
  13. generateTypeDef({ typeDefDir, cwd })

    Reads the newline-delimited JSON files back out of typeDefDir and returns { dts, exports }.

  14. writeFile('customized.d.ts', dts)

  15. writeJsBinding({ platform, binaryName, idents: exports, … })

    Generates the JS loader that imports the .node file.

  16. Outputs: .node · .d.ts · .js

### Key Concepts #### The `NAPI_TYPE_DEF_TMP_FOLDER` Environment Variable When you run `cargo build` with `NAPI_TYPE_DEF_TMP_FOLDER` set, the `napi-derive` proc macro writes one extensionless file per Cargo package to that directory. Each file contains one JSON object per line. This is how type information flows from Rust to TypeScript: ``` Rust Code → napi-derive macro → newline-delimited JSON → generateTypeDef() → .d.ts ``` #### Platform-Specific Binding Names The `parseTriple()` function extracts platform information from a target triple: ```ts const triple = parseTriple('x86_64-unknown-linux-gnu') // Returns: { platform: 'linux', arch: 'x64', abi: 'gnu', platformArchABI: 'linux-x64-gnu', ... } const bindingName = `mylib.${triple.platformArchABI}.node` // Result: 'mylib.linux-x64-gnu.node' ``` #### Removing Old Binding Files On macOS/Linux, copying a new `.node` file over an existing one without first removing it can cause segmentation faults. Always remove the old file first: ```ts await rm(join(currentDir, bindingName)).catch(() => { // ignore error if file doesn't exist }) await copyFile(sourceLib, join(currentDir, bindingName)) ``` ### GenerateTypeDefOptions | Option | Type | Required | Default | Description | | ------------------- | ------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------- | | typeDefDir | string | Yes | | Directory containing intermediate type def files | | cwd | string | Yes | | Working directory for resolving relative paths | | noDtsHeader | boolean | No | false | Skip the default file header | | dtsHeader | string | No | | Custom header string for the .d.ts file; used when neither header-file option is set | | dtsHeaderFile | string | No | | Header file resolved from `cwd`; takes precedence over every other custom header option | | configDtsHeader | string | No | | Header from config (lower priority than dtsHeader) | | configDtsHeaderFile | string | No | | Header file from config; lower priority than `dtsHeaderFile`, but higher than inline header strings | | constEnum | boolean | No | true | Generate const enum instead of regular enum | | runtimeStringEnum | boolean | No | false | With `constEnum: false`, generate runtime enums for `#[napi(string_enum)]`; otherwise generate type-only string unions | ### WriteJsBindingOptions | Option | Type | Required | Default | Description | | ----------- | -------- | -------- | ---------- | -------------------------------------------------------------------------- | | platform | boolean | No | false | Required to generate JS binding; adds platform triple | | noJsBinding | boolean | No | false | Skip JS binding generation | | idents | string[] | Yes | | Exported identifiers from generateTypeDef | | jsBinding | string | No | 'index.js' | Custom filename for the JS binding | | esm | boolean | No | false | Generate ESM format instead of CommonJS | | binaryName | string | Yes | | Name of the native binary | | packageName | string | Yes | | Package name for require/import statements | | version | string | Yes | | Package version | | outputDir | string | Yes | | Directory to write the JS binding file | ## Other Exported APIs ### NapiCli Class The main class for programmatic access to all CLI commands: ```ts import { NapiCli } from '@napi-rs/cli' const cli = new NapiCli() // Available methods: cli.build(options) // Build the project cli.artifacts(options) // Collect artifacts from CI cli.new(options) // Create new project cli.createNpmDirs(options) // Create npm package directories cli.prePublish(options) // Prepare for publishing cli.rename(options) // Rename project cli.universalize(options) // Create universal binaries cli.version(options) // Update versions ``` ### Command Creators Parse CLI arguments into command option objects: ```ts import { createBuildCommand, createArtifactsCommand, createCreateNpmDirsCommand, createPrePublishCommand, createRenameCommand, createUniversalizeCommand, createVersionCommand, createNewCommand, } from '@napi-rs/cli' // Parse arguments as if running `napi build --release --platform` const buildCmd = createBuildCommand(['--release', '--platform']) const options = buildCmd.getOptions() ``` ### Utility Functions ```ts import { parseTriple, readNapiConfig } from '@napi-rs/cli' // Parse target triple string const triple = parseTriple('x86_64-unknown-linux-gnu') // { platform: 'linux', arch: 'x64', abi: 'gnu', ... } // The first argument is the exact package.json path. The optional second // argument is a standalone napi config that takes precedence. const config = await readNapiConfig( '/path/to/project/package.json', '/path/to/project/napi.config.json', ) ``` ## Aborting a Build The `build()` method returns an `abort` function to cancel the build: ```ts const { task, abort } = await cli.build(options) // Handle SIGINT to abort cleanly process.on('SIGINT', () => { abort() process.exit(1) }) const outputs = await task ``` --- # Create npm directories Create npm package dirs for different platforms ## When you need this `napi create-npm-dirs` creates one `npm/` directory per entry of the `napi.targets` config, each containing a ready-to-publish `package.json` for that platform's package. Committing these directories is no longer recommended; create them in CI instead. You need this command before [`napi artifacts`](./artifacts) can copy built binaries into the platform packages, and before [`napi pre-publish`](./pre-publish) can publish them. In the release pipeline this command sits after the platform `napi build` jobs and before `napi artifacts`. See [Release native packages](/docs/deep-dive/release) for the complete pipeline. ## Usage ```sh # CLI napi create-npm-dirs [--options] ``` ```typescript // Programmatically import { NapiCli } from '@napi-rs/cli' new NapiCli().createNpmDirs({ // options }) ``` ## Examples Create the platform package directories in a CI release job: ```sh napi create-npm-dirs ``` Preview which directories and files would be created, without touching the file system: ```sh napi create-npm-dirs --dry-run ``` ## Options | Options | CLI Options | type | required | default | description | | --------------- | ------------------- | ------- | -------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | | --help,-h | | | | get help | | cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path | | configPath | --config-path,-c | string | false | | Path to napi config json file | | packageJsonPath | --package-json-path | string | false | package.json | Path to package.json | | npmDir | --npm-dir | string | false | npm | Path to the folder where the npm packages put | | dryRun | --dry-run | boolean | false | false | Dry run without touching file system | --- # Artifacts `napi artifacts` recursively finds built `.node` and `.wasm` files, validates their binary names and target suffixes, and copies them into the matching per-platform npm packages. Native files are also copied to the root package directory so the generated loader can resolve a local binding. ## Usage ```sh napi artifacts [--options] ``` ```ts import { NapiCli } from '@napi-rs/cli' await new NapiCli().artifacts({ outputDir: 'artifacts', npmDir: 'npm', }) ``` ## Options | Option | CLI syntax | Type | Required | Default | Description | | ----------------- | --------------------- | -------- | :------: | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `cwd` | `--cwd` | `string` | No | `process.cwd()` | Base directory for config, package, artifact-download, npm-package, and build-output paths. | | `configPath` | `--config-path,-c` | `string` | No | | Standalone napi config JSON file. | | `packageJsonPath` | `--package-json-path` | `string` | No | `package.json` | Root package containing the napi config. | | `outputDir` | `--output-dir,-o,-d` | `string` | No | `./artifacts` | Directory recursively searched for downloaded `.node` and `.wasm` files. This corresponds to the artifact-download path, not `npm/`. | | `npmDir` | `--npm-dir` | `string` | No | `npm` | Directory containing the generated per-platform packages. | | `buildOutputDir` | `--build-output-dir` | `string` | No | `cwd` | Directory containing generated WASI JavaScript and worker files. Relative paths resolve from `--cwd`. | There is no `--dist` option. Use `--output-dir` for downloaded build artifacts and `--npm-dir` for platform package destinations. ## CI workflow Each build job should upload files named with the configured binary name and target ABI, for example: ```text cool.darwin-arm64.node cool.linux-x64-gnu.node cool.win32-x64-msvc.node cool.wasm32-wasi.wasm ``` In the collection job, create the package directories, download all artifacts, then collect them: ```yaml - name: Create platform packages run: yarn napi create-npm-dirs - name: Download all build artifacts uses: actions/download-artifact@v8 with: path: artifacts - name: Move artifacts into packages run: yarn napi artifacts --output-dir artifacts --npm-dir npm ``` `actions/download-artifact` normally creates one nested directory per uploaded artifact. The CLI searches recursively, so both of these layouts work: ```text artifacts/ ├── bindings-aarch64-apple-darwin/ │ └── cool.darwin-arm64.node ├── bindings-x86_64-unknown-linux-gnu/ │ └── cool.linux-x64-gnu.node └── bindings-x86_64-pc-windows-msvc/ └── cool.win32-x64-msvc.node ``` After collection: ```text . ├── cool.darwin-arm64.node ├── cool.linux-x64-gnu.node ├── cool.win32-x64-msvc.node └── npm/ ├── darwin-arm64/cool.darwin-arm64.node ├── linux-x64-gnu/cool.linux-x64-gnu.node └── win32-x64-msvc/cool.win32-x64-msvc.node ``` ## Matching rules For each discovered file, the CLI: 1. Splits the final dot-delimited suffix from the file name, such as `linux-x64-gnu`. 2. Requires the remaining base name to equal `napi.binaryName`. A mismatched binary name is warned about and skipped. 3. Finds the configured target package whose directory matches that platform suffix. A file with no target package causes the command to fail, except source binaries that are intentionally combined into a configured universal binary. 4. Writes the file into that target package and the root package directory. ::: warning The command trusts the suffix in the file name. It does not inspect the native binary to prove its architecture, libc, minimum OS, or Node-API level. Test every artifact on the runtime it claims to support before publishing. ::: ## WASI artifacts When `napi.targets` contains a WASI target, `napi artifacts` also copies the generated support files into `npm/wasm32-wasi`: - `.wasi.cjs` - `.wasi-browser.js` - `wasi-worker.mjs` - `wasi-worker-browser.mjs` By default these files are read from `--cwd`. Pass `--build-output-dir` when the WASI build wrote them elsewhere; a relative value is resolved from `--cwd`. The downloaded `.wasm` file is still discovered under `--output-dir`. After collecting and verifying all targets, [`napi pre-publish`](./pre-publish) versions and publishes the platform packages. `napi pre-publish` does not copy missing artifacts for you. --- # Universalize Combine built binaries into one universal binary ## When you need this On macOS, `napi universalize` combines the separate `x86_64` (Intel) and `aarch64` (Apple Silicon) `.node` binaries into a single universal binary with `lipo`. You need it only when you ship a `darwin-universal` target: your `napi.targets` config must contain a universal-arch target for the current platform, and both per-arch binaries must already be built. It currently runs on macOS only. In the release pipeline this command sits between `napi build` (run once per macOS architecture) and [`napi artifacts`](./artifacts). See [Release native packages](/docs/deep-dive/release) for the complete pipeline. ## Usage ```sh # CLI napi universalize [--options] ``` ```typescript // Programmatically import { NapiCli } from '@napi-rs/cli' new NapiCli().universalize({ // options }) ``` ## Examples A typical macOS CI job: build both architectures, then combine them: ```sh napi build --release --target x86_64-apple-darwin napi build --release --target aarch64-apple-darwin napi universalize ``` Combine binaries that were written to a custom output directory (must match the `--output-dir` used by `napi build`): ```sh napi universalize --output-dir ./binaries ``` ## Options | Options | CLI Options | type | required | default | description | | --------------- | ------------------- | ------ | -------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | | --help,-h | | | | get help | | cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path | | configPath | --config-path,-c | string | false | | Path to napi config json file | | packageJsonPath | --package-json-path | string | false | package.json | Path to package.json | | outputDir | --output-dir,-o | string | false | ./ | Path to the folder where all built .node files put, same as --output-dir of build command | --- # Version packages Update version in created npm packages ## When you need this `napi version` copies the `version` field of your root `package.json` into the `package.json` of every per-platform package created by [`napi create-npm-dirs`](./create-npm-dirs). You need it when your release flow versions the platform packages as a separate step, for example when the `npm/` directories are committed or inspected before publishing. If you run [`napi pre-publish`](./pre-publish) directly, it already performs this version sync for you. In the release pipeline this command sits after `napi create-npm-dirs` and before `napi pre-publish`. See [Release native packages](/docs/deep-dive/release) for the complete pipeline. ## Usage ```sh # CLI napi version [--options] ``` ```typescript // Programmatically import { NapiCli } from '@napi-rs/cli' new NapiCli().version({ // options }) ``` ## Examples Sync the root version into all platform packages in the default `npm/` directory: ```sh napi version ``` A typical CI release job, after the platform binaries have been built: ```sh napi create-npm-dirs napi artifacts napi version ``` ## Options | Options | CLI Options | type | required | default | description | | --------------- | ------------------- | ------ | -------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | | --help,-h | | | | get help | | cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path | | configPath | --config-path,-c | string | false | | Path to napi config json file | | packageJsonPath | --package-json-path | string | false | package.json | Path to package.json | | npmDir | --npm-dir | string | false | npm | Path to the folder where the npm packages put | --- # Prepublish `napi pre-publish` (also available as `napi prepublish`) prepares and publishes the per-platform packages for the root package's current version. It can also create a GitHub release and upload the native binaries as release assets. ::: warning This command has network and registry side effects by default. It is not a packaging preview: it can publish multiple immutable npm versions before the root package is published. Run it only from a controlled release job. ::: The command does **not** collect or copy build artifacts. Run [`napi artifacts`](./artifacts) first. ## Usage ```sh napi pre-publish [--options] ``` ```ts import { NapiCli } from '@napi-rs/cli' await new NapiCli().prePublish({ tagStyle: 'npm', ghRelease: true, }) ``` Boolean options accept the `--no-` prefix. For example, use `--no-gh-release` when the release does not run on GitHub. ## Options | Option | CLI syntax | Type | Required | Default | Description | | --------------------- | --------------------------- | -------------- | :------: | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `cwd` | `--cwd` | `string` | No | `process.cwd()` | Working directory. All other relative paths resolve from here. | | `configPath` | `--config-path,-c` | `string` | No | | Standalone napi config JSON file. | | `packageJsonPath` | `--package-json-path` | `string` | No | `package.json` | Root package metadata and release version. | | `npmDir` | `--npm-dir,-p` | `string` | No | `npm` | Directory containing one prepared package per configured target. | | `tagStyle` | `--tag-style,--tagstyle,-t` | `npm \| lerna` | No | `lerna` | How to resolve the GitHub release tag. `npm` uses `v`; `lerna` reads the package tag from the latest release commit. | | `ghRelease` | `--gh-release` | `boolean` | No | `true` | Create/find a GitHub release and upload target binaries when GitHub repository metadata is available. | | `ghReleaseName` | `--gh-release-name` | `string` | No | | Name passed when creating the GitHub release. | | `ghReleaseId` | `--gh-release-id` | `string` | No | | Numeric ID of an existing release to receive assets. No new release is created. | | `skipOptionalPublish` | `--skip-optional-publish` | `boolean` | No | `false` | Do not run the package manager's publish command for per-platform packages. Metadata updates and enabled GitHub asset uploads still occur. | | `dryRun` | `--dry-run` | `boolean` | No | `false` | Skip package-file mutations, npm publication, GitHub release creation, and asset uploads. | ## Exact side effects Without `--dry-run`, the command executes these phases in order: 1. Read the root package and napi config. 2. Set every configured platform package's `version` to the root version. 3. Merge one exact-version platform package entry per configured target into the root package's `optionalDependencies`. Existing entries are preserved, including obsolete target packages. 4. With GitHub releases enabled, resolve release metadata from the latest Git commit and `GITHUB_REPOSITORY`, then create a release unless `--gh-release-id` selects an existing one. 5. For each target whose expected `.node` or `.wasm` file exists in its npm directory, run ` publish` unless `--skip-optional-publish` is set. 6. With GitHub releases enabled, upload that target file as a release asset. An expected target file that is missing produces a warning and is skipped; it does not fail the command. GitHub release creation and asset-upload failures are logged and may not fail npm publication. Your CI must therefore verify the complete artifact set and the final external state independently. `napi pre-publish` never publishes the root package itself. In the generated template it runs as `prepublishOnly`; after it returns successfully, the surrounding `npm publish` operation publishes the root package. ## Required release state Before running the command with real credentials, verify all of the following: - The root `package.json` version is final and has never been published. - `repository` points to the real GitHub repository. npm provenance validates repository and workflow identity. - `napi.targets` contains exactly the packages intended for this release. - Existing `optionalDependencies` have been reviewed. The command adds or updates configured targets but does not remove stale platform entries. - [`napi create-npm-dirs`](./create-npm-dirs) has created every target package. - [`napi artifacts`](./artifacts) has placed every expected binary in both the target package and the root workspace. - Every target has passed a runtime test on the environment it claims to support. - The configured npm client is authenticated for the root package and every target package. - `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and `contents: write` are available when GitHub releases are enabled. - The workflow has `id-token: write` and npm provenance is enabled if the release is expected to carry provenance. For the generated single-package workflow, use npm tag style: **package.json** ```json { "scripts": { "prepublishOnly": "napi prepublish -t npm" } } ``` The default `lerna` style is only for a Lerna release commit whose body lists the package tag that should be published. ## Preview safely Run the command's own dry-run mode directly: ```sh DEBUG=napi:* yarn napi prepublish -t npm --dry-run ``` This confirms that config and Git release metadata can be read, but it does not verify that target binaries exist and it does not test registry authorization. To inspect the npm tarball without triggering the real `prepublishOnly` script, disable lifecycle scripts explicitly: ```sh npm pack --dry-run --ignore-scripts ``` ::: danger Do not use `npm publish --dry-run` as a safety substitute. npm can still run lifecycle scripts, and a `prepublishOnly` script containing `napi prepublish` can publish the platform packages with real credentials. ::: ## Partial failure and recovery The release is not transactional. npm does not let you overwrite a published name and version, and this command cannot roll back packages that already exist. If a run fails: 1. Stop automatic retries until you know which packages and assets exist. 2. Check every target with `npm view @ version`, check the root package separately, and inspect the GitHub release assets. 3. Keep the same build artifacts. Never publish changed bits under a version that already exists for another target. 4. Re-run the same version to publish missing targets. The CLI recognizes npm's standard "previously published versions" error and skips those packages; other registry errors still fail the run. 5. Pass `--gh-release-id ` to reuse an existing GitHub release, or `--no-gh-release` if release assets are intentionally managed elsewhere. 6. Use `--skip-optional-publish` only after confirming **all** platform packages already exist. It does not validate that condition for you. If every platform package exists but the root publication failed, publish the unchanged root tarball from the trusted release job with lifecycle scripts disabled, for example `npm publish --ignore-scripts --access public`. Preserve the same provenance configuration. If the root package was already published while a platform package is missing, publish the missing package immediately or deprecate the broken root version; npm provides no atomic rollback. See [Release native packages](/docs/deep-dive/release) for the complete CI runbook. --- # Release native packages napi-rs distributes prebuilt addons as npm packages. Consumers install one small root package, and the package manager selects a matching optional package for the current operating system, CPU, and libc. No compiler or install-time download script is required on the consumer's machine. ::: warning A multi-platform publication is not atomic. npm versions are immutable, and a failure can occur after some platform packages exist but before the root package is published. Treat release jobs as production changes, not as build previews. ::: ## Distribution model For a root package such as `@scope/addon`, napi-rs creates packages such as: ```text @scope/addon @scope/addon-darwin-arm64 @scope/addon-win32-x64-msvc @scope/addon-linux-x64-gnu @scope/addon-linux-x64-musl ``` Each platform package contains one native artifact and declares npm `os`, `cpu`, and where applicable `libc` constraints. The root package lists exact versions of those packages in `optionalDependencies`; its generated loader then loads the package matching the running system. This model avoids the two common alternatives: - Shipping Rust/C/C++ source and requiring every consumer to install a native toolchain. - Downloading a binary from GitHub or a CDN in `postinstall`, which introduces install-time network and private-network failures. ## Commands in the release pipeline | Command | Responsibility | | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | | [`napi create-npm-dirs`](../cli/create-npm-dirs) | Create one package directory for every configured target. | | [`napi build`](../cli/build) | Build one target per invocation. CI runs it once for every matrix row. | | [`napi artifacts`](../cli/artifacts) | Collect downloaded `.node`/`.wasm` files into the root and platform packages. | | [`napi pre-publish`](../cli/pre-publish) | Synchronize versions and optional dependencies, publish platform packages, and optionally create/upload a GitHub release. | | `npm publish` | Publish the root package. In the template, this invokes `napi prepublish -t npm` through `prepublishOnly` first. | `napi pre-publish` does not build or collect artifacts, and it does not publish the root package by itself. ## One-time release setup Before the first release: 1. Use an npm scope or confirm that the root name and every suffixed target package name are available. 2. Set the final `name`, `repository`, `license`, and `publishConfig` in `package.json`. The repository must match the GitHub workflow for npm provenance. 3. Review `napi.binaryName` and `napi.targets`. Every target needs a package, build job, and runtime test; an accepted target triple alone is not a support guarantee. 4. Configure an npm automation token as the `NPM_TOKEN` Actions secret, unless you have deliberately replaced the template with npm trusted publishing. The identity must be allowed to publish the root and every platform name. 5. Keep `contents: write` for GitHub release creation and `id-token: write` for npm provenance in the publish job. 6. Run the normal branch/PR workflow successfully before enabling a release. See [Support and compatibility](/docs/more/support-compatibility) and [Cross build](../cross-build) before expanding the generated matrix. ## Preflight every version Before creating a version commit, verify: - The release commit is built from the intended clean branch and reviewed source. - Local formatting, Rust checks, JavaScript tests, generated declarations, and a local native load all pass. - The CI matrix builds every entry in `napi.targets`, and every produced file has the expected `binaryName.platform-arch-abi` suffix. - The new root and platform versions do not already exist on npm. - `npm whoami` succeeds with the release identity and the token is valid for all package names. - The changelog and Node-API/runtime support statements match the release. Inspect the root tarball without running lifecycle scripts: ```sh npm pack --dry-run --ignore-scripts ``` Do not rely on `npm publish --dry-run`: npm lifecycle scripts may still invoke `napi prepublish`, which can publish the real platform packages. Use [`napi pre-publish --dry-run`](../cli/pre-publish#preview-safely) separately, knowing that it does not validate artifact completeness or registry authorization. ## Release with the generated workflow The maintained templates publish from their GitHub Actions workflow. The job: 1. Waits for lint, build, and runtime-test jobs. 2. Downloads all workflow artifacts with `actions/download-artifact@v8`. 3. Creates the target npm directories. 4. Runs `napi artifacts` to populate the root and platform packages. 5. Enables npm provenance. 6. Runs `npm publish` for the root package. Its `prepublishOnly` script runs `napi prepublish -t npm`, which publishes the platform packages and uploads GitHub release assets first. 7. Publishes a stable version with the default npm tag, or a prerelease with the `next` tag. The current template decides whether to publish from the latest commit message. `npm version` already writes the bare version as the commit message (its `message` config defaults to `%s`); only the Git tag carries the `v` prefix (`tag-version-prefix` defaults to `v`), and the template's publish gate accepts both `1.2.3` and `v1.2.3`. So `npm version patch` alone already produces a commit message the gate matches — passing `-m "%s"` below is optional and only pins the message format: ```sh # Creates the version commit and v-prefixed Git tag, but makes the commit # message itself exactly the new version (for example, 1.2.3). npm version patch -m "%s" git push --follow-tags ``` For a prerelease: ```sh npm version prerelease --preid next -m "%s" git push --follow-tags ``` Review the generated `.github/workflows/CI.yml` before using these commands. If your project has changed its trigger or release tooling, follow the checked in workflow rather than this template convention. ## Release gates inside CI Because the CLI warns and continues when an expected target file is missing, add an explicit gate before the publish step. It should prove that: - Every configured target directory exists. - Every directory contains exactly the expected `.node` or `.wasm` file. - WASI packages contain their generated loader and worker support files. - No artifact has an unexpected binary name or target suffix. - Platform runtime tests consumed the same artifacts that will be published. Do not publish the root package unless all platform gates pass. Once the root version exists, clients may immediately attempt to resolve every listed optional dependency. ## Verify the published release A green workflow is not enough. After publication: 1. Read the root metadata with `npm view @scope/addon@ --json` and confirm its dist-tag and exact `optionalDependencies`. 2. Query every platform package at the same version and inspect its `os`, `cpu`, `libc`, and tarball file list. 3. Confirm npm displays provenance when the workflow promised it. 4. Confirm the GitHub release points to the intended tag and contains every expected binary asset. 5. Install the root package into clean projects on representative glibc, musl, macOS, and Windows systems and call a native export. 6. Test native-to-WASI fallback separately when WASI is part of the release. Keep the release workflow URL and verification results with the release notes. ## Recover from a partial release Do not immediately bump the version or rebuild. First inventory npm packages, the root package, and GitHub assets for the failed version. Published binaries must never be replaced with different bits under the same version. The recovery tools are: - Re-run `napi prepublish -t npm` with the unchanged artifacts to publish missing platform packages. Already-published versions are skipped when npm returns its standard duplicate-version error. - Pass `--gh-release-id ` to upload to an existing release instead of creating another one. - Pass `--skip-optional-publish` only after independently confirming that every platform package already exists. - If only the root package remains, publish the unchanged root tarball from the trusted release job with lifecycle scripts disabled so the platform phase is not repeated. Follow the detailed [partial failure and recovery procedure](../cli/pre-publish#partial-failure-and-recovery). If the root was published with a missing platform dependency, publish the missing package immediately or deprecate the broken root version; npm has no atomic rollback. --- # Native module > Some contents are borrowed from https://xcoder.in/2017/07/01/nodejs-addon-history/ ## The Nature of Native Modules Let's start with the most essential C++ module development for Node.js. For example, we have a legitimate native module `pinyin.linux-x64-gnu.node` under Linux, which is actually a binary file that couldn't be seen properly in a text editor, until we came across the binary viewer. ![](/assets/hex.png) The sharp-eyed reader will see that its Magic Number[^1] is `0x7F454C46` and the ASCII code it presses is ELF, so the answer is obvious: it is a **_DLL_** file for Linux. In fact, not just on Linux. When a C++ module of Node.js is compiled under OSX, you get a DLL with the suffix `*.node` which is essentially `*.dylib`, and under Windows, you get a DLL with the suffix `*.node` which is essentially `*.dll`. Such a module, when required in Node.js, is required via `process.dlopen()`. Let's take a look at the DLOpen[^2] function in Node.js [v10.23.0](https://github.com/nodejs/node/blob/v10.23.0/src/node.cc#L1232): ```cpp // DLOpen is process.dlopen(module, filename, flags). void DLOpen(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); auto context = env->context(); Local module; Local exports; Local exports_v; // initialize `module`, `module.exports` values if (!args[0]->ToObject(context).ToLocal(&module) || // this line is equal to `exports = module.exports` !module->Get(context, env->exports_string()).ToLocal(&exports_v) || !exports_v->ToObject(context).ToLocal(&exports)) { return; // Exception pending. } node::Utf8Value filename(env->isolate(), args[1]); // Cast DLib dlib(*filename, flags); bool is_opened = dlib.Open(); node_module* const mp = static_cast( uv_key_get(&thread_local_modpending)); uv_key_set(&thread_local_modpending, nullptr); ... // transfer the handle in dynamic lib to the `mp` mp->nm_dso_handle = dlib.handle_; mp->nm_link = modlist_addon; modlist_addon = mp; if (mp->nm_context_register_func != nullptr) { mp->nm_context_register_func(exports, module, context, mp->nm_priv); } else if (mp->nm_register_func != nullptr) { mp->nm_register_func(exports, module, mp->nm_priv); } else { dlib.Close(); env->ThrowError("Module has no declared entry point."); return; } } ``` Logically, the loading process actually looks like this. - Load the link library via `uv_dlopen`. - Hook the loaded library into the native module chain table. - Initialize the module with `mp->nm_register_func()`, and get the module and module.exports that are there. The flow down is similar to this flowchart: ![nm flow](/assets/nm-flow.png) ## How to build native module ::: warning This section is **historical context**, not current setup advice. It describes how C++ native modules were built before NAPI-RS, and some links, package names, and Node.js versions below are old or deprecated. A NAPI-RS project does not use `node-gyp` at all: `napi build` drives `cargo` directly. For the toolchain a NAPI-RS project actually needs, see [Getting started](/docs/introduction/getting-started#prerequisites). ::: ### `node-waf` Before Node.js 0.8, developers used the `node-waf` to build their library. Of course the `node-waf` is not the node-waf in npm registry, the original `node-waf` has been fallen to disrepair for years. This thing was configured with a file named `wscript`. From Node.js 0.8, it had `node-gyp` builtin, so people didn't need wscript anymore. But because this temporary shortage, many libraries using C++ to build Node.js addon contains both `binding.gyp` and `wscript` in that time. You can see files back to that age in this library [node-mysql-libmysqlclient](https://github.com/Sannis/node-mysql-libmysqlclient/tree/9545ea7485fcc8b07b7c56c5ec3575938bfd4e5f). For node-gyp support it had `binding.gyp` and still preserved the `wscript` file. ### `node-gyp` This stuff It has been with Node.js since Node.js v0.8, before that its default compilation helper package was `node-waf`(see below), which should be familiar to old Noder. #### `GYP` `node-gyp` is based on `GYP`[^3]. It recognizes the `binding.gyp`[^4] file in a package or project, and then generates compilable projects for each system based on that configuration file, such as **Visual Studio project files (\*.sln, etc.)** for Windows and Makefiles for Unix. `node-gyp` can also invoke system compilation tools (such as GCC) to compile the project to a final DLL \*.node file. > As you can see from the above description, compiling C++ native modules on Windows relies on the Microsoft C++ toolchain, which is why you will need to have it pre-installed to install some Node.js packages.
> In fact, for users who don't need Visual Studio, the full IDE is not necessary, since node-gyp only relies on its compiler. Those who want to streamline the installation can install the **Visual Studio Build Tools** with the "Desktop development with C++" workload from [https://visualstudio.microsoft.com/downloads/](https://visualstudio.microsoft.com/downloads/) instead. > > Historical note: this advice used to point at a 2015-era "Visual C++ Build Tools" installer and the `windows-build-tools` npm package. That package is deprecated, and current node-gyp documentation recommends the Visual Studio Build Tools described above. Now that we have that out of the way, let's take a look at the basic structure of `binding.gyp`: **binding.gyp** ```text { "targets": [{ "target_name": "addon1", "sources": [ "1/addon.cc", "1/myobject.cc" ] }, { "target_name": "addon2", "sources": [ "2/addon.cc", "2/myobject.cc" ] }, { "target_name": "addon3", "sources": [ "3/addon.cc", "3/myobject.cc" ] }, { "target_name": "addon4", "sources": [ "4/addon.cc", "4/myobject.cc" ] }] } ``` This configuration tells the following story: - Four C++ native modules are defined. - The source code for each module is **\*.addon.cc** and **\*.myobject.cc**, respectively. - The names of the four modules are **addon1** to **addon4**. - The hidden story: These modules exist in **build/Release/addon\*.node** after compiling. For more information on the GYP configuration file, you can go to the official documentation, which has a link to GYP in a footnote. #### Something more in `node-gyp` In addition to being GYP-based itself, node-gyp does a few extra things. First of all, when we compile a C++ native extension, it goes to the specified directory (usually `~/.node-gyp`) and searches for our current version of Node.js headers and statically linked libraries, and if they don't exist, it feverishly goes to the Node.js website to download them. This is a directory structure for the specific version of Node.js headers and libraries downloaded from node-gyp on macOS: ```text /Users/napi-rs/.node-gyp └── 14.15.1 └── include └── node ├── common.gypi ├── config.gypi ├── cppgc ├── js_native_api.h ├── js_native_api_types.h ├── libplatform ├── node.h ├── node_api.h ├── node_api_types.h ├── node_buffer.h ├── node_object_wrap.h ├── node_version.h ├── openssl ├── uv ├── uv.h ├── v8-fast-api-calls.h ├── v8-internal.h ├── v8-platform.h ├── v8-profiler.h ├── v8-util.h ├── v8-value-serializer-version.h ├── v8-version-string.h ├── v8-version.h ├── v8-wasm-trap-handler-posix.h ├── v8-wasm-trap-handler-win.h ├── v8.h └── v8config.h ``` This header directory will be merged into our `binding.gyp` in the form of an `"include_dirs"` field when `node-gyp` is compiled; in short, all the headers can be `#include` directly. node-gyp is a command line program that can be run directly from `$ node-gyp` after installation. It has some subcommands for you to use. - `$ node-gyp configure`: generates project files, such as Makefiles, from binding.gyp in the current directory. - `$ node-gyp build`: build and compile the current project, which must be preceded by `configure`. - `$ node-gyp clean`: cleans the resulting build files and output directories, in other words, cleans the directories. - `$ node-gyp rebuild`: This is equivalent to the execution of `clean`, `configure` and `build` in sequence. - `$ node-gyp install`: Manually download the header files and library files of the current version of Node.js to the appropriate directory. ## Conclusion In this chapter, we introduced what the Node.js native addon is and how to compile it. In the next chapter, we will review the history of API changes related to the native addon in Node.js and formally introduce our protagonist: the N-API. ## References [^1]: https://en.wikipedia.org/wiki/Magic_number_(programming) [^2]: https://github.com/nodejs/node/blob/v6.9.4/src/node.cc#L2427-L2502 [^3]: GYP means _Generate Your Projects_, a build system developed by Google. For more details: https://gyp.gsrc.io. [^4]: The config file of GYP usually has an extension of _.gyp, or _.gypi. It is a JSON-like file. --- # History > Some contents are borrowed from https://xcoder.in/2017/07/01/nodejs-addon-history/ ## The Feudal era: Using `v8 C++` headers directly In very early ages, developers were using `v8/Node C++` headers directly to build Node.js native addon. ```cpp Handle Echo(const Arguments& args) { HandleScope scope; if(args.Length() < 1) { ThrowException( Exception::TypeError( String::New("Wrong number of arguments."))); return scope.Close(Undefined()); } return scope.Close(args[0]); } void Init(Handle exports) { exports->Set(String::NewSymbol("echo"), FunctionTemplate::New(Echo)->GetFunction()); } ``` This code snippets defined a simple Node.js function: `echo`. It always return the first argument passed in. And it equals to this simple `Node.js` codes: ```js exports.echo = function () { if (arguments.length < 1) throw new Error('Wrong number of arguments.') return arguments[0] } ``` If you publish these codes as a `npm` package, it can only work with `node 0.10.x`. But why? The short answer is **_v8 and Node.js API's change fast._** For example in `Node.js 6.x`, the way of define `JsFunction` changed: ```cpp Handle Echo(const Arguments& args); // 0.10.x void Echo(FunctionCallbackInfo& args); // 6.x ``` So native packages developed in this way can only support only few versions of Node.js, when the API of `v8` or `Node.js` changed, these packages couldn't be compiled any more. And if maintainers updated the API to latest Node.js and `v8`, the package couldn't be compiled under the older Node.js, again. ## The Castle era: Native Abstractions for Node.js Back to 2013, with the fast iteration of the `Node.js` and `v8`, packages used the old way to build native addon grow with pains. And [`NAN`](https://github.com/nodejs/nan) came out. It's shorten for **Native Abstractions for Node.js**. > NAN was built by [Rod Vagg](https://github.com/rvagg) and then [Benjamin Byholm](https://github.com/kkoopa). NAN was belong to Rod Vaggs' GitHub account from the beginning, and transferred to `io.js` organization in the dark age of `Node.js` split to `io.js` and `Node.js`;After they got back together,NAN finally transferred into `Node.js` organization. After NAN came out, the develop experience in native addon packages came to **_the Castle age_**, and last to nowadays. It's still a litter abstract for the full description of NAN: **_Native abstractions for Node.js_**. To be specifically, it's a bunch of **_C macros_**. You can define a JavaScript function like this for example: ```cpp NAN_METHOD(Echo) { } ``` The macro of NAN will be expanded to different CPP codes in during compiling according different Node.js version: ```cpp Handle Echo(const Arguments& args); // 0.10.x void Echo(FunctionCallbackInfo& args); // 6.x ``` `NAN_METHOD` will be expanded by NAN to the codes snippets below. There are tons of macros in NAN rather than `NAN_METHOD`, developers can using it to do almost anything. For example the `Nan::HandleScope` allow you to declare **_handle scope_**, `Nan::AsyncWorker` allow you to spawn task on `libuv`. So in the **The Castle age**, here is what the `c++` native addon look like: ```cpp NAN_METHOD(Echo) { if(info.Length() < 1) { Nan::ThrowError("Wrong number of arguments."); return info.GetReturnValue().Set(Nan::Undefined()); } info.GetReturnValue().Set(info[0]); } NAN_MODULE_INIT(InitAll) { Nan::Set( target, Nan::New("echo").ToLocalChecked(), Nan::GetFunction(Nan::New(Echo)).ToLocalChecked()); } ``` The benefit of writing codes in this way is because the codes could auto upgrade with the NAN upgraded, the codes could be compatible with every versions of Node.js. > Even a good thing like the NAN has a mission, and anything outside of that mission will be gradually stripped away. Versions such as 0.10.x and 0.12.x, for example, should be retired, and the NAN will gradually drop compatibility and support for them. ## Age of Empires: ABI-compliant N-API Since the release of Node.js v8.0.0, Node.js has introduced a brand new interface for developing C++ native modules, **N-API**. > According to the official documentation, it is pronounced with a single N, plus API, which means that the four English letters are pronounced separately. How does this differ from the previous three eras? Why would it be a further age of empire? First of all, we know that even under NAN development, code written once needs to be recompiled under different versions of Node.js, otherwise Node.js won't load a C++ extension properly if the versions don't match. In other words, write once, compile everywhere. N-API, as compared to NAN, black-boxes all the underlying data structures of Node.js and abstracts them into the interface of N-API. Different versions of Node.js use the same interface, which is stably ABI-compatible, that is, the Application Binary Interface (ABI). This allows compiled C++ extensions to be used directly without recompilation, as long as the ABI version number is the same across Node.js versions. In fact, Node.js that supports the N-API interface does specify the current ABI version used by Node.js. In order to achieve the hidden goal above, the posture of using N-API looks like this: - Provide the header file `node_api.h`. - Any N-API call returns a `napi_status` enum to indicate whether the call was successful or not. - The return value of N-API is occupied by `napi_status`, so the real return value is inherited from the incoming arguments. - All JavaScript datatypes are wrapped in the black box type `napi_value`, no longer types like `v8::Object`, `v8::Number`, and so on. - If the function call is unsuccessful, the `napi_get_last_error_info` function can be used to get information about the last error. For more details about functions of N-API, visit its [documentation](https://nodejs.org/api/n-api.html), but for now, let's take a look at something a little less abstract to give you an impression of N-API. ### Module initialization In the **_Feudal_** and NAN eras, module initialization was left to the macros supplied by Node.js. ```cpp NODE_MODULE(addon, Init) ``` In the current N-API, it becomes a macro of N-API. ```cpp NODE_MODULE(addon, Init) ``` Accordingly, this initialization function `Init` will be written in a different way. For example, it is written in two different ways in the feudal era and in the NAN era: ```cpp // Feudal style void Init(Local exports) { NODE_SET_METHOD(exports, "echo", Echo); } // NAN style NAN_MODULE_INIT(Init) { Nan::Set( target, Nan::New("echo").ToLocalChecked(), Nan::GetFunction(Nan::New(Echo)).ToLocalChecked()); } ``` The `Init` function should look like this when it comes to N-API: ```cpp void Init(napi_env env, napi_value exports, napi_value module, void* priv) { napi_status status; // Description constructs for setting exports napi_property_descriptor desc = { "echo", 0, Echo, 0, 0, 0, napi_default, 0 }; // set "echo" into `module.exports` status = napi_define_properties(env, exports, 1, &desc); } ```
`napi_property_descriptor` is a description structure for setting object properties, which is declared as follows: ```cpp typedef struct { const char* utf8name; napi_value name; napi_callback method; napi_callback getter; napi_callback setter; napi_value value; napi_property_attributes attributes; void* data; } napi_property_descriptor; ``` > So the desc in the `Init` function above means that something called "echo" is set under the object to be installed, the function is `Echo`, all the other `getters`, `setters`, and so on are empty pointers, and the property is `napi_default`.
### Declare Functions Do you remember the two previous function declarations? Move over for the third time: ```cpp Handle Echo(const Arguments& args); // 0.10.x void Echo(FunctionCallbackInfo& args); // 6.x ``` In N-API, you no longer need to have a `C++` background, `C` is sufficient. Because in N-API, declaring an Echo looks like this: ```c napi_value Echo(napi_env env, napi_callback_info info) { napi_status status; size_t argc = 1; napi_value argv[1]; status = napi_get_cb_info(env, info, &argc, argv, 0, 0); if(status != napi_ok || argc < 1) { napi_throw_type_error(env, "Wrong number of arguments"); return 0; // `napi_value` is actually a pointer, returning a null pointer means no return value. } return argv[0]; } ``` Step-by-step analysis of the above code: - `napi_get_cb_info` Gets information about the parameters of the current function request, including the number of parameters and their bodies (which are represented as an array of napi_value). - To see if there is an error in the call (status is not equal to napi_ok) or if the number of parameters is less than 1. - If there is an error in the call or the number of arguments is less than 1, an error object is thrown at the JavaScript level via `napi_throw_type_error` and returned. - Proceed if there are no errors. - Returns `argv[0]`, the first argument ## Conclusion This session explains the change in approach to native C++ module development in the Node.js: - From node-waf to node-gyp, it's a change in build tools, maybe GN or something else in the future. - From code-breaking to the advent of NAN, the Node.js community has seen its fair share of loves and hates, all the way to the new kid on the block, N-API, which has brought new blood into the development of native C++ modules. I hope this helps you understand the sour history of Node.js native module development, and the reasons and background for the emergence of N-API. --- # Cross build Cross-compiling a **NAPI-RS** addon means producing a `.node` binary for a target platform (say `aarch64-unknown-linux-gnu`) on a different host (say a Linux x64 CI runner). `napi build` supports this with two recommended mechanisms: - **`--use-napi-cross`** for Linux glibc targets on a Linux x64/arm64 host — a gcc cross toolchain downloaded from npm, pinned to a glibc 2.17 floor. - **`--cross-compile`** (**`-x`**) for Windows MSVC targets from a non-Windows host (via `cargo-xwin`), and for musl targets (via `cargo-zigbuild`). It also covers glibc, macOS and FreeBSD targets through `cargo-zigbuild` when `--use-napi-cross` or a native runner is not available on your host. Android, WASI and OpenHarmony targets need no cross flag at all: the CLI configures their toolchains from platform environment variables (NDK / WASI SDK / OHOS SDK) regardless of which cross flag, if any, is passed. The [decision matrix](#decision-matrix) below has the per-target detail. NAPI-RS standardized on the zig/xwin toolchains because they are much more lightweight than container-based cross-compilation ([napi-rs#491](https://github.com/napi-rs/napi-rs/issues/491)). This page tells you which mechanism to use for your host/target pair, and how to deal with the two things that most often go wrong: glibc versions and C/C++ dependencies. For what each flag does exactly — spawned commands, environment variables, combination rules — see the [`napi build` flag reference](./cli/build#cross-compilation-flags). The [cross-build demo project](https://github.com/napi-rs/cross-build) shows these mechanisms building addons for many platforms from a single Linux CI host. ## Decision matrix The **Generated CI** column shows what the CI workflow scaffolded by `napi new` does for that target. It is the reference setup that is known to work — when in doubt, copy it. | Target | Generated CI (reference setup) | From Linux x64/arm64 | From macOS | From Windows | | --------------------------------------------------- | ------------------------------------------- | --------------------------------- | ----------------- | ----------------- | | `x86_64-apple-darwin` | `macos-latest`, no flag | `-x`[^1] | no flag | not supported | | `aarch64-apple-darwin` | `macos-latest`, no flag (native) | `-x`[^1] | no flag | not supported | | `x86_64-pc-windows-msvc` | `windows-latest`, no flag | `-x`[^2] | `-x`[^2] | no flag | | `i686-pc-windows-msvc` | `windows-latest`, no flag | `-x`[^2] | `-x`[^2] | no flag | | `aarch64-pc-windows-msvc` | `windows-latest` (x64), no flag | `-x`[^2] | `-x`[^2] | no flag | | `x86_64-unknown-linux-gnu` | `ubuntu-latest`, `--use-napi-cross` | `--use-napi-cross` | `-x`[^3] | `-x`[^3] | | `aarch64-unknown-linux-gnu` | `ubuntu-latest`, `--use-napi-cross` | `--use-napi-cross` | `-x`[^3] | `-x`[^3] | | `armv7-unknown-linux-gnueabihf` | `ubuntu-latest`, `--use-napi-cross` | `--use-napi-cross` | `-x`[^3] | `-x`[^3] | | `x86_64-unknown-linux-musl` | `ubuntu-latest`, `-x` + zig setup step | `-x` + zig | `-x` + zig | `-x` + zig | | `aarch64-unknown-linux-musl` | `ubuntu-latest`, `-x` + zig setup step | `-x` + zig | `-x` + zig | `-x` + zig | | `aarch64-linux-android` / `armv7-linux-androideabi` | `ubuntu-latest`, no flag (preinstalled NDK) | no flag + NDK env | no flag + NDK env | no flag + NDK env | | `wasm32-wasip1-threads` | `ubuntu-latest`, no flag | no flag | no flag | no flag | | `x86_64-unknown-freebsd` | FreeBSD 15 VM job, no flag (native) | `-x` + zig[^4] | `-x` + zig[^4] | `-x` + zig[^4] | | `powerpc64le` / `s390x` `-unknown-linux-gnu` | no generated job | `--use-napi-cross` | — | — | | `loongarch64` / `riscv64gc` `-unknown-linux-gnu` | no generated job | no flag + a cross gcc you install | — | — | [^1]: zig can link macOS binaries for **pure-Rust crates only** — dependencies that link Apple frameworks need a real macOS SDK (`SDKROOT`). Prefer a macOS runner. [^2]: cargo-xwin downloads the Microsoft CRT and Windows SDK itself; the Microsoft license applies. It needs `clang` installed (e.g. `brew install llvm` on macOS). [^3]: `--use-napi-cross` only works on Linux x64/arm64 hosts (the downloaded toolchain is a Linux binary), so from macOS or Windows use `-x` instead — but the glibc floor becomes zig's default, not 2.17. See [Glibc versions](#glibc-versions). [^4]: Under `-x`, FreeBSD routes through cargo-zigbuild like every other non-Windows target — have `zig` on `PATH`; Linux hosts are the most battle-tested route. If you want your tests to run on FreeBSD too, run them in a FreeBSD VM. See the [FreeBSD recipe](#freebsd). ## Decision tree ```mermaid flowchart TD A[I want target T from host H] --> B{Is T the host triple?} B -- yes --> N0[no flag] B -- no --> C{Is T Windows MSVC?} C -- "H is Windows" --> N1["no flag - MSVC cross-links all Windows arches"] C -- "H is macOS or Linux" --> X1["-x (cargo-xwin downloads the MS SDK)"] C -- no --> WG{Is T Windows GNU or gnullvm?} WG -- yes --> WGN["no flag + mingw/llvm-mingw + LIBNODE_PATH"] WG -- no --> D{Is T macOS?} D -- "H is macOS" --> N2[no flag + rustup target] D -- "H is Linux" --> X2["-x (zig, pure Rust only - prefer a macOS runner)"] D -- no --> E{Is T Linux glibc?} E -- "H is Linux x64/arm64" --> NC["--use-napi-cross (glibc 2.17 floor)"] E -- "H is macOS or Windows" --> X3["-x (zig default glibc, not 2.17)"] E -- no --> F{Is T Linux musl?} F -- yes --> X4["-x + zig on PATH"] F -- no --> G{Is T Android, WASI or OpenHarmony?} G -- yes --> N3["no flag - set NDK / WASI_SDK / OHOS env"] G -- no --> H2{Is T FreeBSD?} H2 -- yes --> VM["FreeBSD VM (reference) or -x + zig"] ``` The `-x` Windows path is MSVC-only. On a non-Windows host, the CLI rejects an explicit Windows GNU or gnullvm target before Cargo metadata, tool downloads, or cargo-subcommand installation. Build those targets without a cross flag and provide mingw-w64 or llvm-mingw plus `LIBNODE_PATH`; see the Windows note in [Recipes per target](#recipes-per-target). ## The three flags at a glance | | `--use-napi-cross` | `--cross-compile` / `-x` | `--use-cross` (legacy) | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | **Status** | Recommended for Linux glibc targets | Recommended for Windows MSVC targets from a non-Windows host and for musl; the zig fallback for glibc/macOS/FreeBSD when the preferred path is unavailable | **Legacy, not recommended** | | **Mechanism** | Env vars only: downloads a gcc cross toolchain from npm ([`@napi-rs/cross-toolchain`](https://github.com/napi-rs/cross-toolchain)) and points linker/CC/sysroot env at it; the command stays `cargo build` | Swaps the cargo subcommand: `cargo zigbuild` for non-Windows targets, or `cargo xwin build` for Windows MSVC targets from a non-Windows host; explicit Windows GNU/gnullvm targets are rejected before either command runs | Swaps the binary: `cross build` runs the build inside a Docker/Podman container | | **Targets** | Five Linux glibc triples: x64, arm64, armv7, ppc64le, s390x | Linux (gnu and musl) and macOS targets via zig; Windows MSVC via xwin | Whatever cross-rs has images for — Linux only, no macOS or Windows MSVC images | | **glibc floor** | 2.17 | zig's default (2.28 for zig 0.12–0.14) | The image's glibc (mostly 2.31; `:centos` variants 2.17) | | **Prerequisites** | Linux x64/arm64 host, `npm` on `PATH`; the toolchain is downloaded and cached automatically | `zig` on `PATH` for the zigbuild path, `clang` for the xwin path (the CLI never installs or checks either); the selected cargo subcommand (cargo-zigbuild or cargo-xwin) is auto-installed on first use | `cross` installed manually, plus a running Docker >= 20.10 or Podman >= 3.4 | | **C/C++ dependencies** | Compiled with the bundled gcc; the aarch64 gcc is old — see [known limitation](#native-dependencies) | Compiled with `zig cc`; Apple-framework dependencies need a macOS SDK | Full container toolchain — last resort for autotools/CMake build scripts | Pick exactly one flag per build. Any pair is rejected before Cargo metadata, toolchain downloads, or cargo-subcommand installation. See [the combination rules](./cli/build#pick-exactly-one). ## Recipes per target Whatever mechanism you pick, the target's Rust standard library must be installed first: `rustup target add `. Each recipe ends with one copy-paste command and a note on how the generated CI builds the same target. ### Linux glibc (x64, arm64, armv7) From a Linux x64/arm64 host, use `--use-napi-cross`: it builds against glibc 2.17, so the binary loads on virtually every glibc distro. From macOS or Windows, use `-x` instead (zig runs on both) — at the cost of zig's higher default glibc floor. ```sh napi build --release --target aarch64-unknown-linux-gnu --use-napi-cross ``` The generated CI builds `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu` and `armv7-unknown-linux-gnueabihf` on `ubuntu-latest` with exactly this flag. ### Linux musl (x64, arm64) Use `-x` from any host, with `zig` installed and on `PATH`. The CLI automatically appends `-C target-feature=-crt-static` to `RUSTFLAGS` for musl targets. Do not reach for musl to fix a `GLIBC_x.yy not found` error — that is a glibc-floor problem, see [Glibc versions](#glibc-versions). ```sh napi build --release --target aarch64-unknown-linux-musl --cross-compile ``` The generated CI builds both musl targets on `ubuntu-latest` with `-x`, after a setup-zig step. ### Windows (MSVC) from macOS or Linux Use `-x`: the build goes through cargo-xwin, which downloads the Microsoft CRT and Windows SDK itself (the Microsoft license applies). You need `clang` installed (`apt install clang` / `brew install llvm`). For `i686`, the CLI sets `XWIN_ARCH=x86` automatically. On a Windows host no flag is needed at all — MSVC cross-links x64, x86 and arm64 natively. ```sh napi build --release --target x86_64-pc-windows-msvc --cross-compile ``` The generated CI builds all three MSVC targets on `windows-latest` with no flag; use `-x` when you have no Windows runner. What about `*-pc-windows-gnu`? `x86_64-pc-windows-gnu` is an accepted CLI target since [napi-rs#2935](https://github.com/napi-rs/napi-rs/pull/2935) (the generated JS loader picks the `win32-x64-gnu` binary when Node itself is a MINGW build); the other windows-gnu arches are not accepted. Do **not** use `-x` for it: from a non-Windows host, the CLI rejects that combination before Cargo metadata or cargo-xwin installation because cargo-xwin supports MSVC triples only. Build it with no cross flag instead: `rustup target add x86_64-pc-windows-gnu`, install a mingw-w64 toolchain (`apt install mingw-w64` / `brew install mingw-w64`), and set `LIBNODE_PATH` to a directory containing `libnode.dll` from MSYS2's Node — napi-build links windows-gnu addons directly against it. This target is usually built inside MSYS2/MINGW, where both prerequisites are already available. There are still no official Node.js windows-gnu builds, so unless you specifically target MSYS2/MINGW Node, build for the `*-pc-windows-msvc` triple instead — historical context in [napi-rs#2001](https://github.com/napi-rs/napi-rs/issues/2001). ### macOS On a macOS host, no cross flag is needed — add the other architecture with `rustup target add` and build. The generated CI also sets `MACOSX_DEPLOYMENT_TARGET: '10.13'` to pin the minimum macOS version. From Linux, `-x` works for pure-Rust crates only: dependencies that link Apple frameworks need a real macOS SDK (`SDKROOT`), so prefer a macOS runner. Building macOS targets from Windows is not supported. ```sh napi build --release --target aarch64-apple-darwin ``` The generated CI builds both darwin targets natively on `macos-latest` with no flag. ### Android No cross flag. On a non-Android host, the CLI configures the toolchain from `ANDROID_NDK_LATEST_HOME` (preinstalled on GitHub `ubuntu-latest` runners) and stops before Cargo if the variable is missing. On an Android host, it leaves the native toolchain environment untouched. ```sh napi build --release --target aarch64-linux-android ``` The generated CI builds `aarch64-linux-android` and `armv7-linux-androideabi` on `ubuntu-latest` with no flag. ### WASI No cross flag. Linking is handled by rustup's bundled `rust-lld`. `WASI_SDK_PATH` is optional — but if set, it must point to an existing directory — and the CLI reads it whether or not a cross flag is passed. ```sh napi build --release --target wasm32-wasip1-threads ``` The generated CI already builds `wasm32-wasip1-threads` on `ubuntu-latest` — no flag needed. ### FreeBSD There are two working setups. The reference setup is the generated CI's: build natively inside a FreeBSD 15 VM (via `cross-platform-actions/action`) on an `ubuntu-latest` runner — no cross flag. The generated job only builds and uploads the artifact; if you want your tests to run on FreeBSD too, add that step to the VM script yourself. FreeBSD can also be cross-compiled from Linux: under `-x` it routes through cargo-zigbuild like every other non-Windows target — run it on a Linux host with zig installed. The usual zig caveats apply: C/C++ dependencies are compiled by `zig cc` (see [Native dependencies](#native-dependencies)). ```sh napi build --release --target x86_64-unknown-freebsd --cross-compile ``` The generated CI builds natively in the FreeBSD 15 VM; the `-x` command above is the cross-compile alternative from a Linux host. ## Glibc versions A `*-linux-gnu` binary links glibc dynamically, and at load time it requires at least the glibc version it was built against. **Your binary inherits the build host's glibc as its floor**: build on a bleeding-edge distro without a cross flag, and users on older distros get: ``` Error: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found ``` This error means: build against an older glibc. It does **not** mean: switch to a musl target. - `--use-napi-cross` pins the floor to **glibc 2.17** (manylinux2014 lineage) regardless of the host distro. - `-x` builds against **zig's default glibc** — 2.28 for zig 0.12–0.14 — not 2.17. - Pinning an explicit version by suffixing the triple (`--target aarch64-unknown-linux-gnu.2.17`) is **not supported yet**: the suffix breaks the CLI's artifact lookup. Watch [napi-rs#3176](https://github.com/napi-rs/napi-rs/issues/3176). ## Verify the artifact Before publishing, check that the binary is the architecture you intended and requires no more glibc than you targeted: ```sh # CPU architecture and file format file my-package.linux-arm64-gnu.node # Highest glibc symbol version the binary requires objdump -T my-package.linux-arm64-gnu.node | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -1 ``` Expect at most `GLIBC_2.17` when built with `--use-napi-cross`, and zig's default when built with `-x`. ## Native dependencies C/C++ dependencies are the most common obstacle in cross-compilation: crates like `ring`, `openssl-sys` or `zstd-sys` compile C source via a build script, which needs a C compiler that targets your _target_ — configuring rustc alone is not enough. - **cc-based crates (`ring`, etc.)**: set `TARGET_CC=clang` — clang is inherently a cross compiler. `TARGET_CC` takes precedence over `CC` (since `@napi-rs/cli` 3.0.0-alpha.92). ```sh TARGET_CC=clang napi build --release --target aarch64-unknown-linux-gnu --use-napi-cross ``` - **Known limitation — `aws-lc-sys`**: the default rustls backend (pulled in transitively by `reqwest`, `hyper-rustls`, etc.) fails to build with `--use-napi-cross` for aarch64, because the bundled gcc is too old ([cross-toolchain#4](https://github.com/napi-rs/cross-toolchain/issues/4)). Work around it with `TARGET_CC=clang`, or use `-x` instead. - **TLS / OpenSSL**: prefer rustls with the `ring` backend, or enable the `vendored` feature of `openssl-sys` so OpenSSL is compiled from source with the cross toolchain instead of linking host libraries. - **Last resort**: dependencies whose build scripts run autotools or CMake and pick up host binutils may only build in the legacy container path (`--use-cross`), where the entire toolchain matches the target. ## Docker images are deprecated ::: warning The prebuilt Docker images (`ghcr.io/napi-rs/napi-rs/nodejs-rust:*`) and the `*.Dockerfile` based builds are **deprecated**. Migrate to `--use-napi-cross` (Linux glibc targets) or `-x` (musl targets) on a plain `ubuntu-latest` runner. ::: | Old image (`ghcr.io/napi-rs/napi-rs/...`) | New setup on plain `ubuntu-latest` | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `nodejs-rust:lts-debian` | `napi build --release --target x86_64-unknown-linux-gnu --use-napi-cross` — the same glibc 2.17 floor the Debian image provided | | `nodejs-rust:lts-debian-aarch64` | `napi build --release --target aarch64-unknown-linux-gnu --use-napi-cross` | | `nodejs-rust:lts-alpine` | install zig, then `napi build --release --target x86_64-unknown-linux-musl -x` | | `nodejs-rust:lts-debian-zig` / `lts-alpine-zig` | install zig, then `napi build --release --target -x` | If you are still on the images, follow two rules. First, run plain `napi build --target ` inside them with **no cross flags** — the image already pins the toolchain and glibc, and adding cross flags on top of that is what breaks builds. Second, pin the image by digest (`nodejs-rust@sha256:...`), because the `lts-*` tags change over time. ## Add a target to an existing project 1. Add the triple to `targets` in your `napi` config (see [napi config](./cli/napi-config)). 2. Run `napi create-npm-dirs` to scaffold the per-platform npm packages. 3. Add a CI matrix entry for the target — copy the closest job from the generated CI (the [decision matrix](#decision-matrix) tells you the runner and flag). 4. After upgrading `@napi-rs/cli` — especially across major versions — regenerate your CI workflow from a fresh `napi new` scaffold rather than patching it, so it does not drift from what the CLI expects. ## See also - [`napi build` cross-compilation flag reference](./cli/build#cross-compilation-flags) — exact commands, environment variable contract, combination rules - [FAQ: Build for Linux alpine](./more/faq#build-for-linux-alpine) — musl specifics ## Sponsor our team https://github.com/sponsors/napi-rs/ Integrating and properly configuring cross-platform compilation toolchain in the open source community can be very tedious and labor-intensive. Understanding these compilation parameters and resolving potential bugs can be very time-consuming and difficult to test. Special thanks for our team member [@messense](https://github.com/messense) who has been working on `cargo-xwin` and `cargo-zigbuild` which is enabled us to build Windows native addons on non-Windows systems. If you are using **NAPI-RS** in your company, please consider sponsoring our team to support the development of NAPI-RS. We will be very grateful for your support. --- # Support and compatibility “Supported” can mean several different things for a native addon. napi-rs keeps these boundaries separate: | Question | Source of truth | | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | Can a compiled addon load on a Node.js release? | The Node-API level compiled into that addon and the Node-API versions provided by the runtime. | | Can `@napi-rs/cli` run? | The CLI package's own Node.js engine requirement. | | Is a Node/runtime combination continuously tested? | The current napi-rs source CI workflow. | | Does `napi new` generate a build and publish path for a target? | The selected Yarn or pnpm template's checked-in matrix and `napi.targets`. | | Can Rust compile the target triple? | Rust target support plus the required linker, SDK, native dependencies, and cross-build mechanism. | | Does upstream Node.js publish a binary for the target? | Node.js release artifacts, which are narrower than the triples the napi-rs CLI can parse. | An accepted target triple or ABI-compatible Node-API level is not, by itself, a promise that every combination above is tested. ## Node-API ABI compatibility Node-API provides ABI stability across Node.js versions. A native binary built against Node-API level `N` can generally load on later Node.js releases that still provide level `N`, without rebuilding for every Node major. That guarantee does not cover: - APIs introduced after the selected Node-API level. - Operating-system, CPU, libc, C++ runtime, or minimum deployment-target compatibility. - Bugs in an alternate runtime's Node-API implementation. - Native libraries linked by your own dependencies. `napi new` asks for the minimum Node-API level and writes both the corresponding `napiN` Cargo feature and `engines.node` range into the generated project. The scaffold currently offers Node-API levels 1 through 9 and defaults to level 4. Choose the lowest level that provides the APIs you use, then test on its oldest claimed Node.js runtime. Features such as async support can still raise the effective Node-API floor. ## CLI and Rust requirements - `@napi-rs/cli` declares `>=23.5.0 || ^22.13.0 || ^20.17.0`, matching its interactive-prompt dependency. Use an up-to-date **Node.js 22 LTS release (22.13+) or Node.js 24+** for current CLI builds. An addon may still target an older compatible Node.js runtime even though the CLI that builds it cannot run there. - The current napi-rs v3 workspace declares **Rust 1.88** as its minimum Rust version. - The generated template's `engines.node` describes the addon package, not the build CLI. ## What the napi-rs source repository tests The primary [napi-rs source CI matrix](https://github.com/napi-rs/napi-rs/blob/main/.github/workflows/test-release.yaml) currently exercises **Node.js 22, 24, and 26** across its main Linux, macOS, and Windows jobs. Additional Docker target tests currently use Node.js 22 and 24. This is the project's current regression coverage, not the complete Node-API compatibility range. A Node version outside that matrix may be ABI-compatible, but it is not accurate to call it continuously tested by the current source workflow. The generated package templates maintain their own smaller test matrices. Read the workflow copied into your project and treat that checked-in file as the support contract for your package. ## JavaScript runtimes | Runtime | Status | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Node.js native addons** | Primary runtime. Release claims should still be limited to the Node versions and platforms your package tests. | | **Bun native addons** | Best effort. The source repository runs a latest-Bun job, but the test step is `continue-on-error`, so Bun failures do not block napi-rs releases. Test your actual addon before claiming support. | | **Deno native addons** | Not part of the current napi-rs source CI matrix. Do not infer Deno support from Node-API compatibility alone. | | **Node.js WASI fallback** | Exercised by the source and generated template workflows for their selected Node versions. It is a different artifact and loader from a native `.node` addon. | | **Browser WASI** | Available through generated browser/worker bindings. It requires WebAssembly threads, workers, and the appropriate cross-origin isolation headers. Test the target browsers explicitly. | | **Bun/Deno WASI fallback** | Known compatibility gaps remain; see [napi-rs issue #2965](https://github.com/napi-rs/napi-rs/issues/2965). Do not present this path as generally supported. | ::: info A package can support a runtime more strongly than napi-rs itself does by adding its own blocking runtime tests. Record those tests in the package's support policy rather than relying on the framework homepage. ::: ## Targets accepted by the CLI The current CLI recognizes target families including: - macOS x64, arm64, and universal binaries. - Windows MSVC x64, x86, and arm64, plus Windows GNU x64. - Linux glibc x64, arm64, armv7, loongarch64, riscv64gc, ppc64le, and s390x. - Linux musl x64, arm64, and armv7. - Android arm64 and armv7. - FreeBSD x64. - OpenHarmony x64 and arm64. - threaded WASI preview-1 targets. This list describes parsing and packaging vocabulary. Some targets require a manually installed linker or SDK, some can only be built from particular hosts, and some do not have official Node.js runtime binaries. ## Targets generated by `napi new` `napi new` copies one of two maintained repositories: - [Yarn package template](https://github.com/napi-rs/package-template) - [pnpm package template](https://github.com/napi-rs/package-template-pnpm) The scaffold filters existing template rows; it does not synthesize a new CI recipe for every accepted triple. The current templates provide build/package paths for the common matrix: | Platform | Template-backed targets | | ------------ | ------------------------- | | macOS | x64, arm64 | | Windows MSVC | x64, x86, arm64 | | Linux glibc | x64, arm64, armv7 | | Linux musl | x64, arm64 | | Android | arm64, armv7 | | FreeBSD | x64 | | WASI | threaded preview-1 target | Both maintained templates currently implement this matrix. Because `napi new` filters their checked-in configuration instead of synthesizing target recipes, treat the generated `package.json` and `.github/workflows/CI.yml` as the support baseline for the new package. Targets such as OpenHarmony, Windows GNU, armv7 musl, universal macOS, loongarch64, riscv64gc, ppc64le, and s390x may be accepted by the CLI without having a complete scaffolded build and publish path. Selecting all targets does not change that. ## Adding or claiming a target Before listing a target as supported: 1. Add the triple to `napi.targets`. 2. Run `napi create-npm-dirs` and inspect the generated package constraints. 3. Add a CI build using the correct host, linker/SDK, and cross-build mode. 4. Upload and collect the artifact with `napi artifacts`. 5. Run the binary on the real or faithfully emulated target environment. 6. Test the oldest OS, libc, deployment target, and Node.js version you claim. 7. Verify a clean install of the root package selects and loads the expected optional package. Use [Cross build](/docs/cross-build) for the host/target decision tree and [Add a target to an existing project](/docs/cross-build#add-a-target-to-an-existing-project) for the full packaging flow. ## How to state support accurately Prefer a claim such as: > One binary per listed platform, built against Node-API 8. CI tests Node.js 22 > and 24 on macOS arm64/x64, Windows x64, and Linux x64 glibc/musl. Other > Node-API-compatible Node.js releases are expected to work but are not in the > blocking matrix. Avoid “all Node versions” or “all platforms.” Include the Node-API level, tested Node versions, OS/CPU/libc matrix, minimum OS or glibc floor, and whether alternate runtimes are blocking, best-effort, or untested. --- # Testing and debugging A native addon has two test boundaries: - Rust tests verify logic that does not need a live JavaScript engine. - JavaScript integration tests load the `.node` library into Node.js and verify conversion, exceptions, promises, garbage collection, and environment lifecycle behavior. Use both. A Rust test cannot prove that a generated binding accepts the intended JavaScript value, and a JavaScript-only suite makes ordinary Rust logic slower and harder to isolate. ## Test pure Rust logic with Cargo Keep algorithms and operating-system integration independent from NAPI-RS values where possible: **src/core.rs** ```rust pub fn normalize_count(value: i32) -> Result { value.try_into().map_err(|_| "count must not be negative") } #[cfg(test)] mod tests { use super::*; #[test] fn rejects_negative_counts() { assert_eq!(normalize_count(-1), Err("count must not be negative")); } } ``` The exported function can be a thin conversion layer: **src/lib.rs** ```rust mod core; use napi::{Error, Result, Status}; use napi_derive::napi; #[napi] pub fn normalize_count(value: i32) -> Result { core::normalize_count(value) .map_err(|reason| Error::new(Status::InvalidArg, reason)) } ``` Run these tests normally: ```sh cargo test ``` ### Test exported pure functions with `noop` Native registration normally refers to symbols supplied by the Node process. If your test binary cannot link those symbols, enable the `noop` feature for both `napi` and `napi-derive` in a test-only crate feature: **Cargo.toml** ```toml [features] test-noop = ["napi/noop", "napi-derive/noop"] ``` ```sh cargo test --features test-noop ``` In `noop` mode, `#[napi]` does not generate the JavaScript registration layer, so an exported function made only of ordinary Rust values can be called by a Rust test. It does **not** create a fake JavaScript engine. Code involving `Env`, `Function`, `Object`, JavaScript references, promises, or conversion through `napi_value` still belongs in a Node integration test. For compile-time macro diagnostics, use [`trybuild`](https://docs.rs/trybuild) tests and commit their `.stderr` snapshots separately from runtime tests. ## Test the generated binding in Node.js Build a debug addon and import the generated loader: **package.json** ```json { "scripts": { "build:debug": "napi build --platform", "test": "node --test" } } ``` **test/add.test.cjs** ```js const assert = require('node:assert/strict') const test = require('node:test') const addon = require('../index.js') test('native add', () => { assert.equal(addon.add(20, 22), 42) }) test('invalid input throws synchronously', () => { assert.throws(() => addon.normalizeCount(-1), /must not be negative/) }) ``` ```sh npm run build:debug npm test ``` Test through the same loader your users import. Requiring a file from `target/debug` bypasses platform selection and can hide packaging defects. At minimum, integration tests should cover: - argument and return-value conversion, including null and omitted values; - synchronous errors and rejected promises; - generated TypeScript with `tsc --noEmit`; - one clean process start and exit; - every Node version and target you advertise as tested. ## Test workers and environment teardown Every `worker_threads` worker has its own Node-API environment. Load the addon inside the worker instead of passing native classes or JavaScript handles from another isolate: **test/worker.cjs** ```js const { parentPort } = require('node:worker_threads') const { add } = require('../index.js') parentPort.postMessage(add(2, 3)) ``` **test/worker.test.cjs** ```js const assert = require('node:assert/strict') const { join } = require('node:path') const test = require('node:test') const { Worker } = require('node:worker_threads') test('loads in a worker isolate', async () => { const worker = new Worker(join(__dirname, 'worker.cjs')) const value = await new Promise((resolve, reject) => { worker.once('message', resolve) worker.once('error', reject) }) assert.equal(value, 5) await worker.terminate() }) ``` Add a separate stress test when the addon owns background work or JavaScript references: 1. Start many workers and require the addon concurrently. 2. Exercise the async API and await normal completion. 3. Ask the worker to stop, cancel owned work, and wait for acknowledgements. 4. Terminate the worker only after the graceful path succeeds. 5. Put abrupt `worker.terminate()` races in a dedicated test so a product limitation is not mistaken for ordinary shutdown behavior. Abrupt termination with native async work still has open runtime-specific failure reports, especially in Bun ([napi-rs#2938](https://github.com/napi-rs/napi-rs/issues/2938)). Treat cancellation and worker shutdown as part of the API contract; a documentation change cannot make an in-flight operating-system call cancellable. ## Test process exit A strong ThreadsafeFunction, open handle, or background worker can keep Node alive. Test exit behavior in a child process so the main test runner cannot hide the leak: **test/exit.test.cjs** ```js const assert = require('node:assert/strict') const { spawn } = require('node:child_process') const { join } = require('node:path') const test = require('node:test') test('process exits after async work', async () => { const child = spawn(process.execPath, [join(__dirname, 'exit-repro.cjs')]) let timer const code = await Promise.race([ new Promise((resolve, reject) => { child.once('exit', resolve) child.once('error', reject) }), new Promise((_, reject) => { timer = setTimeout(() => { child.kill() reject(new Error('child did not exit')) }, 5_000) }), ]).finally(() => clearTimeout(timer)) assert.equal(code, 0) }) ``` If a ThreadsafeFunction should not keep the event loop alive, build it in weak mode. See [Async and concurrency](/docs/more/async-concurrency) for the lifecycle tradeoff. ## Test garbage collection and leaks Garbage collection is nondeterministic. A useful regression test creates a `WeakRef`, drops all strong JavaScript references, requests GC repeatedly, and uses a deadline: **test/leak.cjs** ```js const { NativeResource } = require('../index.js') let resource = new NativeResource() const weak = new WeakRef(resource) resource = undefined const deadline = Date.now() + 10_000 const interval = setInterval(() => { global.gc() if (weak.deref() === undefined) { clearInterval(interval) process.exit(0) } if (Date.now() > deadline) { console.error('NativeResource was not collected before the deadline') process.exit(1) } }, 50) ``` ```sh node --expose-gc test/leak.cjs ``` Do not assert collection immediately after one `global.gc()` call. Also run longer stress jobs under platform memory tools when the addon owns allocations: - AddressSanitizer or LeakSanitizer for Rust/C/C++ memory errors; - Instruments on macOS; - Valgrind on supported Linux configurations; - Application Verifier or WinDbg on Windows. Keep sanitizer builds separate from ordinary release artifacts. ## Start with a useful diagnostic run Before attaching a debugger, reproduce the problem with a debug addon and full CLI/Rust diagnostics: ```sh DEBUG='napi:*' RUST_BACKTRACE=full napi build --platform --verbose DEBUG='napi:*' RUST_BACKTRACE=full node ./repro.cjs ``` Do not pass `--release` or `--strip`. Confirm which Node executable and binding are involved: ```sh node -p "process.execPath" node -p "process.platform + ' ' + process.arch" file ./*.node ``` Use `NAPI_RS_NATIVE_LIBRARY_PATH=/absolute/path/to/addon.node` to make a generated loader try one exact local binary. This is a diagnostic override, not a packaging configuration. ## Debug with VS Code and CodeLLDB Install the CodeLLDB extension and create a build task: **.vscode/tasks.json** ```json { "version": "2.0.0", "tasks": [ { "label": "napi build debug", "type": "shell", "command": "napi build --platform", "problemMatcher": ["$rustc"] } ] } ``` Then launch **Node**, not the `.node` library. Replace `program` with the absolute path printed by `node -p "process.execPath"` if CodeLLDB does not resolve `node` from `PATH`: **.vscode/launch.json** ```json { "version": "0.2.0", "configurations": [ { "name": "Debug NAPI-RS in Node", "type": "lldb", "request": "launch", "program": "node", "args": ["${workspaceFolder}/repro.cjs"], "cwd": "${workspaceFolder}", "sourceLanguages": ["rust"], "env": { "RUST_BACKTRACE": "full", "DEBUG": "napi:*" }, "preLaunchTask": "napi build debug" } ] } ``` Set breakpoints in Rust before the JavaScript first imports the addon. A breakpoint may appear unbound until Node loads the `.node` image. This setup is known to work on macOS, Linux, and WSL. Native Windows debugging with `cppvsdbg` remains an open documentation and tooling gap ([napi-rs#2830](https://github.com/napi-rs/napi-rs/issues/2830)); do not assume a `cppvsdbg` configuration is supported merely because it launches Node. CodeLLDB on Windows or WSL is currently the more reproducible starting point. ## Debug with CLion or another native debugger The same process model applies in every native debugger: 1. Build with `napi build --platform`. 2. Create a **Native Application** configuration. 3. Set the executable to the exact Node executable. 4. Set `repro.cjs` as the program argument and the package as the working directory. 5. Add the build command as a before-launch task. Alternatively, start `node --inspect-brk repro.cjs`, attach the native debugger to that Node PID, set Rust breakpoints, and then continue JavaScript execution. The JavaScript inspector and native debugger can be attached to the same process. Command-line equivalents are useful for crash backtraces: ```sh # macOS or Linux with LLDB lldb -- node ./repro.cjs # at the LLDB prompt run thread backtrace all ``` ```sh # Linux with GDB gdb --args node ./repro.cjs # at the GDB prompt run thread apply all bt ``` ## When breakpoints do not bind Check these in order: 1. The build omitted `--release` and `--strip`. 2. The loader selected the binary you just built, not a platform package in `node_modules`. 3. `process.execPath` is the executable launched by the debugger. 4. The Rust source belongs to the exact Cargo target used for the `.node` file. 5. The breakpoint is reached only after `require()` or `import` loads the addon. 6. On macOS, the binary and Node process have matching architectures. For loader failures, symbol errors, libc mismatches, and stale TypeScript, use the [troubleshooting decision tree](/docs/more/troubleshooting). --- # Async and concurrency guide The right abstraction depends on where work must run and whether Rust needs to call JavaScript while it is running. Start with the smallest abstraction that matches the work; moving a synchronous function to another thread does not make its dependencies safe to use there. ## Decision table | Need | Use | Work runs on | JavaScript result | | ------------------------------------------ | ----------------------------------- | ------------------------------------------------- | ----------------------------- | | Fast conversion or computation | Ordinary `#[napi] fn` | JavaScript thread | Immediate value or throw | | Rust async I/O or async ecosystem | `#[napi] async fn` | NAPI-RS Tokio runtime | `Promise` | | Blocking/CPU work using Node's worker pool | `AsyncTask` | libuv thread pool; `resolve` returns to JS thread | `Promise` | | Call a JS function from an OS/Tokio thread | `ThreadsafeFunction` | Producer thread, callback on JS thread | Callback or awaited return | | Deliver a sequence lazily | iterator or async iterator | Pull-based | `for...of` / `for await...of` | | Stream bytes with Web Streams | `ReadableStream` / `WritableStream` | Tokio plus JS stream callbacks | Web Streams API | Two rules apply to every row: 1. Only the JavaScript thread may use `Env` or raw `napi_value` handles. 2. Data crossing a thread or `await` boundary must be owned for long enough; borrowed JavaScript values are function-scoped. See [Understanding lifetime](/docs/concepts/understanding-lifetime) before moving buffers, objects, or class instances into background work. ## Tokio `async fn` Enable `async` (which enables the NAPI-RS Tokio runtime) and only the Tokio features your crate uses: **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["async"] } napi-derive = "3" tokio = { version = "1", features = ["fs", "time"] } ``` **src/lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub async fn read_config(path: String) -> Result { Ok(tokio::fs::read(path).await?.into()) } ``` The future and its output cross threads, so they must be `Send + 'static`. Prefer owned inputs such as `String`, `Buffer`, and owned typed arrays. Do not hold `JsString<'_>`, `Object<'_>`, or an `Env` across an `await` point. `async fn` is appropriate for async I/O. A long synchronous calculation inside it still occupies a Tokio worker thread; use `tokio::task::spawn_blocking` or an `AsyncTask` for blocking work. ### Cancellation is not automatic Dropping the JavaScript `Promise` does not cancel its Rust future. Design a cancellation protocol for long-running work: - accept an explicit cancellation handle or operation ID; - bridge cancellation to an atomic flag, channel, or library cancellation token owned by Rust; - stop creating JavaScript work after cancellation; - await or abort every spawned Tokio `JoinHandle` during owner shutdown. Detached work spawned with `napi::tokio::spawn` must not outlive the environment or the Rust/JavaScript resources it uses. Keep its `JoinHandle` in an owning class and abort or await it in your shutdown path. ## `AsyncTask` and the libuv worker pool Use [`AsyncTask`](/docs/concepts/async-task) for bounded blocking work that fits Node's shared libuv thread pool. `Task::compute` runs off the JavaScript thread; `resolve`, `reject`, and `finally` run after completion where an `Env` is available. **src/lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; pub struct HashFile { path: String, } #[napi] impl Task for HashFile { type Output = Vec; type JsValue = Buffer; fn compute(&mut self) -> Result { // Blocking file and CPU work is allowed here. Do not call JavaScript. Ok(std::fs::read(&self.path)?) } fn resolve(&mut self, _env: Env, bytes: Self::Output) -> Result { Ok(bytes.into()) } } #[napi] pub fn hash_file(path: String) -> AsyncTask { AsyncTask::new(HashFile { path }) } ``` `AsyncTask::with_signal` accepts an `AbortSignal`, but Node-API can cancel only work that has not started. Once `compute` is running, calling `AbortController.abort()` does not interrupt it. If running work must stop, combine `AbortSignal::on_abort` with your own cooperative flag/channel and make `compute` check it. The libuv pool is shared with Node filesystem, DNS, crypto, and other native work. Flooding it with long CPU tasks can delay unrelated application work. Bound concurrency at the JavaScript API or use a dedicated Rust pool when that is part of your performance design. ## ThreadsafeFunction Use a [`ThreadsafeFunction`](/docs/concepts/threadsafe-function) when a Rust thread must schedule a JavaScript callback. The producer sends owned Rust data; the conversion and callback execute on the owning JavaScript environment. Choose its queue behavior deliberately: - `NonBlocking` returns immediately. With a bounded queue, handle `Status::QueueFull` as backpressure instead of dropping data silently. - `Blocking` waits for queue space. Never use it from the JavaScript thread and avoid it in shutdown paths where the event loop may no longer drain. - A queue size of `0` is unbounded. It avoids `QueueFull` but can turn a slow callback into unbounded memory growth. - A strong ThreadsafeFunction keeps the event loop alive. Build with `.weak::()` when pending callbacks are not a reason to keep the process running. Drop all clones to release a ThreadsafeFunction. Calling `abort` closes it immediately; later calls report `Status::Closing`. ### JavaScript errors and return values `callee_handled::()` uses the Node callback convention: Rust calls the ThreadsafeFunction with a `Result`, and JavaScript receives an error-first callback. With `false`, the Rust call accepts only the value and JavaScript receives no error parameter; handle recoverable native failures before calling it. Use `call_async` when Rust must await the callback result, and use the variant that catches JavaScript-thrown values when those failures are recoverable. Never let a JavaScript exception cross an FFI callback as an unchecked Rust panic. Return or explicitly handle the `napi::Error`. ### AsyncLocalStorage and request context A ThreadsafeFunction is registered as its own Node async resource. Do not assume a callback scheduled later from a Rust thread inherits the `AsyncLocalStorage` store that happened to be active when the native API was called. If context is part of correctness, pass a request ID or context object as owned data and restore it in JavaScript (for example with an `AsyncResource`) rather than relying on ambient state. Promise continuations may preserve JavaScript async context differently from a ThreadsafeFunction callback. Test the exact API/runtime combination you ship. ## Iterators and streams Use an iterator when JavaScript should pull one value at a time. Use an async iterator when producing the next value is asynchronous. Their pull model is usually easier to cancel and bound than pushing every item through an unbounded ThreadsafeFunction queue. Use the `web_stream` feature when consumers require the Web Streams API: **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["web_stream"] } ``` Web Streams are best established for byte-oriented data. Structured Rust objects in `ReadableStream` have an unresolved behavior report ([napi-rs#2826](https://github.com/napi-rs/napi-rs/issues/2826)); add a runtime test before exposing structured chunks as a supported API. Whichever abstraction you choose, define what happens when the consumer stops: - cancel the producer when `return()`, `cancel()`, or `abort()` is called; - release JavaScript references and queue senders; - ensure a blocked producer wakes during shutdown; - decide whether buffered values are delivered or discarded. ## Runtime lifecycle With `tokio_rt`, NAPI-RS creates a Tokio runtime and starts it when the native module is registered. On native Node targets it is shut down after the last Node-API environment using the module exits, and it can be started again for an Electron renderer reload. Register environment-specific resources for each environment. Do not cache one `Env`, JavaScript reference, class constructor, or ThreadsafeFunction globally and reuse it from the main thread in a worker isolate. ### Custom Tokio runtime Install a custom runtime during module initialization, before async exports use the default runtime: **src/lib.rs** ```rust use napi::create_custom_tokio_runtime; #[napi_derive::module_init] fn init() { let runtime = tokio::runtime::Builder::new_multi_thread() .worker_threads(4) .enable_all() .build(); match runtime { Ok(runtime) => create_custom_tokio_runtime(runtime), Err(err) => eprintln!("failed to create custom Tokio runtime: {err}"), } } ``` ::: warning A custom runtime instance is currently consumed once. After `shutdown_async_runtime()` followed by `start_async_runtime()`, NAPI-RS falls back to its default runtime rather than recreating the custom configuration. This is an open product limitation, not a supported restart contract ([napi-rs#3251](https://github.com/napi-rs/napi-rs/issues/3251)). ::: WASI has different runtime teardown constraints. If your WASI API starts the runtime explicitly or exposes a shutdown function, test repeated startup and shutdown in the actual WASI host. See [WebAssembly](/docs/concepts/webassembly). ## Worker shutdown protocol Do not make abrupt termination the normal cancellation mechanism for native work. A resilient worker protocol is: 1. The parent sends `stop`. 2. The worker stops accepting native calls. 3. Rust cancellation tokens are triggered. 4. The worker awaits active promises and drops ThreadsafeFunction producers. 5. The worker replies `stopped` and closes its message port. 6. The parent uses `worker.terminate()` only after a deadline. Node worker lifecycle and Bun worker lifecycle are not interchangeable. Abrupt termination during an async native operation still has an open Bun crash report ([napi-rs#2938](https://github.com/napi-rs/napi-rs/issues/2938)). Mark such a runtime as unsupported for that API, or keep the graceful protocol mandatory, until your own stress test proves otherwise. ## Review checklist Before shipping an async export, answer these questions in its documentation and tests: - Which pool/runtime/thread performs the work? - Can it access JavaScript, and only on the correct environment? - What owns every value across `await` and thread boundaries? - Is the queue bounded, and what happens under backpressure? - How does the caller cancel queued and already-running work? - What keeps the Node event loop alive? - What happens during worker termination, Electron reload, and process exit? - Are JavaScript exceptions and Rust panics converted into defined failures? - Is ambient async context required, or is context passed explicitly? --- # Bundlers and frameworks A `.node` file is a shared library loaded by the JavaScript runtime. It is not JavaScript and should not be transformed, concatenated into a bundle, or sent to a browser. Most integration problems disappear once the generated loader is kept intact and executed by Node at runtime. ## Understand the generated loader With `napi build --platform`, NAPI-RS generates a JavaScript loader that: 1. Detects `process.platform`, `process.arch`, and Linux libc. 2. Tries a local file such as `addon.linux-x64-gnu.node`. 3. Tries the separately published optional package such as `@scope/addon-linux-x64-gnu`. 4. Falls back to the configured WASI binding when native loading failed. 5. Throws one error whose `cause` chain contains the native-candidate load failures. Ordinary WASI fallback failures are not appended to that chain; use `NAPI_RS_FORCE_WASI=error` when diagnosing WASI specifically. The loader also recognizes two diagnostic controls: - `NAPI_RS_NATIVE_LIBRARY_PATH=/absolute/addon.node` **replaces** normal native platform and package selection with one explicit library. If that load fails, the loader records the error and can proceed to a configured WASI fallback, but it does not try the ordinary native candidates. - `NAPI_RS_ENFORCE_VERSION_CHECK=1` rejects a separately published platform package whose version differs from the root package. Keep the loader outside the application bundle whenever possible. It must be able to perform its runtime detection and resolve optional dependencies from a real `node_modules` tree. ## Choose CommonJS or ESM deliberately The native library itself has no module format. Only the generated JavaScript loader is CommonJS or ESM. ### CommonJS package ```sh napi build --platform --js index.cjs ``` **package.json** ```json { "main": "./index.cjs", "types": "./index.d.ts" } ``` ```js const { add } = require('@scope/addon') ``` Use a `.cjs` extension if the package has `"type": "module"`; otherwise Node will parse a CommonJS loader as ESM. ### ESM package ```sh napi build --platform --esm --js index.js ``` **package.json** ```json { "type": "module", "main": "./index.js", "types": "./index.d.ts" } ``` ```js import { add } from '@scope/addon' ``` The generated ESM loader uses `createRequire` internally because Node still loads `.node` libraries through `require`. `--esm` changes the exported wrapper to real static named ESM exports; it does not convert the native binary. ### Dual CommonJS and ESM exports Generate both loaders from the same native artifact: **package.json** ```json { "type": "module", "main": "./index.cjs", "module": "./index.js", "types": "./index.d.ts", "exports": { ".": { "types": "./index.d.ts", "import": "./index.js", "require": "./index.cjs" } }, "scripts": { "build": "napi build --platform --js index.cjs && napi build --platform --esm --js index.js" } } ``` Test both `import()` and `require()` in CI. Test runners that transpile ESM can exercise a different path than plain Node, which is why an ESM-only Jest error is not proof that the native library failed to load. ## Recommended bundler strategy: externalize The most robust application build leaves the root addon package external. The deployment then contains: - the generated loader; - the root package metadata; - the matching optional platform package and its `.node` file. This is the packaging model recommended for the open bundler request ([napi-rs#1948](https://github.com/napi-rs/napi-rs/issues/1948)). It avoids hashed asset names, relocated `__dirname`, and bundlers eagerly following every platform-specific `require` branch. ::: warning Marking a dependency external means the deployed runtime must still be able to resolve it. Copy production dependencies, install them in the deployment image, or provide them through a serverless layer. Externalization alone does not package the addon. ::: ### esbuild **build.mjs** ```js import * as esbuild from 'esbuild' await esbuild.build({ entryPoints: ['src/server.js'], bundle: true, platform: 'node', format: 'esm', outdir: 'dist', external: ['@scope/addon', '@scope/addon-*'], }) ``` Copy/install `@scope/addon` and its matching optional dependency beside the bundle. Do not use a `file`/`copy` loader unless you have deliberately chosen a single-platform, inline-binary package and verified the final relative paths. ### webpack **webpack.config.cjs** ```js module.exports = { target: 'node', externals: { '@scope/addon': 'commonjs @scope/addon', }, } ``` If the import name is computed or wrapped, use an externals function/plugin that keeps the entire addon package external. `node-loader` can copy a direct `.node` import, but it does not by itself preserve the generated loader's platform and optional-package control flow. ### A single-platform inline binary Sometimes an internal application ships only one known target and keeps the `.node` file in the root package rather than separate optional packages. In that case: 1. Keep the generated wrapper as an external file. 2. Copy the `.node` file without a content hash. 3. Preserve the relative path expected by the wrapper. 4. Fail the build if more than one target could reach the deployment. 5. Test from the final archive/image, not from the source tree. This is a deployment-specific optimization, not a portable npm package. ## Vite SSR and Astro Native addons are server-only dependencies. Keep them out of Vite dependency optimization and SSR bundling: **vite.config.ts** ```ts import { defineConfig } from 'vite' export default defineConfig({ optimizeDeps: { exclude: ['@scope/addon'], }, ssr: { external: ['@scope/addon'], }, }) ``` Import the addon only from server modules. A component that is bundled for the browser cannot load a native `.node` library. Astro uses Vite, so the same externalization applies through its `vite` config. When a CommonJS package does not expose named exports to Rollup, either emit the NAPI-RS wrapper with `--esm` or load the CommonJS package in server code: ```ts import { createRequire } from 'node:module' const require = createRequire(import.meta.url) const { add } = require('@scope/addon') ``` The Astro integration report remains open as [napi-rs#2206](https://github.com/napi-rs/napi-rs/issues/2206). Verify the adapter's final server output because an adapter may run another bundling step after Vite. ## Next.js Use the addon only in the Node.js runtime: route handlers, server actions, or server components that are not assigned to the Edge runtime. Externalize the package from the server bundle: **next.config.mjs** ```js /** @type {import('next').NextConfig} */ const nextConfig = { serverExternalPackages: ['@scope/addon'], } export default nextConfig ``` **app/api/add/route.ts** ```ts export const runtime = 'nodejs' import { add } from '@scope/addon' export function GET() { return Response.json({ value: add(20, 22) }) } ``` Do not import the addon from a Client Component, middleware using the Edge runtime, or code shared with either one. Ensure your deployment platform copies the external package and its optional binary dependency. ## Electron Electron can load a Node-API addon in the main process and in Node-enabled preload/renderer contexts. Prefer loading it in the main process or a preload script and expose a narrow IPC API; enabling unrestricted Node integration in a renderer expands the application's security boundary. For packaged applications: - build/install the binary for Electron's actual operating system and CPU; - keep `.node` files outside ASAR compression (`asarUnpack`) or use the packager's native-module unpack support; - keep optional platform packages in production dependencies; - test an installed/package artifact, including window reload and shutdown; - test every Electron architecture you distribute. Node-API reduces dependence on a particular V8 ABI, but it does not make a Linux x64 binary load on Windows or macOS. If another dependency uses the V8 addon ABI instead of Node-API, it may still need Electron-specific rebuilding. ## Serverless and containers Build and install for the **deployment runtime**, not the developer laptop. For example, a Linux Lambda deployment needs a Linux binary for the function's x64 or arm64 architecture and compatible glibc version. A reliable flow is: 1. Build/publish separate platform packages from CI. 2. Install production dependencies for the deployment platform. 3. Externalize the addon from the JavaScript bundle. 4. Copy the root and optional platform packages into the image, function, or layer. 5. Start the final artifact in the provider's base image and call one native export as a smoke test. For Linux libc and target selection, follow [Cross build](/docs/cross-build). If the provider does not allow native addons but supplies the required WASI runtime features, consider the documented [WASI fallback](/docs/concepts/webassembly) and test that host explicitly. ## Diagnose a bundled deployment Run these probes inside the final container/archive environment: ```sh node -p "process.execPath" node -p "process.platform + ' ' + process.arch" node -p "process.report?.getReport?.().header.glibcVersionRuntime || 'no glibc version reported'" npm ls @scope/addon ``` Then import the external package with plain Node. If it works before bundling but not from the final bundle, inspect whether the bundler relocated the loader, renamed the `.node` file, removed an optional dependency, or selected an Edge/ browser runtime. The [troubleshooting guide](/docs/more/troubleshooting) shows how to print the native loader failures in the `cause` chain and how to force a separate WASI diagnostic. --- # Troubleshooting First identify the failing layer. A Rust compiler error, a generated-loader error, and a crash after a successful import have different owners and require different evidence. | Failure point | Start with | | ---------------------------------------------------- | ------------------------------------------------------------- | | `cargo` or `napi build` exits non-zero | [Build failures](#build-failures) | | `require()` / `import` cannot load the package | [Loader failures](#loader-failures) | | Loader finds a binary but the OS rejects it | [Binary and platform failures](#binary-and-platform-failures) | | Runtime values work but `.d.ts` is wrong | [TypeScript generation](#typescript-generation) | | Promise hangs, process will not exit, worker crashes | [Async and lifecycle failures](#async-and-lifecycle-failures) | | WASI/browser initialization fails | [WASI failures](#wasi-failures) | Reduce the issue to one exported function and one plain Node script before adding a test runner, bundler, framework, or Electron. If the plain script works, the integration layer is part of the reproduction. ## Capture the environment Run these commands in the same shell, container, or CI job that fails: ```sh node -p "process.version" node -p "process.execPath" node -p "process.platform + ' ' + process.arch" node -p "JSON.stringify(process.versions, null, 2)" rustc -vV cargo -V napi --version ``` On Linux, also record the runtime libc: ```sh node -p "process.report?.getReport?.().header.glibcVersionRuntime || 'musl or unknown'" ldd --version 2>&1 | head -1 ``` Enable both CLI and Rust diagnostics: ```sh DEBUG='napi:*' RUST_BACKTRACE=full napi build --platform --verbose DEBUG='napi:*' RUST_BACKTRACE=full node ./repro.cjs ``` Keep the first error and its complete cause/backtrace. A later “build failed” line is usually only a summary. ## Build failures ### `No crate found in manifest` `--cwd` is the base for every relative path. Verify what the CLI will read: ```sh pwd ls -l Cargo.toml package.json cargo metadata --manifest-path Cargo.toml --format-version 1 --no-deps ``` In a split workspace, pass all paths explicitly. If the manifest is a virtual workspace, also pass the exact Cargo package name: ```sh napi build \ --cwd packages/addon \ --manifest-path ../../Cargo.toml \ --package my-addon-native \ --package-json-path package.json \ --output-dir . \ --platform ``` See [Manual setup](/docs/introduction/manual-setup) for the meaning of each option. ### Cargo succeeds but NAPI-RS cannot copy the artifact Confirm that the selected package contains a `cdylib` target: **Cargo.toml** ```toml [lib] crate-type = ["cdylib"] ``` Check whether `CARGO_BUILD_TARGET_DIR`, `--target-dir`, a custom Cargo profile, or `CARGO_BUILD_TARGET` redirected Cargo output. Use the same `--target` and `--profile` values for the build and copy step. `DEBUG=napi:*` prints the exact source and destination paths used by the CLI. ### A C/C++ dependency cannot find a compiler or library Rust target installation is only one part of a native cross-build. Build scripts for `openssl-sys`, `ring`, `zstd-sys`, and similar crates also need a C compiler and libraries for the target. Do not point them at host libraries. Use the [cross-build decision matrix](/docs/cross-build), then inspect the first failing compiler invocation. Record `CC`, target-specific `CC_*`, linker, SDK, sysroot, and `pkg-config` variables. For WASI C/C++ dependencies, configure `WASI_SDK_PATH` as described in [WebAssembly](/docs/concepts/webassembly). ## Loader failures The generated loader records native-candidate load failures in an error `cause` chain. Print it instead of reporting only “Cannot find native binding”: **load-repro.cjs** ```js try { require('./index.js') } catch (error) { let current = error let depth = 0 while (current) { console.error(`[cause ${depth}]`, current.stack || current) current = current.cause depth += 1 } process.exitCode = 1 } ``` The individual native causes distinguish a missing file from a wrong architecture, missing shared library, or unsupported Node-API symbol. An ordinary failed WASI fallback is not appended to this chain. To diagnose that path explicitly, rerun with `NAPI_RS_FORCE_WASI=error`; the thrown error then chains the WASI binding failures. ### The optional platform package is missing Record the detected platform and installed dependency tree: ```sh node -p "process.platform + ' ' + process.arch" npm ls your-package find node_modules -type f \( -name '*.node' -o -name '*.wasm' \) ``` Common causes are: - installation used `--no-optional` or omitted optional dependencies; - a lockfile generated on another platform did not include the current target; - a deployment copied only production JavaScript and discarded `.node` files; - pnpm/Yarn supported-architecture settings exclude the deployment CPU/libc; - the root and optional platform packages are different versions. Set `NAPI_RS_ENFORCE_VERSION_CHECK=1` to turn the last case into an explicit version-mismatch error. If npm omitted an optional platform dependency because of its lockfile behavior, remove both `node_modules` and the affected lockfile, then install again on the target platform. Inspect or save the old lockfile first when it is needed for a bug report. ### Force one exact native library To separate loader selection from binary loading, point the generated loader at one absolute path: ```sh NAPI_RS_NATIVE_LIBRARY_PATH="$PWD/addon.linux-x64-gnu.node" node load-repro.cjs ``` If that succeeds, the binary is valid and normal platform/package selection is the failing layer. If it fails, the new cause is the operating system's direct loader error. Do not ship this environment variable as the normal package configuration. ### CommonJS/ESM parse or export errors - A CommonJS wrapper inside a `"type": "module"` package must use `.cjs`. - Generate a real ESM wrapper with `napi build --platform --esm` when consumers need static named ESM exports. - Importing a CommonJS wrapper through a transpiling test runner is not the same as testing with plain Node. - Keep native packages external to server bundles. See [Integrations and bundlers](/docs/more/integrations) for tested package shapes and externalization recipes. ## Binary and platform failures Inspect the actual file selected by the loader: ```sh file ./addon.*.node ``` Then inspect dynamic dependencies: ```sh # Linux ldd ./addon.linux-x64-gnu.node # macOS otool -L ./addon.darwin-arm64.node # Windows Developer Command Prompt dumpbin /DEPENDENTS addon.win32-x64-msvc.node ``` Typical messages mean: | Message | Likely cause | | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `wrong ELF class`, `Exec format error`, `not a valid Win32 application` | CPU or operating-system mismatch | | `GLIBC_x.y not found` | Binary was built against newer glibc than the runtime | | `lib*.so` / `.dylib` / `.dll` not found | A non-system native dependency was not shipped or its search path is wrong | | `undefined symbol: napi_*` | Addon enabled a newer Node-API level than the runtime provides, or was linked/loaded incorrectly | | `invalid ELF header` while loading on Alpine | glibc binary selected for a musl runtime, or the reverse | Do not rename a musl binary to a `gnu` suffix or an x64 binary to an arm64 suffix. The suffix is a selection contract, not a conversion. For `GLIBC_x.y not found`, rebuild against an older glibc as documented in [Cross build: glibc versions](/docs/cross-build#glibc-versions). Switching to a musl target is only correct when the deployment actually uses musl. ## TypeScript generation If no `.d.ts` file is emitted, verify that the selected Cargo package directly depends on `napi-derive` with its `type-def` feature. Default features include it; disabling default features requires adding it back explicitly: **Cargo.toml** ```toml napi-derive = { version = "3", default-features = false, features = ["strict", "type-def"] } ``` If a declaration is missing or stale: 1. Confirm the export is compiled for the current target and is not hidden by `#[cfg(...)]` or `#[napi(skip_typescript)]`. 2. Confirm the CLI selected the intended Cargo package and `package.json`. 3. Rebuild without watch mode and inspect `DEBUG=napi:*` output. 4. Remove only the generated type-definition cache under `target/napi-rs`, then rebuild. 5. Run `tsc --noEmit` against the newly generated file. Do not hand-edit generated declarations; the next build overwrites them. Use `ts_args_type`, `ts_return_type`, `dtsHeader`, or a hand-written public wrapper when the Rust-to-TypeScript mapping intentionally differs. ## Async and lifecycle failures ### A Promise never settles Determine which abstraction owns it: - Tokio `async fn`: check for blocking work on the async runtime and detached tasks that never complete. - `AsyncTask`: check whether `compute`, `resolve`, `reject`, or `finally` is blocked. `AbortSignal` only cancels work that has not started unless the task implements cooperative cancellation. - ThreadsafeFunction: handle `QueueFull` and `Closing`; do not block while the JavaScript thread is waiting on the producer. - Stream/iterator: make cancellation wake the producer and close every sender. Add timestamps and operation IDs on both sides of the boundary. A Rust log that says “queued” and a JavaScript log that says “awaiting” do not prove the completion callback ran. ### Node does not exit Move the reproduction to a child process with a deadline. Then look for: - a strong ThreadsafeFunction that should have been weak; - undropped ThreadsafeFunction clones or JavaScript references; - Tokio tasks without an owner/shutdown path; - workers, timers, streams, or sockets left open by either JavaScript or Rust. Test the real exit path rather than calling `process.exit()`, which hides active handles and skipped cleanup. ### Worker termination crashes or hangs Load the addon independently in every worker isolate. Do not share `Env`, class constructors, or JavaScript handles globally between isolates. Implement a graceful stop/cancel/await protocol before `worker.terminate()`. Abrupt termination during active native async work remains a runtime-sensitive limitation, with an open Bun report in [napi-rs#2938](https://github.com/napi-rs/napi-rs/issues/2938). Reproduce in plain Node and the target Bun/Electron runtime separately. See [Async and concurrency](/docs/more/async-concurrency) and [Testing and debugging](/docs/more/testing-debugging) for lifecycle tests. ### Native panic or process abort Run a debug build with `RUST_BACKTRACE=full` and attach a native debugger. A panic cannot always be recovered safely across an FFI boundary. Convert expected failures to `napi::Result`; reserve panics for violated internal invariants, and document whether `#[napi(catch_unwind)]` is used. Follow [Testing and debugging](/docs/more/testing-debugging) for CodeLLDB, LLDB, GDB, worker stress, and leak tests. ## WASI failures ### `SharedArrayBuffer is not defined` or memory creation fails The browser page is not cross-origin isolated. Serve the main document and subresources with: ```text Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp ``` Confirm in the browser console: ```js console.log(globalThis.crossOriginIsolated, typeof SharedArrayBuffer) ``` Also ensure cross-origin scripts, workers, WASM, and images satisfy the selected COEP policy; one blocked subresource can make an otherwise correct build fail. ### The WASI optional package was not installed WASI packages use `cpu: ["wasm32"]` and are skipped by default. Configure the package manager's supported architectures or install with npm's `--cpu=wasm32` as shown in [WebAssembly: install the package](/docs/concepts/webassembly#install-the-webassembly-package). With a loader generated by `@napi-rs/cli` 3.7 or newer: - `NAPI_RS_FORCE_WASI=true` attempts the WASI path even when native loaded. - `NAPI_RS_FORCE_WASI=error` also throws if no WASI binding can be found. - `1`, `0`, `false`, and other strings do not force WASI. Use `error` in tests so a missing WASI package cannot silently fall back to the native addon. ### Browser worker errors are invisible Set `napi.wasm.browser.errorEvent` to `true`. The generated worker forwards an error to the window as `napi-rs-worker-error`: ```js window.addEventListener('napi-rs-worker-error', (event) => { console.error(event.detail) }) ``` ### Works in Node but not Bun or Deno Do not assume Node's WASI implementation exists with the same API elsewhere. WASI execution in Bun and Deno has an open incompatibility report ([napi-rs#2965](https://github.com/napi-rs/napi-rs/issues/2965)). Mark the runtime unsupported or provide a separately tested loader until that product gap is resolved. ## Report an actionable issue Include: - minimal Rust source, `Cargo.toml`, `build.rs`, `package.json`, and JavaScript reproduction; - complete commands and the first error with its `cause` chain; - Node/runtime, CLI, Rust, host, target, CPU, and libc versions; - whether plain Node works before a test runner or bundler is added; - output of `file` and the platform dependency inspection command; - whether the artifact is debug/release, native/WASI, local/optional package; - for lifecycle bugs, a bounded stress test and the exact shutdown sequence. ::: info Remove credentials, absolute private paths, and proprietary input data, but do not remove the platform, target triple, or original operating-system loader message. Those details often identify the failing layer immediately. ::: --- # Cross-build FAQ This page collects target-specific build questions. For installation, loading, runtime, type-generation, and publishing failures, start with the [troubleshooting guide](./troubleshooting). For choosing a cross-compilation strategy, use the [Cross build](../cross-build) decision guide. ## Build for `Linux alpine` > https://github.com/rust-lang/rust/pull/40113#issuecomment-323193341 You cannot set compile `crate-type` to `cdylib` when the compile target is `*-unknown-linux-musl` by default. If you want to do so, you need to pass `-C target-feature=-crt-static` to `rustc`. The **NAPI-RS** CLI handles this for you: for any `*musl*` target, `napi build` automatically appends `-C target-feature=-crt-static` to the `RUSTFLAGS` environment variable, and the project generated by `napi new` builds its musl targets this way out of the box. Because the CLI exports this via the `RUSTFLAGS` environment variable, any `rustflags` in your `.cargo/config.toml` are ignored for musl builds (environment variables take precedence in Cargo). If you need extra `rustc` flags for a musl target, add them to the `RUSTFLAGS` environment variable instead of `.cargo/config.toml`. If you use the [`mimalloc`](https://github.com/purpleprotocol/mimalloc_rust) allocator, enable its `local_dynamic_tls` feature for musl targets; otherwise the addon can fail at runtime with a thread-local storage allocation error. See [Cross build](../cross-build) for how to build musl targets from any host. ## `GLIBC_x.yy` not found ``` Error: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found ``` A `*-linux-gnu` addon links glibc dynamically and requires at least the glibc version of the machine that **built** it. If the build host runs a newer distro than the deploy host, the addon fails to load with this error. The fix is to build against an older glibc: - On a Linux x64/arm64 host, build with `--use-napi-cross` — it pins the glibc floor to **2.17**, which loads on virtually every glibc distro. - From a macOS or Windows host, build with `--cross-compile` (`-x`) — the floor becomes zig's default glibc: newer than 2.17, but independent of your host distro. Do **not** switch to a `*-musl` target to fix this error — musl is a different libc for Alpine-style distros, not a way to lower a glibc requirement. See [Glibc versions](../cross-build#glibc-versions) for the details. ## rustls / `aws-lc-sys` fails with `--use-napi-cross` on aarch64 If your crate depends on `rustls` — often transitively, via `reqwest` or `hyper-rustls` — its default backend `aws-lc-sys` fails to cross-compile for `aarch64-unknown-linux-gnu` with `--use-napi-cross`: the gcc bundled in [`@napi-rs/cross-toolchain`](https://github.com/napi-rs/cross-toolchain) is too old for `aws-lc-sys` ([cross-toolchain#4](https://github.com/napi-rs/cross-toolchain/issues/4)). Two workarounds: - Compile the C parts with `clang` instead of the bundled gcc: ```sh TARGET_CC=clang TARGET_CXX=clang++ napi build --release --target aarch64-unknown-linux-gnu --use-napi-cross ``` - Use `--cross-compile` (`-x`) instead of `--use-napi-cross`. See the [Native dependencies](../cross-build#native-dependencies) section of the Cross build guide for the other recurring C/C++ cross-compilation problems. ## Build for `Windows i686` There is `codegen` error when compile target is `i686-windows-*`: [Rust issue 67497](https://github.com/rust-lang/rust/issues/67497). There is a workaround to avoid this issue: - Set `lto` to false. If you haven't set lto in your `Cargo.toml`, the value is false by default, so you can ignore this step. - Set `codegen-units` to `32` (or higher). The default value of `codegen-units` is `16` when the compile target is release. You can set `CARGO_PROFILE_RELEASE_CODEGEN_UNITS=32` and `CARGO_PROFILE_RELEASE_LTO='false'` to make the compiler happy when targeting `i686-windows-*`. Here is an [example](https://github.com/napi-rs/package-template/blob/50ecdec7c7d31c60b693d5d52be6e13ba9b32bf8/.github/workflows/CI.yaml#L89-L91). --- # V2 to V3 Migration Guide This guide is for maintainers of an existing **NAPI-RS v2** project who want to upgrade to **v3**. It covers the `package.json` config changes, the rewritten CLI, and the Rust API changes, in the order you should apply them. For the background and the design goals behind these changes, see the [**Announcing NAPI-RS v3**](/blog/announce-v3) blog post. ## Prerequisites Before you start: - **Node.js** `^20.17.0 || ^22.13.0 || >=23.5.0` for the new `@napi-rs/cli`. Node.js 22.13+ or 24+ is recommended. This is a build-time requirement only; it does not change the runtime support of the addon you ship. See [Support and compatibility](/docs/more/support-compatibility). - **Rust 1.88 or newer**. - A clean Git working tree, so you can review every change the migration makes. ## TL;DR: breaking changes | Area | v2 | v3 | | ------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------ | | Config | `napi.name` | `napi.binaryName` | | Config | `napi.triples.defaults` + `napi.triples.additional` | Flat `napi.targets` array | | Config | `napi.package.name` | `napi.packageName` (the old nested field is **not** read by v3) | | CLI | `napi build --cargo-cwd ./crates/napi` | `napi build --manifest-path ./crates/napi/Cargo.toml` | | CLI | `napi build --cargo-flags="--locked"` | `napi build -- --locked` | | CLI | `napi create-npm-dir` | [`napi create-npm-dirs`](/docs/cli/create-npm-dirs) | | CLI | `napi universal` | [`napi universalize`](/docs/cli/universalize) | | Rust | `JsObject`, `JsFunction`, `JsBuffer`, `Ref`, … | New owned/scoped types; old types moved behind the `compat-mode` feature | | Rust | `napi::module_init` | `napi_derive::module_init` | | Rust | `#[module_exports]` (`compat-mode` in `napi-derive`) | `#[napi(module_exports)]` | | Rust | `ThreadsafeFunction` with ref-count lifecycle | Rewritten ownership-based [`ThreadsafeFunction`](/docs/concepts/threadsafe-function) | ## Step 1: Update the dependencies Update the Rust crates and the CLI: **Cargo.toml** ```toml [dependencies] napi = "3" napi-derive = "3" [build-dependencies] napi-build = "3" ``` ```sh npm install --save-dev @napi-rs/cli@latest ``` Then run a first build and let the compiler list every API that changed: ```sh napi build ``` ## Step 2: Update the `napi` config The `napi` configuration in `package.json` has changed. The `name` field is now `binaryName`: **package.json** ```diff { "name": "@napi-rs/package-template", "version": "1.0.0", "napi": { - "name": "my-package", + "binaryName": "my-package", } } ``` The `triples` config has been removed. Set a flat `targets` array instead. What used to be `triples.defaults` was: ```text "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", "x86_64-apple-darwin" ``` In v3 you list every target explicitly, including the former defaults: **package.json** ```diff { "name": "@napi-rs/package-template", "version": "1.0.0", "napi": { - "triples": { - "defaults": true, - "additional": [ - "aarch64-apple-darwin", - "x86_64-unknown-linux-musl", - "aarch64-unknown-linux-musl" - ] - } + "targets": [ + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "x86_64-unknown-linux-musl", + "aarch64-unknown-linux-musl" + ] } } ``` The v3 CLI still reads `napi.name` and `napi.triples` for compatibility, but new projects should not use them. The old nested `napi.package.name` field is **not** read at all: move that value to `napi.packageName` yourself. See [NAPI Config](/docs/cli/napi-config) for the full field reference. ## Step 3: Update CLI usage The CLI has been rewritten. The breaking changes that affect existing scripts and CI jobs are: ### `--cargo-cwd` removed Use `--manifest-path` to point at the `Cargo.toml` of the crate: ```diff - napi build --cargo-cwd ./crates/napi + napi build --manifest-path ./crates/napi/Cargo.toml ``` In v2, path flags of `napi build` were resolved relative to `--cargo-cwd || process.cwd()`. In v3, they are resolved relative to `--cwd || process.cwd()`. ### `--cargo-flags` removed Flags after `--` are now passed through to the `cargo build` command: ```diff - napi build --cargo-flags="--locked" + napi build -- --locked ``` The `--locked` flag is passed to `cargo build`, resulting in `cargo build --locked`. ### `create-npm-dir` renamed to `create-npm-dirs` See [**create-npm-dirs**](/docs/cli/create-npm-dirs) for more details. Besides the renamed command and flags, it is no longer recommended to commit the `npm/*` directories. Create them in CI with `napi create-npm-dirs` instead, like this: https://github.com/napi-rs/package-template/blob/main/.github/workflows/CI.yml ### `napi universal` renamed to `napi universalize` See [**universalize**](/docs/cli/universalize) for more details. ## Step 4: Update the Rust code ### Some `JsValues` are now behind the `compat-mode` feature flag The full list of these values is: - `JsObject` - `JsFunction` - `JsNull` - `JsBoolean` - `JsUndefined` - `JsBuffer` - `JsBufferView` - `JsArrayBuffer` - `JsArrayBufferView` - `JsTypedArray` - `JsBigint` - `Ref` These APIs are not safe; see [**Lifetime in V3**](/blog/announce-v3#lifetime) for more details. Migrate these APIs to the new APIs. See [Values](/docs/concepts/values), [Function](/docs/concepts/function), [Reference](/docs/concepts/reference), and [TypedArray](/docs/concepts/typed-array) for more details. If you cannot migrate yet, enable the `compat-mode` feature flag as an escape hatch (see below). ### `ThreadsafeFunction` [`ThreadsafeFunction`](/docs/concepts/threadsafe-function) has been totally rewritten. See [**ThreadsafeFunction in V3**](/blog/announce-v3#threadsafefunction) for the background, and the new [`ThreadsafeFunction`](/docs/concepts/threadsafe-function) API doc for day-to-day usage. ### `napi::module_init` moved to `napi_derive::module_init` This is due to breaking changes in the upstream [`ctor`](https://github.com/mmastrac/rust-ctor) crate: ```diff - #[napi::module_init] + #[napi_derive::module_init] fn init() { // ... } ``` ### New `#[napi(module_exports)]` This replaces the `#[module_exports]` macro from `compat-mode`. You can now drop the `compat-mode` feature from `napi-derive` and use only the modern `#[napi]` macros. See [`module_exports`](/docs/concepts/napi-attributes#module_exports) for the accepted signature. ## Step 5: The `compat-mode` escape hatch If a full migration is not possible right now, enable the `compat-mode` feature to keep using the deprecated types while you migrate incrementally: **Cargo.toml** ```toml [dependencies] napi = { version = "3", features = ["compat-mode"] } ``` This is not recommended as a permanent state: the types behind `compat-mode` have known safety issues, and the feature exists only to make the upgrade path incremental. ## FAQ ### Do I have to migrate everything at once? No. Enable `compat-mode`, upgrade the config and the CLI first, then migrate one API at a time until you can remove the feature flag. ### Where did `napi build --cargo-flags` go? Everything after `--` is forwarded to `cargo build`. Run `napi build -- --locked` instead of `napi build --cargo-flags="--locked"`. ### Does v3 change which Node.js versions my addon supports? The new CLI requires Node.js `^20.17.0 || ^22.13.0 || >=23.5.0` at build time. The runtime support of the addon you produce is a separate question; it depends on the Node-API level you compile against. See [Support and compatibility](/docs/more/support-compatibility). ### My build fails after removing `compat-mode`. What now? Read the compiler errors from the top: each one points at a deprecated type and its modern replacement. The concept pages [Values](/docs/concepts/values), [Function](/docs/concepts/function), [Reference](/docs/concepts/reference), and [TypedArray](/docs/concepts/typed-array) show the v3 equivalent of every removed API. --- # Examples The [`napi-rs/napi-rs`](https://github.com/napi-rs/napi-rs) repository keeps a set of runnable example projects under [`examples/`](https://github.com/napi-rs/napi-rs/tree/main/examples). They are the same code the CI test suite exercises, so every snippet compiles and runs against the current release. ## The example projects | Project | What it shows | | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`examples/napi`](https://github.com/napi-rs/napi-rs/tree/main/examples/napi) | The kitchen sink: one `src/*.rs` file per feature area, built for native, `wasm32-wasip1`, and `wasm32-wasip1-threads`, with browser, workerd, and Electron test targets. Most code snippets on this site are adapted from here. | | [`examples/napi-compat-mode`](https://github.com/napi-rs/napi-rs/tree/main/examples/napi-compat-mode) | The v2-era low-level `compat-mode` API (`CallContext`, `JsBuffer`, `JsFunction`, …) organized by Node-API version. Useful when migrating a v2 addon; see the [V2 to V3 migration guide](/docs/more/v2-v3-migration-guide). | | [`examples/napi-shared`](https://github.com/napi-rs/napi-rs/tree/main/examples/napi-shared) | Sharing `#[napi]` classes and object shapes between Rust crates (consumed by `examples/napi`). | | [`examples/napi-cargo-test`](https://github.com/napi-rs/napi-rs/tree/main/examples/napi-cargo-test) | Testing pure Rust logic with plain `cargo test` — no Node.js — using the `noop` feature on `napi` and `napi-derive`. | | [`examples/binary`](https://github.com/napi-rs/napi-rs/tree/main/examples/binary) | Building a Rust `bin` target with `napi-raw build` instead of a cdylib addon. | | [`examples/custom-async-runtime`](https://github.com/napi-rs/napi-rs/tree/main/examples/custom-async-runtime) | A complete hand-rolled [`AsyncRuntime`](/docs/concepts/async-runtime) backend — scheduler, blocking pool, lifecycle hooks — with threadless `wasm32-wasip1`, [workerd](https://github.com/napi-rs/napi-rs/tree/main/examples/custom-async-runtime/workerd), and [browser](https://github.com/napi-rs/napi-rs/tree/main/examples/custom-async-runtime/browser) targets. | | [`examples/shared-async-runtime`](https://github.com/napi-rs/napi-rs/tree/main/examples/shared-async-runtime) | The end-to-end consumer of the published [`napi-async-runtime`](https://crates.io/crates/napi-async-runtime) crate: install it from `#[module_init]`, then use `async fn`, `sleep_until`, and `spawn_blocking` without Tokio. | ## Inside `examples/napi` The main example crate is organized one file per topic. Quick index: ### Values & types - [`number.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/number.rs), [`string.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/string.rs), [`bigint.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/bigint.rs), [`date.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/date.rs) — primitive conversions, including Latin-1/UTF-16 strings and BigInt. - [`either.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/either.rs) — `Either` through `Either4` union arguments and returns. - [`nullable.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/nullable.rs) — `Option`, `Null`, and `Undefined` handling. - [`map.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/map.rs), [`set.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/set.rs) — object/Map and Set conversions. - [`typed_array.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/typed_array.rs) — buffers, typed arrays, and zero-copy slices (see [TypedArray](/docs/concepts/typed-array)). - [`serde.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/serde.rs) — `serde-json` conversions to and from Rust structs. - [`enum.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/enum.rs), [`type.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/type.rs) — enums and exported type aliases. ### Classes - [`class.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/class.rs), [`constructor.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/constructor.rs), [`class_factory.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/class_factory.rs) — constructors, factories, getters/setters, and class methods (see [Class](/docs/concepts/class)). - [`external.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/external.rs), [`reference.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/reference.rs) — `External` and `Reference` / `WeakReference`. - [`type_tag.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/type_tag.rs) — the unforgeable per-class type tags (see [`type_tag`](/docs/concepts/napi-attributes#type_tag)). - [`object.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/object.rs) — creating and inspecting JavaScript objects. ### Async & threads - [`async.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/async.rs), [`promise.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/promise.rs) — exported `async fn`s and awaiting JavaScript promises (see [async fn](/docs/concepts/async-fn), [Promise](/docs/concepts/promise)). - [`task.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/task.rs) — `Task` / `AsyncTask` on the libuv pool, with `AbortSignal` (see [AsyncTask](/docs/concepts/async-task)). - [`threadsafe_function.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/threadsafe_function.rs) — every `ThreadsafeFunction` flavor (see [ThreadsafeFunction](/docs/concepts/threadsafe-function)). - [`generator.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/generator.rs) — sync and async iterator classes (see [Iterators](/docs/concepts/iterators)). ### Streams & fetch - [`stream.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/stream.rs) — accepting and returning Web `ReadableStream`s, including streams of `#[napi(object)]` structs (see [Web Streams](/docs/concepts/streams)). - [`fetch.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/fetch.rs) — a `fetch` built on `reqwest`, resolving a promise with a Web `Response` via `AsyncBlockBuilder::build_with_map`. ### WASI, Env & scopes - [`wasm.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/wasm.rs) — threadsafe functions and worker patterns that also run on WASI targets (see [WebAssembly](/docs/concepts/webassembly)). - [`env.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/env.rs) — low-level `Env` APIs: `run_script`, module file names, versions. - [`scope.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/scope.rs) — `HandleScope` / `EscapableHandleScope` for loops that create many short-lived values (see [Env](/docs/concepts/env#handlescope-and-escapablehandlescope)). - [`lifetime.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/lifetime.rs) — borrowed vs. owned values across calls (see [Understanding Lifetime](/docs/concepts/understanding-lifetime)). ### Modules, functions & errors - [`js_mod.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/js_mod.rs) — nested JavaScript namespaces via `#[napi] mod` (see [Exports](/docs/concepts/exports#namespaces)). - [`function.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/function.rs), [`callback.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/callback.rs) — the `Function` type and callbacks. - [`fn_strict.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/fn_strict.rs), [`fn_return_if_invalid.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/fn_return_if_invalid.rs), [`fn_ts_override.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/fn_ts_override.rs) — argument validation and TypeScript overrides (see [`#[napi]` attributes](/docs/concepts/napi-attributes)). - [`error.rs`](https://github.com/napi-rs/napi-rs/blob/main/examples/napi/src/error.rs) — throwing and returning errors (see [Errors and panics](/docs/concepts/error-handling)). ## Run them locally The examples live in the napi-rs workspace and build with the in-repo CLI: ```sh git clone https://github.com/napi-rs/napi-rs.git cd napi-rs yarn yarn build # build the @napi-rs/* packages (CLI, wasm-runtime, ...) yarn build:test # build the @examples/* packages with the in-repo CLI cd examples/napi yarn test # ava ``` The WASI and browser variants have their own scripts — see each example's `package.json` (for example `test:wasi` in `examples/shared-async-runtime`, or `test-workerd.mjs` / `browser/runtime.spec.js` in `examples/custom-async-runtime`). --- # Functions and Callbacks in NAPI-RS Callbacks are the heartbeat of JavaScript's ecosystem. Whether you're building a bundler like [`Rolldown`](https://github.com/rolldown/rolldown) and [`Rspack`](https://github.com/web-infra-dev/rspack), creating a database driver, or developing a build tool, you'll need to master the art of calling JavaScript functions from Rust. This guide takes you from basic callbacks to advanced patterns that power production-grade Node.js native addons. ::: info Here is a project that you can download and play with the demo code: https://github.com/napi-rs/callback-example ::: ## Why Callbacks Matter in Native Addons JavaScript's event-driven nature means callbacks are everywhere. When building native addons with NAPI-RS, you'll encounter callbacks in numerous scenarios: ### Real-World Use Cases **🔧 Build Tools & Bundlers** Modern bundlers rely heavily on plugin systems. Each plugin registers callbacks for different build phases: ```javascript // A typical bundler plugin API myBundler.plugin({ name: 'my-plugin', setup(build) { // Transform files during compilation build.onLoad({ filter: /\.tsx?$/ }, async (args) => { const content = await transformTypeScript(args.path) return { contents: content } }) // Report build progress build.onProgress((percentage, message) => { console.log(`${percentage}% - ${message}`) }) // Handle compilation errors build.onError((error) => { notifyDevelopers(error) }) }, }) ``` **📊 Data Processing Pipelines** Stream processing and ETL operations need callbacks for data transformation: ```javascript // Processing large datasets with progress callbacks nativeProcessor.processCSV({ file: 'sales_data.csv', onRow: (row) => validateAndTransform(row), onProgress: (processed, total) => updateProgressBar(processed, total), onComplete: (results) => saveToDatabase(results), onError: (error) => handleProcessingError(error), }) ``` **🗄️ Database Drivers** Native database drivers use callbacks for query results and connection events: ```javascript // Database operations with callbacks db.query('SELECT * FROM users', (err, results) => { if (err) return handleError(err) processResults(results) }) db.on('connection', () => console.log('Connected')) db.on('error', (err) => reconnect()) ``` **🎮 Game Engines & Real-time Systems** Performance-critical applications need callbacks for frame updates and events: ```javascript // Game engine callbacks gameEngine.onUpdate((deltaTime) => { updatePhysics(deltaTime) renderFrame() }) gameEngine.onCollision((objectA, objectB) => { handleCollisionPhysics(objectA, objectB) }) ``` ## The Challenge: Bridging Rust and JavaScript When building these systems in Rust for performance, you face unique challenges: 1. **Lifetime Management**: JavaScript functions can be garbage collected, but Rust needs explicit lifetime guarantees 2. **Thread Safety**: Rust's threading model differs from JavaScript's event loop 3. **Type Safety**: Converting between Rust's strict types and JavaScript's dynamic nature 4. **Performance**: Minimizing overhead when crossing the FFI boundary ::: warning Understanding these challenges is crucial for building stable native addons. Improper callback handling can lead to crashes, memory leaks, or deadlocks in production environments. ::: ## What You'll Learn This guide progressively builds your understanding: - **Part 1**: Basic synchronous callbacks - the foundation - **Part 2**: Function lifetimes and references - solving the GC problem - **Part 3**: ThreadsafeFunction - callbacks across threads - **Part 4**: Building and configuring ThreadsafeFunction - **Part 5**: Advanced patterns and error handling - **Part 6**: Async operations and promises By the end, you'll understand how to build robust callback systems that power applications like [`Rolldown`](https://github.com/rolldown/rolldown) (the Fast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.) and Parcel (the zero-config bundler). Let's start with the fundamentals. ## Part 1: Synchronous Function Callbacks ### Basic Callbacks When you receive a JavaScript function as a parameter, you can call it synchronously within the same function scope: ```rust use napi::bindgen_prelude::*; #[napi] pub fn process_user_data( username: String, callback: Function ) -> Result { // Transform username to uppercase let processed = username.to_uppercase(); // Call the JS callback with the processed data let greeting = callback.call(processed)?; Ok(greeting) } ``` **Generated TypeScript:** ```typescript export declare function processUserData( username: string, callback: (arg: string) => string, ): string ``` **Usage in JavaScript:** ```javascript const result = processUserData('alice', (name) => `Hello, ${name}!`) console.log(result) // Output: "Hello, ALICE!" ``` ### Multiple Arguments with FnArgs For callbacks with multiple arguments, use `FnArgs`: ```rust #[napi] pub fn calculate_salary( base_amount: f64, callback: Function, f64> ) -> Result { let tax = base_amount * 0.2; let bonus = 1000.0; let department = "Engineering".to_string(); // Pass multiple arguments using FnArgs callback.call((base_amount, tax, department).into()) } ``` **Generated TypeScript:** ```typescript export declare function calculateSalary( baseAmount: number, callback: (arg0: number, arg1: number, arg2: string) => number, ): number ``` **Usage:** ```javascript const total = calculateSalary(50000, (base, tax, dept) => { console.log(`Department: ${dept}`) return base - tax + (dept === 'Engineering' ? 5000 : 0) }) console.log(total) // Output: 45000 ``` ## Part 2: Function Lifetime and References ### Understanding Function Scope JavaScript functions passed to Rust only live within the current function call scope. Once your Rust function returns, the JavaScript function becomes invalid and can't be called anymore. This is a safety mechanism - JavaScript's garbage collector needs to know when objects are still in use. ::: danger Never attempt to store a raw `Function` for later use without creating a proper reference. This will cause your application to crash when the function is garbage collected! ::: ```rust // ❌ THIS WON'T WORK - Function becomes invalid after return #[napi] pub fn broken_timer(callback: Function) -> Result<()> { std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_secs(1)); // ERROR: callback is no longer valid here! // The JavaScript function was cleaned up when broken_timer returned callback.call("Too late!".to_string()); // This will crash! }); Ok(()) // Function returns, callback becomes invalid } ``` ### Function References Function references solve this by creating a persistent handle that keeps the JavaScript function alive beyond the current scope. Think of it like telling JavaScript: "Hey, I'm still using this function, don't clean it up yet!" ::: info `FunctionRef` is lightweight and perfect for main-thread async operations. It's the go-to solution when you need to delay a callback but stay within the JavaScript event loop. ::: **When You Need References:** 1. **Async operations**: When spawning futures that will complete later 2. **Delayed callbacks**: Timer-based or event-driven callbacks 3. **Storing for later**: Keeping functions in structs or global state 4. **Thread boundaries**: Before passing to another thread (though ThreadsafeFunction is often better) ```rust use napi::{Env, PromiseRaw}; // ✅ THIS WORKS - Using a reference keeps the function alive #[napi(ts_return_type = "Promise")] pub fn schedule_notification<'env>( env: &'env Env, delay_ms: u32, callback: Function<'env, String, ()> ) -> Result> { // Create a reference to keep the function alive let callback_ref = callback.create_ref()?; env.spawn_future_with_callback( async move { tokio::time::sleep( std::time::Duration::from_millis(delay_ms as u64) ).await; Ok("Notification triggered!".to_string()) }, move |env, message| { // Borrow the function back from reference let callback = callback_ref.borrow_back(env)?; callback.call(message)?; Ok(()) } ) } ``` ### FunctionRef Limitations `FunctionRef` has an important restriction: **you can only borrow it back in contexts where you have access to `Env`**. This means: - ✅ Works in main thread callbacks that provide `Env` - ✅ Works in `spawn_future_with_callback` (provides `Env` in callback) - ❌ Doesn't work in regular `std::thread::spawn` - ❌ Doesn't work in standalone async tasks without `Env` ```rust #[napi] pub fn store_callback(callback: Function) -> Result<()> { let callback_ref = callback.create_ref()?; // ❌ THIS WON'T WORK - No Env in regular thread std::thread::spawn(move || { // Error: Can't borrow_back without Env! // callback_ref.borrow_back(???)?; }); Ok(()) } ``` ## Part 3: ThreadsafeFunction - Cross-Thread Callbacks When you need to call JavaScript from background threads, `ThreadsafeFunction` is the solution. Unlike `FunctionRef`, it's designed specifically for thread safety. ### Basic ThreadsafeFunction ```rust use std::thread; use napi::threadsafe_function::{ ThreadsafeFunction, ThreadsafeFunctionCallMode }; #[napi] pub fn monitor_system_resources( callback: ThreadsafeFunction ) { thread::spawn(move || { for i in 0..5 { let cpu_usage = 40.0 + (i as f64 * 5.0); // Call from background thread callback.call( Ok(cpu_usage), ThreadsafeFunctionCallMode::NonBlocking ); thread::sleep(std::time::Duration::from_secs(1)); } }); } ``` **Generated TypeScript:** ```typescript export declare function monitorSystemResources( callback: (err: Error | null, arg: number) => void, ): void ``` **Usage:** ```javascript monitorSystemResources((err, cpuUsage) => { if (err) { console.error('Error:', err) return } console.log(`CPU Usage: ${cpuUsage}%`) }) ``` ### FunctionRef vs ThreadsafeFunction | Aspect | FunctionRef | ThreadsafeFunction | | ------------------ | ------------------------------- | ---------------------------- | | **Thread safety** | Not thread-safe, needs `Env` | Fully thread-safe | | **Use case** | Async operations on main thread | Cross-thread callbacks | | **Performance** | Lightweight | More overhead | | **Queue** | No queueing | Built-in queue | | **Error handling** | Simple Result | Configurable (fatal/handled) | | **When to use** | Delays/timers in main thread | Background threads, workers | ::: tip **Rule of thumb**: Use `FunctionRef` when staying on the main thread with access to `Env`. Use `ThreadsafeFunction` when crossing thread boundaries. ::: ## Part 4: Building ThreadsafeFunction from Function You can convert a regular `Function` into a `ThreadsafeFunction` using the builder pattern: ```rust #[napi] pub fn start_file_watcher( callback: Function, ()> ) -> Result<()> { // Convert to ThreadsafeFunction with configuration let tsfn = callback .build_threadsafe_function() .max_queue_size::<10>() // Optional: limit queue size .build()?; thread::spawn(move || { let files = vec![ ("config.json", 1024), ("data.csv", 2048), ("index.html", 512) ]; for (filename, size) in files { tsfn.call( Ok((filename.to_string(), size).into()), ThreadsafeFunctionCallMode::Blocking ); thread::sleep(std::time::Duration::from_millis(500)); } }); Ok(()) } ``` ### Builder Options - **`max_queue_size::()`**: Limit the queue to N items (0 = unlimited) - **`weak::()`**: Create weak reference that won't keep process alive - **`callee_handled::()`**: Make errors fatal instead of passing to callback - **`error_status::()`**: Use custom error type ::: tip Set a `max_queue_size` when dealing with high-frequency events to prevent memory exhaustion. This is especially important for monitoring systems or real-time data streams. ::: ## Part 5: ThreadsafeFunction Generic Parameters Understanding the generic parameters helps you use ThreadsafeFunction effectively: ```rust ThreadsafeFunction< T, // Input type passed to call() Return, // Return type from JavaScript CallJsArgs, // Arguments for JS function (usually same as T) ErrorStatus, // Error type (default: Status) CalleeHandled, // true: errors go to callback, false: fatal Weak, // true: weak reference, false: strong MaxQueueSize // Queue size limit (0 = unlimited) > ``` ### Call Modes: Blocking vs NonBlocking - **NonBlocking**: Returns immediately if queue is full - **Blocking**: Waits until space is available in queue ::: warning Be careful with `Blocking` mode in high-throughput scenarios. If the JavaScript event loop can't keep up, your Rust threads will block indefinitely. Consider using `NonBlocking` with proper backpressure handling. ::: ```rust #[napi] pub fn process_events( high_priority: ThreadsafeFunction, low_priority: ThreadsafeFunction ) { thread::spawn(move || { // High priority: block to ensure delivery high_priority.call( Ok("CRITICAL: System alert".to_string()), ThreadsafeFunctionCallMode::Blocking ); // Low priority: drop if queue is full low_priority.call( Ok("INFO: Regular update".to_string()), ThreadsafeFunctionCallMode::NonBlocking ); }); } ``` ### Custom Error Handling ```rust // Custom error type pub struct NetworkError(String); impl AsRef for NetworkError { fn as_ref(&self) -> &str { &self.0 } } impl From for NetworkError { fn from(_: Status) -> Self { NetworkError("Network failure".to_string()) } } #[napi] pub fn download_file_with_progress( url: String, callback: ThreadsafeFunction ) { thread::spawn(move || { for progress in (0..=100).step_by(20) { if progress == 60 { // Simulate network error callback.call( Err(Error::new( NetworkError("Connection lost".to_string()), format!("Failed at {}%", progress) )), ThreadsafeFunctionCallMode::Blocking ); return; } callback.call(progress, ThreadsafeFunctionCallMode::Blocking); thread::sleep(std::time::Duration::from_millis(200)); } }); } ``` ### Weak References Use weak references when the ThreadsafeFunction shouldn't keep the process alive: ::: info Weak references are perfect for optional logging, monitoring, or telemetry callbacks that shouldn't prevent your application from shutting down gracefully. ::: ```rust #[napi] pub fn background_logger( callback: Function ) -> Result<()> { let tsfn = callback .build_threadsafe_function() .weak::() // Won't prevent process exit .build()?; thread::spawn(move || { loop { tsfn.call( Ok("Background log entry".to_string()), ThreadsafeFunctionCallMode::NonBlocking ); thread::sleep(std::time::Duration::from_secs(10)); } }); Ok(()) } ``` ## Part 6: Async Operations with ThreadsafeFunction ThreadsafeFunction supports async/await patterns for bidirectional async communication: ```rust #[napi] pub async fn fetch_user_profile( user_id: u32, callback: ThreadsafeFunction> ) -> Result { // Call async and await the Promise let profile_data = callback.call_async(Ok(format!("user_{}", user_id))).await?; let enhanced_profile = profile_data.await?; Ok(format!("Enhanced: {}", enhanced_profile)) } ``` **Generated TypeScript:** ```typescript export declare function fetchUserProfile( userId: number, callback: (err: Error | null, arg: string) => Promise, ): Promise ``` **Usage:** ```javascript const profile = await fetchUserProfile(123, async (err, userId) => { if (err) throw err // Simulate async database fetch const data = await database.getUser(userId) return data.name }) console.log(profile) // Output: "Enhanced: John Doe" ``` ## Best Practices ::: tip 1. **Choose the right type**: - Use `Function` for sync callbacks within the same scope - Use `FunctionRef` for async operations on the main thread - Use `ThreadsafeFunction` for cross-thread calls 2. **Error handling**: - Set `CalleeHandled` to `false` for critical errors that should terminate - Use custom error types for domain-specific error handling 3. **Performance considerations**: - `FunctionRef` is lightweight for main-thread operations - `ThreadsafeFunction` has overhead but is necessary for thread safety - Set `MaxQueueSize` to prevent memory issues with high-frequency callbacks 4. **Lifecycle management**: - Use weak references for optional callbacks that shouldn't block termination - Strong references keep the process alive until explicitly released 5. **Call modes**: - Use `Blocking` for critical data that must be delivered - Use `NonBlocking` for optional updates that can be dropped ::: ## Further Reading - [Function API Documentation](https://napi.rs/docs/concepts/function) - [ThreadsafeFunction Documentation](https://napi.rs/docs/concepts/threadsafe-function) - [FunctionRef Documentation](https://napi.rs/docs/concepts/reference#functionref) ::: tip Want to see these patterns in action? Check out the source code of [`Rolldown`](https://github.com/rolldown/rolldown) to see how they implement plugin callbacks and build hooks. ::: --- # Announce V3 > 🦀 NAPI-RS v3 - WebAssembly! Safer API design and new cross compilation features. > > 📅 2025/07/07 It has been 4 years since the release of **NAPI-RS** V2. During this time, the **NAPI-RS** community has been developing rapidly. We have identified many problems with the API design in the community. For example, `ThreadsafeFunction` has always been difficult to use. The main reason is that Node-API's ThreadsafeFunction is designed too complexly, which causes the Rust encapsulation to leak too much of the underlying complexity. However, through collaboration with the [`Rolldown`](https://github.com/rolldown/rolldown) and [`Rspack`](https://github.com/web-infra-dev/rspack) teams, we have finally found a design that can balance API complexity and correctness. `WebAssembly` is the biggest update this time. In V3, you can compile your project into `WebAssembly` with almost no code changes. If the compilation target is `wasm32-wasip1-threads` or higher, you can directly run code that uses Rust features like `std::thread` and `tokio` in the browser without any additional modifications. Cross compilation is also a big update. In previous versions, you need to use [`nodejs-rust:lts-debian`](https://github.com/napi-rs/napi-rs/pkgs/container/napi-rs%2Fnodejs-rust/326314378?tag=lts-debian-aarch64) or [`nodejs-rust:lts-debian-aarch64`](https://github.com/napi-rs/napi-rs/pkgs/container/napi-rs%2Fnodejs-rust/326314378?tag=lts-debian-aarch64) docker images to build your project. These images are huge, it slows down the CI build time, and it's hard to sync the tools and infrastructure with the community. Now let's dive into the new features of V3. ## `WebAssembly` Supporting `WebAssembly` means a lot for the **NAPI-RS** community. There are several scenarios that only `WebAssembly` can handle: 1. Provides the playground and reproducible environment in the browser, like [Rolldown repl](https://repl.rolldown.rs/) and [Oxc playground](https://playground.oxc.rs/). 2. Provides fallback packages for platforms that don't have pre-built binaries. For some projects, it's hard to maintain pre-built binaries for all possible platforms. 3. Make the project usable in [`StackBlitz`](https://stackblitz.com/). ### Why don't use [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) instead One of the main reasons is that you don't need to write 2 different bindings for the same project. For example, the [Oxc project maintained a `wasm-bindgen` binding](https://github.com/oxc-project/oxc/blob/oxlint_v0.15.0/crates/oxc_wasm/src/lib.rs) before. However, as the project grew larger, the APIs gradually increased, and the maintenance cost became higher and higher. It was often necessary to port the same logic from the Node.js binding to the `wasm-bindgen` binding. Besides that, using `wasm-bindgen` has many limitations, such as the inability to use `std::thread` and third-party libraries that depend on `std::thread`. For example, you may need to write code like: ```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::>(); #[cfg(target_arch = "wasm32")] const hash = entries.iter().map(|chunk| chunk.compute_hash()).collect::>(); ... ``` With **NAPI-RS**, you can compile the codes and run them without pain. Another pain point is if you are using crates written in `C` or `C++`, setting up the `wasm-bindgen` build process is very complex. See: See [**WebAssembly**](/docs/concepts/webassembly) for more details. ### Sample App using **NAPI-RS** WebAssembly This is a sample app using **NAPI-RS** WebAssembly. You can transform the image to `webp` `jpeg` or `avif` with different quality. ::: info The `webp` feature is coming from the [`libwebp-sys`](https://github.com/NoXF/libwebp-sys) crate. It's using the [`libwebp`](https://github.com/webmproject/libwebp) under the hood. The `libwebp` is a C library, but you can feel free to use it in **NAPI-RS** project, and build it into `WebAssembly` without any additional modifications. The `avif` feature is coming from [libavif](https://github.com/AOMediaCodec/libavif). It's a C/C++ mixed library. You can also feel free to use it in **NAPI-RS** project. ::: ## `API Improvements` There are a lot of improvements in the API design, both usability and security have been improved. ### `lifetime` The lifetime is introduced in **NAPI-RS** V3, see [**Understanding Lifetime**](/docs/concepts/understanding-lifetime) for more details. In **V2**, due to the complexity of designing codegen and APIs, we didn't have time to add lifetimes to the APIs, which led to some issues. 1. Some types have safety issues, such as the previous `JsObject`, which could escape and be used outside the scope of `#[napi] fn` calls, when in fact its underlying `napi_value` had already become invalid. We can now constrain such behavior using Rust's lifetimes 2. `#[napi] struct` previously couldn't contain lifetimes, which caused some usability issues **For example**: **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn promise_finally_callback(mut promise: PromiseRaw<()>, config: Object) -> Result<()> { // ❌ compile Error // borrowed data escapes outside of function // `config` escapes the function body here // lib.rs(5, 62): `config` is a reference that is only valid in the function body // lib.rs(5, 62): has type `napi::bindgen_prelude::Object<'1>` // borrowed data escapes outside of function argument requires that `'1` must outlive `'static` promise.finally(|env| { let on_finally = config.get_named_property::>("on_finally")?; Ok(()) })?; Ok(()) } ``` There is `Reference` API for this case in **V3**, you can see [**JavaScript Value Reference**](/docs/concepts/reference#javascript-value-reference) for more details. ### `ThreadsafeFunction` `ThreadsafeFunction` has been redesigned in **V3**. In previous versions, the API of `ThreadsafeFunction` is too low level, and it's not safe at all. In the new API, we have hidden Node-API concepts such as `ref` `unref`, and `acquire` `release`, using ownership to encapsulate these APIs, and prohibiting lifecycle management using the underlying ref count model. If you want to pass `ThreadsafeFunction` to different threads, we now allow using `std::sync::Arc` to achieve this. **lib.rs** ```rust {10} use std::sync::Arc; use napi::{ bindgen_prelude::*, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}, }; use napi_derive::napi; #[napi] pub fn pass_threadsafe_function(tsf: Arc>) -> Result<()> { for i in 0..100 { let tsf = tsf.clone(); std::thread::spawn(move || { tsf.call(Ok(i), ThreadsafeFunctionCallMode::NonBlocking); }); } Ok(()) } ``` TypeScript type generation for `ThreadsafeFunction` has also been improved, you can only generate `(...args: any[]) => any` type in the previous version, but since **V3** defines the `FnArgs` and `Return` types in the generic, you can now generate the correct type for `ThreadsafeFunction`. The example above will generate the following TypeScript type: **index.d.ts** ```ts export declare function passThreadsafeFunction( tsf: (err: Error | null, arg: number) => number, ): void ``` ### `Function` Like the `ThreadsafeFunction` API, the `Function` has also been redesigned in **V3**. The `JsFunction` API is deprecated in **V3**, the new `Function` API can generate the correct TypeScript types, more safety and easier to use. **For example**: **lib.rs** ```rust use napi::bindgen_prelude::*; use napi_derive::napi; #[napi] pub fn call_function(callback: Function) -> Result { callback.call(1) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export declare function callFunction(callback: (arg: number) => number): number ``` For more details you can see [**Function**](/docs/concepts/function). ## `Cross Compilation` Cross compilation is painful for the **Rust** community. There is the [`rust-cross`](https://github.com/cross-rs/cross) project, but it's not easy to use: 1. The `GLIBC` version for GNU Linux distributions is too new, for example `arm-unknown-linux-gnueabihf` only supports `GLIBC 2.31`. 2. Configuring compilation for the older `GLIBC 2.17` (still a version that many enterprises need to support) is very complex. 3. Runs on `QEMU` and Docker, which is slow and limited. **NAPI-RS** V3 introduces a new cross compilation feature, with the `napi build --use-napi-cross` flag, this is the supports matrix for the cross compilation: | 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 | ✅ | ✅ | All these targets are supported to **GLIBC 2.17**. This is the repo for the cross toolchain. We basically extract the necessary tools and files from the [`manylinux-cross`](https://github.com/rust-cross/manylinux-cross) project, then upload them to the `npm` registry. The `@napi-rs/cli` will then pick the correct toolchain and inject environment variables into the build process. We also integrate `cargo-zigbuild` and `cargo-xwin` in the `@napi-rs/cli`, so you can build many different targets on a single machine. See [**Cross Compilation**](/docs/cross-build) for more details. ## `Brand new pnpm package template` We have supported **yarn** as the package manager for the [`package-template`](https://github.com/napi-rs/package-template) project since **V2**, because yarn's [supportedArchitectures](https://yarnpkg.com/configuration/yarnrc#supportedArchitectures) feature is very friendly to cross-platform compilation and testing, and it has good Docker support. As `pnpm` becomes popular, we also support [package-template-pnpm](https://github.com/napi-rs/package-template-pnpm) in **V3**. ### **Why there is no `npm` package template?** Because of this issue: The `npm` team spent several years resolving this critical issue for native addons, although the issue itself wasn't complex. While this issue has been fixed in `npm` 11, the Node.js team encountered other problems when upgrading to `npm` 11, resulting in both `Node.js` LTS versions and default Docker images still using `npm` 10, which contains this bug. ::: info From this issue, we can see that the `npm` team does not prioritize native addon scenarios, so currently **NAPI-RS** neither supports nor recommends using `npm` as a package manager. It's hard to say whether the `npm` team will fix similar critical issues in a timely manner in the future. ::: ## `@napi-rs/cli` API You can now easily integrate the **NAPI-RS** tools into your JavaScript infra: ```ts // Programmatically 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 ``` All napi commands have corresponding APIs, you can visit [`cli`](/docs/cli/build) to learn more. ## Community is growing fast! When **V2** was released, only [Next.js](https://nextjs.org/), [Parcel](https://parceljs.org/), [SWC](https://swc.rs/) were using **NAPI-RS**. Today, **NAPI-RS** has been widely used in developing various types of applications. [Cursor](https://www.cursor.com/) is using **NAPI-RS** to build their Desktop and Node.js server high performance addons. is using **NAPI-RS** to build their `oxide` high performance engine. Tailwind CSS is also one of our platinum sponsors! is using **NAPI-RS** for their Electron Desktop App and Node.js server high performance addons. is using **NAPI-RS** to build their Electron Desktop App crypto components. At the same time, with the rise of AI, **NAPI-RS** has also begun participating in the development of AI tools. For example, is a vector database that uses **NAPI-RS** to provide a Node.js embedded experience; **Chroma** is an open-source search and retrieval database for AI applications. [Tokenizers](https://github.com/huggingface/tokenizers) is a tokenizer library developed by **Hugging Face**. TensorZero is an open-source stack for industrial-grade LLM applications, they also use **NAPI-RS** to build their `tensorzero-node` client. In the frontend build field, **NAPI-RS** has been widely used. Almost all Bundler use **NAPI-RS** to improve their performance: - Rolldown - Rollup - Rspack - Parcel **Monorepo tools:** - - Nx And Oxc provides Linter, Transformer and all kinds of apis via **NAPI-RS**. TypeScript team is exploring to use **NAPI-RS** to build API layer for the [`typescript-go`](https://github.com/microsoft/typescript-go) project.
and **Bun** have improved their [**Node-API**](https://nodejs.org/api/n-api.html) compatibility, so almost all **NAPI-RS** projects can run in **Deno** and **Bun**. ## Calling for sponsorship **NAPI-RS** will continue to improve the development experience, and it requires more time and effort to maintain the project. Please consider sponsoring the project - it will help us improve the project and make it better. --- # Announce V2 > 🦀 NAPI-RS v2 - [Faster 🚀](https://github.com/Brooooooklyn/rust-to-nodejs-overhead-benchmark) , 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. Work for `v2` started on [Aug 10, 2021](https://github.com/napi-rs/napi-rs/pull/696) 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** ```rust use napi_derive::napi; #[napi] fn sum(a: u32, b: u32) -> u32 { a + b } ``` **v1** ```rust 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 { let a = ctx.get::(0)?.get_uint32()?; let b = ctx.get::(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`](https://github.com/napi-rs/node-rs/tree/main/packages/helper) to help you load the right native addon. But this package has many problem with the existing JavaScript toolchain. Like [#316](https://github.com/napi-rs/node-rs/issues/316) and [#491](https://github.com/napi-rs/node-rs/issues/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`. **lib.rs** ```rust {6} use futures::prelude::*; use napi::bindgen_prelude::*; use tokio::fs; #[napi] async fn read_file_async(path: String) -> Result { 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** ```ts export function readFileAsync(path: string): Promise ``` ### Await Promise in the Rust This sounds crazy, but you can do it in **NAPI-RS**! **lib.rs** ```rust use napi::bindgen_prelude::*; #[napi] pub async fn async_plus_100(p: Promise) -> Result { let v = p.await?; Ok(v + 100) } ``` **test.mjs** ```js {4} 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` 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`](https://github.com/PyO3/pyo3/blob/main/examples/maturin-starter/src/lib.rs) and [`node-bindgen`](https://github.com/infinyon/node-bindgen#javascript-class), you can define a class in Rust with `struct` and `#[napi]` macro. **lib.rs** ```rust // 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 { self.engine.query(query).await } #[napi(getter)] pub fn status(&self) -> napi::Result { self.engine.status() } #[napi(setter)] pub fn count(&mut self, count: u32) { self.engine.count = count; } } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export class QueryEngine { static withInitialCount(count: number): QueryEngine constructor() query(query: string): Promise get status(): number set count(count: number) } ``` See [`class`](../docs/concepts/class) for more details. ### Rust `enum` into JavaScript `Object` **lib.rs** ```rust #[napi] enum Kind { Duck, Dog, Cat, } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export const enum Kind { Duck, Dog, Cat, } ``` ### exports Rust `const` **lib.rs** ```rust #[napi] pub const DEFAULT_COST: u32 = 12; ``` **index.d.ts** ```ts export const DEFAULT_COST: number ``` ### Abortable `AsyncTask` **lib.rs** ```rust 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 { Ok(fib(self.input)) } fn resolve(&mut self, env: Env, output: u32) -> Result { enc.create_uint32(output) } } #[napi] fn async_fib(input: u32, signal: Option) -> AsyncTask { AsyncTask::with_optional_signal(AsyncFib { input }, signal) } ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **index.d.ts** ```ts export function asyncFib(input: number, signal?: AbortSignal | null) => Promise ``` ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ **test.mjs** ```js {6} import { asyncFib } from './index.js' const controller = new AbortController() asyncFib(20, controller.signal).catch((e) => { console.error(e) // Error: AbortError }) controller.abort() ``` See [`AsyncTask`](../docs/concepts/async-task) for more details. ### Support export Rust `mod` as JavaScript `Object` **lib.rs** ```rust #[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** ```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](https://github.com/rust-lang/rust/commit/60fe8b3a65be709fe2163b8ab438ef14209055cc). ### `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. ```diff struct BufferLength(Ref); impl Task for BufferLength { type Output = usize; type JsValue = JsNumber; fn compute(&mut self) -> Result { Ok(self.0.len() + 1) } - fn resolve(self, env: Env, output: Self::Output) -> Result { - self.0.unref(env)?; + fn resolve(&mut self, env: Env, output: Self::Output) -> Result { env.create_uint32(output as u32) } - fn reject(self, err: Error) -> Result { - 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`: ```diff - 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](https://github.com/denoland/deno/issues/12577#issuecomment-977570758) for the context. ## **Thanks** [yiliuliuyi](https://github.com/forehalo) for initiating the `v2` alpha version. And most of the `#[napi]` macro was implemented by him. [Jared Palmer](https://github.com/jaredpalmer) for reviewing the full documentation and the blog. [`node-bindgen`](https://github.com/infinyon/node-bindgen) [`neon`](https://github.com/neon-bindings/neon) and [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) inspiring many of API designs in the `v2`. ::: tip 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 ✨: