Skip to content

TypedArray

TypedArray describes an array-like view of an underlying binary data buffer. NAPI-RS can expose a view of that storage to Rust without copying it, subject to the lifetime and synchronization rules below.

Buffer

Buffer is a subclass of JavaScript's Uint8Array. It is often used to share data between Node.js and Rust.

A Buffer can be created from Vec<u8>. Where the runtime permits external buffers, NAPI-RS transfers the allocation to the JavaScript Buffer without a copy, and its finalizer releases the Vec<u8> after JavaScript collects the buffer. If the runtime rejects external buffers, NAPI-RS falls back to copying the bytes into a runtime-owned buffer.

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

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

INFO

On runtimes that support external buffers, the underlying Vec<u8> is not copied in this path.

WARNING

The Electron will not be able to create Buffer in zero copy way. See V8 Memory Cage for more details. NAPI-RS will copy the data of the Vec<u8> into the underlying Buffer in this case.

Buffer and TypedArray Types

NAPI-RS provides two categories of buffer types for different use cases. For more details on how lifetimes work for these types, see Understanding Lifetime.

Owned Types

These types can outlive the current native call and cross async boundaries:

  • Buffer - Reference-backed Node.js Buffer wrapper
  • Uint8Array, Int32Array, Float64Array, etc. - Owned typed-array wrappers

For a value received from JavaScript, NAPI-RS creates a napi_ref. It keeps the JavaScript object and backing store alive until the Rust wrapper is dropped. Dropping the wrapper only releases Rust's reference; JavaScript may retain the same object independently.

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

#[napi]
pub fn process_buffer(env: &Env, buffer: Buffer) -> Result<AsyncBlock<Buffer>> { 
  // Copy while the synchronous JavaScript callback still has control.
  let mut data = buffer.to_vec();
  AsyncBlockBuilder::new(async move {
    data.reverse();
    Ok(data.into())
  })
  .build(env)
}

INFO

AsyncBlock and AsyncBlockBuilder are re-exported under napi's async feature, so this example does not compile without it. Enable the feature on the napi dependency in your Cargo.toml: napi = { version = "3", features = ["async"] }. The tokio_time feature is only required for the napi::tokio::time::sleep helper shown later.

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

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

WARNING

Buffer and the owned typed-array wrappers implement Send and Sync so their lifetime and cleanup can cross threads. Those traits do not synchronize the shared bytes. JavaScript can retain and mutate the same backing store while Rust holds a wrapper. Accessing it on a Rust worker while JavaScript or another Rust thread may mutate it is a data race and can cause undefined behavior—even if Rust only reads. Copy the bytes before dispatching work, as above, or enforce an ownership protocol that rules out unsynchronized access.

Borrowed Types (BufferSlice, Uint8ArraySlice, etc.)

These types borrow data and are lifetime-bound to the function scope:

  • BufferSlice<'env> - Zero-copy Buffer slice
  • Uint8ArraySlice<'env>, Int32ArraySlice<'env>, etc. - Zero-copy TypedArray slices
  • ArrayBuffer<'env> - Zero-copy ArrayBuffer view
  • &[u8]/&[i8]/&[f32]/&[f64]... - Zero-copy slice
lib.rs
rust
use napi_derive::napi;

#[napi]
pub fn sum_array_slice(input: &[u32]) -> u32 { 
  // Zero-copy access to the underlying data
  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

When to Use Each Type

Use &[u8]/&[i8]/&[f32]/&[f64]... when:

  • You need zero-copy performance
  • Working in synchronous context only
  • Data lifetime is bounded to the function call

Use BufferSlice<'env> or Uint8ArraySlice<'env>/Int32ArraySlice<'env>/... when:

  • You need zero-copy performance
  • You need to convert them into owned types in some scenarios
  • You need to convert them into Object or Unknown

Use Buffer when:

  • You need to store the buffer beyond the function call
  • Working with async functions

Common Usage Patterns

Converting Between Types

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>> {
  // Convert BufferSlice to owned Buffer for async usage
  let buffer = slice.into_buffer(env)?; 
  // Copy before the async work can run concurrently with 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

Async vs Sync Patterns

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

// ✅ Correct: Using owned Buffer in async context
#[napi]
pub async fn process_async(buffer: Buffer) -> Result<Buffer> {
    // Buffer can cross await boundaries
    napi::tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    Ok(buffer)
}

// ❌ Won't compile: BufferSlice cannot cross await boundaries
// #[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]
// ✅ Correct: Convert slice to owned for async usage
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)
}

All of the AsyncBlock examples above build their futures with the napi crate's async support, which is gated behind the async feature on the napi dependency (napi = { version = "3", features = ["async"] }). That feature is what re-exports AsyncBlock/AsyncBlockBuilder and the Tokio runtime. The napi::tokio::time::sleep helper used above additionally requires the tokio_time feature.

Memory Management

Copied Buffers

In some cases, you cannot transfer ownership of the data to a Buffer or typed array. Use copy_from to create a copy instead.

WARNING

If you create the Buffer or TypedArray in this way, the ownership of the data will not be transferred to the Buffer or TypedArray, but the underlying data will be copied, there should be performance overhead of the data copy.

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 Buffers

Sometimes, you may want to create a Buffer or TypedArray from data types that can deref to [u8] or get the raw pointer like *mut u8. And you don't want to copy the whole data into a Vec<u8> which can be very expensive. We provide the from_external method to achieve this, but it's unsafe and you must ensure the data is valid until the finalize callback is called.

INFO

The finalize_hint parameter is passed to the finalizer. In the first example below, the boxed slice is both the allocation owner and the hint, so it stays alive until the callback receives and drops it. If the runtime rejects external buffers, NAPI-RS first copies the bytes and then invokes that callback immediately during from_external; otherwise the callback runs when JavaScript finalizes the external buffer. Do not require the callback to be deferred until garbage collection.

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();

  // make sure the data is valid until the finalize callback is called
  std::mem::forget(data);

  unsafe {
    BufferSlice::from_external(env, data_ptr, len, data_ptr, move |_, ptr| {
      // Cleanup data when JavaScript GC runs
      std::mem::drop(Vec::from_raw_parts(ptr, len, capacity));
    })
  }
}

Safety Considerations

External Buffer Safety

When using from_external methods, ensure:

  1. Pointer Validity: The pointer must remain valid until the finalize callback
  2. Memory Layout: The memory must be compatible with the expected type
  3. Proper Cleanup: The finalize callback must properly deallocate memory
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();

  // ⚠️ CRITICAL: Must forget the Vec to prevent double-free
  std::mem::forget(data);

  unsafe {
    BufferSlice::from_external(env, ptr, len, ptr, move |_, ptr| {
      // ✅ Properly reconstruct and drop the Vec
      std::mem::drop(Vec::from_raw_parts(ptr, len, capacity));
      // Vec automatically deallocates when dropped
    })
  }
}

Unsafe mutable access

The unsafe as_mut methods expose a mutable slice into storage that JavaScript may also access. Calling the method is only sound when you can guarantee that JavaScript and every other Rust alias will neither read nor write the backing store for the entire mutable borrow. Violating that contract can cause undefined behavior. In cross-thread code, prefer an owned copy unless you have an explicit synchronization and ownership protocol spanning both JavaScript and Rust.