37 KiB
NAPI-RS Framework Research Report
Comprehensive research on the napi-rs project based on the source at
/workspace/napi-rsand supplementary documentation from https://napi.rs
Table of Contents
- Project Structure and Key Packages/Crates
- Setting Up a New napi-rs Project
- Core Patterns for Exposing Rust to JavaScript
- Result/Option Types and Error Propagation
- Async Support (Promises, async functions)
- Build System Configuration
- @napi-rs/cli Tool and Multi-Platform Builds
- Thread-Safe Function Patterns (tsfn)
- Serde/Serialization with napi-rs Types
- 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, definesbindgen_prelude/crates/napi/src/error.rs--Error<S>struct,Result<T, S>,JsError/JsTypeError/JsRangeError/crates/napi/src/threadsafe_function.rs--ThreadsafeFunctionimplementation (869 lines)/crates/napi/src/tokio_runtime.rs-- Tokio runtime management andexecute_tokio_future/crates/napi/src/task.rs--TaskandScopedTasktraits for async work on libuv threads/crates/napi/src/async_work.rs--AsyncWorkPromisefor 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.tsgeneration 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:
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:
Cargo.tomlwithcrate-type = ["cdylib"]build.rscallingnapi_build::setup()package.jsonwith@napi-rs/clidevDependency and napi config- 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.,
fibonaccistaysfibonacci,my_funcbecomesmyFunc) - 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)]-- EnableObjectFinalizetrait 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)]-- GeneratenullableTypeScript types instead ofoptional#[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 typests_return_type-- Override function return typets_generic_types-- Add generic type parametersts_type-- Override field type in objectsskip_typescript-- Skip generation in.d.tsjs_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>(witherror_anyhowfeature)From<serde_json::Error>(withserde-jsonfeature)
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
asyncfeature (which includestokio_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--resolvetakesEnvby value; output and JsValue must be'staticScopedTask<'task>--resolvetakes&'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:
- Sets
cargo:rerun-if-env-changedfor various NAPI environment variables - On macOS: adds linker flags
-Wl,-undefined,dynamic_lookup(needed because Node.js symbols are resolved at runtime, not link time) - On Windows (GNU): configures GNU toolchain linker settings
- On Android/WASI: platform-specific setup
- On GNU libc / FreeBSD: adds
-Wl,-z,nodeleteto 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:
-
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 -
Universalize (macOS only -- combine arm64 + x64 into a single universal binary):
napi universalize -
Create npm directories for per-platform packages:
napi create-npm-dirs -
Artifacts -- Collect
.nodefiles from GitHub Actions CI:napi artifacts --output-dir ./artifacts -
Pre-publish -- Copy platform-specific
.nodefiles 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 Errorandimpl de::Error for Errorimpl From<serde_json::Error> for Errorenv.from_js_value()andenv.to_js_value()for serde types- Automatic (de)serialization between
serde_json::Valueand JS values - Automatic (de)serialization between
serde_json::Mapand 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-runtimeand@emnapi/runtimepackages 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_unstablecfg is used for WASM-specific tokio configuration
10.7 Breaking Changes Notes
- Version 3.x uses declarative
#[ctor]for module registration (as of commitba6597b3) compat-modefeature enables deprecated v2 types/traits- The
ThreadSafeCallContexttype was renamed toThreadsafeCallContext(v2.17.0+) - Manual
refer()/unref()/abort()methods on ThreadsafeFunction are deprecated; useClone/Dropinstead
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