Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion napi/src/promise.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::future::Future;
use std::os::raw::{c_char, c_void};
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use crate::{check_status, sys, Env, JsError, NapiValue, Result};

Expand All @@ -10,10 +12,27 @@ pub struct FuturePromise<T, V: NapiValue> {
tsfn: sys::napi_threadsafe_function,
async_resource_name: sys::napi_value,
resolver: Box<dyn FnOnce(&mut Env, T) -> Result<V>>,
/// Set to `true` when the N-API environment begins teardown. When this is
/// true, calling `napi_resolve_deferred` / `napi_reject_deferred` can crash
/// in Node.js >= 22 because the V8 `Persistent` backing the `Deferred` may
/// already be invalidated (SIGSEGV in `GlobalHandles::Destroy`).
env_tearing_down: Arc<AtomicBool>,
/// Raw pointer to the cleanup hook data, needed to unregister the hook on
/// normal completion. Null if the hook has already been unregistered.
cleanup_hook_data: *mut c_void,
}

unsafe impl<T, V: NapiValue> Send for FuturePromise<T, V> {}

struct CleanupHookData {
flag: Arc<AtomicBool>,
}

unsafe extern "C" fn env_teardown_cleanup(data: *mut c_void) {
let hook_data = Box::from_raw(data as *mut CleanupHookData);
hook_data.flag.store(true, Ordering::Release);
}

impl<T, V: NapiValue> FuturePromise<T, V> {
#[inline]
pub fn create(
Expand All @@ -32,12 +51,22 @@ impl<T, V: NapiValue> FuturePromise<T, V> {
)
})?;

let env_tearing_down = Arc::new(AtomicBool::new(false));
let hook_data = Box::into_raw(Box::new(CleanupHookData {
flag: Arc::clone(&env_tearing_down),
}));
unsafe {
sys::napi_add_env_cleanup_hook(env, Some(env_teardown_cleanup), hook_data as *mut c_void);
}

Ok(FuturePromise {
deferred: raw_deferred,
resolver,
env,
tsfn: ptr::null_mut(),
async_resource_name,
env_tearing_down,
cleanup_hook_data: hook_data as *mut c_void,
})
}

Expand Down Expand Up @@ -100,8 +129,31 @@ unsafe extern "C" fn call_js_cb<T, V: NapiValue>(
context: *mut c_void,
data: *mut c_void,
) {
let mut env = Env::from_raw(raw_env);
let future_promise = Box::from_raw(context as *mut FuturePromise<T, V>);
let env_tearing_down = future_promise.env_tearing_down.load(Ordering::Acquire);
let cleanup_hook_data = future_promise.cleanup_hook_data;

if env_tearing_down {
// The environment is tearing down. Calling `napi_resolve_deferred` /
// `napi_reject_deferred` would crash inside V8's `GlobalHandles::Destroy`
// because the `Persistent` backing the `Deferred` may already be invalidated.
//
// Drop the future_promise and value to free Rust-side state, but skip every
// N-API cleanup call that would touch the partially-torn-down environment.
// The cleanup hook data is freed by the hook itself during teardown.
let value: Result<T> = ptr::read(data as *const _);
drop(value);
drop(future_promise);
return;
}

// Normal completion: unregister the cleanup hook since we no longer need it.
if !cleanup_hook_data.is_null() {
sys::napi_remove_env_cleanup_hook(raw_env, Some(env_teardown_cleanup), cleanup_hook_data);
drop(Box::from_raw(cleanup_hook_data as *mut CleanupHookData));
}

let mut env = Env::from_raw(raw_env);
let value: Result<T> = ptr::read(data as *const _);
let resolver = future_promise.resolver;
let deferred = future_promise.deferred;
Expand Down