feat(napi): impl BufferSlice and Uint8ClampedSlice (#1979)

This commit is contained in:
LongYinan 2024-02-25 01:00:28 +08:00 committed by GitHub
parent f88a041fa3
commit 8ca1967bd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 347 additions and 4 deletions

View file

@ -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<Self> {
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<sys::napi_value> {
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<sys::napi_value> {
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<Self> {
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<sys::napi_value> {
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<sys::napi_value> {
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<T: Into<Vec<u8>>> From<T> for Uint8Array {
fn from(data: T) -> Self {
Uint8Array::new(data.into())

View file

@ -18,7 +18,98 @@ thread_local! {
pub (crate) static BUFFER_DATA: Mutex<HashSet<*mut u8>> = 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<Self> {
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<sys::napi_value> {
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<sys::napi_value> {
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`.

View file

@ -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<number>

View file

@ -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) => {

View file

@ -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) => {

View file

@ -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<number>

View file

@ -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

View file

@ -104,6 +104,16 @@ fn i64_array_to_array(input: &[i64]) -> Vec<i64> {
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,
}