-
Notifications
You must be signed in to change notification settings - Fork 285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handle early finalization of TSFN by Node.js #744
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
|
||
use std::ffi::c_void; | ||
use std::mem::MaybeUninit; | ||
use std::sync::{Arc, Mutex}; | ||
|
||
use crate::napi::bindings as napi; | ||
use crate::raw::{Env, Local}; | ||
|
@@ -34,6 +35,7 @@ unsafe impl Sync for Tsfn {} | |
/// function for scheduling tasks to execute on a JavaScript thread. | ||
pub struct ThreadsafeFunction<T> { | ||
tsfn: Tsfn, | ||
is_finalized: Arc<Mutex<bool>>, | ||
callback: fn(Option<Env>, T), | ||
} | ||
|
||
|
@@ -76,6 +78,7 @@ impl<T: Send + 'static> ThreadsafeFunction<T> { | |
callback: fn(Option<Env>, T), | ||
) -> Self { | ||
let mut result = MaybeUninit::uninit(); | ||
let is_finalized = Arc::new(Mutex::new(false)); | ||
|
||
assert_eq!( | ||
napi::create_threadsafe_function( | ||
|
@@ -87,8 +90,8 @@ impl<T: Send + 'static> ThreadsafeFunction<T> { | |
// Always set the reference count to 1. Prefer using | ||
// Rust `Arc` to maintain the struct. | ||
1, | ||
std::ptr::null_mut(), | ||
None, | ||
Arc::into_raw(is_finalized.clone()) as *mut _, | ||
Some(Self::finalize), | ||
std::ptr::null_mut(), | ||
Some(Self::callback), | ||
result.as_mut_ptr(), | ||
|
@@ -98,6 +101,7 @@ impl<T: Send + 'static> ThreadsafeFunction<T> { | |
|
||
Self { | ||
tsfn: Tsfn(result.assume_init()), | ||
is_finalized: is_finalized, | ||
callback, | ||
} | ||
} | ||
|
@@ -115,12 +119,28 @@ impl<T: Send + 'static> ThreadsafeFunction<T> { | |
data, | ||
})); | ||
|
||
let status = | ||
unsafe { napi::call_threadsafe_function(self.tsfn.0, callback as *mut _, is_blocking) }; | ||
// Hold the lock before entering `call_threadsafe_function` so that | ||
// `finalize_cb` would never complete. | ||
let mut is_finalized = self.is_finalized.lock().unwrap(); | ||
|
||
let status = { | ||
if *is_finalized { | ||
napi::Status::Closing | ||
} else { | ||
unsafe { | ||
napi::call_threadsafe_function(self.tsfn.0, callback as *mut _, is_blocking) | ||
} | ||
} | ||
}; | ||
|
||
if status == napi::Status::Ok { | ||
Ok(()) | ||
} else { | ||
// Prevent further calls to `call_threadsafe_function` | ||
if status == napi::Status::Closing { | ||
*is_finalized = true; | ||
} | ||
|
||
// If the call failed, the callback won't execute | ||
let callback = unsafe { Box::from_raw(callback) }; | ||
|
||
|
@@ -149,6 +169,14 @@ impl<T: Send + 'static> ThreadsafeFunction<T> { | |
); | ||
} | ||
|
||
// Provides a C ABI wrapper for a napi callback notifying us about tsfn | ||
// being finalized. | ||
unsafe extern "C" fn finalize(_env: Env, data: *mut c_void, _hint: *mut c_void) { | ||
let is_finalized = Arc::from_raw(data as *mut Mutex<bool>); | ||
|
||
*is_finalized.lock().unwrap() = true; | ||
} | ||
|
||
// Provides a C ABI wrapper for invoking the user supplied function pointer | ||
unsafe extern "C" fn callback( | ||
env: Env, | ||
|
@@ -167,6 +195,14 @@ impl<T: Send + 'static> ThreadsafeFunction<T> { | |
|
||
impl<T> Drop for ThreadsafeFunction<T> { | ||
fn drop(&mut self) { | ||
let is_finalized = self.is_finalized.lock().unwrap(); | ||
|
||
// tsfn was already finalized by `Environment::CleanupHandles()` in | ||
// Node.js | ||
if *is_finalized { | ||
return; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't you need to drop the lock before calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, actually I don't the idea here is that we want There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mutex isn't re-entrant, though, so in the non-early-exit case you'll lock, call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
unsafe { | ||
napi::release_threadsafe_function( | ||
self.tsfn.0, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be an
AtomicBool
instead of aMutex<bool>
? I'm also not clear why it needs to be anArc
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arc
provides a nice way of passing the pointer to the C function, because unlikeBox
you can clone it before turning into a raw pointer, and then decrement the reference count when turning it back from the raw pointer to theArc
instance and dropping it.I think it could be
AtomicBool
. Let me try that in a moment.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to
AtomicBool
. @jrose-signal must be really happy to see this happen 😉There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it has to be something indirect because ThreadsafeFunction is passed around by value. Box is probably safe because the finalizer callback is guaranteed to be invoked before
Drop::drop
completes (either Node invokes it on shutdown or we invoke it indrop
), but it's a lot easier to mess that up somehow.