diff --git a/crates/napi/src/bindgen_runtime/js_values/arraybuffer.rs b/crates/napi/src/bindgen_runtime/js_values/arraybuffer.rs index 9eff82cb..3bfb450e 100644 --- a/crates/napi/src/bindgen_runtime/js_values/arraybuffer.rs +++ b/crates/napi/src/bindgen_runtime/js_values/arraybuffer.rs @@ -10,7 +10,7 @@ use std::sync::{ #[cfg(all(feature = "napi4", not(feature = "noop"), not(target_family = "wasm")))] use crate::bindgen_prelude::{CUSTOM_GC_TSFN, CUSTOM_GC_TSFN_DESTROYED, THREADS_CAN_ACCESS_ENV}; pub use crate::js_values::TypedArrayType; -use crate::{check_status, sys, Error, Result, Status}; +use crate::{check_status, sys, Error, Result, Status, ValueType}; use super::{FromNapiValue, ToNapiValue, TypeName, ValidateNapiValue}; @@ -480,6 +480,27 @@ macro_rules! impl_from_slice { }, "Get TypedArray info failed" )?; + if typed_array_type != $typed_array_type as i32 { + return Err(Error::new( + Status::InvalidArg, + format!("Expected $name, got {}", typed_array_type), + )); + } + Ok(if length == 0 { + &mut [] + } else { + unsafe { core::slice::from_raw_parts_mut(data as *mut $rust_type, length) } + }) + } + } + + impl FromNapiValue for &[$rust_type] { + unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result { + let mut typed_array_type = 0; + let mut length = 0; + let mut data = ptr::null_mut(); + let mut array_buffer = ptr::null_mut(); + let mut byte_offset = 0; check_status!( unsafe { sys::napi_get_typedarray_info( @@ -500,7 +521,65 @@ macro_rules! impl_from_slice { format!("Expected $name, got {}", typed_array_type), )); } - Ok(unsafe { core::slice::from_raw_parts_mut(data as *mut $rust_type, length) }) + Ok(if length == 0 { + &[] + } else { + unsafe { core::slice::from_raw_parts_mut(data as *mut $rust_type, length) } + }) + } + } + + impl TypeName for &mut [$rust_type] { + fn type_name() -> &'static str { + concat!("TypedArray<", stringify!($rust_type), ">") + } + + fn value_type() -> crate::ValueType { + crate::ValueType::Object + } + } + + impl TypeName for &[$rust_type] { + fn type_name() -> &'static str { + concat!("TypedArray<", stringify!($rust_type), ">") + } + + fn value_type() -> crate::ValueType { + crate::ValueType::Object + } + } + + impl ValidateNapiValue for &[$rust_type] { + unsafe fn validate(env: sys::napi_env, napi_val: sys::napi_value) -> Result { + let mut is_typed_array = false; + check_status!( + unsafe { sys::napi_is_typedarray(env, napi_val, &mut is_typed_array) }, + "Failed to validate napi typed array" + )?; + if !is_typed_array { + return Err(Error::new( + Status::InvalidArg, + "Expected a TypedArray value".to_owned(), + )); + } + Ok(ptr::null_mut()) + } + } + + impl ValidateNapiValue for &mut [$rust_type] { + unsafe fn validate(env: sys::napi_env, napi_val: sys::napi_value) -> Result { + let mut is_typed_array = false; + check_status!( + unsafe { sys::napi_is_typedarray(env, napi_val, &mut is_typed_array) }, + "Failed to validate napi typed array" + )?; + if !is_typed_array { + return Err(Error::new( + Status::InvalidArg, + "Expected a TypedArray value".to_owned(), + )); + } + Ok(ptr::null_mut()) } } }; @@ -568,6 +647,106 @@ impl_typed_array!(BigUint64Array, u64, TypedArrayType::BigUint64); #[cfg(feature = "napi6")] impl_from_slice!(BigUint64Array, u64, TypedArrayType::BigUint64); +/// Zero copy Uint8ClampedArray slice shared between Rust and Node.js. +/// It can only be used in non-async context and the lifetime is bound to the fn closure. +/// If you want to use Node.js `Uint8ClampedArray` in async context or want to extend the lifetime, use `Uint8ClampedArray` instead. +pub struct Uint8ClampedSlice<'scope> { + pub(crate) inner: &'scope mut [u8], + raw_value: sys::napi_value, +} + +impl<'scope> FromNapiValue for Uint8ClampedSlice<'scope> { + unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result { + let mut typed_array_type = 0; + let mut length = 0; + let mut data = ptr::null_mut(); + let mut array_buffer = ptr::null_mut(); + let mut byte_offset = 0; + check_status!( + unsafe { + sys::napi_get_typedarray_info( + env, + napi_val, + &mut typed_array_type, + &mut length, + &mut data, + &mut array_buffer, + &mut byte_offset, + ) + }, + "Get TypedArray info failed" + )?; + if typed_array_type != TypedArrayType::Uint8Clamped as i32 { + return Err(Error::new( + Status::InvalidArg, + format!("Expected $name, got {}", typed_array_type), + )); + } + Ok(Self { + inner: if length == 0 { + &mut [] + } else { + unsafe { core::slice::from_raw_parts_mut(data.cast(), length) } + }, + raw_value: napi_val, + }) + } +} + +impl ToNapiValue for Uint8ClampedSlice<'_> { + #[allow(unused_variables)] + unsafe fn to_napi_value(env: sys::napi_env, val: Self) -> Result { + Ok(val.raw_value) + } +} + +impl TypeName for Uint8ClampedSlice<'_> { + fn type_name() -> &'static str { + "Uint8ClampedArray" + } + + fn value_type() -> ValueType { + ValueType::Object + } +} + +impl ValidateNapiValue for Uint8ClampedSlice<'_> { + unsafe fn validate(env: sys::napi_env, napi_val: sys::napi_value) -> Result { + let mut is_typedarray = false; + check_status!( + unsafe { sys::napi_is_typedarray(env, napi_val, &mut is_typedarray) }, + "Failed to validate typed buffer" + )?; + if !is_typedarray { + return Err(Error::new( + Status::InvalidArg, + "Expected a TypedArray value".to_owned(), + )); + } + Ok(ptr::null_mut()) + } +} + +impl AsRef<[u8]> for Uint8ClampedSlice<'_> { + fn as_ref(&self) -> &[u8] { + self.inner + } +} + +impl<'scope> Deref for Uint8ClampedSlice<'scope> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.inner + } +} + +impl<'scope> DerefMut for Uint8ClampedSlice<'scope> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner + } +} + impl>> From for Uint8Array { fn from(data: T) -> Self { Uint8Array::new(data.into()) diff --git a/crates/napi/src/bindgen_runtime/js_values/buffer.rs b/crates/napi/src/bindgen_runtime/js_values/buffer.rs index 4ef145a0..d5857ec6 100644 --- a/crates/napi/src/bindgen_runtime/js_values/buffer.rs +++ b/crates/napi/src/bindgen_runtime/js_values/buffer.rs @@ -18,7 +18,98 @@ thread_local! { pub (crate) static BUFFER_DATA: Mutex> = Default::default(); } +/// Zero copy buffer slice shared between Rust and Node.js. +/// It can only be used in non-async context and the lifetime is bound to the fn closure. +/// If you want to use Node.js Buffer in async context or want to extend the lifetime, use `Buffer` instead. +pub struct BufferSlice<'scope> { + pub(crate) inner: &'scope mut [u8], + raw_value: sys::napi_value, +} + +impl<'scope> FromNapiValue for BufferSlice<'scope> { + unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result { + let mut buf = ptr::null_mut(); + let mut len = 0usize; + check_status!( + unsafe { sys::napi_get_buffer_info(env, napi_val, &mut buf, &mut len) }, + "Failed to get Buffer pointer and length" + )?; + // From the docs of `napi_get_buffer_info`: + // > [out] data: The underlying data buffer of the node::Buffer. If length is 0, this may be + // > NULL or any other pointer value. + // + // In order to guarantee that `slice::from_raw_parts` is sound, the pointer must be non-null, so + // let's make sure it always is, even in the case of `napi_get_buffer_info` returning a null + // ptr. + Ok(Self { + inner: if len == 0 { + &mut [] + } else { + unsafe { slice::from_raw_parts_mut(buf.cast(), len) } + }, + raw_value: napi_val, + }) + } +} + +impl ToNapiValue for BufferSlice<'_> { + #[allow(unused_variables)] + unsafe fn to_napi_value(env: sys::napi_env, val: Self) -> Result { + Ok(val.raw_value) + } +} + +impl TypeName for BufferSlice<'_> { + fn type_name() -> &'static str { + "Buffer" + } + + fn value_type() -> ValueType { + ValueType::Object + } +} + +impl ValidateNapiValue for BufferSlice<'_> { + unsafe fn validate(env: sys::napi_env, napi_val: sys::napi_value) -> Result { + let mut is_buffer = false; + check_status!( + unsafe { sys::napi_is_buffer(env, napi_val, &mut is_buffer) }, + "Failed to validate napi buffer" + )?; + if !is_buffer { + return Err(Error::new( + Status::InvalidArg, + "Expected a Buffer value".to_owned(), + )); + } + Ok(ptr::null_mut()) + } +} + +impl AsRef<[u8]> for BufferSlice<'_> { + fn as_ref(&self) -> &[u8] { + self.inner + } +} + +impl<'scope> Deref for BufferSlice<'scope> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.inner + } +} + +impl<'scope> DerefMut for BufferSlice<'scope> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.inner + } +} + /// Zero copy u8 vector shared between rust and napi. +/// It's designed to be used in `async` context, so it contains overhead to ensure the underlying data is not dropped. +/// For non-async context, use `BufferRef` instead. +/// /// Auto reference the raw JavaScript value, and release it when dropped. /// So it is safe to use it in `async fn`, the `&[u8]` under the hood will not be dropped until the `drop` called. /// Clone will create a new `Reference` to the same underlying `JavaScript Buffer`. diff --git a/examples/napi/__tests__/__snapshots__/typegen.spec.ts.md b/examples/napi/__tests__/__snapshots__/typegen.spec.ts.md index d82ccfe6..d25a3cc1 100644 --- a/examples/napi/__tests__/__snapshots__/typegen.spec.ts.md +++ b/examples/napi/__tests__/__snapshots__/typegen.spec.ts.md @@ -255,6 +255,10 @@ Generated by [AVA](https://avajs.dev). ␊ export function acceptThreadsafeFunctionTupleArgs(func: (err: Error | null, arg0: number, arg1: boolean, arg2: string) => any): void␊ ␊ + export function acceptUint8ClampedSlice(input: Uint8ClampedArray): bigint␊ + ␊ + export function acceptUint8ClampedSliceAndBufferSlice(a: Buffer, b: Uint8ClampedArray): bigint␊ + ␊ export function add(a: number, b: number): number␊ ␊ export const enum ALIAS {␊ @@ -672,6 +676,8 @@ Generated by [AVA](https://avajs.dev). ␊ export function validateBuffer(b: Buffer): number␊ ␊ + export function validateBufferSlice(input: Buffer): number␊ + ␊ export function validateDate(d: Date): number␊ ␊ export function validateDateTime(d: Date): number␊ @@ -696,6 +702,10 @@ Generated by [AVA](https://avajs.dev). ␊ export function validateTypedArray(input: Uint8Array): number␊ ␊ + export function validateTypedArraySlice(input: Uint8Array): number␊ + ␊ + export function validateUint8ClampedSlice(input: Uint8ClampedArray): number␊ + ␊ export function validateUndefined(i: undefined): boolean␊ ␊ export function withAbortController(a: number, b: number, signal: AbortSignal): Promise␊ diff --git a/examples/napi/__tests__/__snapshots__/typegen.spec.ts.snap b/examples/napi/__tests__/__snapshots__/typegen.spec.ts.snap index a5c5f0a0..9d2db18f 100644 Binary files a/examples/napi/__tests__/__snapshots__/typegen.spec.ts.snap and b/examples/napi/__tests__/__snapshots__/typegen.spec.ts.snap differ diff --git a/examples/napi/__tests__/strict.spec.ts b/examples/napi/__tests__/strict.spec.ts index 5e8acbf2..be7efd72 100644 --- a/examples/napi/__tests__/strict.spec.ts +++ b/examples/napi/__tests__/strict.spec.ts @@ -3,6 +3,8 @@ import test from 'ava' const { validateArray, validateTypedArray, + validateTypedArraySlice, + validateBufferSlice, validateBigint, validateBuffer, validateBoolean, @@ -38,6 +40,21 @@ test('should validate arraybuffer', (t) => { code: 'InvalidArg', message: 'Expected a TypedArray value', }) + + t.is(validateTypedArraySlice(new Uint8Array([1, 2, 3])), 3) + + // @ts-expect-error + t.throws(() => validateTypedArraySlice(1), { + code: 'InvalidArg', + message: 'Expected a TypedArray value', + }) + + t.is(validateBufferSlice(Buffer.from('hello')), 5) + // @ts-expect-error + t.throws(() => validateBufferSlice(2), { + code: 'InvalidArg', + message: 'Expected a Buffer value', + }) }) test('should validate BigInt', (t) => { @@ -123,7 +140,7 @@ test('should validate Map', (t) => { }) }) -test.only('should validate promise', async (t) => { +test('should validate promise', async (t) => { t.is( await validatePromise( new Promise((resolve) => { diff --git a/examples/napi/__tests__/values.spec.ts b/examples/napi/__tests__/values.spec.ts index ecc6458b..d826e170 100644 --- a/examples/napi/__tests__/values.spec.ts +++ b/examples/napi/__tests__/values.spec.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url' import { spy } from 'sinon' -import type { AliasedStruct, Animal as AnimalClass } from '../index.js' +import { type AliasedStruct, type Animal as AnimalClass } from '../index.js' import { test } from './test.framework.js' @@ -105,6 +105,8 @@ const { i64ArrayToArray, f32ArrayToArray, f64ArrayToArray, + acceptUint8ClampedSlice, + acceptUint8ClampedSliceAndBufferSlice, convertU32Array, createExternalTypedArray, mutateTypedArray, @@ -694,6 +696,15 @@ test('TypedArray', (t) => { const bird = new Bird('Carolyn') t.is(bird.acceptSliceMethod(new Uint8Array([1, 2, 3])), 3) + + t.is(acceptUint8ClampedSlice(new Uint8ClampedArray([1, 2, 3])), 3n) + t.is( + acceptUint8ClampedSliceAndBufferSlice( + Buffer.from([1, 2, 3]), + new Uint8ClampedArray([1, 2, 3]), + ), + 6n, + ) }) test('reset empty buffer', (t) => { diff --git a/examples/napi/index.d.ts b/examples/napi/index.d.ts index 800e9771..15894b04 100644 --- a/examples/napi/index.d.ts +++ b/examples/napi/index.d.ts @@ -245,6 +245,10 @@ export function acceptThreadsafeFunctionFatal(func: (arg: number) => any): void export function acceptThreadsafeFunctionTupleArgs(func: (err: Error | null, arg0: number, arg1: boolean, arg2: string) => any): void +export function acceptUint8ClampedSlice(input: Uint8ClampedArray): bigint + +export function acceptUint8ClampedSliceAndBufferSlice(a: Buffer, b: Uint8ClampedArray): bigint + export function add(a: number, b: number): number export const enum ALIAS { @@ -662,6 +666,8 @@ export function validateBoolean(i: boolean): boolean export function validateBuffer(b: Buffer): number +export function validateBufferSlice(input: Buffer): number + export function validateDate(d: Date): number export function validateDateTime(d: Date): number @@ -686,6 +692,10 @@ export function validateSymbol(s: symbol): boolean export function validateTypedArray(input: Uint8Array): number +export function validateTypedArraySlice(input: Uint8Array): number + +export function validateUint8ClampedSlice(input: Uint8ClampedArray): number + export function validateUndefined(i: undefined): boolean export function withAbortController(a: number, b: number, signal: AbortSignal): Promise diff --git a/examples/napi/src/fn_strict.rs b/examples/napi/src/fn_strict.rs index 0d4de886..7a8ffae2 100644 --- a/examples/napi/src/fn_strict.rs +++ b/examples/napi/src/fn_strict.rs @@ -18,6 +18,21 @@ fn validate_typed_array(input: Uint8Array) -> u32 { input.len() as u32 } +#[napi(strict)] +fn validate_typed_array_slice(input: &[u8]) -> u32 { + input.len() as u32 +} + +#[napi(strict)] +fn validate_uint8_clamped_slice(input: Uint8ClampedSlice) -> u32 { + input.len() as u32 +} + +#[napi(strict)] +fn validate_buffer_slice(input: BufferSlice) -> u32 { + input.len() as u32 +} + #[napi(strict)] fn validate_bigint(input: BigInt) -> i128 { input.get_i128().0 diff --git a/examples/napi/src/typed_array.rs b/examples/napi/src/typed_array.rs index e814aa08..620e250b 100644 --- a/examples/napi/src/typed_array.rs +++ b/examples/napi/src/typed_array.rs @@ -104,6 +104,16 @@ fn i64_array_to_array(input: &[i64]) -> Vec { input.to_vec() } +#[napi] +fn accept_uint8_clamped_slice(input: Uint8ClampedSlice) -> usize { + input.len() +} + +#[napi] +fn accept_uint8_clamped_slice_and_buffer_slice(a: BufferSlice, b: Uint8ClampedSlice) -> usize { + a.len() + b.len() +} + struct AsyncBuffer { buf: Buffer, }