类型转换
每个导出参数都必须实现 FromNapiValue(或某个引用转换 trait),每个返回值都必须实现 ToNapiValue。生成的 TypeScript 类型是有用的文档,但真正决定转换是否可用的是 Rust trait 实现。
本参考描述 napi-rs v3 的 bindgen 运行时。有关 JsString 和 JsObject 等底层句柄,请参阅 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 |
i8、u8、i16、u16、i32、u32 |
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 |
u64、u128、i128、usize、isize、i64n |
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 |
OsString、PathBuf |
string |
双向 | 拥有所有权。Windows 使用 UTF-16 并保留未配对代理项。Unix 输出遇到非 Unicode 路径时会拒绝,而不会替换字节。 | 基础 API |
&OsStr、&Path |
string |
Rust → JS | 借用输出。平台注意事项与拥有所有权的形式相同。 | 基础 API |
Symbol |
symbol |
双向 | 普通输入转换会丢弃该值,不保留身份或描述。#[napi(strict)] 会先校验它确实是 symbol,但仍不会保留。返回 Symbol 时会根据 Rust 描述符状态创建一个 symbol。要保留现有值,请使用作用域内的 JsSymbol。Symbol::for_desc 需要 Node-API 9。 |
基础 API;全局 symbol 需要 napi9 |
i64 特意映射到 number,而 i64n 映射到 bigint。只有当 JavaScript API 确实要暴露 BigInt 时,才应优先使用该包装类型。
#[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 输出类型。
Option、null 与 undefined
Option<T> 有意采用非对称映射:
| 位置 | 接受或生成的 JavaScript | 生成的 TypeScript |
|---|---|---|
| 函数参数 | T、null 或 undefined;两个 nullish 值都会变为 None |
T | null | undefined,并且通常是可选的尾部参数 |
| 函数返回值 | Some(T) 变为 T;None 变为 null |
T | null |
使用默认 use_nullable = false 的 #[napi(object)] 或结构化形状字段 |
缺失或 undefined 变为 None;显式 null 会交给内部 T 转换,通常失败;输出时省略 None |
field?: T |
使用 use_nullable = true 的 #[napi(object)] 或结构化形状字段 |
缺失或 undefined 会报错;null 变为 None;None 输出为 null |
field: T | null |
| 公开类字段 | 访问器始终存在。Option getter 对 None 输出 null,可写 setter 接受普通的 nullish Option 输入。use_nullable 改变生成的属性/构造函数形状,而不决定访问器是否存在。 |
默认:field?: T;启用 use_nullable:field: T | null |
当区分本身就是 API 的一部分时,请使用 Null 或 Undefined。只接受一种 nullish 值时,请使用 Either<T, Null> 或 Either<T, Undefined>。
#[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
中调用。该联合仍然接受 undefined 和 null。
使用 Either 表示联合类型
Either<A, B> 一直到 Either26<A, ..., Z> 都会映射为 TypeScript 联合类型。输入时,napi-rs 按从左到右的顺序,用每个类型的 ValidateNapiValue 实现测试变体,然后转换第一个匹配项。
#[napi]
pub fn normalize_id(value: Either<u32, String>) -> String {
match value {
Either::A(number) => number.to_string(),
Either::B(text) => text,
}
}
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,并可在作用域内透传 | 带 get、get_ref、set 和 insert 的作用域句柄;避免预先转换整个数组。 |
基础 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)。如果需要增量访问而不是拥有所有权的副本,请使用作用域内的 Array、Object、typed-array 视图或 stream。
对象、类与自定义形状
| Rust 类型或声明 | JavaScript / TypeScript | 方向 | 所有权 / 身份 |
|---|---|---|---|
Object<'env> |
object |
作用域内双向 | 直接的作用域句柄。每次属性访问都会跨越 Node-API 边界。 |
ObjectRef |
object |
双向 | 持有 Node-API 引用,因此对象可以比回调存活更久。 |
Unknown<'env> |
unknown |
作用域内双向 | 未检查的作用域句柄;请显式检查/强制转换。 |
#[napi(object)] struct |
普通对象 / interface | 由 object_from_js 和 object_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 字段只需要输出转换,而跳过的类字段没有访问器。对于嵌套类值,请接受 &T、ClassInstance<T>,或使用 Array::get_ref;Vec<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 |
Int8Array、Uint8Array、… |
对应的 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,会启用 chrono 和 napi5;精度由自 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 句柄 | 支持 then、catch 和 finally,无需把 promise 移到其他线程。 |
Rust async fn 返回值 |
Promise<T> |
Rust → JS | async 或 tokio_rt;Result::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] 属性。
选择所有权模型
请按以下优先顺序选择:
- 当复制成本可接受,且值必须跨线程或跨越 await 点时,使用拥有所有权的 Rust 值(
String、Vec<T>、#[napi(object)])。 - 对同步的零拷贝或低拷贝访问,使用作用域句柄和切片(
Object<'env>、Array<'env>、BufferSlice<'env>、typed-array 切片)。 - 当 JavaScript 拥有的数据必须比回调存活更久时,使用保留引用的包装类型(
Buffer、ObjectRef、FunctionRef、Reference<T>)。 - 从其他线程调用 JavaScript 时使用
ThreadsafeFunction;绝不要把作用域 JavaScript 句柄移到那个线程。 - 当数据应增量生成,而不是复制到一个集合中时,使用 stream。
编译器会通过生命周期和 Send 强制执行其中许多边界,但 unsafe 方法或原始 Node-API 句柄可以绕过这些限制。在这样做之前,请阅读理解生命周期。