diff --git a/.github/workflows/test-release.yaml b/.github/workflows/test-release.yaml index 47f3657c..e3c6e093 100644 --- a/.github/workflows/test-release.yaml +++ b/.github/workflows/test-release.yaml @@ -602,6 +602,18 @@ jobs: settings: - features: 'napi1,napi2,napi3,napi4,napi5,napi6,napi7,napi8,napi9,experimental,async,chrono_date,latin1,full' package: 'napi' + - features: 'napi3' + package: 'napi' + - features: 'napi3,compat-mode' + package: 'napi' + - features: 'napi9' + package: 'napi' + - features: 'napi3,serde-json' + package: 'napi' + - features: 'napi9,serde-json' + package: 'napi' + - features: 'async,compat-mode' + package: 'napi' - features: 'compat-mode,strict,type-def,noop,full,default' package: 'napi-derive' - features: 'noop' diff --git a/crates/napi/src/bindgen_runtime/js_values/array.rs b/crates/napi/src/bindgen_runtime/js_values/array.rs index b9974827..176cafb2 100644 --- a/crates/napi/src/bindgen_runtime/js_values/array.rs +++ b/crates/napi/src/bindgen_runtime/js_values/array.rs @@ -2,6 +2,7 @@ use std::{marker::PhantomData, ptr}; use crate::{bindgen_prelude::*, check_status, Value}; +#[derive(Clone, Copy)] pub struct Array<'env> { pub(crate) env: sys::napi_env, pub(crate) inner: sys::napi_value, diff --git a/crates/napi/src/bindgen_runtime/js_values/class.rs b/crates/napi/src/bindgen_runtime/js_values/class.rs index cd52a6d7..944d47b1 100644 --- a/crates/napi/src/bindgen_runtime/js_values/class.rs +++ b/crates/napi/src/bindgen_runtime/js_values/class.rs @@ -12,6 +12,7 @@ use crate::{ check_status, sys, Env, JsValue, Property, PropertyAttributes, Value, ValueType, }; +#[derive(Clone, Copy)] pub struct This<'env, T = Object<'env>> { pub object: T, _phantom: &'env PhantomData<()>, @@ -46,12 +47,6 @@ impl<'env, T: JsValue<'env>> JsValue<'env> for This<'_, T> { } } -impl<'env, T: JsValue<'env>> JsValue<'env> for &This<'_, T> { - fn value(&self) -> Value { - self.object.value() - } -} - impl FromNapiValue for This<'_, T> { unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result { Ok(Self { @@ -146,7 +141,7 @@ impl<'env, T: 'env> ClassInstance<'env, T> { U: FromNapiValue + JsValue<'this>, { let property = Property::new(name)? - .with_value(&self) + .with_value(self) .with_property_attributes(attributes); check_status!( @@ -171,16 +166,6 @@ impl<'env, T: 'env> ClassInstance<'env, T> { } } -impl<'env, T: 'env> JsValue<'env> for &ClassInstance<'env, T> { - fn value(&self) -> Value { - Value { - env: self.env, - value: self.value, - value_type: ValueType::Object, - } - } -} - impl<'env, T: 'env> TypeName for ClassInstance<'env, T> where &'env T: TypeName, diff --git a/crates/napi/src/bindgen_runtime/js_values/function.rs b/crates/napi/src/bindgen_runtime/js_values/function.rs index 524315f4..62fb979e 100644 --- a/crates/napi/src/bindgen_runtime/js_values/function.rs +++ b/crates/napi/src/bindgen_runtime/js_values/function.rs @@ -105,6 +105,7 @@ impl_tuple_conversion!( A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z ); +#[derive(Clone, Copy)] /// A JavaScript function. /// It can only live in the scope of a function call. /// If you want to use it outside the scope of a function call, you can turn it into a reference. diff --git a/crates/napi/src/bindgen_runtime/js_values/promise_raw.rs b/crates/napi/src/bindgen_runtime/js_values/promise_raw.rs index 84806da6..0de84900 100644 --- a/crates/napi/src/bindgen_runtime/js_values/promise_raw.rs +++ b/crates/napi/src/bindgen_runtime/js_values/promise_raw.rs @@ -18,7 +18,7 @@ pub struct PromiseRaw<'env, T> { _phantom: &'env PhantomData, } -impl<'env, T> JsValue<'env> for PromiseRaw<'env, T> { +impl<'env, T: FromNapiValue> JsValue<'env> for PromiseRaw<'env, T> { fn value(&self) -> Value { Value { env: self.env, @@ -28,7 +28,7 @@ impl<'env, T> JsValue<'env> for PromiseRaw<'env, T> { } } -impl<'env, T> JsObjectValue<'env> for PromiseRaw<'env, T> {} +impl<'env, T: FromNapiValue> JsObjectValue<'env> for PromiseRaw<'env, T> {} impl TypeName for PromiseRaw<'_, T> { fn type_name() -> &'static str { diff --git a/crates/napi/src/bindgen_runtime/js_values/scope.rs b/crates/napi/src/bindgen_runtime/js_values/scope.rs index 034927fe..74a9bb7a 100644 --- a/crates/napi/src/bindgen_runtime/js_values/scope.rs +++ b/crates/napi/src/bindgen_runtime/js_values/scope.rs @@ -1,6 +1,6 @@ use std::ptr; -use crate::{check_status, sys, Env, JsValue, Result}; +use crate::{bindgen_runtime::FromNapiValue, check_status, sys, Env, JsValue, Result}; pub struct HandleScope { pub(crate) scope: sys::napi_handle_scope, @@ -16,7 +16,29 @@ impl HandleScope { Ok(Self { scope }) } - pub fn run(self, arg: A, f: impl FnOnce(A) -> Result) -> Result + /// # Safety + /// + /// This function is unsafe because it will invalidate the JsValue created within the HandleScope. + /// + /// For example: + /// + /// ```no_run + /// #[napi] + /// pub fn shorter_scope(env: &Env, arr: Array) -> Result> { + /// let len = arr.len(); + /// let mut result = Vec::with_capacity(len as usize); + /// for i in 0..len { + /// let scope = HandleScope::create(env)?; + /// let value: Unknown = arr.get_element(i)?; + /// ^^^ this will be invalidated after the scope is closed + /// let len = unsafe { scope.close(value, |v| match v.get_type()? { + /// ValueType::String => Ok(v.utf8_len()? as u32), + /// _ => Ok(0), + /// })? }; + /// } + /// } + /// ``` + pub unsafe fn close(self, arg: A, f: impl FnOnce(A) -> Result) -> Result where A: JsValuesTuple, { @@ -30,6 +52,57 @@ impl HandleScope { } } +pub struct EscapableHandleScope<'env> { + pub(crate) scope: sys::napi_escapable_handle_scope, + pub(crate) env: sys::napi_env, + pub(crate) phantom: std::marker::PhantomData<&'env ()>, +} + +impl<'env, 'scope: 'env> EscapableHandleScope<'scope> { + pub fn with< + T, + Args: JsValuesTuple, + F: 'env + FnOnce(EscapableHandleScope<'env>, Args) -> Result, + >( + env: &'env Env, + args: Args, + scope_fn: F, + ) -> Result { + let mut scope = ptr::null_mut(); + check_status!( + unsafe { sys::napi_open_escapable_handle_scope(env.0, &mut scope) }, + "Failed to open handle scope" + )?; + let scope: EscapableHandleScope<'env> = Self { + scope, + env: env.0, + phantom: std::marker::PhantomData, + }; + scope_fn(scope, args) + } + + pub fn escape + FromNapiValue>(&self, value: V) -> Result { + let mut result = ptr::null_mut(); + check_status!( + unsafe { sys::napi_escape_handle(self.env, self.scope, value.raw(), &mut result) }, + "Failed to escape handle" + )?; + unsafe { V::from_napi_value(self.env, result) } + } +} + +impl Drop for EscapableHandleScope<'_> { + fn drop(&mut self) { + let status = unsafe { sys::napi_close_escapable_handle_scope(self.env, self.scope) }; + if status != sys::Status::napi_ok { + panic!( + "Failed to close handle scope: {}", + crate::Status::from(status) + ); + } + } +} + pub trait JsValuesTuple { fn env(&self) -> sys::napi_env; } diff --git a/crates/napi/src/env.rs b/crates/napi/src/env.rs index 7942e1a2..a42e2653 100644 --- a/crates/napi/src/env.rs +++ b/crates/napi/src/env.rs @@ -26,9 +26,9 @@ use crate::bindgen_runtime::FinalizeContext; use crate::bindgen_runtime::FunctionCallContext; #[cfg(all(feature = "tokio_rt", feature = "napi4"))] use crate::bindgen_runtime::PromiseRaw; -#[cfg(feature = "napi4")] -use crate::bindgen_runtime::ToNapiValue; -use crate::bindgen_runtime::{FromNapiValue, Function, JsValuesTupleIntoVec, Object, Unknown}; +use crate::bindgen_runtime::{ + FromNapiValue, Function, JsValuesTupleIntoVec, Object, ToNapiValue, Unknown, +}; #[cfg(feature = "napi3")] use crate::cleanup_env::{CleanupEnvHook, CleanupEnvHookData}; #[cfg(feature = "serde-json")] diff --git a/crates/napi/src/js_values/buffer.rs b/crates/napi/src/js_values/buffer.rs index ccbd35e2..9bb21e44 100644 --- a/crates/napi/src/js_values/buffer.rs +++ b/crates/napi/src/js_values/buffer.rs @@ -3,7 +3,7 @@ use std::ops::{Deref, DerefMut}; use std::ptr; use crate::{ - bindgen_runtime::{TypeName, ValidateNapiValue}, + bindgen_runtime::{FromNapiValue, TypeName, ValidateNapiValue}, check_status, sys, Env, Error, JsValue, Ref, Result, Status, Unknown, Value, ValueType, }; @@ -34,6 +34,16 @@ impl ValidateNapiValue for JsBuffer { } } +impl FromNapiValue for JsBuffer { + unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result { + Ok(JsBuffer(Value { + env, + value: napi_val, + value_type: ValueType::Object, + })) + } +} + impl JsValue<'_> for JsBuffer { fn value(&self) -> Value { self.0 diff --git a/crates/napi/src/js_values/date.rs b/crates/napi/src/js_values/date.rs index b2d2579a..6edf5203 100644 --- a/crates/napi/src/js_values/date.rs +++ b/crates/napi/src/js_values/date.rs @@ -1,10 +1,9 @@ use std::marker::PhantomData; use std::ptr; -use super::{check_status, JsObjectValue}; use crate::{ - bindgen_runtime::{FromNapiValue, TypeName, ValidateNapiValue}, - sys, Error, JsValue, Result, Status, Value, ValueType, + bindgen_runtime::{FromNapiValue, JsObjectValue, TypeName, ValidateNapiValue}, + check_status, sys, Error, JsValue, Result, Status, Value, ValueType, }; #[derive(Clone, Copy)] diff --git a/crates/napi/src/js_values/global.rs b/crates/napi/src/js_values/global.rs index ffd829fd..6d1d7d05 100644 --- a/crates/napi/src/js_values/global.rs +++ b/crates/napi/src/js_values/global.rs @@ -6,6 +6,19 @@ pub struct JsGlobal<'env>( pub(crate) std::marker::PhantomData<&'env ()>, ); +impl FromNapiValue for JsGlobal<'_> { + unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result { + Ok(JsGlobal( + Value { + env, + value: napi_val, + value_type: ValueType::Object, + }, + std::marker::PhantomData, + )) + } +} + impl<'env> JsValue<'env> for JsGlobal<'env> { fn value(&self) -> Value { self.0 diff --git a/crates/napi/src/js_values/mod.rs b/crates/napi/src/js_values/mod.rs index ec3333d9..588ffc40 100644 --- a/crates/napi/src/js_values/mod.rs +++ b/crates/napi/src/js_values/mod.rs @@ -12,11 +12,11 @@ use crate::bindgen_runtime::finalize_closures; #[cfg(feature = "compat-mode")] use crate::{ bindgen_runtime::{FromNapiValue, ValidateNapiValue}, - type_of, Callback, Error, Status, + check_status, type_of, Callback, Error, Status, }; use crate::{ bindgen_runtime::{JsObjectValue, ToNapiValue}, - check_status, sys, Result, ValueType, + sys, Result, ValueType, }; #[cfg(feature = "compat-mode")] diff --git a/crates/napi/src/js_values/value.rs b/crates/napi/src/js_values/value.rs index 5b5fa523..6e5672f8 100644 --- a/crates/napi/src/js_values/value.rs +++ b/crates/napi/src/js_values/value.rs @@ -1,6 +1,7 @@ use std::fmt::{self, Display}; use std::ptr; +use crate::bindgen_runtime::EscapableHandleScope; use crate::{ bindgen_runtime::{FromNapiValue, Object, Unknown}, {check_status, sys, JsNumber, JsString, Result, ValueType}, @@ -19,7 +20,7 @@ impl Display for Value { } } -pub trait JsValue<'env>: Sized { +pub trait JsValue<'env>: Sized + FromNapiValue { fn value(&self) -> Value; fn raw(&self) -> sys::napi_value { @@ -157,4 +158,20 @@ pub trait JsValue<'env>: Sized { })?; Ok(result) } + + fn escape<'scope, E: JsValue<'scope> + FromNapiValue>( + &self, + escapable_handle_scope: EscapableHandleScope<'scope>, + ) -> Result { + let mut result = ptr::null_mut(); + unsafe { + sys::napi_escape_handle( + escapable_handle_scope.env, + escapable_handle_scope.scope, + self.raw(), + &mut result, + ) + }; + unsafe { ::from_napi_value(self.value().env, result) } + } } diff --git a/examples/napi/__tests__/__snapshots__/values.spec.ts.md b/examples/napi/__tests__/__snapshots__/values.spec.ts.md index 80be2855..d5b20a61 100644 --- a/examples/napi/__tests__/__snapshots__/values.spec.ts.md +++ b/examples/napi/__tests__/__snapshots__/values.spec.ts.md @@ -824,6 +824,8 @@ Generated by [AVA](https://avajs.dev). value: number␊ }␊ ␊ + export declare function shorterEscapableScope(createString: () => string | null): string␊ + ␊ export declare function shorterScope(arr: unknown[]): Array␊ ␊ export declare function shutdownRuntime(): void␊ diff --git a/examples/napi/__tests__/__snapshots__/values.spec.ts.snap b/examples/napi/__tests__/__snapshots__/values.spec.ts.snap index e6852926..d51e433c 100644 Binary files a/examples/napi/__tests__/__snapshots__/values.spec.ts.snap and b/examples/napi/__tests__/__snapshots__/values.spec.ts.snap differ diff --git a/examples/napi/__tests__/values.spec.ts b/examples/napi/__tests__/values.spec.ts index a00a5d60..4d03cd6b 100644 --- a/examples/napi/__tests__/values.spec.ts +++ b/examples/napi/__tests__/values.spec.ts @@ -233,6 +233,7 @@ import { shutdownRuntime, callAsyncWithUnknownReturnValue, shorterScope, + shorterEscapableScope, } from '../index.cjs' // import other stuff in `#[napi(module_exports)]` import nativeAddon from '../index.cjs' @@ -1666,3 +1667,19 @@ test('shorter scope', (t) => { const result = shorterScope(['hello', { foo: 'bar' }, 'world', true]) t.deepEqual(result, [5, 1, 5, 0]) }) + +test('escapable handle scope', (t) => { + function makeIterFunction() { + let i = 0 + return () => { + if (i >= 10_000) { + return null + } + i++ + return Math.random().toString().repeat(100) + } + } + t.notThrows(() => { + shorterEscapableScope(makeIterFunction()) + }) +}) diff --git a/examples/napi/example.wasi-browser.js b/examples/napi/example.wasi-browser.js index c42fba5b..0f423926 100644 --- a/examples/napi/example.wasi-browser.js +++ b/examples/napi/example.wasi-browser.js @@ -292,6 +292,7 @@ export const roundtripStr = __napiModule.exports.roundtripStr export const runScript = __napiModule.exports.runScript export const setNullByteProperty = __napiModule.exports.setNullByteProperty export const setSymbolInObj = __napiModule.exports.setSymbolInObj +export const shorterEscapableScope = __napiModule.exports.shorterEscapableScope export const shorterScope = __napiModule.exports.shorterScope export const shutdownRuntime = __napiModule.exports.shutdownRuntime export const spawnThreadInThread = __napiModule.exports.spawnThreadInThread diff --git a/examples/napi/example.wasi.cjs b/examples/napi/example.wasi.cjs index 53280515..6ea843a4 100644 --- a/examples/napi/example.wasi.cjs +++ b/examples/napi/example.wasi.cjs @@ -316,6 +316,7 @@ module.exports.roundtripStr = __napiModule.exports.roundtripStr module.exports.runScript = __napiModule.exports.runScript module.exports.setNullByteProperty = __napiModule.exports.setNullByteProperty module.exports.setSymbolInObj = __napiModule.exports.setSymbolInObj +module.exports.shorterEscapableScope = __napiModule.exports.shorterEscapableScope module.exports.shorterScope = __napiModule.exports.shorterScope module.exports.shutdownRuntime = __napiModule.exports.shutdownRuntime module.exports.spawnThreadInThread = __napiModule.exports.spawnThreadInThread diff --git a/examples/napi/index.cjs b/examples/napi/index.cjs index b400ec22..d1313462 100644 --- a/examples/napi/index.cjs +++ b/examples/napi/index.cjs @@ -606,6 +606,7 @@ module.exports.roundtripStr = nativeBinding.roundtripStr module.exports.runScript = nativeBinding.runScript module.exports.setNullByteProperty = nativeBinding.setNullByteProperty module.exports.setSymbolInObj = nativeBinding.setSymbolInObj +module.exports.shorterEscapableScope = nativeBinding.shorterEscapableScope module.exports.shorterScope = nativeBinding.shorterScope module.exports.shutdownRuntime = nativeBinding.shutdownRuntime module.exports.spawnThreadInThread = nativeBinding.spawnThreadInThread diff --git a/examples/napi/index.d.cts b/examples/napi/index.d.cts index 6163d2fc..74e81c19 100644 --- a/examples/napi/index.d.cts +++ b/examples/napi/index.d.cts @@ -786,6 +786,8 @@ export interface Shared { value: number } +export declare function shorterEscapableScope(createString: () => string | null): string + export declare function shorterScope(arr: unknown[]): Array export declare function shutdownRuntime(): void diff --git a/examples/napi/src/scope.rs b/examples/napi/src/scope.rs index 0fa8fd07..25ed429b 100644 --- a/examples/napi/src/scope.rs +++ b/examples/napi/src/scope.rs @@ -7,15 +7,56 @@ pub fn shorter_scope(env: &Env, arr: Array) -> Result> { for i in 0..len { let scope = HandleScope::create(env)?; let value: Unknown = arr.get_element(i)?; - let len = scope.run(value, |v| match v.get_type()? { - ValueType::String => { - let string = unsafe { v.cast::() }?; - Ok(string.utf8_len()? as u32) - } - ValueType::Object => Ok(1), - _ => Ok(0), - })?; + let len = unsafe { + scope.close(value, |v| match v.get_type()? { + ValueType::String => { + let string = v.cast::()?; + Ok(string.utf8_len()? as u32) + } + ValueType::Object => Ok(1), + _ => Ok(0), + })? + }; result.push(len); } Ok(result) } + +#[napi] +pub fn shorter_escapable_scope<'env>( + env: &'env Env, + create_string: Function<(), Option>, +) -> Result> { + let mut longest_string = env.create_string("")?; + let mut prev_len = 0; + loop { + if let Some(maybe_longest) = EscapableHandleScope::with( + env, + (create_string, longest_string), + move |scope, (create_string, prev)| { + let elem = create_string.call(())?; + if let Some(string) = elem { + let len = string.utf8_len()?; + if len > prev.utf8_len()? { + return Ok(Some(Either::A(string.escape::(scope)?))); + } + } else { + return Ok(Some(Either::B(()))); + } + Ok(None) + }, + )? { + match maybe_longest { + Either::A(longest) => { + if longest.utf8_len()? == prev_len { + break; + } + prev_len = longest.utf8_len()?; + longest_string = longest; + } + Either::B(_) => break, + } + } + } + Ok(longest_string) +}