Compare commits

...

11 Commits
main ... signal

Author SHA1 Message Date
Fedor Indutny
6d0dbd1328 Merge tag '0.8.3' into signal 2021-06-02 10:48:12 -07:00
Fedor Indutny
1d483d122b Revert "Fix use-after-free"
This reverts commit b30d2919fb.
2021-06-02 10:46:09 -07:00
K.J. Valencik
5ec4d1d42f
Merge pull request #751 from neon-bindings/kv/v0.8.3
v0.8.3
2021-06-02 13:26:29 -04:00
K.J. Valencik
8839407535
v0.8.3 2021-06-02 13:03:36 -04:00
K.J. Valencik
86d33b330d
Merge pull request #750 from neon-bindings/kv/memory-leak
Memory leak in `Root`
2021-06-02 11:28:42 -04:00
K.J. Valencik
51a17a7386
fix(neon): Fix a leak from global event queue Arc not being dropped 2021-06-02 09:54:58 -04:00
K.J. Valencik
8b260721a2
fix(neon-runtime): Fix a leak from references not being deleted
Fixes https://github.com/neon-bindings/neon/issues/746
2021-06-01 17:01:25 -04:00
K.J. Valencik
dd0af64272
Merge pull request #744 from indutny/fix/tsfn-crash
Handle early finalization of TSFN by Node.js
2021-06-01 17:01:10 -04:00
Fedor Indutny
b5a672eabb Handle early finalization of TSFN by Node.js
Thread-safe functions can get released early by Node.js itself during
the teardown of the Environment that holds them. When this happens -
`finalize_cb` of the thread-safe function is invoked to notify us. This
change makes us remember if the thread-safe function was finalized
before we got into the `Drop` implementation for it so that we don't
deallocate it twice.

Additionally, we should not invoke `call_threadsafe_function` after
either `finalize_cb` call or `napi_closing` return value from previous
`call_threadsafe_function`. Handle this by holding a mutex around the
call, which doesn't introduce any additional locking because
`call_threadsafe_function` itself holds another mutex while active.
2021-05-22 15:58:32 -07:00
Fedor Indutny
b30d2919fb Fix use-after-free 2021-05-19 17:31:24 -07:00
Fedor Indutny
13da056159 Reuse ThreadsafeFunction in EventQueue
Node.js optimizes subsequent ThreadsafeFunction invocations to happen
during the same event loop tick, but only if the same instance of
ThreadsafeFunction is used. The performance improvement is most
noticeable when used in Electron, because scheduling a new UV tick in
Electron is very costly.

With this change EventQueue will use an
existing instance of ThreadsafeTrampoline (wrapper around
ThreadsafeFunction) if compiled with napi-6 feature, or it will fallback
to creating a new ThreadsafeFunction per EventQueue instance.

Fix: #727
2021-05-19 14:37:06 -07:00
19 changed files with 297 additions and 58 deletions

View File

@ -22,6 +22,7 @@ Neon owes its existence to the contributions of these fine people.
* [Himself65](https://github.com/Himself65)
* [Maciej Hirsz](https://github.com/maciejhirsz)
* [Amal Hussein](https://github.com/nomadtechie)
* [Fedor Indutny](https://github.com/indutny)
* [Usagi Ito](https://github.com/usagi)
* [Jeroen (jrd-rocks)](https://github.com/jrd-rocks)
* [Keegan (mhsjlw)](https://github.com/mhsjlw)

View File

@ -1,6 +1,6 @@
[package]
name = "neon"
version = "0.8.2"
version = "0.8.3"
authors = ["Dave Herman <david.herman@gmail.com>"]
description = "A safe abstraction layer for Node.js."
readme = "README.md"
@ -12,7 +12,7 @@ build = "build.rs"
edition = "2018"
[build-dependencies]
neon-build = { version = "=0.8.2", path = "crates/neon-build" }
neon-build = { version = "=0.8.3", path = "crates/neon-build" }
[dev-dependencies]
lazy_static = "1.4.0"
@ -25,8 +25,8 @@ failure = "0.1.5" # used for a doc example
cslice = "0.2"
semver = "0.9.0"
smallvec = "1.4.2"
neon-runtime = { version = "=0.8.2", path = "crates/neon-runtime" }
neon-macros = { version = "=0.8.2", path = "crates/neon-macros", optional = true }
neon-runtime = { version = "=0.8.3", path = "crates/neon-runtime" }
neon-macros = { version = "=0.8.3", path = "crates/neon-macros", optional = true }
[features]
default = ["legacy-runtime"]

View File

@ -41,7 +41,7 @@ As a rule, you should choose the **oldest version of N-API that has the APIs you
```toml
[dependencies.neon]
version = "0.8.2"
version = "0.8.3"
default-features = false
features = ["napi-4"]
```

View File

@ -1,3 +1,8 @@
# Version 0.8.3
* Fix crash caused by non-thread safety in napi_threadsafefunction on early termination (https://github.com/neon-bindings/neon/pull/744)
* Fix memory leak in `Root` (https://github.com/neon-bindings/neon/pull/750)
# Version 0.8.2
* More docs improvements

2
cli/package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "neon-cli",
"version": "0.8.2",
"version": "0.8.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "neon-cli",
"version": "0.8.2",
"version": "0.8.3",
"description": "Build and load native Rust/Neon modules.",
"author": "Dave Herman <david.herman@gmail.com>",
"repository": {

View File

@ -1,6 +1,6 @@
[package]
name = "neon-build"
version = "0.8.2"
version = "0.8.3"
authors = ["Dave Herman <david.herman@gmail.com>"]
description = "Build logic required for Neon projects."
repository = "https://github.com/neon-bindings/neon"
@ -9,4 +9,4 @@ edition = "2018"
build = "build.rs"
[dependencies]
neon-sys = { version = "=0.8.2", path = "../neon-sys", optional = true }
neon-sys = { version = "=0.8.3", path = "../neon-sys", optional = true }

View File

@ -1,6 +1,6 @@
[package]
name = "neon-macros"
version = "0.8.2"
version = "0.8.3"
authors = ["Dave Herman <david.herman@gmail.com>"]
description = "Procedural macros supporting Neon"
repository = "https://github.com/neon-bindings/neon"

View File

@ -1,6 +1,6 @@
[package]
name = "neon-runtime"
version = "0.8.2"
version = "0.8.3"
authors = ["Dave Herman <david.herman@gmail.com>"]
description = "Bindings to the Node.js native addon API, used by the Neon implementation."
repository = "https://github.com/neon-bindings/neon"
@ -10,7 +10,7 @@ edition = "2018"
[dependencies]
cfg-if = "1.0.0"
libloading = { version = "0.6.5", optional = true }
neon-sys = { version = "=0.8.2", path = "../neon-sys", optional = true }
neon-sys = { version = "=0.8.3", path = "../neon-sys", optional = true }
smallvec = "1.4.2"
[dev-dependencies]

View File

@ -173,6 +173,8 @@ mod napi1 {
fn reference_unref(env: Env, reference: Ref, result: *mut u32) -> Status;
fn delete_reference(env: Env, reference: Ref) -> Status;
fn get_reference_value(env: Env, reference: Ref, result: *mut Value) -> Status;
fn strict_equals(env: Env, lhs: Value, rhs: Value, result: *mut bool) -> Status;

View File

@ -26,7 +26,7 @@ pub unsafe fn reference(env: Env, value: napi::Ref) -> usize {
result.assume_init() as usize
}
pub unsafe fn unreference(env: Env, value: napi::Ref) -> usize {
pub unsafe fn unreference(env: Env, value: napi::Ref) {
let mut result = MaybeUninit::uninit();
assert_eq!(
@ -34,7 +34,9 @@ pub unsafe fn unreference(env: Env, value: napi::Ref) -> usize {
napi::Status::Ok,
);
result.assume_init() as usize
if result.assume_init() == 0 {
assert_eq!(napi::delete_reference(env, value), napi::Status::Ok);
}
}
pub unsafe fn get(env: Env, value: napi::Ref) -> Local {

View File

@ -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;
}
unsafe {
napi::release_threadsafe_function(
self.tsfn.0,

View File

@ -1,6 +1,6 @@
[package]
name = "neon-sys"
version = "0.8.2"
version = "0.8.3"
authors = ["David Herman <david.herman@gmail.com>"]
description = "Exposes the low-level V8/NAN C/C++ APIs. Will be superseded by N-API."
edition = "2018"

View File

@ -154,10 +154,18 @@ use crate::context::internal::Env;
#[cfg(all(feature = "napi-4", feature = "event-queue-api"))]
use crate::event::EventQueue;
use crate::handle::{Handle, Managed};
#[cfg(all(feature = "napi-6", feature = "event-queue-api"))]
use crate::lifecycle::InstanceData;
#[cfg(feature = "legacy-runtime")]
use crate::object::class::Class;
use crate::object::{Object, This};
use crate::result::{JsResult, NeonResult, Throw};
#[cfg(all(
feature = "napi-4",
not(feature = "napi-6"),
feature = "event-queue-api"
))]
use crate::trampoline::ThreadsafeTrampoline;
use crate::types::binary::{JsArrayBuffer, JsBuffer};
#[cfg(feature = "napi-1")]
use crate::types::boxed::{Finalize, JsBox};
@ -552,7 +560,17 @@ pub trait Context<'a>: ContextInternal<'a> {
#[cfg(all(feature = "napi-4", feature = "event-queue-api"))]
/// Creates an unbounded queue of events to be executed on a JavaScript thread
fn queue(&mut self) -> EventQueue {
EventQueue::new(self)
#[cfg(feature = "napi-6")]
let shared_trampoline = InstanceData::threadsafe_trampoline(self);
#[cfg(not(feature = "napi-6"))]
let shared_trampoline = {
let shared_trampoline = ThreadsafeTrampoline::new(self.env());
shared_trampoline.unref(self.env().to_raw());
shared_trampoline
};
EventQueue::with_shared_trampoline(self, shared_trampoline)
}
}

View File

@ -1,10 +1,9 @@
use neon_runtime::raw::Env;
use neon_runtime::tsfn::ThreadsafeFunction;
use std::sync::{Arc, RwLock};
use crate::context::internal::ContextInternal;
use crate::context::{Context, TaskContext};
use crate::result::NeonResult;
type Callback = Box<dyn FnOnce(Env) + Send + 'static>;
use crate::trampoline::ThreadsafeTrampoline;
/// Queue for scheduling Rust closures to execute on the JavaScript main thread.
///
@ -50,8 +49,9 @@ type Callback = Box<dyn FnOnce(Env) + Send + 'static>;
/// ```
pub struct EventQueue {
tsfn: ThreadsafeFunction<Callback>,
trampoline: Arc<RwLock<ThreadsafeTrampoline>>,
has_ref: bool,
has_shared_trampoline: bool,
}
impl std::fmt::Debug for EventQueue {
@ -64,20 +64,43 @@ impl EventQueue {
/// Creates an unbounded queue for scheduling closures on the JavaScript
/// main thread
pub fn new<'a, C: Context<'a>>(cx: &mut C) -> Self {
let tsfn = unsafe { ThreadsafeFunction::new(cx.env().to_raw(), Self::callback) };
let trampoline = ThreadsafeTrampoline::new(cx.env().to_raw());
Self {
tsfn,
trampoline: Arc::new(RwLock::new(trampoline)),
has_ref: true,
has_shared_trampoline: false,
}
}
pub(crate) fn with_shared_trampoline<'a, C: Context<'a>>(
cx: &mut C,
trampoline: Arc<RwLock<ThreadsafeTrampoline>>,
) -> Self {
trampoline
.write()
.unwrap()
.increment_references(cx.env().to_raw());
Self {
trampoline: trampoline,
has_ref: true,
has_shared_trampoline: true,
}
}
/// Allow the Node event loop to exit while this `EventQueue` exists.
/// _Idempotent_
pub fn unref<'a, C: Context<'a>>(&mut self, cx: &mut C) -> &mut Self {
self.has_ref = false;
if !self.has_ref {
return self;
}
unsafe { self.tsfn.unref(cx.env().to_raw()) }
self.has_ref = false;
self.trampoline
.write()
.unwrap()
.decrement_references(cx.env().to_raw());
self
}
@ -85,9 +108,15 @@ impl EventQueue {
/// Prevent the Node event loop from exiting while this `EventQueue` exists. (Default)
/// _Idempotent_
pub fn reference<'a, C: Context<'a>>(&mut self, cx: &mut C) -> &mut Self {
self.has_ref = true;
if self.has_ref {
return self;
}
unsafe { self.tsfn.reference(cx.env().to_raw()) }
self.has_ref = true;
self.trampoline
.write()
.unwrap()
.increment_references(cx.env().to_raw());
self
}
@ -107,17 +136,8 @@ impl EventQueue {
where
F: FnOnce(TaskContext) -> NeonResult<()> + Send + 'static,
{
let callback = Box::new(move |env| {
let env = unsafe { std::mem::transmute(env) };
// Note: It is sufficient to use `TaskContext`'s `InheritedHandleScope` because
// N-API creates a `HandleScope` before calling the callback.
TaskContext::with_context(env, move |cx| {
let _ = f(cx);
});
});
self.tsfn.call(callback, None).map_err(|_| EventQueueError)
let trampoline = self.trampoline.read().unwrap();
trampoline.try_send(f).map_err(|_| EventQueueError)
}
/// Returns a boolean indicating if this `EventQueue` will prevent the Node event
@ -125,16 +145,29 @@ impl EventQueue {
pub fn has_ref(&self) -> bool {
self.has_ref
}
}
// Monomorphized trampoline funciton for calling the user provided closure
fn callback(env: Option<Env>, callback: Callback) {
if let Some(env) = env {
callback(env);
} else {
crate::context::internal::IS_RUNNING.with(|v| {
*v.borrow_mut() = false;
});
impl Drop for EventQueue {
fn drop(&mut self) {
if !self.has_ref {
return;
}
// If we own the trampoline - it is going to be dropped as well.
// There is no need to decrement its references.
if !self.has_shared_trampoline {
return;
}
let trampoline = self.trampoline.clone();
self.send(move |cx| {
trampoline
.write()
.unwrap()
.decrement_references(cx.env().to_raw());
Ok(())
});
}
}

View File

@ -1,6 +1,6 @@
use std::ffi::c_void;
use std::marker::PhantomData;
use std::mem::ManuallyDrop;
#[cfg(feature = "napi-6")]
use std::sync::Arc;
use neon_runtime::reference;
@ -32,7 +32,9 @@ unsafe impl Sync for NapiRef {}
/// A `Root<T>` may be sent across threads, but the referenced object may
/// only be accessed on the JavaScript thread that created it.
pub struct Root<T> {
internal: NapiRef,
// `Option` is used to skip `Drop` when `Root::drop` or `Root::into_inner` is used.
// It will *always* be `Some` when a user is interacting with `Root`.
internal: Option<NapiRef>,
#[cfg(feature = "napi-6")]
drop_queue: Arc<ThreadsafeFunction<NapiRef>>,
_phantom: PhantomData<T>,
@ -65,7 +67,7 @@ impl<T: Object> Root<T> {
let internal = unsafe { reference::new(env, value.to_raw()) };
Self {
internal: NapiRef(internal as *mut _),
internal: Some(NapiRef(internal as *mut _)),
#[cfg(feature = "napi-6")]
drop_queue: InstanceData::drop_queue(cx),
_phantom: PhantomData,
@ -86,7 +88,7 @@ impl<T: Object> Root<T> {
/// ```
pub fn clone<'a, C: Context<'a>>(&self, cx: &mut C) -> Self {
let env = cx.env();
let internal = self.internal.0 as *mut _;
let internal = self.as_napi_ref().0 as *mut _;
unsafe {
reference::reference(env.to_raw(), internal);
@ -104,7 +106,7 @@ impl<T: Object> Root<T> {
/// object.
pub fn drop<'a, C: Context<'a>>(self, cx: &mut C) {
let env = cx.env().to_raw();
let internal = ManuallyDrop::new(self).internal.0 as *mut _;
let internal = self.into_napi_ref().0 as *mut _;
unsafe {
reference::unreference(env, internal);
@ -114,7 +116,7 @@ impl<T: Object> Root<T> {
/// Return the referenced JavaScript object and allow it to be garbage collected
pub fn into_inner<'a, C: Context<'a>>(self, cx: &mut C) -> Handle<'a, T> {
let env = cx.env();
let internal = ManuallyDrop::new(self).internal.0 as *mut _;
let internal = self.into_napi_ref().0 as *mut _;
let local = unsafe { reference::get(env.to_raw(), internal) };
@ -130,10 +132,26 @@ impl<T: Object> Root<T> {
/// can be used in place of a clone immediately followed by a call to `into_inner`.
pub fn to_inner<'a, C: Context<'a>>(&self, cx: &mut C) -> Handle<'a, T> {
let env = cx.env();
let local = unsafe { reference::get(env.to_raw(), self.internal.0 as *mut _) };
let local = unsafe { reference::get(env.to_raw(), self.as_napi_ref().0 as *mut _) };
Handle::new_internal(T::from_raw(env, local))
}
fn as_napi_ref(&self) -> &NapiRef {
self.internal
.as_ref()
// `unwrap` will not `panic` because `internal` will always be `Some`
// until the `Root` is consumed.
.unwrap()
}
fn into_napi_ref(mut self) -> NapiRef {
self.internal
.take()
// `unwrap` will not `panic` because this is the only method place
// `internal` is replaced with `None` and it consumes `self`.
.unwrap()
}
}
// Allows putting `Root<T>` directly in a container that implements `Finalize`
@ -147,6 +165,11 @@ impl<T: Object> Finalize for Root<T> {
impl<T> Drop for Root<T> {
#[cfg(not(feature = "napi-6"))]
fn drop(&mut self) {
// If `None`, the `NapiRef` has already been manually dropped
if self.internal.is_none() {
return;
}
// Destructors are called during stack unwinding, prevent a double
// panic and instead prefer to leak.
if std::thread::panicking() {
@ -165,6 +188,9 @@ impl<T> Drop for Root<T> {
#[cfg(feature = "napi-6")]
fn drop(&mut self) {
let _ = self.drop_queue.call(self.internal.clone(), None);
// If `None`, the `NapiRef` has already been manually dropped
if let Some(internal) = self.internal.take() {
let _ = self.drop_queue.call(internal.clone(), None);
}
}
}

View File

@ -106,6 +106,9 @@ pub use neon_macros::*;
#[cfg(feature = "napi-6")]
mod lifecycle;
#[cfg(all(feature = "napi-4", feature = "event-queue-api"))]
mod trampoline;
#[cfg(all(feature = "legacy-runtime", feature = "napi-1"))]
compile_error!("Cannot enable both `legacy-runtime` and `napi-*` features.\n\nTo use `napi-*`, disable `legacy-runtime` by setting `default-features` to `false` in Cargo.toml\nor with cargo's --no-default-features flag.");

View File

@ -10,6 +10,8 @@
use std::mem;
use std::sync::Arc;
#[cfg(feature = "event-queue-api")]
use std::sync::RwLock;
use neon_runtime::raw::Env;
use neon_runtime::reference;
@ -17,6 +19,8 @@ use neon_runtime::tsfn::ThreadsafeFunction;
use crate::context::Context;
use crate::handle::root::NapiRef;
#[cfg(feature = "event-queue-api")]
use crate::trampoline::ThreadsafeTrampoline;
/// `InstanceData` holds Neon data associated with a particular instance of a
/// native module. If a module is loaded multiple times (e.g., worker threads), this
@ -30,6 +34,10 @@ pub(crate) struct InstanceData {
/// given the cost of FFI, this optimization is omitted until the cost of an
/// `Arc` is demonstrated as significant.
drop_queue: Arc<ThreadsafeFunction<NapiRef>>,
/// Used in EventQueue to invoke Rust callbacks with Napi environment.
#[cfg(feature = "event-queue-api")]
threadsafe_trampoline: Arc<RwLock<ThreadsafeTrampoline>>,
}
fn drop_napi_ref(env: Option<Env>, data: NapiRef) {
@ -62,8 +70,18 @@ impl InstanceData {
queue
};
#[cfg(feature = "event-queue-api")]
let threadsafe_trampoline = {
let mut trampoline = ThreadsafeTrampoline::new(env);
trampoline.decrement_references(env);
trampoline
};
let data = InstanceData {
drop_queue: Arc::new(drop_queue),
#[cfg(feature = "event-queue-api")]
threadsafe_trampoline: Arc::new(RwLock::new(threadsafe_trampoline)),
};
unsafe { &mut *neon_runtime::lifecycle::set_instance_data(env, data) }
@ -73,4 +91,12 @@ impl InstanceData {
pub(crate) fn drop_queue<'a, C: Context<'a>>(cx: &mut C) -> Arc<ThreadsafeFunction<NapiRef>> {
Arc::clone(&InstanceData::get(cx).drop_queue)
}
/// Helper to return a reference to the `invoke_callback` field of `InstanceData`
#[cfg(feature = "event-queue-api")]
pub(crate) fn threadsafe_trampoline<'a, C: Context<'a>>(
cx: &mut C,
) -> Arc<RwLock<ThreadsafeTrampoline>> {
Arc::clone(&InstanceData::get(cx).threadsafe_trampoline)
}
}

87
src/trampoline.rs Normal file
View File

@ -0,0 +1,87 @@
use neon_runtime::raw::Env;
use neon_runtime::tsfn::{CallError, ThreadsafeFunction};
use crate::context::TaskContext;
use crate::result::NeonResult;
pub(crate) type Callback = Box<dyn FnOnce(Env) + Send + 'static>;
pub(crate) struct ThreadsafeTrampoline {
tsfn: ThreadsafeFunction<Callback>,
ref_count: u32,
}
impl ThreadsafeTrampoline {
/// Creates an unbounded queue for scheduling closures on the JavaScript
/// main thread
pub(crate) fn new(env: Env) -> Self {
let tsfn = unsafe { ThreadsafeFunction::new(env, Self::callback) };
Self {
tsfn: tsfn,
ref_count: 1,
}
}
/// Schedules a closure to execute on the JavaScript thread that created
/// this ThreadsafeTrampoline.
/// Returns an `Error` if the task could not be scheduled.
pub(crate) fn try_send<F>(&self, f: F) -> Result<(), CallError<Callback>>
where
F: FnOnce(TaskContext) -> NeonResult<()> + Send + 'static,
{
let callback = Box::new(move |env| {
let env = unsafe { std::mem::transmute(env) };
// Note: It is sufficient to use `TaskContext`'s `InheritedHandleScope` because
// N-API creates a `HandleScope` before calling the callback.
TaskContext::with_context(env, move |cx| {
let _ = f(cx);
});
});
self.tsfn.call(callback, None)
}
/// References a trampoline to prevent exiting the event loop until it has been dropped. (Default)
/// Safety: `Env` must be valid for the current thread
/// _Not idempotent_
pub(crate) fn increment_references(&mut self, env: Env) {
self.ref_count += 1;
if self.ref_count != 1 {
return;
}
unsafe {
self.tsfn.reference(env);
}
}
/// Unreferences a trampoline to allow exiting the event loop before it has been dropped.
/// Safety: `Env` must be valid for the current thread
/// _Not idempotent_
pub(crate) fn decrement_references(&mut self, env: Env) {
assert!(
self.ref_count > 0,
"ThreadsafeTrampoline reference underflow"
);
self.ref_count -= 1;
if self.ref_count != 0 {
return;
}
unsafe {
self.tsfn.unref(env);
}
}
// Monomorphized trampoline function for calling the user provided closure
fn callback(env: Option<Env>, callback: Callback) {
if let Some(env) = env {
callback(env);
} else {
crate::context::internal::IS_RUNNING.with(|v| {
*v.borrow_mut() = false;
});
}
}
}