Skip to content

类型转换

每个导出参数都必须实现 FromNapiValue(或某个引用转换 trait),每个返回值都必须实现 ToNapiValue。生成的 TypeScript 类型是有用的文档,但真正决定转换是否可用的是 Rust trait 实现。

本参考描述 napi-rs v3 的 bindgen 运行时。有关 JsStringJsObject 等底层句柄,请参阅 Env 与底层值

方向图例

标记 含义
JS → Rust 该类型可用作导出函数的参数。
Rust → JS 该类型可以返回或赋给 JavaScript 值。
作用域内 Rust 值借用了 Node-API 环境或 JavaScript 回调作用域,不能逃逸出该作用域。
拥有所有权 转换会创建或保留 Rust 拥有的数据;在满足该类型 Send 规则的前提下,它可以比回调存活更久。

WARNING

TypeScript 映射不代表两个转换方向都可用。例如,u64 会生成 bigint,但只能用于输出; 接受任意 JavaScript bigint 时应使用 BigInt,以便检查窄化转换是否无损。

原始值

Rust 类型 JavaScript / TypeScript 方向 所有权与注意事项 特性 / 最低 Node-API
() / Undefined undefined;函数返回会变为 void 双向 零大小标记。启用 strict 时,输入必须是 undefined 基础 API
Null null 双向 显式 null 标记。普通输入转换会接受并丢弃任意值;启用 strict 时,输入必须是 null 基础 API
bool boolean 双向 复制。 基础 API
i8u8i16u16i32u32 number 双向 整数转换;JavaScript 仍以 Number 存储。 基础 API
f32 number Rust → JS 会扩展为 JavaScript 双精度数;没有 FromNapiValue 实现。输入请使用 f64 基础 API
f64 number 双向 JavaScript Number 是 IEEE-754 双精度浮点数。 基础 API
i64 number 双向 使用 Node-API 的有符号 64 位 Number 转换。超出 JavaScript 安全整数范围的值可能丢失精度。 基础 API
BigInt bigint 双向 保留一个符号位和小端序 u64 字。其 getter 会报告窄化转换是否无损。 napi6
u64u128i128usizeisizei64n bigint Rust → JS 仅限输出,以免任意 JavaScript BigInt 被静默窄化。 napi6
String string 双向 拥有所有权的 UTF-8 字符串。 基础 API
&str string Rust → JS 仅限借用的 Rust 输出;JavaScript 字符串不能作为 &str 传入。输入请使用 String 基础 API
Utf16String string 双向 拥有 UTF-16 码元;适用于需要精确保留 UTF-16 表示的场景。 基础 API
Latin1String string 双向 拥有 Latin-1 字节。将其格式化为 UTF-8 需要 latin1 基础 API;解码/显示需要 latin1
OsStringPathBuf string 双向 拥有所有权。Windows 使用 UTF-16 并保留未配对代理项。Unix 输出遇到非 Unicode 路径时会拒绝,而不会替换字节。 基础 API
&OsStr&Path string Rust → JS 借用输出。平台注意事项与拥有所有权的形式相同。 基础 API
Symbol symbol 双向 普通输入转换会丢弃该值,不保留身份或描述。#[napi(strict)] 会先校验它确实是 symbol,但仍不会保留。返回 Symbol 时会根据 Rust 描述符状态创建一个 symbol。要保留现有值,请使用作用域内的 JsSymbolSymbol::for_desc 需要 Node-API 9。 基础 API;全局 symbol 需要 napi9

i64 特意映射到 number,而 i64n 映射到 bigint。只有当 JavaScript API 确实要暴露 BigInt 时,才应优先使用该包装类型。

lib.rs
rust
#[napi]
pub fn inspect_bigint(value: BigInt) -> Result<u64> {
  let (negative, narrowed, lossless) = value.get_u64();
  if negative || !lossless {
    return Err(Error::from_reason("value does not fit in u64"));
  }
  Ok(narrowed)
}

上面的返回类型是 bigint,因为 u64 是 BigInt 输出类型。

Optionnullundefined

Option<T> 有意采用非对称映射:

位置 接受或生成的 JavaScript 生成的 TypeScript
函数参数 Tnullundefined;两个 nullish 值都会变为 None T | null | undefined,并且通常是可选的尾部参数
函数返回值 Some(T) 变为 TNone 变为 null T | null
使用默认 use_nullable = false#[napi(object)] 或结构化形状字段 缺失或 undefined 变为 None;显式 null 会交给内部 T 转换,通常失败;输出时省略 None field?: T
使用 use_nullable = true#[napi(object)] 或结构化形状字段 缺失或 undefined 会报错;null 变为 NoneNone 输出为 null field: T | null
公开类字段 访问器始终存在。Option getter 对 None 输出 null,可写 setter 接受普通的 nullish Option 输入。use_nullable 改变生成的属性/构造函数形状,而不决定访问器是否存在。 默认:field?: T;启用 use_nullablefield: T | null

当区分本身就是 API 的一部分时,请使用 NullUndefined。只接受一种 nullish 值时,请使用 Either<T, Null>Either<T, Undefined>

lib.rs
rust
#[napi]
pub fn optional_name(value: Option<String>) -> Option<String> {
  value.filter(|name| !name.is_empty())
}

#[napi]
pub fn null_but_not_undefined(value: Either<String, Null>) -> bool {
  matches!(value, Either::B(Null))
}

INFO

非尾部的可选参数可能会生成必需的联合类型,以确保其后的必需参数仍可在 TypeScript 中调用。该联合仍然接受 undefinednull

使用 Either 表示联合类型

Either<A, B> 一直到 Either26<A, ..., Z> 都会映射为 TypeScript 联合类型。输入时,napi-rs 按从左到右的顺序,用每个类型的 ValidateNapiValue 实现测试变体,然后转换第一个匹配项。

lib.rs
rust
#[napi]
pub fn normalize_id(value: Either<u32, String>) -> String {
  match value {
    Either::A(number) => number.to_string(),
    Either::B(text) => text,
  }
}
index.d.ts
ts
export function normalizeId(value: number | string): string

如果多个备选类型可能重叠,请从最具体到最宽泛排序。例如,普通 Object 的校验无法证明完整的对象 schema。Either 是运行时联合类型,并不是会在任意用户代码运行后回溯的 serde 风格无标签枚举。

数组、元组、map 与 set

Rust 类型 JavaScript / TypeScript 方向 转换行为 特性
Vec<T> Array<T> 双向 复制/转换每个元素。输入要求每个元素都实现 FromNapiValue 基础 API
[T; N] Array<T> Rust → JS 创建 JavaScript 数组。 基础 API
Rust 元组(不超过生成代码支持的元数) TypeScript 元组 / JS Array 双向 输入长度必须至少等于元组长度;逐个转换索引元素。 基础 API
Array<'env> unknown[] JS → Rust,并可在作用域内透传 getget_refsetinsert 的作用域句柄;避免预先转换整个数组。 基础 API
HashMap<K, V>BTreeMap<K, V> Record<K, V> / 普通对象 双向 使用自身可枚举的字符串键属性。这不是 JavaScript Map。键必须可以与字符串互相转换。 基础 API
IndexMap<K, V> Record<K, V> / 普通对象 双向 使用同样的自身可枚举字符串键属性形状;在 JavaScript 属性规则允许的范围内保留 Rust 插入顺序。 object_indexmap
HashSet<T>BTreeSet<T> Set<T> 双向 构造或迭代真正的 JavaScript Set 基础 API
IndexSet<T> Set<T> 双向 保留插入顺序的 Rust set。 object_indexmap

Vec<T> 和集合转换的复杂度为 O(n)。如果需要增量访问而不是拥有所有权的副本,请使用作用域内的 ArrayObject、typed-array 视图或 stream。

对象、类与自定义形状

Rust 类型或声明 JavaScript / TypeScript 方向 所有权 / 身份
Object<'env> object 作用域内双向 直接的作用域句柄。每次属性访问都会跨越 Node-API 边界。
ObjectRef object 双向 持有 Node-API 引用,因此对象可以比回调存活更久。
Unknown<'env> unknown 作用域内双向 未检查的作用域句柄;请显式检查/强制转换。
#[napi(object)] struct 普通对象 / interface object_from_jsobject_to_js 控制,二者默认都启用 JavaScript 输入会转换为新的、拥有所有权的 Rust 结构体。修改它不会修改源对象。
#[napi] struct JavaScript 类 通过类引用和实例 保留原生类身份。方法接收 &self/&mut self;公开字段会成为访问器。
ClassInstance<'env, T> T 的实例 JS → Rust / 作用域输出 当对象字段或集合需要 JavaScript 类实例本身时使用。
#[napi(transparent)] struct Wrapper(T) T 相同的表示 按方向控制 Rust newtype,不创建 JavaScript 包装对象。
#[napi(array)] 元组结构体 JavaScript 数组 / TypeScript 元组 按方向控制 有名称的 Rust 类型,使用位置式 JavaScript 表示。
结构化 #[napi] enum 可辨识对象联合 按方向控制 拥有所有权的转换;判别字段默认为 type

不要把类当作对象形状。普通的可读写公开类字段需要为 getter 提供输出转换,并为 setter 提供输入转换;readonly 字段只需要输出转换,而跳过的类字段没有访问器。对于嵌套类值,请接受 &TClassInstance<T>,或使用 Array::get_refVec<T> 需要拥有所有权的 FromNapiValue 实现,因此不适合接收类实例列表。

有关不同形状的示例,请参阅对象枚举#[napi] 属性

Buffer、ArrayBuffer 与 typed array

Rust 类型 JavaScript / TypeScript 方向 生命周期与数据行为 特性 / 最低 Node-API
BufferSlice<'env> Node.js Buffer 在同步作用域内双向 用于同步代码的可变借用视图。不要跨越 await 持有它。 基础 API
Buffer Node.js Buffer 双向 保留对 JavaScript 拥有的数据的引用,适用于异步使用。克隆后仍引用相同的底层 buffer。 使用 napi4 可获得最佳生命周期处理
ArrayBuffer<'env> ArrayBuffer JS → Rust,并可在作用域内透传 与环境绑定的借用字节。 基础 API
Int8ArrayUint8Array、… 对应的 typed array 双向 拥有所有权/保留引用的包装类型,适用于异步使用。 BigInt array 变体需要 napi6
Int8ArraySlice<'env>Uint8ArraySlice<'env>、… 对应的 typed array 作用域内双向 用于同步代码的借用视图。 BigInt array 变体需要 napi6
&[i8]&[u8]&[i16]、… 对应的 typed array 同步回调内 JS → Rust 借用切片;不能比回调存活更久。 BigInt 切片需要 napi6

当运行时接受外部后备存储时,外部 Buffer 和 ArrayBuffer 可以实现零拷贝。运行时也可能拒绝外部 buffer;此时 BufferSlice::from_data 等构造函数会回退为复制。不要承诺在所有 Node 兼容运行时上都能实现零拷贝。参阅 Typed array理解生命周期

Date 与 serde JSON

Rust 类型 JavaScript / TypeScript 方向 特性 / 注意事项
Date (JsDate) Date 作用域内底层值 napi5
chrono::DateTime<Tz>NaiveDateTime Date 双向 chrono_date,会启用 chrononapi5;精度由自 epoch 起的毫秒数决定。
serde_json::Value 与 JSON 兼容的 JavaScript 值 双向 serde-json;拒绝函数、undefined、symbol 和 external 值。
serde_json::Map<String, Value> 普通对象 双向 serde-json
serde_json::Number 根据值和启用的 API,可能是 Number、BigInt 或 string 双向 serde-json;启用 napi6 后,超出安全整数范围的整数会输出为 BigInt。

serde_json::Value 不能无损表示任意 JavaScript 值。特别是,大型输入 BigInt 在适合时可能变成 JSON number,否则变成十进制字符串。如果 BigInt 身份和精确的窄化规则很重要,请使用 BigInt

serde-json-ordered 还会启用 serde_json 的 preserve_order 行为。

函数、Promise 与 stream

Rust 类型 JavaScript / TypeScript 方向 生命周期 / 特性
Function<'env, Args, Return> 有类型的 JavaScript 函数 作用域内 JS → Rust;可在作用域内透传 只能在其所属线程调用 JavaScript。多个位置参数请使用 FnArgs<(...)>
FunctionRef<Args, Return> 有类型的 JavaScript 函数 JS → Rust / 保留引用 拥有 Node-API 引用,但不实现 ToNapiValue。要传回 JavaScript,请调用 borrow_back(env) 获得作用域内 Function;仍只能在所属环境/线程中使用。
ThreadsafeFunction<...> 有类型的回调 JS → Rust,随后可从其他线程调用 napi4;参阅 ThreadsafeFunction
Promise<T> Promise<T> 仅 JS → Rust 可等待的 Rust future。正常导出的异步用法需要异步运行时。
PromiseRaw<'env, T> Promise<T> 作用域内 JS promise 句柄 支持 thencatchfinally,无需把 promise 移到其他线程。
Rust async fn 返回值 Promise<T> Rust → JS asynctokio_rtResult::Err 会 reject。
AsyncTask<T> Promise<T::JsValue> Rust → JS 在 libuv worker pool 中运行 compute
ReadableStream<'env, T> Web ReadableStream<T> 双向 web_stream;构造要求 T: Send + 'static,且 Rust stream 也为 Send + 'static
WriteableStream<'env> Web WritableStream JS → Rust,并可在作用域内透传 web_stream;Rust API 目前拼写为 WriteableStream,类型生成器不会规范化该拼写,因此公开参数应使用 ts_arg_type = "WritableStream"

ReadableStream::new 会检查运行时是否提供全局 ReadableStream。仅有 Node-API 4 并不保证 Web Streams 全局对象存在;with_readable_stream_class 可以显式接收兼容的构造函数。

外部原生数据

External<T> 向 JavaScript 暴露一个不透明、带类型标签的原生分配。它不会被序列化,生成的类型为 ExternalObject<T>

  • 返回拥有所有权的 External<T>,可将它转移到 JavaScript external 值中。
  • 接受 &External<T>&mut External<T>,可以借用并检查所包装值的类型。
  • 当 Rust 必须持有指向 external 的 JavaScript 引用时,请使用 ExternalRef<T>
  • External::new_with_size_hint 会向 JavaScript 垃圾回收器报告原生分配大小;该数字是 GC 记账提示,不是内存限制。

有关生命周期细节,请参阅 External

校验不是强制转换

大多数生成的函数都根据其 FromNapiValue 实现执行转换。添加 #[napi(strict)] 后,会先调用 ValidateNapiValue 并拒绝顶层 JavaScript 类型不匹配的值。它不会把字符串强制转换为数字,也不会在转换前递归校验所有属性。

#[napi(return_if_invalid)] 执行相同的校验,但在输入无效时返回 undefined,而不是抛出异常。有关约束,请参阅 #[napi] 属性

选择所有权模型

请按以下优先顺序选择:

  1. 当复制成本可接受,且值必须跨线程或跨越 await 点时,使用拥有所有权的 Rust 值(StringVec<T>#[napi(object)])。
  2. 对同步的零拷贝或低拷贝访问,使用作用域句柄和切片(Object<'env>Array<'env>BufferSlice<'env>、typed-array 切片)。
  3. 当 JavaScript 拥有的数据必须比回调存活更久时,使用保留引用的包装类型(BufferObjectRefFunctionRefReference<T>)。
  4. 从其他线程调用 JavaScript 时使用 ThreadsafeFunction;绝不要把作用域 JavaScript 句柄移到那个线程。
  5. 当数据应增量生成,而不是复制到一个集合中时,使用 stream。

编译器会通过生命周期和 Send 强制执行其中许多边界,但 unsafe 方法或原始 Node-API 句柄可以绕过这些限制。在这样做之前,请阅读理解生命周期