use std::os::raw::c_void;
use std::ptr;
#[cfg(target_family = "wasm")]
use std::sync::atomic::AtomicU32;
use std::{
marker::PhantomData,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock, Weak,
},
};
#[cfg(target_family = "wasm")]
static PENDING_DEFERRED_SETTLES: AtomicU32 = AtomicU32::new(0);
#[cfg(all(target_family = "wasm", not(feature = "noop")))]
pub(crate) fn pending_deferred_settles() -> u32 {
PENDING_DEFERRED_SETTLES.load(Ordering::SeqCst)
}
#[cfg(target_family = "wasm")]
thread_local! {
static WASM_ENV_CLEANUP_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
#[cfg(target_family = "wasm")]
fn settles_synchronously(owner_thread: std::thread::ThreadId) -> bool {
owner_thread == std::thread::current().id()
&& WASM_ENV_CLEANUP_DEPTH.with(|depth| depth.get() != 0)
}
#[cfg(target_family = "wasm")]
struct WasmHandleScope {
env: sys::napi_env,
scope: sys::napi_handle_scope,
}
#[cfg(target_family = "wasm")]
impl WasmHandleScope {
fn open(env: sys::napi_env) -> Option<Self> {
let mut scope = ptr::null_mut();
let status = unsafe { sys::napi_open_handle_scope(env, &mut scope) };
debug_assert!(
status == sys::Status::napi_ok,
"Open handle scope in JsDeferred failed"
);
(status == sys::Status::napi_ok).then_some(Self { env, scope })
}
}
#[cfg(target_family = "wasm")]
impl Drop for WasmHandleScope {
fn drop(&mut self) {
let status = unsafe { sys::napi_close_handle_scope(self.env, self.scope) };
debug_assert!(
status == sys::Status::napi_ok,
"Close handle scope in JsDeferred failed"
);
}
}
#[cfg(all(
target_family = "wasm",
not(feature = "noop"),
any(feature = "tokio_rt", feature = "async-runtime")
))]
pub(crate) struct WasmEnvCleanupBarrier(());
#[cfg(all(
target_family = "wasm",
not(feature = "noop"),
any(feature = "tokio_rt", feature = "async-runtime")
))]
impl WasmEnvCleanupBarrier {
pub(crate) fn enter() -> Self {
WASM_ENV_CLEANUP_DEPTH.with(|depth| depth.set(depth.get().saturating_add(1)));
Self(())
}
}
#[cfg(all(
target_family = "wasm",
not(feature = "noop"),
any(feature = "tokio_rt", feature = "async-runtime")
))]
impl Drop for WasmEnvCleanupBarrier {
fn drop(&mut self) {
WASM_ENV_CLEANUP_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
}
}
#[cfg(feature = "deferred_trace")]
use crate::{bindgen_runtime::JsObjectValue, JsValue};
use crate::{
bindgen_runtime::{Object, ToNapiValue},
check_status, sys, Env, Error, Result,
};
#[cfg(feature = "deferred_trace")]
#[repr(transparent)]
#[derive(Clone)]
struct DeferredTrace(sys::napi_ref);
#[cfg(feature = "deferred_trace")]
impl DeferredTrace {
fn new(raw_env: sys::napi_env) -> Result<Self> {
let env = Env::from_raw(raw_env);
let reason = env.create_string("none")?;
let mut js_error = ptr::null_mut();
check_status!(
unsafe { sys::napi_create_error(raw_env, ptr::null_mut(), reason.raw(), &mut js_error) },
"Create error in DeferredTrace failed"
)?;
let mut result = ptr::null_mut();
check_status!(
unsafe { sys::napi_create_reference(raw_env, js_error, 1, &mut result) },
"Create reference in DeferredTrace failed"
)?;
Ok(Self(result))
}
fn into_rejected(self, raw_env: sys::napi_env, err: Error) -> Result<sys::napi_value> {
let env = Env::from_raw(raw_env);
let mut raw = ptr::null_mut();
check_status!(
unsafe { sys::napi_get_reference_value(raw_env, self.0, &mut raw) },
"Failed to get referenced value in DeferredTrace"
)?;
let err_value = (|| -> Result<sys::napi_value> {
if let Some(err_raw_value) = unsafe { err.referenced_value(raw_env) } {
return Ok(err_raw_value);
}
let mut obj = Object::from_raw(raw_env, raw);
obj.set_named_property("message", &err.reason)?;
obj.set_named_property(
"code",
env.create_string_from_std(format!("{}", err.status))?,
)?;
Ok(raw)
})();
let delete_status = unsafe { sys::napi_delete_reference(raw_env, self.0) };
let err_value = err_value?;
check_status!(
delete_status,
"Failed to delete the reference in DeferredTrace"
)?;
Ok(err_value)
}
}
type FinalizeCallback = Arc<RwLock<Option<Box<dyn FnOnce(sys::napi_env)>>>>;
struct DeferredData<Data: ToNapiValue, Resolver: FnOnce(Env) -> Result<Data>> {
resolver: Result<Resolver>,
#[cfg(feature = "deferred_trace")]
trace: DeferredTrace,
tsfn: sys::napi_threadsafe_function,
finalize_callback: FinalizeCallback,
}
struct DeferredHandle {
pending_tsfn: Mutex<Option<sys::napi_threadsafe_function>>,
}
struct DeferredHookData {
handle: Weak<DeferredHandle>,
hook_registered: AtomicBool,
}
unsafe impl Send for DeferredHandle {}
unsafe impl Sync for DeferredHandle {}
pub struct JsDeferred<Data: ToNapiValue, Resolver: FnOnce(Env) -> Result<Data>> {
handle: Arc<DeferredHandle>,
#[cfg(feature = "deferred_trace")]
trace: DeferredTrace,
finalize_callback: FinalizeCallback,
#[cfg(target_family = "wasm")]
env: sys::napi_env,
#[cfg(target_family = "wasm")]
raw_deferred: sys::napi_deferred,
#[cfg(target_family = "wasm")]
owner_thread: std::thread::ThreadId,
_data: PhantomData<Data>,
_resolver: PhantomData<Resolver>,
}
impl<Data: ToNapiValue, Resolver: FnOnce(Env) -> Result<Data>> Clone
for JsDeferred<Data, Resolver>
{
fn clone(&self) -> Self {
Self {
handle: self.handle.clone(),
#[cfg(feature = "deferred_trace")]
trace: self.trace.clone(),
finalize_callback: self.finalize_callback.clone(),
#[cfg(target_family = "wasm")]
env: self.env,
#[cfg(target_family = "wasm")]
raw_deferred: self.raw_deferred,
#[cfg(target_family = "wasm")]
owner_thread: self.owner_thread,
_data: PhantomData,
_resolver: PhantomData,
}
}
}
unsafe impl<Data: ToNapiValue, Resolver: FnOnce(Env) -> Result<Data>> Send
for JsDeferred<Data, Resolver>
{
}
impl<Data: ToNapiValue, Resolver: FnOnce(Env) -> Result<Data>> JsDeferred<Data, Resolver> {
pub(crate) fn new(env: &Env) -> Result<(Self, Object<'_>)> {
let handle = Arc::new(DeferredHandle {
pending_tsfn: Mutex::new(None),
});
let hook_data_ptr = Box::into_raw(Box::new(DeferredHookData {
handle: Arc::downgrade(&handle),
hook_registered: AtomicBool::new(false),
}));
let (tsfn, _raw_deferred, promise) = match js_deferred_new_raw(
env,
Some(napi_resolve_deferred::<Data, Resolver>),
hook_data_ptr.cast(),
) {
Ok(created) => created,
Err(err) => {
drop(unsafe { Box::from_raw(hook_data_ptr) });
return Err(err);
}
};
*handle
.pending_tsfn
.lock()
.expect("JsDeferred pending lock failed") = Some(tsfn);
#[cfg(not(target_family = "wasm"))]
{
if let Err(err) = check_status!(
unsafe {
sys::napi_add_env_cleanup_hook(
env.0,
Some(deferred_env_teardown_cb),
hook_data_ptr.cast(),
)
},
"Register env cleanup hook in JsDeferred failed"
) {
if let Some(tsfn) = handle
.pending_tsfn
.lock()
.expect("JsDeferred pending lock failed")
.take()
{
unsafe {
sys::napi_release_threadsafe_function(tsfn, sys::ThreadsafeFunctionReleaseMode::abort)
};
}
return Err(err);
}
unsafe { &*hook_data_ptr }
.hook_registered
.store(true, Ordering::Release);
}
#[cfg(feature = "deferred_trace")]
let trace = match DeferredTrace::new(env.0) {
Ok(trace) => trace,
Err(err) => {
if let Some(tsfn) = handle
.pending_tsfn
.lock()
.expect("JsDeferred pending lock failed")
.take()
{
unsafe {
sys::napi_release_threadsafe_function(tsfn, sys::ThreadsafeFunctionReleaseMode::abort)
};
}
return Err(err);
}
};
let deferred = Self {
handle,
#[cfg(feature = "deferred_trace")]
trace,
finalize_callback: Default::default(),
#[cfg(target_family = "wasm")]
env: env.0,
#[cfg(target_family = "wasm")]
raw_deferred: _raw_deferred,
#[cfg(target_family = "wasm")]
owner_thread: std::thread::current().id(),
_data: PhantomData,
_resolver: PhantomData,
};
Ok((deferred, promise))
}
pub fn resolve(self, resolver: Resolver) {
self.call_tsfn(Ok(resolver))
}
pub fn reject(self, error: Error) {
self.call_tsfn(Err(error))
}
#[allow(clippy::arc_with_non_send_sync)]
pub fn set_finalize_callback(
&mut self,
finalize_callback: Option<Box<dyn FnOnce(sys::napi_env)>>,
) {
self.finalize_callback = Arc::new(RwLock::new(finalize_callback));
}
fn call_tsfn(self, result: Result<Resolver>) {
let mut pending = self
.handle
.pending_tsfn
.lock()
.expect("JsDeferred pending lock failed");
let Some(tsfn) = pending.take() else {
return;
};
let data = DeferredData {
resolver: result,
#[cfg(feature = "deferred_trace")]
trace: self.trace,
tsfn,
finalize_callback: self.finalize_callback.clone(),
};
#[cfg(target_family = "wasm")]
let data = if settles_synchronously(self.owner_thread) {
let (env, raw_deferred) = (self.env, self.raw_deferred);
match WasmHandleScope::open(env) {
Some(_scope) => {
drop(pending);
settle_deferred::<Data, Resolver>(env, raw_deferred, data);
return;
}
None => data,
}
} else {
data
};
#[cfg(target_family = "wasm")]
PENDING_DEFERRED_SETTLES.fetch_add(1, Ordering::SeqCst);
let status = unsafe {
sys::napi_call_threadsafe_function(
tsfn,
Box::into_raw(Box::from(data)).cast(),
sys::ThreadsafeFunctionCallMode::blocking,
)
};
#[cfg(target_family = "wasm")]
if status != sys::Status::napi_ok {
release_pending_deferred_settle();
}
debug_assert!(
status == sys::Status::napi_ok,
"Call threadsafe function in JsDeferred failed"
);
}
}
#[cfg(not(target_family = "wasm"))]
unsafe extern "C" fn deferred_env_teardown_cb(data: *mut c_void) {
let hook_data = unsafe { &*data.cast::<DeferredHookData>() };
hook_data.hook_registered.store(false, Ordering::Release);
let Some(handle) = hook_data.handle.upgrade() else {
return;
};
let mut pending = handle
.pending_tsfn
.lock()
.expect("JsDeferred pending lock failed");
if let Some(tsfn) = pending.take() {
let status = unsafe {
sys::napi_release_threadsafe_function(tsfn, sys::ThreadsafeFunctionReleaseMode::abort)
};
debug_assert!(
status == sys::Status::napi_ok,
"Abort deferred threadsafe function on env teardown failed"
);
}
}
unsafe extern "C" fn deferred_finalize_cb(
env: sys::napi_env,
finalize_data: *mut c_void,
_finalize_hint: *mut c_void,
) {
let hook_registered = unsafe { &*finalize_data.cast::<DeferredHookData>() }
.hook_registered
.load(Ordering::Acquire);
#[cfg(not(target_family = "wasm"))]
if !env.is_null() && hook_registered {
unsafe {
sys::napi_remove_env_cleanup_hook(env, Some(deferred_env_teardown_cb), finalize_data)
};
}
#[cfg(target_family = "wasm")]
{
let _ = env;
let _ = hook_registered;
}
let hook_data = unsafe { Box::from_raw(finalize_data.cast::<DeferredHookData>()) };
if let Some(handle) = hook_data.handle.upgrade() {
handle
.pending_tsfn
.lock()
.expect("JsDeferred pending lock failed")
.take();
}
}
fn js_deferred_new_raw(
env: &Env,
resolve_deferred: sys::napi_threadsafe_function_call_js,
finalize_data: *mut c_void,
) -> Result<(
sys::napi_threadsafe_function,
sys::napi_deferred,
Object<'_>,
)> {
#[cfg(all(not(feature = "noop"), not(target_family = "wasm")))]
crate::bindgen_runtime::retain_current_module_for_unload_safety();
let mut raw_promise = ptr::null_mut();
let mut raw_deferred = ptr::null_mut();
check_status!(
unsafe { sys::napi_create_promise(env.0, &mut raw_deferred, &mut raw_promise) },
"Create promise in JsDeferred failed"
)?;
let mut async_resource_name = ptr::null_mut();
check_status!(
unsafe {
sys::napi_create_string_utf8(
env.0,
c"napi_resolve_deferred".as_ptr().cast(),
22,
&mut async_resource_name,
)
},
"Create async resource name in JsDeferred failed"
)?;
let mut tsfn = ptr::null_mut();
check_status!(
unsafe {
sys::napi_create_threadsafe_function(
env.0,
ptr::null_mut(),
ptr::null_mut(),
async_resource_name,
0,
1,
finalize_data,
Some(deferred_finalize_cb),
raw_deferred.cast(),
resolve_deferred,
&mut tsfn,
)
},
"Create threadsafe function in JsDeferred failed"
)?;
let promise = Object::from_raw(env.0, raw_promise);
Ok((tsfn, raw_deferred, promise))
}
#[cfg(target_family = "wasm")]
fn release_pending_deferred_settle() {
let _ = PENDING_DEFERRED_SETTLES.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |pending| {
Some(pending.saturating_sub(1))
});
}
extern "C" fn napi_resolve_deferred<Data: ToNapiValue, Resolver: FnOnce(Env) -> Result<Data>>(
env: sys::napi_env,
_js_callback: sys::napi_value,
context: *mut c_void,
data: *mut c_void,
) {
#[cfg(target_family = "wasm")]
release_pending_deferred_settle();
let deferred_data: Box<DeferredData<Data, Resolver>> = unsafe { Box::from_raw(data.cast()) };
if env.is_null() {
unsafe {
sys::napi_release_threadsafe_function(
deferred_data.tsfn,
sys::ThreadsafeFunctionReleaseMode::release,
)
};
return;
}
settle_deferred::<Data, Resolver>(env, context.cast(), *deferred_data);
}
fn settle_deferred<Data: ToNapiValue, Resolver: FnOnce(Env) -> Result<Data>>(
env: sys::napi_env,
deferred: sys::napi_deferred,
deferred_data: DeferredData<Data, Resolver>,
) {
let tsfn: sys::napi_threadsafe_function = deferred_data.tsfn;
let finalize_callback = RwLock::write(&deferred_data.finalize_callback)
.expect("RwLock Poison")
.take();
let result = deferred_data
.resolver
.and_then(|resolver| resolver(Env::from_raw(env)))
.and_then(|res| unsafe { ToNapiValue::to_napi_value(env, res) });
let release_tsfn_result = check_status!(
unsafe {
sys::napi_release_threadsafe_function(tsfn, sys::ThreadsafeFunctionReleaseMode::release)
},
"Release threadsafe function in JsDeferred failed"
);
if let Err(e) = release_tsfn_result.and(result).and_then(|res| {
check_status!(
unsafe { sys::napi_resolve_deferred(env, deferred, res) },
"Resolve deferred value failed"
)
.map(|_| {
#[cfg(feature = "deferred_trace")]
{
let _status = unsafe { sys::napi_delete_reference(env, deferred_data.trace.0) };
if _status != sys::Status::napi_ok && cfg!(debug_assertions) {
eprintln!(
"Failed to delete reference in deferred {}",
crate::Status::from(_status)
);
}
}
})
}) {
#[cfg(feature = "deferred_trace")]
let error = deferred_data.trace.into_rejected(env, e);
#[cfg(not(feature = "deferred_trace"))]
let error = unsafe { ToNapiValue::to_napi_value(env, e) };
match error {
Ok(error) => {
unsafe { sys::napi_reject_deferred(env, deferred, error) };
if let Some(finalize_callback) = finalize_callback {
finalize_callback(env);
}
}
Err(err) => {
if let Some(finalize_callback) = finalize_callback {
finalize_callback(env);
}
if cfg!(debug_assertions) {
eprintln!("Failed to reject deferred: {err:?}");
let mut err = ptr::null_mut();
let mut err_msg = ptr::null_mut();
unsafe {
sys::napi_create_string_utf8(env, c"Rejection failed".as_ptr().cast(), 0, &mut err_msg);
sys::napi_create_error(env, ptr::null_mut(), err_msg, &mut err);
sys::napi_reject_deferred(env, deferred, err);
}
}
}
}
} else if let Some(finalize_callback) = finalize_callback {
finalize_callback(env);
}
}