Files
taskgraph_ts/docs/research/napi_rs_framework.md

37 KiB

NAPI-RS Framework Research Report

Comprehensive research on the napi-rs project based on the source at /workspace/napi-rs and supplementary documentation from https://napi.rs


Table of Contents

  1. Project Structure and Key Packages/Crates
  2. Setting Up a New napi-rs Project
  3. Core Patterns for Exposing Rust to JavaScript
  4. Result/Option Types and Error Propagation
  5. Async Support (Promises, async functions)
  6. Build System Configuration
  7. @napi-rs/cli Tool and Multi-Platform Builds
  8. Thread-Safe Function Patterns (tsfn)
  9. Serde/Serialization with napi-rs Types
  10. Version Compatibility Notes

1. Project Structure and Key Packages/Crates

The napi-rs repository at /workspace/napi-rs is a monorepo using Cargo workspaces (Rust) and Yarn workspaces (JavaScript). The root Cargo.toml defines the workspace members.

Rust Crates (/crates/)

Crate Path Version Purpose
napi crates/napi/ 3.8.5 Main runtime library. Provides the high-level Node-API bindings, type conversions, error types, async runtime, thread-safe functions, and all bindgen_prelude types.
napi-sys crates/sys/ 3.2.1 Low-level FFI bindings. Raw napi_* C function declarations and type definitions. Uses libloading for dynamic symbol resolution.
napi-derive crates/macro/ 3.5.4 Procedural macro crate. Provides the #[napi] attribute macro that is the primary way to expose Rust code to JavaScript.
napi-derive-backend crates/backend/ 5.0.3 Code generation backend for napi-derive. Handles AST parsing, Rust-to-JS codegen, and TypeScript type definition generation.
napi-build crates/build/ 2.3.1 Build script utilities. Called from build.rs to configure linker flags for each platform (macOS dynamic_lookup, Android, WASI, Windows GNU).

JavaScript Packages

Package Path Version Purpose
@napi-rs/cli cli/ 3.6.2 CLI tool for scaffolding, building, packaging, and publishing napi-rs projects.
@examples/napi examples/napi/ (private) Comprehensive test suite showcasing all napi-rs features.

Key Source Files

  • /crates/napi/src/lib.rs -- Main library entry; re-exports modules, defines bindgen_prelude
  • /crates/napi/src/error.rs -- Error<S> struct, Result<T, S>, JsError/JsTypeError/JsRangeError
  • /crates/napi/src/threadsafe_function.rs -- ThreadsafeFunction implementation (869 lines)
  • /crates/napi/src/tokio_runtime.rs -- Tokio runtime management and execute_tokio_future
  • /crates/napi/src/task.rs -- Task and ScopedTask traits for async work on libuv threads
  • /crates/napi/src/async_work.rs -- AsyncWorkPromise for libuv-based async tasks
  • /crates/napi/src/bindgen_runtime/ -- Core trait implementations: ToNapiValue, FromNapiValue, TypeName, ValidateNapiValue, class registration, module registration, iterator support
  • /crates/backend/src/typegen.rs -- TypeScript .d.ts generation logic (981 lines)
  • /crates/macro/src/lib.rs -- #[napi], #[module_init], #[module_exports] proc macros
  • /crates/build/src/lib.rs -- Platform-specific linker configuration

2. Setting Up a New napi-rs Project

The @napi-rs/cli provides a new command that generates a fully configured project:

napi new <path> [options]

Available options:

Option CLI Flag Default Description
path <path> -- Directory where project is created
name --name,-n directory name Project name
minNodeApiVersion --min-node-api,-v 4 Minimum N-API version
packageManager --package-manager yarn Package manager (yarn 4.x only for now)
license --license,-l MIT License
targets --targets,-t [] Compilation targets
enableDefaultTargets --enable-default-targets true Enable default platform targets
enableAllTargets --enable-all-targets false Enable all platform targets
enableTypeDef --enable-type-def true Auto-generate TypeScript definitions
enableGithubActions --enable-github-actions true Generate GitHub Actions CI workflow
testFramework --test-framework ava JS test framework (ava only for now)

Example:

napi new ./my-addon --name my-addon --min-node-api 4

Alternative: package-template

The GitHub repository napi-rs/package-template is the canonical template referenced in the README.

Minimal Manual Setup

If not using scaffolding, a minimal napi-rs project requires:

  1. Cargo.toml with crate-type = ["cdylib"]
  2. build.rs calling napi_build::setup()
  3. package.json with @napi-rs/cli devDependency and napi config
  4. Rust source with #[napi] annotated functions

(See Section 6 for full configuration details.)


3. Core Patterns for Exposing Rust to JavaScript

3.1 Functions

The #[napi] attribute on functions exposes them as JavaScript functions:

use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn fibonacci(n: u32) -> u32 {
    match n {
        1 | 2 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

Key behaviors:

  • Function name is converted to camelCase in JavaScript (e.g., fibonacci stays fibonacci, my_func becomes myFunc)
  • JSDoc comments (///) on Rust functions become TypeScript documentation
  • The #[napi(js_name = "...")] attribute allows custom JavaScript naming

3.2 Structs as JavaScript Classes

There are two patterns for structs:

a) Struct with #[napi] -- JavaScript Class

#[napi]
pub struct Animal {
    #[napi(readonly)]
    pub kind: Kind,
    name: String,
    optional_value: Option<i32>,
}

#[napi]
impl Animal {
    #[napi(constructor)]
    pub fn new(kind: Kind, name: String) -> Self {
        Animal { kind, name, optional_value: None }
    }

    #[napi(factory)]
    pub fn with_kind(kind: Kind) -> Self {
        Animal { kind, name: "Default".to_owned(), optional_value: None }
    }

    #[napi(getter)]
    pub fn get_name(&self) -> &str { self.name.as_str() }

    #[napi(setter)]
    pub fn set_name(&mut self, name: String) { self.name = name; }

    #[napi(getter, js_name = "type")]
    pub fn kind(&self) -> Kind { self.kind }

    #[napi]
    pub fn whoami(&self) -> String { /* ... */ }

    #[napi]
    pub fn get_dog_kind() -> Kind { Kind::Dog }  // static method
}

Key attributes on struct:

  • #[napi(constructor)] -- All public fields become constructor parameters; a default constructor is generated
  • #[napi(js_name = "Assets")] -- Rename in JavaScript
  • #[napi(custom_finalize)] -- Enable ObjectFinalize trait for custom cleanup

Key attributes on impl methods:

  • #[napi(constructor)] -- Mark as constructor
  • #[napi(factory)] -- Static factory method
  • #[napi(getter)] / #[napi(setter)] -- Define getters/setters
  • #[napi(js_name = "type")] -- Custom JS name
  • #[napi(ts_arg_type = "...")] -- Override TypeScript arg type
  • #[napi(skip_typescript)] -- Exclude from TypeScript definitions
  • #[napi(writable = false)] -- Make property read-only in JS
  • #[napi(catch_unwind)] -- Catch panics and convert to JS errors

b) Struct with #[napi(object)] -- JavaScript Plain Object (Interface)

#[napi(object)]
struct AllOptionalObject {
    pub name: Option<String>,
    pub age: Option<u32>,
}

Key attributes:

  • #[napi(object)] -- Generate as a TypeScript interface, not a class
  • #[napi(object, object_to_js = false)] -- Only deserialize from JS (input-only)
  • #[napi(object, object_from_js = false)] -- Only serialize to JS (output-only)
  • #[napi(object, use_nullable = true)] -- Generate nullable TypeScript types instead of optional
  • #[napi(ts_type = "object")] -- Override TypeScript field type
  • #[napi(js_name = "customField")] -- Rename field in JS

3.3 Enums

a) Numeric Enums (default)

#[napi]
#[derive(Debug, Clone, Copy)]
pub enum Kind {
    Dog,   // 0
    Cat,   // 1
    Duck,  // 2
}

Custom discriminant values with step resolution:

#[napi]
pub enum CustomNumEnum {
    One = 1,   // 1
    Two,       // 2
    Three = 3, // 3
    Four,      // 4
    Six = 6,
    Eight = 8,
    Nine,  // 9
    Ten,   // 10
}

b) String Enums

#[napi(string_enum)]
pub enum Status {
    Pristine,  // "Pristine"
    Loading,   // "Loading"
    Ready,     // "Ready"
}

#[napi(string_enum = "lowercase")]
pub enum StringEnum {
    VariantOne,   // "variantone"
    VariantTwo,   // "varianttwo"
}

#[napi(string_enum)]
pub enum CustomStringEnum {
    #[napi(value = "my-custom-value")]
    Foo,        // "my-custom-value"
    Bar,        // "Bar"
    Baz,        // "Baz"
}

c) Structured Enums (Tagged Unions)

#[napi(discriminant = "type2")]
pub enum StructuredKind {
    Hello,
    Greeting { name: String },
    Birthday { name: String, age: u8 },
    Tuple(u32, u32),
}

#[napi(discriminant_case = "lowercase")]
pub enum StructuredKindLowercase {
    Hello,           // { type2: "hello" }
    Greeting { name: String },  // { type2: "greeting", name: "..." }
}

3.4 Transparent Types

Newtype pattern that wraps an existing JS-compatible type:

#[napi(transparent)]
struct MyVec(Vec<Either<u32, String>>);

#[napi]
fn get_my_vec() -> MyVec {
    MyVec(vec![Either::A(42), Either::B("a string".to_owned())])
}

3.5 Constants

#[napi]
/// This is a const
pub const DEFAULT_COST: u32 = 12;

#[napi(skip_typescript)]
pub const TYPE_SKIPPED_CONST: u32 = 12;

3.6 Callbacks (Fn/FnMut/FnOnce traits)

#[napi]
pub fn get_cwd<T: Fn(String) -> Result<()>>(callback: T) {
    callback(std::env::current_dir().unwrap().to_string_lossy().to_string()).unwrap();
}

#[napi]
pub fn test_callback<T>(callback: T) -> Result<()>
where
    T: Fn(String) -> Result<()>,
{
    callback(std::env::current_dir()?.to_string_lossy().to_string())
}

3.7 TypeScript Customization

The #[napi] macro supports several attributes for TypeScript generation:

  • ts_args_type -- Override all function argument types
  • ts_return_type -- Override function return type
  • ts_generic_types -- Add generic type parameters
  • ts_type -- Override field type in objects
  • skip_typescript -- Skip generation in .d.ts
  • js_name -- Rename in JS/TS
#[napi(
    ts_generic_types = "T",
    ts_args_type = "functionInput: () => T | Promise<T>, callback: (err: Error | null, result: T) => void",
    ts_return_type = "T | Promise<T>"
)]
fn callback_return_promise<'env>(/* ... */) -> Result<Unknown<'env>> { /* ... */ }

3.8 Either Types

napi-rs provides Either<A, B>, Either3<A, B, C>, and Either4<A, B, C, D> for union types:

#[napi]
fn either_string_or_number(input: Either<String, u32>) -> u32 {
    match input {
        Either::A(s) => s.len() as u32,
        Either::B(n) => n,
    }
}

#[napi]
fn receive_class_or_number(either: Either<u32, &JsClassForEither>) -> u32 {
    match either {
        Either::A(n) => n + 1,
        Either::B(_) => 100,
    }
}

#[napi]
pub async fn promise_in_either(input: Either<u32, Promise<u32>>) -> Result<bool> {
    match input {
        Either::A(a) => Ok(a > 10),
        Either::B(b) => {
            let r = b.await?;
            Ok(r > 10)
        }
    }
}

3.9 Module Exports / Module Init

Custom module initialization can be done with #[napi(module_exports)] or #[napi_derive::module_init]:

#[napi(module_exports)]
pub fn exports(mut export: Object) -> Result<()> {
    let symbol = Symbol::for_desc("NAPI_RS_SYMBOL");
    export.set_named_property("NAPI_RS_SYMBOL", symbol)?;
    Ok(())
}

// For custom tokio runtime:
#[napi_derive::module_init]
fn init() {
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();
    create_custom_tokio_runtime(rt);
}

4. Result/Option Types and Error Propagation

4.1 The Result Type

napi-rs defines its own Result type alias:

pub type Result<T, S = Status> = std::result::Result<T, Error<S>>;

Returning Result<T> from a #[napi] function causes errors to be thrown as JavaScript errors. Returning Ok(value) resolves normally.

4.2 The Error Struct

pub struct Error<S: AsRef<str> = Status> {
    pub status: S,
    pub reason: String,
    pub cause: Option<Box<Error>>,
    maybe_raw: sys::napi_ref,
    maybe_env: sys::napi_env,
}

Creating errors:

// Standard error with status code
Err(Error::new(Status::InvalidArg, "Manual Error".to_owned()))

// Error with cause (chained errors)
let mut err = Error::new(Status::GenericFailure, "Manual Error".to_owned());
err.set_cause(Error::new(Status::InvalidArg, "Inner Error".to_owned()));
Err(err)

// From a reason string (uses GenericFailure status)
Error::from_reason("something went wrong")

4.3 Error Type Variants

napi-rs provides specialized error types for different JavaScript error classes:

Rust Type JavaScript Equivalent
JsError Error
JsTypeError TypeError
JsRangeError RangeError
JsSyntaxError (napi9) SyntaxError

4.4 Custom Error Status

You can define custom error status types by implementing AsRef<str> and From<Status>:

pub enum CustomError {
    NapiError(Error<Status>),
    Panic,
}

impl AsRef<str> for CustomError {
    fn as_ref(&self) -> &str {
        match self {
            CustomError::Panic => "Panic",
            CustomError::NapiError(e) => e.status.as_ref(),
        }
    }
}

#[napi]
pub fn custom_status_code() -> Result<(), CustomError> {
    Err(Error::new(CustomError::Panic, "don't panic"))
}

4.5 Automatic Conversions from std::io::Error and Others

The Error struct implements From for common Rust error types:

  • From<std::io::Error>
  • From<std::ffi::NulError>
  • From<anyhow::Error> (with error_anyhow feature)
  • From<serde_json::Error> (with serde-json feature)

This allows using ? operator to propagate standard Rust errors.

4.6 Catch Unwind

The #[napi(catch_unwind)] attribute catches Rust panics and converts them to JavaScript errors:

#[napi(catch_unwind)]
pub fn panic() {
    panic!("Don't panic");
}

4.7 Option Type Mapping

Rust Option<T> JavaScript
Option<u32> number | undefined
Option<String> string | undefined
None undefined (or null depending on context)
Option<Struct> where Struct is a class Struct | null

Null and Undefined are explicit types:

#[napi]
fn return_null() -> Null { Null }

#[napi]
fn return_undefined() -> Undefined {}

For objects, #[napi(object, use_nullable = true)] generates nullable TS types (e.g., string | null) instead of optional (e.g., string | undefined).


5. Async Support (Promises, async functions)

5.1 Async Functions (tokio-based)

With the async feature enabled, any async fn annotated with #[napi] returns a JavaScript Promise:

// Cargo.toml: napi = { version = "3", features = ["async"] }

#[napi]
async fn read_file_async(path: String) -> Result<Buffer> {
    Ok(tokio::fs::read(path).await?.into())
}

#[napi]
async fn async_multi_two(arg: u32) -> Result<u32> {
    tokio::task::spawn(async move { Ok(arg * 2) })
        .await
        .unwrap()
}

Requirements:

  • Enable the async feature (which includes tokio_rt)
  • napi-rs manages a Tokio runtime internally (multi-threaded by default)
  • Async errors are properly propagated as rejected promises

5.2 Custom Tokio Runtime

You can provide a custom Tokio runtime configuration:

#[napi_derive::module_init]
fn init() {
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .thread_stack_size(32 * 1024 * 1024)
        .build()
        .unwrap();
    napi::bindgen_prelude::create_custom_tokio_runtime(rt);
}

5.3 async_runtime Attribute

Run a synchronous function inside the async runtime context:

#[napi(async_runtime)]
pub fn within_async_runtime_if_available() {
    tokio::spawn(async {
        println!("within_runtime_if_available");
    });
}

5.4 AsyncTask (libuv thread pool)

For CPU-intensive work that should run on the libuv thread pool rather than the Tokio runtime, use the Task trait:

pub struct DelaySum(u32, u32);

#[napi]
impl napi::Task for DelaySum {
    type Output = u32;
    type JsValue = u32;

    fn compute(&mut self) -> Result<Self::Output> {
        // Runs on libuv thread pool
        std::thread::sleep(std::time::Duration::from_millis(100));
        Ok(self.0 + self.1)
    }

    fn resolve(&mut self, _env: napi::Env, output: Self::Output) -> Result<Self::JsValue> {
        Ok(output)
    }

    fn finally(self, _env: napi::Env) -> Result<()> {
        Ok(())
    }
}

#[napi]
pub fn with_abort_controller(a: u32, b: u32, signal: AbortSignal) -> AsyncTask<DelaySum> {
    AsyncTask::with_signal(DelaySum(a, b), signal)
}

Task vs ScopedTask:

  • Task -- resolve takes Env by value; output and JsValue must be 'static
  • ScopedTask<'task> -- resolve takes &'task Env; JsValue can borrow from the env (e.g., BufferSlice<'task>, Array<'task>)

5.5 Promise and PromiseRaw

Work directly with JavaScript Promise objects:

// Await a Promise from JavaScript
#[napi]
pub async fn async_plus_100(p: Promise<u32>) -> Result<u32> {
    let v = p.await?;
    Ok(v + 100)
}

// Create resolved/rejected promises
#[napi]
pub fn create_resolved_promise<'env>(env: &'env Env, value: u32) -> Result<PromiseRaw<'env, u32>> {
    PromiseRaw::resolve(env, value)
}

#[napi]
pub fn create_rejected_promise<'env>(env: &'env Env, message: String) -> Result<PromiseRaw<'env, u32>> {
    PromiseRaw::reject(env, Error::from_reason(message))
}

// Chain .then/.catch/.finally
#[napi]
pub fn call_then_on_promise(input: PromiseRaw<u32>) -> Result<PromiseRaw<String>> {
    input.then(|v| Ok(format!("{}", v.value)))
}

#[napi]
pub fn call_catch_on_promise(input: PromiseRaw<'_, u32>) -> Result<PromiseRaw<'_, String>> {
    input.catch(|e: CallbackContext<String>| Ok(e.value))
}

5.6 Spawning Futures Manually

env.spawn_future(async move { Ok(some_value) })
env.spawn_future_with_callback(async move { Ok(some_value) }, |env, val| {
    env.create_string(format!("{}", val))
})

6. Build System Configuration

6.1 Cargo.toml

[package]
name = "my-addon"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
napi = { version = "3", features = ["napi4"] }  # or more features
napi-derive = "3"

[build-dependencies]
napi-build = "2"

[profile.release]
lto = true

Critical: crate-type = ["cdylib"] is required so cargo builds a C-style shared library that Node can dynamically load.

6.2 Feature Flags on napi Crate

Feature Requires Description
napi1 through napi10 incremental Progressive N-API version support
async / tokio_rt napi4 Tokio runtime + async fn support
serde-json -- serde Serialize/Deserialize for JS <-> Rust
serde-json-ordered serde-json Preserves key order with serde_json/preserve_order
latin1 -- Latin1 string decoding via encoding_rs
chrono_date napi5 chrono::DateTime support
error_anyhow -- From<anyhow::Error> for napi::Error
web_stream napi4, tokio_rt Web Streams API support
deferred_trace napi4 Deferred stack trace
object_indexmap -- indexmap::IndexMap support
tracing -- tracing crate integration
dyn-symbols -- Dynamic symbol resolution (default)
compat-mode -- Deprecated types/traits for v2 compatibility
noop -- Generate no-op code (for testing)

Common feature combinations:

  • Minimal: napi = { version = "3", default-features = false, features = ["napi4"] }
  • With async: napi = { version = "3", features = ["napi4", "async"] }
  • Full: napi = { version = "3", features = ["full"] } (includes napi10, async, serde-json, experimental, chrono_date, latin1)

6.3 napi-derive Features

Feature Description
type-def (default) Auto-generate TypeScript .d.ts definitions
strict (default) Strict type checking in macro expansion
compat-mode Deprecated attribute compatibility
tracing Tracing in macro expansion
noop Generate no-op code

6.4 build.rs

Every napi-rs project must have a build.rs that calls napi_build::setup():

fn main() {
    napi_build::setup();
}

What napi_build::setup() does:

  1. Sets cargo:rerun-if-env-changed for various NAPI environment variables
  2. On macOS: adds linker flags -Wl,-undefined,dynamic_lookup (needed because Node.js symbols are resolved at runtime, not link time)
  3. On Windows (GNU): configures GNU toolchain linker settings
  4. On Android/WASI: platform-specific setup
  5. On GNU libc / FreeBSD: adds -Wl,-z,nodelete to prevent DSO unloading issues with pthread_key_create destructors

6.5 package.json (napi config)

{
  "name": "my-addon",
  "devDependencies": {
    "@napi-rs/cli": "^3.0.0"
  },
  "napi": {
    "name": "jarvis",
    "binaryName": "example",
    "wasm": {
      "initialMemory": 16384,
      "browser": { "fs": true, "buffer": true }
    },
    "dtsHeader": "type MaybePromise<T> = T | Promise<T>",
    "dtsHeaderFile": "./dts-header.d.ts",
    "targets": ["wasm32-wasip1-threads"]
  },
  "scripts": {
    "build": "napi build --release",
    "build:debug": "napi build",
    "build:platform": "napi build --platform"
  }
}

The napi.name / napi.binaryName field determines the output .node file name. The naming convention converts hyphens to underscores: my-addon -> my_addon.node.


7. @napi-rs/cli Tool and Multi-Platform Builds

7.1 CLI Version: 3.6.2

The @napi-rs/cli package (at /workspace/napi-rs/cli/) is the primary tooling for building, packaging, and releasing napi-rs projects.

7.2 Commands Overview

Command Description
napi new Create a new project with pre-configured boilerplate
napi build Build the napi-rs project
napi create-npm-dirs Create per-platform npm package directories
napi artifacts Copy build artifacts from GitHub Actions
napi rename Rename the project
napi universalize Combine binaries into a universal binary (e.g., macOS arm64 + x64)
napi version Update version across per-platform npm packages
napi pre-publish Prepare packages for npm publish

7.3 Build Command Details

napi build [--release] [--platform] [--target <triple>] [options]

Key options:

Option Description
--target,-t Target triple (passed to cargo build --target)
--platform Add platform triple suffix (e.g., .linux-x64-gnu.node)
--release,-r Build in release mode
--js Path/filename for generated JS binding
--no-js Disable JS binding generation
--dts Path/filename for generated TypeScript definitions
--strip,-s Strip debug symbols for minimum file size
--cross-compile,-x Cross-compile using cargo-xwin / cargo-zigbuild
--use-cross Use cross instead of cargo
--use-napi-cross Use @napi-rs/cross-toolchain for Linux ARM/ARM64/x64 GNU
--watch,-w Watch and rebuild continuously
--features,-F Space-separatedCargo features to activate
--output-dir,-o Output directory for built files
--esm Generate ESM JS binding instead of CJS
--const-enum Generate const enums in TypeScript

7.4 Multi-Platform Build Workflow

The typical cross-compilation workflow:

  1. Build for each target:

    napi build --platform --target x86_64-unknown-linux-gnu
    napi build --platform --target aarch64-apple-darwin
    napi build --platform --target x86_64-pc-windows-msvc
    
  2. Universalize (macOS only -- combine arm64 + x64 into a single universal binary):

    napi universalize
    
  3. Create npm directories for per-platform packages:

    napi create-npm-dirs
    
  4. Artifacts -- Collect .node files from GitHub Actions CI:

    napi artifacts --output-dir ./artifacts
    
  5. Pre-publish -- Copy platform-specific .node files into per-platform npm packages:

    napi pre-publish --npm-dir npm
    

7.5 Supported Platforms

Platform Architectures Variants
Windows x64, x86, arm64 MSVC, GNU
macOS x64, aarch64 -
Linux x64, aarch64, arm, riscv64, s390x, ppc64le, loong64 gnu, musl, gnueabihf, musleabihf
FreeBSD x64 -
Android aarch64, armv7 -

8. Thread-Safe Function Patterns (tsfn)

Thread-safe functions (TSFNs) are the mechanism for calling JavaScript from background threads. They are the cornerstone of async and concurrent interop in napi-rs.

8.1 Basic Usage

use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};

#[napi]
pub fn call_threadsafe_function(
    tsfn: Arc<ThreadsafeFunction<u32, UnknownReturnValue>>,
) -> Result<()> {
    for n in 0..100 {
        let tsfn = tsfn.clone();
        thread::spawn(move || {
            tsfn.call(Ok(n), ThreadsafeFunctionCallMode::NonBlocking);
        });
    }
    Ok(())
}

8.2 Type Parameters

ThreadsafeFunction<
    T: 'static,                    // Input type (what you send from Rust)
    Return: FromNapiValue,          // Return type from JS callback
    CallJsBackArgs: JsValuesTupleIntoVec,  // Arguments passed to JS callback
    ErrorStatus: AsRef<str> + From<Status>,  // Custom error status
    const CalleeHandled: bool,      // Whether callback follows (err, result) pattern
    const Weak: bool,               // Weak reference (won't prevent event loop exit)
    const MaxQueueSize: usize,       // Max queued calls (0 = unlimited)
>

8.3 Caller-Handled vs Callee-Handled

Callee-handled (CalleeHandled = true, default): Follows Node.js callback convention, prepending null as the first arg on success:

// Callee-handled: JS receives (null, value) on success, (error, undefined) on failure
let tsfn: ThreadsafeFunction<u32, UnknownReturnValue>;
tsfn.call(Ok(42), ThreadsafeFunctionCallMode::NonBlocking);

Caller-handled (CalleeHandled = false): No error-first argument. On error, calls napi_fatal_exception:

// Fatal mode: JS receives just (value), no error-first convention
let tsfn: ThreadsafeFunction<u32, (), u32, Status, false>;
tsfn.call(42, ThreadsafeFunctionCallMode::NonBlocking);

8.4 Async Call with Return Value

#[napi]
pub async fn tsfn_return_promise(func: ThreadsafeFunction<u32, Promise<u32>>) -> Result<u32> {
    let val = func.call_async(Ok(1)).await?.await?;
    Ok(val + 2)
}

8.5 Call with Return Value Callback

#[napi]
pub fn tsfn_call_with_callback(tsfn: ThreadsafeFunction<(), String>) -> napi::Result<()> {
    tsfn.call_with_return_value(
        Ok(()),
        ThreadsafeFunctionCallMode::NonBlocking,
        |value: Result<String>, _| {
            let value = value.expect("Failed to retrieve value from JS");
            println!("{}", value);
            Ok(())
        },
    );
    Ok(())
}

8.6 Building from a Function

The builder pattern allows customizing TSFN behavior:

#[napi]
pub fn build_threadsafe_function_from_function(
    callback: Function<FnArgs<(u32, u32)>, u32>,
) -> Result<()> {
    let tsfn = callback.build_threadsafe_function().build()?;
    let tsfn_fatal = callback
        .build_threadsafe_function()
        .callee_handled::<true>()
        .build()?;
    let tsfn_max_queue = callback
        .build_threadsafe_function()
        .max_queue_size::<1>()
        .build()?;
    let tsfn_weak = callback
        .build_threadsafe_function()
        .weak::<true>()
        .build()?;
    Ok(())
}

8.7 Custom Error Status

pub struct ErrorStatus(String);
impl AsRef<str> for ErrorStatus {
    fn as_ref(&self) -> &str { &self.0 }
}
impl From<Status> for ErrorStatus {
    fn from(value: Status) -> Self { ErrorStatus(value.to_string()) }
}

#[napi]
pub fn threadsafe_function_throw_error_with_status(
    cb: ThreadsafeFunction<bool, UnknownReturnValue, bool, ErrorStatus>,
) -> Result<()> {
    thread::spawn(move || {
        cb.call(
            Err(Error::new(ErrorStatus("CustomErrorStatus".to_string()), "ThrowFromNative".to_owned())),
            ThreadsafeFunctionCallMode::Blocking,
        );
    });
    Ok(())
}

8.8 Weak Threadsafe Functions

Weak TSFNs do not prevent the Node.js event loop from exiting:

#[napi]
pub async fn tsfn_weak(
    tsfn: ThreadsafeFunction<(), (), (), Status, false, true>,
) -> napi::Result<()> {
    tsfn.call_async(()).await
}

8.9 Tuple Arguments

#[napi]
pub fn accept_threadsafe_function_tuple_args(
    func: ThreadsafeFunction<FnArgs<(u32, bool, String)>>,
) {
    thread::spawn(move || {
        func.call(
            Ok((1, false, "NAPI-RS".into()).into()),
            ThreadsafeFunctionCallMode::NonBlocking,
        );
    });
}

9. Serde/Serialization with napi-rs Types

9.1 Feature Flag

Enable serde-json feature:

[dependencies]
napi = { version = "3", features = ["serde-json"] }

This adds:

  • impl ser::Error for Error and impl de::Error for Error
  • impl From<serde_json::Error> for Error
  • env.from_js_value() and env.to_js_value() for serde types
  • Automatic (de)serialization between serde_json::Value and JS values
  • Automatic (de)serialization between serde_json::Map and JS objects

9.2 Using #[napi(object)] with Serde

#[napi(object)]
#[derive(Serialize, Deserialize, Debug)]
struct PackageJson {
    pub name: String,
    pub version: String,
    pub dependencies: Option<Map<String, Value>>,
    #[serde(rename = "devDependencies")]
    pub dev_dependencies: Option<Map<String, Value>>,
}

#[napi]
fn read_package_json() -> Result<PackageJson> {
    let raw = fs::read_to_string("package.json")?;
    let p: PackageJson = serde_json::from_str(&raw)?;
    Ok(p)
}

When a struct has both #[napi(object)] and #[derive(Serialize, Deserialize)], napi-rs uses serde for bidirectional conversion. The struct acts as a TypeScript interface with automatic serialization from JS objects and deserialization to JS objects.

9.3 Direct serde_json::Value

#[napi]
fn test_serde_roundtrip(data: Value) -> Value {
    data  // serde_json::Value <-> JavaScript any
}

#[napi]
fn test_serde_big_number_precision(number: String) -> Value {
    let data = format!("{{\"number\":{}}}", number);
    serde_json::from_str(&data).unwrap()
}

9.4 Manual Serde with env.from_js_value / env.to_js_value

#[derive(Serialize, Debug, Deserialize)]
struct BytesObject {
    #[serde(with = "serde_bytes")]
    code: Vec<u8>,
}

#[napi]
fn test_serde_buffer_bytes(obj: Object, env: Env) -> napi::Result<usize> {
    let obj: BytesObject = env.from_js_value(obj)?;
    Ok(obj.code.len())
}

9.5 Class with Serde

#[napi]
struct PackageJsonReader {
    i: Value,
}

#[napi]
impl PackageJsonReader {
    #[napi(constructor)]
    pub fn new() -> Result<Self> {
        let raw = fs::read_to_string("package.json")?;
        Ok(Self { i: serde_json::from_str(&raw)? })
    }

    #[napi]
    pub fn read(&self) -> &Value {
        &self.i
    }
}

9.6 serde_bytes Support

For Vec<u8> fields that should be serialized as binary data (Buffer in JS), use serde_bytes:

#[derive(Serialize, Deserialize)]
struct BytesObject {
    #[serde(with = "serde_bytes")]
    code: Vec<u8>,
}

9.7 Ordered JSON

Use serde-json-ordered feature to preserve JSON key insertion order:

napi = { version = "3", features = ["serde-json-ordered"] }

10. Version Compatibility Notes

10.1 Current Checkout Version

Based on git log and Cargo.toml files at /workspace/napi-rs:

Component Version
napi crate 3.8.5
napi-sys crate 3.2.1
napi-derive crate 3.5.4
napi-derive-backend crate 5.0.3
napi-build crate 2.3.1
@napi-rs/cli 3.6.2
Git tag napi-v3.8.5
Rust MSRV 1.88.0

10.2 Versioning Scheme

napi-rs uses a monorepo with independent crate versioning. The major version of the napi crate (v3) and napi-derive (v3) should match. napi-build is v2.x. The npm CLI package follows its own SemVer (v3.6.x).

10.3 N-API Version Matrix

napi-rs supports N-API versions 1 through 10 via feature flags:

Feature N-API Version Min Node.js Key Capabilities
napi1 1 v8.0.0 Basic types, functions
napi2 2 v8.10.0 Thread-safe functions (experimental)
napi3 3 v9.11.0 Cleanup hooks
napi4 4 v10.6.0 Thread-safe functions (stable), tokio_rt
napi5 5 v10.17.0 / v12.0.0 Date
napi6 6 v10.7.0 / v12.0.0 BigInt
napi7 7 v10.12.0 Detached array buffers
napi8 8 v10.23+ / v12.23+ Async cleanup hooks
napi9 9 v14.21+ / v16.17+ SyntaxError, object property management
napi10 10 v18.17.0 create_object_with_properties

10.4 Type Conversion Table

From the README features table:

Rust Type JavaScript Type N-API Version Feature Flag
u32 Number 1 --
i32 / i64 Number 1 --
f64 Number 1 --
bool Boolean 1 --
String / &str String 1 --
Latin1String String 1 latin1
UTF16String String 1 --
Object Object 1 --
serde_json::Map Object 1 serde-json
serde_json::Value any 1 serde-json
Array Array 1 --
Vec<T> Array 1 --
Buffer Buffer 1 --
External<T> External 1 --
Null null 1 --
Undefined / () undefined 1 --
T: Fn(...) -> Result<T> Function 1 --
Async/Future Promise 4 async
AsyncTask Promise 1 --
JsGlobal global 1 --
JsSymbol Symbol 1 --
Int8Array/Uint8Array... TypedArray 1 --
JsFunction threadsafe function 4 napi4
BigInt BigInt 6 napi6

10.5 Electron Support

napi-rs works in Electron. The tokio runtime is designed to handle Electron renderer process environment recycling (Node env exits and recreates on window reload). The start_async_runtime() and shutdown_async_runtime() functions manage the runtime lifecycle.

10.6 WebAssembly Support

napi-rs has experimental WASM support via the wasm32-wasip1-threads target. Key notes:

  • The @napi-rs/wasm-runtime and @emnapi/runtime packages provide the WASM runtime
  • Build with: napi build --platform --target wasm32-wasip1-threads
  • Some features (like tokio/net, file I/O) are conditionally compiled out for WASM
  • tokio_unstable cfg is used for WASM-specific tokio configuration

10.7 Breaking Changes Notes

  • Version 3.x uses declarative #[ctor] for module registration (as of commit ba6597b3)
  • compat-mode feature enables deprecated v2 types/traits
  • The ThreadSafeCallContext type was renamed to ThreadsafeCallContext (v2.17.0+)
  • Manual refer()/unref()/abort() methods on ThreadsafeFunction are deprecated; use Clone/Drop instead

Appendix: Type Conversion Quick Reference

Primitive Types

Rust TypeScript
u32, i32, f64 number
i64, u64 number (or BigInt with napi6)
bool boolean
String, &str string
() void
Null null
Undefined undefined

Compound Types

Rust TypeScript
Vec<T> Array<T>
Buffer / Uint8Array Buffer / Uint8Array
Object object
Option<T> T | undefined (or T | null)
Either<A, B> A | B
Either3<A, B, C> A | B | C
Either4<A, B, C, D> A | B | C | D
Result<T> throw/rethrow
Promise<T> Promise<T>
Function<Args, Return> Function
serde_json::Value any

Class Patterns

Rust Attribute TypeScript
#[napi] struct class
#[napi(object)] struct interface
#[napi(transparent)] struct same as inner type
#[napi] enum numeric enum
#[napi(string_enum)] enum string union type
#[napi(discriminant = "type")] enum tagged union / discriminated union

Report generated from source at /workspace/napi-rs (commit ba6597b3, tag napi-v3.8.5) and https://napi.rs