# 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](#1-project-structure-and-key-packagescrates) 2. [Setting Up a New napi-rs Project](#2-setting-up-a-new-napi-rs-project) 3. [Core Patterns for Exposing Rust to JavaScript](#3-core-patterns-for-exposing-rust-to-javascript) 4. [Result/Option Types and Error Propagation](#4-resultoption-types-and-error-propagation) 5. [Async Support (Promises, async functions)](#5-async-support-promises-async-functions) 6. [Build System Configuration](#6-build-system-configuration) 7. [@napi-rs/cli Tool and Multi-Platform Builds](#7-napi-rscli-tool-and-multi-platform-builds) 8. [Thread-Safe Function Patterns (tsfn)](#8-thread-safe-function-patterns-tsfn) 9. [Serde/Serialization with napi-rs Types](#9-serdeserialization-with-napi-rs-types) 10. [Version Compatibility Notes](#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` struct, `Result`, `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 ### Recommended: `napi new` (Scaffolding) The `@napi-rs/cli` provides a `new` command that generates a fully configured project: ```sh napi new [options] ``` **Available options:** | Option | CLI Flag | Default | Description | |--------|----------|---------|-------------| | `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:** ```sh napi new ./my-addon --name my-addon --min-node-api 4 ``` ### Alternative: package-template The GitHub repository [napi-rs/package-template](https://github.com/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: ```rust 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 ```rust #[napi] pub struct Animal { #[napi(readonly)] pub kind: Kind, name: String, optional_value: Option, } #[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) ```rust #[napi(object)] struct AllOptionalObject { pub name: Option, pub age: Option, } ``` **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) ```rust #[napi] #[derive(Debug, Clone, Copy)] pub enum Kind { Dog, // 0 Cat, // 1 Duck, // 2 } ``` Custom discriminant values with step resolution: ```rust #[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 ```rust #[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) ```rust #[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: ```rust #[napi(transparent)] struct MyVec(Vec>); #[napi] fn get_my_vec() -> MyVec { MyVec(vec![Either::A(42), Either::B("a string".to_owned())]) } ``` ### 3.5 Constants ```rust #[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) ```rust #[napi] pub fn get_cwd Result<()>>(callback: T) { callback(std::env::current_dir().unwrap().to_string_lossy().to_string()).unwrap(); } #[napi] pub fn test_callback(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 ```rust #[napi( ts_generic_types = "T", ts_args_type = "functionInput: () => T | Promise, callback: (err: Error | null, result: T) => void", ts_return_type = "T | Promise" )] fn callback_return_promise<'env>(/* ... */) -> Result> { /* ... */ } ``` ### 3.8 Either Types napi-rs provides `Either`, `Either3`, and `Either4` for union types: ```rust #[napi] fn either_string_or_number(input: Either) -> u32 { match input { Either::A(s) => s.len() as u32, Either::B(n) => n, } } #[napi] fn receive_class_or_number(either: Either) -> u32 { match either { Either::A(n) => n + 1, Either::B(_) => 100, } } #[napi] pub async fn promise_in_either(input: Either>) -> Result { 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]`: ```rust #[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: ```rust pub type Result = std::result::Result>; ``` Returning `Result` from a `#[napi]` function causes errors to be thrown as JavaScript errors. Returning `Ok(value)` resolves normally. ### 4.2 The `Error` Struct ```rust pub struct Error = Status> { pub status: S, pub reason: String, pub cause: Option>, maybe_raw: sys::napi_ref, maybe_env: sys::napi_env, } ``` **Creating errors:** ```rust // 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` and `From`: ```rust pub enum CustomError { NapiError(Error), Panic, } impl AsRef 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` - `From` - `From` (with `error_anyhow` feature) - `From` (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: ```rust #[napi(catch_unwind)] pub fn panic() { panic!("Don't panic"); } ``` ### 4.7 Option Type Mapping | Rust `Option` | JavaScript | |------------------|-----------| | `Option` | `number \| undefined` | | `Option` | `string \| undefined` | | `None` | `undefined` (or `null` depending on context) | | `Option` where Struct is a class | `Struct \| null` | Null and Undefined are explicit types: ```rust #[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`: ```rust // Cargo.toml: napi = { version = "3", features = ["async"] } #[napi] async fn read_file_async(path: String) -> Result { Ok(tokio::fs::read(path).await?.into()) } #[napi] async fn async_multi_two(arg: u32) -> Result { 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: ```rust #[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: ```rust #[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: ```rust pub struct DelaySum(u32, u32); #[napi] impl napi::Task for DelaySum { type Output = u32; type JsValue = u32; fn compute(&mut self) -> Result { // 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 { Ok(output) } fn finally(self, _env: napi::Env) -> Result<()> { Ok(()) } } #[napi] pub fn with_abort_controller(a: u32, b: u32, signal: AbortSignal) -> AsyncTask { 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: ```rust // Await a Promise from JavaScript #[napi] pub async fn async_plus_100(p: Promise) -> Result { 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::resolve(env, value) } #[napi] pub fn create_rejected_promise<'env>(env: &'env Env, message: String) -> Result> { PromiseRaw::reject(env, Error::from_reason(message)) } // Chain .then/.catch/.finally #[napi] pub fn call_then_on_promise(input: PromiseRaw) -> Result> { input.then(|v| Ok(format!("{}", v.value))) } #[napi] pub fn call_catch_on_promise(input: PromiseRaw<'_, u32>) -> Result> { input.catch(|e: CallbackContext| Ok(e.value)) } ``` ### 5.6 Spawning Futures Manually ```rust 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 ```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` 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()`: ```rust 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) ```json { "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 | Promise", "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 ```sh napi build [--release] [--platform] [--target ] [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](https://github.com/cross-rs/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: ```sh 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): ```sh napi universalize ``` 3. **Create npm directories** for per-platform packages: ```sh napi create-npm-dirs ``` 4. **Artifacts** -- Collect `.node` files from GitHub Actions CI: ```sh napi artifacts --output-dir ./artifacts ``` 5. **Pre-publish** -- Copy platform-specific `.node` files into per-platform npm packages: ```sh 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 ```rust use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; #[napi] pub fn call_threadsafe_function( tsfn: Arc>, ) -> Result<()> { for n in 0..100 { let tsfn = tsfn.clone(); thread::spawn(move || { tsfn.call(Ok(n), ThreadsafeFunctionCallMode::NonBlocking); }); } Ok(()) } ``` ### 8.2 Type Parameters ```rust 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 + From, // 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: ```rust // Callee-handled: JS receives (null, value) on success, (error, undefined) on failure let tsfn: ThreadsafeFunction; tsfn.call(Ok(42), ThreadsafeFunctionCallMode::NonBlocking); ``` **Caller-handled** (`CalleeHandled = false`): No error-first argument. On error, calls `napi_fatal_exception`: ```rust // Fatal mode: JS receives just (value), no error-first convention let tsfn: ThreadsafeFunction; tsfn.call(42, ThreadsafeFunctionCallMode::NonBlocking); ``` ### 8.4 Async Call with Return Value ```rust #[napi] pub async fn tsfn_return_promise(func: ThreadsafeFunction>) -> Result { let val = func.call_async(Ok(1)).await?.await?; Ok(val + 2) } ``` ### 8.5 Call with Return Value Callback ```rust #[napi] pub fn tsfn_call_with_callback(tsfn: ThreadsafeFunction<(), String>) -> napi::Result<()> { tsfn.call_with_return_value( Ok(()), ThreadsafeFunctionCallMode::NonBlocking, |value: Result, _| { 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: ```rust #[napi] pub fn build_threadsafe_function_from_function( callback: Function, u32>, ) -> Result<()> { let tsfn = callback.build_threadsafe_function().build()?; let tsfn_fatal = callback .build_threadsafe_function() .callee_handled::() .build()?; let tsfn_max_queue = callback .build_threadsafe_function() .max_queue_size::<1>() .build()?; let tsfn_weak = callback .build_threadsafe_function() .weak::() .build()?; Ok(()) } ``` ### 8.7 Custom Error Status ```rust pub struct ErrorStatus(String); impl AsRef for ErrorStatus { fn as_ref(&self) -> &str { &self.0 } } impl From for ErrorStatus { fn from(value: Status) -> Self { ErrorStatus(value.to_string()) } } #[napi] pub fn threadsafe_function_throw_error_with_status( cb: ThreadsafeFunction, ) -> 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: ```rust #[napi] pub async fn tsfn_weak( tsfn: ThreadsafeFunction<(), (), (), Status, false, true>, ) -> napi::Result<()> { tsfn.call_async(()).await } ``` ### 8.9 Tuple Arguments ```rust #[napi] pub fn accept_threadsafe_function_tuple_args( func: ThreadsafeFunction>, ) { 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: ```toml [dependencies] napi = { version = "3", features = ["serde-json"] } ``` This adds: - `impl ser::Error for Error` and `impl de::Error for Error` - `impl From 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 ```rust #[napi(object)] #[derive(Serialize, Deserialize, Debug)] struct PackageJson { pub name: String, pub version: String, pub dependencies: Option>, #[serde(rename = "devDependencies")] pub dev_dependencies: Option>, } #[napi] fn read_package_json() -> Result { 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 ```rust #[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 ```rust #[derive(Serialize, Debug, Deserialize)] struct BytesObject { #[serde(with = "serde_bytes")] code: Vec, } #[napi] fn test_serde_buffer_bytes(obj: Object, env: Env) -> napi::Result { let obj: BytesObject = env.from_js_value(obj)?; Ok(obj.code.len()) } ``` ### 9.5 Class with Serde ```rust #[napi] struct PackageJsonReader { i: Value, } #[napi] impl PackageJsonReader { #[napi(constructor)] pub fn new() -> Result { 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` fields that should be serialized as binary data (Buffer in JS), use `serde_bytes`: ```rust #[derive(Serialize, Deserialize)] struct BytesObject { #[serde(with = "serde_bytes")] code: Vec, } ``` ### 9.7 Ordered JSON Use `serde-json-ordered` feature to preserve JSON key insertion order: ```toml 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` | Array | 1 | -- | | `Buffer` | Buffer | 1 | -- | | `External` | External | 1 | -- | | `Null` | null | 1 | -- | | `Undefined` / `()` | undefined | 1 | -- | | `T: Fn(...) -> Result` | 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` | `Array` | | `Buffer` / `Uint8Array` | `Buffer` / `Uint8Array` | | `Object` | `object` | | `Option` | `T \| undefined` (or `T \| null`) | | `Either` | `A \| B` | | `Either3` | `A \| B \| C` | | `Either4` | `A \| B \| C \| D` | | `Result` | throw/rethrow | | `Promise` | `Promise` | | `Function` | `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*