Skip to content

TypedArray

TypedArray 描述了底层二进制数据缓冲区 上的类数组视图。NAPI-RS 可以让 Rust 在不复制数据的情况下访问这块存储, 但必须遵守下文的生命周期和同步规则。

Buffer

Buffer 是 JavaScript的 Uint8Array 的子类,它经常用于在 Node.js 和 Rust 之间共享数据。

可以使用 Vec<u8> 创建 Buffer。如果运行时允许 external buffer, NAPI-RS 会在不复制数据的情况下把这次分配交给 JavaScript Buffer,并在 JavaScript 回收 buffer 后由 finalizer 释放 Vec<u8>。如果运行时拒绝 external buffer,NAPI-RS 会回退为把字节复制到运行时拥有的 buffer 中。

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn create_buffer() -> Buffer {
  vec![0, 1, 2].into() 
}

INFO

在支持 external buffer 的运行时中,这种方式不会复制底层 Vec<u8>

WARNING

Electron 无法以零拷贝的方式创建 Buffer。详见 V8 内存隔离区(V8 Memory Cage)。在这种情况下, NAPI-RS 会把 Vec<u8> 的数据复制到底层 Buffer 中。

Buffer 与 TypedArray 类型

NAPI-RS 针对不同的使用场景提供了两类 buffer 类型。有关这些类型的生命周期如何工作的更多细节,请参见理解生命周期

拥有型类型(Owned Types)

这些类型可以活过当前原生调用,并跨越异步边界:

  • Buffer —— 以引用为后盾的 Node.js Buffer 包装器
  • Uint8ArrayInt32ArrayFloat64Array 等 —— 拥有型 typed array 包装器

对于从 JavaScript 接收的值,NAPI-RS 会创建一个 napi_ref,使 JavaScript 对象及其底层存储一直存活到 Rust 包装器被丢弃。丢弃包装器只会释放 Rust 持有的引用;JavaScript 仍可能独立保留同一个对象。

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn process_buffer(env: &Env, buffer: Buffer) -> Result<AsyncBlock<Buffer>> { 
  // 在同步的 JavaScript 回调仍持有控制权时进行复制。
  let mut data = buffer.to_vec();
  AsyncBlockBuilder::new(async move {
    data.reverse();
    Ok(data.into())
  })
  .build(env)
}

INFO

AsyncBlockAsyncBlockBuilder 是在 napi 的 async feature 下重新导出的, 因此不启用它这个示例就无法编译。请在 Cargo.tomlnapi 依赖上启用该 feature:napi = { version = "3", features = ["async"] }tokio_time feature 仅在使用后文的 napi::tokio::time::sleep 辅助函数时才需要。

⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

index.d.ts
ts
export declare function processBuffer(buffer: Buffer): Promise<Buffer>

WARNING

Buffer 和拥有型 typed array 包装器实现了 SendSync,因此包装器 的生命周期和清理可以跨线程;这些 trait 并不会同步共享字节。Rust 持有 包装器时,JavaScript 仍可保留并修改同一块底层存储。如果 JavaScript 或另一个 Rust 线程可能修改它,Rust 工作线程同时访问这块内存就会形成 数据竞争,并可能导致未定义行为——即使 Rust 只读取也一样。请在分派工作 前复制字节,或者实施一个能排除所有未同步访问的所有权协议。

借用型类型(Borrowed Types,BufferSliceUint8ArraySlice 等)

这些类型借用数据,其生命周期被绑定到函数作用域内:

  • BufferSlice<'env> —— 零拷贝的 Buffer 切片
  • Uint8ArraySlice<'env>Int32ArraySlice<'env> 等 —— 零拷贝的 TypedArray 切片
  • ArrayBuffer<'env> —— 零拷贝的 ArrayBuffer 视图
  • &[u8]/&[i8]/&[f32]/&[f64]... —— 零拷贝切片
lib.rs
rust
use napi_derive::napi;

#[napi]
pub fn sum_array_slice(input: &[u32]) -> u32 { 
  // 零拷贝地访问底层数据
  input.iter().sum()
}

⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

index.d.ts
ts
export declare function sumArraySlice(input: Uint32Array): number
index.ts
ts
import { sumArraySlice } from './index.js'

const input = new Uint32Array([1, 2, 3, 4, 5])

const result = sumArraySlice(input) 
console.log(result) // 15

何时使用哪种类型

在以下情况使用 &[u8]/&[i8]/&[f32]/&[f64]...

  • 你需要零拷贝的性能
  • 仅在同步上下文中工作
  • 数据的生命周期被限定在该函数调用之内

在以下情况使用 BufferSlice<'env>Uint8ArraySlice<'env>/Int32ArraySlice<'env>/...

  • 你需要零拷贝的性能
  • 在某些场景下你需要把它们转换为拥有型类型
  • 你需要把它们转换为 ObjectUnknown

在以下情况使用 Buffer

  • 你需要在函数调用结束后继续保存该 buffer
  • 你在使用异步函数

常见用法模式

在类型之间转换

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn buffer_slice_to_buffer(env: &Env, slice: BufferSlice) -> Result<AsyncBlock<u8>> {
  // 将 BufferSlice 转换为拥有型 Buffer 以用于异步场景
  let buffer = slice.into_buffer(env)?; 
  // 在异步工作可能与 JavaScript 并发运行之前先复制。
  let data = buffer.to_vec();
  AsyncBlockBuilder::new(async move { 
    Ok(data.iter().sum())
  })
  .build(env)
}

⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

index.d.ts
ts
export declare function bufferSliceToBuffer(slice: Buffer): Promise<number>
index.ts
ts
import { bufferSliceToBuffer } from './index.js'

const slice = Buffer.from([1, 2, 3, 4, 5])

const result = await bufferSliceToBuffer(slice) 
console.log(result) // 15

异步与同步模式

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

// ✅ 正确:在异步上下文中使用拥有型 Buffer
#[napi]
pub async fn process_async(buffer: Buffer) -> Result<Buffer> {
    // Buffer 可以跨越 await 边界
    napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    Ok(buffer)
}

// ❌ 无法编译:BufferSlice 不能跨越 await 边界
// #[napi]
// pub async fn process_async_slice(slice: BufferSlice<'_>) -> Result<BufferSlice<'_>> {
//     napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await;
//     Ok(slice) // Error: slice doesn't live long enough
// }

#[napi]
// ✅ 正确:将 slice 转换为拥有型以用于异步场景
pub fn process_slice_async(env: &Env, slice: BufferSlice<'_>) -> Result<AsyncBlock<Buffer>> {
  let buffer = slice.into_buffer(env)?;
  AsyncBlockBuilder::new(async move { Ok(buffer) }).build(env)
}

上面所有的 AsyncBlock 示例都借助 napi crate 的异步支持来构建它们的 future,而这依赖于 napi 依赖上的 async feature(napi = { version = "3", features = ["async"] })。正是该 feature 重新导出了 AsyncBlock/AsyncBlockBuilder 以及 Tokio 运行时。上面用到的 napi::tokio::time::sleep 辅助函数还额外需要 tokio_time feature。

内存管理

复制的 Buffer

在某些情况下,你无法把数据的所有权转移给 Buffer 或 typed array。这时可以 使用 copy_from 创建一份副本。

WARNING

如果以这种方式创建 BufferTypedArray,数据的所有权不会被转移给 BufferTypedArray,而是会复制底层数据,因此会有数据复制带来的 性能开销。

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn create_copied_buffer(env: &Env) -> Result<BufferSlice<'_>> {
  let data = b"Hello, World!";
  BufferSlice::copy_from(env, data)
}

External Buffer

有时,你可能希望从那些能够 deref[u8] 或能取得像 *mut u8 这样的裸指针的数据类型创建 BufferTypedArray,同时又不想把整块数据复制进 Vec<u8>——那可能非常昂贵。我们提供了 from_external 方法来实现这一点,但它是 unsafe 的,你必须确保数据在 finalize 回调被调用之前一直有效。

INFO

finalize_hint 参数会被传给 finalizer。在下面的第一个示例中,boxed slice 既是分配的所有者,也是这个 hint,因此它会一直存活到回调收到并丢弃它为止。 如果运行时拒绝 external buffer,NAPI-RS 会先复制字节,然后在 from_external 执行期间立即调用该回调;否则该回调会在 JavaScript 终结这个 external buffer 时运行。不要指望这个回调会被推迟到垃圾回收时才执行。

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn create_shared_buffer(env: &Env) -> Result<BufferSlice<'_>> {
  let mut data = vec![1, 2, 3, 4, 5].into_boxed_slice();
  let data_ptr = data.as_mut_ptr();
  let len = data.len();

  unsafe {
    BufferSlice::from_external(env, data_ptr, len, data, move |_, boxed_data| {
      drop(boxed_data);
    })
  }
}

#[napi]
pub fn create_external_buffer(env: &Env) -> Result<BufferSlice<'_>> {
  let mut data = vec![1, 2, 3, 4, 5];
  let data_ptr = data.as_mut_ptr();
  let len = data.len();
  let capacity = data.capacity();

  // 确保数据在 finalize 回调被调用之前一直有效
  std::mem::forget(data);

  unsafe {
    BufferSlice::from_external(env, data_ptr, len, data_ptr, move |_, ptr| {
      // 当 JavaScript GC 运行时清理数据
      std::mem::drop(Vec::from_raw_parts(ptr, len, capacity));
    })
  }
}

安全性注意事项

External Buffer 的安全性

使用 from_external 方法时,请确保:

  1. 指针有效性:指针必须在 finalize 回调之前保持有效
  2. 内存布局:内存必须与期望的类型兼容
  3. 正确清理:finalize 回调必须正确地释放内存
lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi]
pub fn unsafe_external_example(env: &Env) -> Result<BufferSlice<'_>> {
  let mut data = vec![1u8, 2, 3, 4, 5];
  let ptr = data.as_mut_ptr();
  let len = data.len();
  let capacity = data.capacity();

  // ⚠️ 关键:必须 forget 掉 Vec 以防止二次释放(double-free)
  std::mem::forget(data);

  unsafe {
    BufferSlice::from_external(env, ptr, len, ptr, move |_, ptr| {
      // ✅ 正确地重建并丢弃 Vec
      std::mem::drop(Vec::from_raw_parts(ptr, len, capacity));
      // Vec 在被丢弃时会自动释放内存
    })
  }
}

不安全的可变访问

unsafe 的 as_mut 方法会暴露一个指向存储的可变切片,而 JavaScript 可能也会 访问这块存储。只有当你能保证在整个可变借用期间,JavaScript 以及其他所有 Rust 别名都不会读取或写入这块底层存储时,调用该方法才是健全(sound)的。 违反这一契约可能导致未定义行为。在跨线程代码中,除非你拥有一个横跨 JavaScript 和 Rust 的显式同步与所有权协议,否则请优先使用拥有型的副本。