feat(napi): EscapableHandleScope API (#2652)

This commit is contained in:
LongYinan 2025-05-19 19:52:21 +08:00 committed by GitHub
parent 92b094e487
commit 7182db3a81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 215 additions and 39 deletions

View File

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

View File

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

View File

@ -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<T: FromNapiValue> FromNapiValue for This<'_, T> {
unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result<Self> {
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,

View File

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

View File

@ -18,7 +18,7 @@ pub struct PromiseRaw<'env, T> {
_phantom: &'env PhantomData<T>,
}
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<T: FromNapiValue> TypeName for PromiseRaw<'_, T> {
fn type_name() -> &'static str {

View File

@ -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<A, T>(self, arg: A, f: impl FnOnce(A) -> Result<T>) -> Result<T>
/// # 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<Vec<u32>> {
/// 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<A, T>(self, arg: A, f: impl FnOnce(A) -> Result<T>) -> Result<T>
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<T>,
>(
env: &'env Env,
args: Args,
scope_fn: F,
) -> Result<T> {
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<V: JsValue<'env> + FromNapiValue>(&self, value: V) -> Result<V> {
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;
}

View File

@ -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")]

View File

@ -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<Self> {
Ok(JsBuffer(Value {
env,
value: napi_val,
value_type: ValueType::Object,
}))
}
}
impl JsValue<'_> for JsBuffer {
fn value(&self) -> Value {
self.0

View File

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

View File

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

View File

@ -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")]

View File

@ -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<E> {
let mut result = ptr::null_mut();
unsafe {
sys::napi_escape_handle(
escapable_handle_scope.env,
escapable_handle_scope.scope,
self.raw(),
&mut result,
)
};
unsafe { <E as FromNapiValue>::from_napi_value(self.value().env, result) }
}
}

View File

@ -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<number>
export declare function shutdownRuntime(): void␊

View File

@ -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())
})
})

View File

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

View File

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

View File

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

View File

@ -786,6 +786,8 @@ export interface Shared {
value: number
}
export declare function shorterEscapableScope(createString: () => string | null): string
export declare function shorterScope(arr: unknown[]): Array<number>
export declare function shutdownRuntime(): void

View File

@ -7,15 +7,56 @@ pub fn shorter_scope(env: &Env, arr: Array) -> Result<Vec<u32>> {
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::<JsString>() }?;
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::<JsString>()?;
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<JsString>>,
) -> Result<JsString<'env>> {
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::<JsString>(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)
}