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.
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 wrapperUint8Array,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.
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.
⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
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 sliceUint8ArraySlice<'env>,Int32ArraySlice<'env>, etc. - Zero-copy TypedArray slicesArrayBuffer<'env>- Zero-copy ArrayBuffer view&[u8]/&[i8]/&[f32]/&[f64]...- Zero-copy slice
use napi_derive::napi;
#[napi]
pub fn sum_array_slice(input: &[u32]) -> u32 {
// Zero-copy access to the underlying data
input.iter().sum()
}
⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
export declare function sumArraySlice(input: Uint32Array): number
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
ObjectorUnknown
Use Buffer when:
- You need to store the buffer beyond the function call
- Working with async functions
Common Usage Patterns
Converting Between Types
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)
}
⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
export declare function bufferSliceToBuffer(slice: Buffer): Promise<number>
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
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.
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.
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:
- Pointer Validity: The pointer must remain valid until the finalize callback
- Memory Layout: The memory must be compatible with the expected type
- Proper Cleanup: The finalize callback must properly deallocate memory
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.