2
2
#include < unordered_map>
3
3
#include " Logger.hpp"
4
4
5
- // This file provides a React Native-friendly implementation of Node-API's
6
- // thread-safe function primitive. In RN we don't own/libuv, so we:
7
- // - Use CallInvoker to hop onto the JS thread instead of uv_async.
8
- // - Track a registry mapping unique IDs to shared_ptrs for lookup/lifetime.
9
- // - Emulate ref/unref semantics without affecting any event loop.
10
-
5
+ // Global registry to map unique IDs to ThreadSafeFunction instances.
6
+ // We use IDs instead of raw pointers to avoid any use-after-free issues.
11
7
static std::unordered_map<std::uintptr_t ,
12
8
std::shared_ptr<callstack::nodeapihost::ThreadSafeFunction>>
13
9
registry;
14
10
static std::mutex registryMutex;
15
11
static std::atomic<std::uintptr_t > nextId{1 };
16
12
17
- // Constants for better readability
18
13
static constexpr size_t INITIAL_REF_COUNT = 1 ;
19
14
20
15
namespace callstack ::nodeapihost {
@@ -44,13 +39,10 @@ ThreadSafeFunction::ThreadSafeFunction(
44
39
context_{context},
45
40
callJsCb_{callJsCb} {
46
41
if (jsFunc) {
47
- // Keep JS function alive across async hops; fatal here mirrors Node-API's
48
- // behavior when environment is irrecoverable.
42
+ // Keep JS function alive across async hops
49
43
const auto status =
50
44
napi_create_reference (env, jsFunc, INITIAL_REF_COUNT, &jsFuncRef_);
51
45
if (status != napi_ok) {
52
- // Consider throwing an exception instead of fatal error in future
53
- // versions
54
46
napi_fatal_error (nullptr ,
55
47
0 ,
56
48
" Failed to create JS function reference" ,
@@ -101,6 +93,7 @@ std::shared_ptr<ThreadSafeFunction> ThreadSafeFunction::create(
101
93
std::shared_ptr<ThreadSafeFunction> ThreadSafeFunction::get (
102
94
napi_threadsafe_function func) {
103
95
std::lock_guard lock{registryMutex};
96
+ // Cast the handle back to ID for registry lookup
104
97
const auto id = reinterpret_cast <std::uintptr_t >(func);
105
98
const auto it = registry.find (id);
106
99
return it != registry.end () ? it->second : nullptr ;
@@ -120,7 +113,7 @@ napi_status ThreadSafeFunction::getContext(void** result) noexcept {
120
113
}
121
114
122
115
napi_status ThreadSafeFunction::call (
123
- void * data, napi_threadsafe_function_call_mode isBlocking) {
116
+ void * data, napi_threadsafe_function_call_mode isBlocking) noexcept {
124
117
if (isClosingOrAborted ()) {
125
118
return napi_closing;
126
119
}
@@ -152,7 +145,7 @@ napi_status ThreadSafeFunction::call(
152
145
return napi_ok;
153
146
}
154
147
155
- napi_status ThreadSafeFunction::acquire () {
148
+ napi_status ThreadSafeFunction::acquire () noexcept {
156
149
if (closing_.load (std::memory_order_acquire)) {
157
150
return napi_closing;
158
151
}
@@ -161,7 +154,7 @@ napi_status ThreadSafeFunction::acquire() {
161
154
}
162
155
163
156
napi_status ThreadSafeFunction::release (
164
- napi_threadsafe_function_release_mode mode) {
157
+ napi_threadsafe_function_release_mode mode) noexcept {
165
158
// Node-API semantics: abort prevents further JS calls and wakes any waiters.
166
159
if (mode == napi_tsfn_abort) {
167
160
aborted_.store (true , std::memory_order_relaxed);
@@ -185,8 +178,8 @@ napi_status ThreadSafeFunction::release(
185
178
}
186
179
187
180
napi_status ThreadSafeFunction::ref () noexcept {
188
- // In libuv, this would keep the loop alive. In RN we don't own or expose a
189
- // libuv loop. We just track the state for API parity .
181
+ // In libuv, this allows the loop to exit if nothing else is keeping it
182
+ // alive. In RN this is a no-op beyond state tracking .
190
183
referenced_.store (true , std::memory_order_relaxed);
191
184
return napi_ok;
192
185
}
@@ -199,6 +192,7 @@ napi_status ThreadSafeFunction::unref() noexcept {
199
192
}
200
193
201
194
void ThreadSafeFunction::finalize () {
195
+ // Ensure finalization happens exactly once
202
196
bool expected = false ;
203
197
if (!finalizeScheduled_.compare_exchange_strong (
204
198
expected, true , std::memory_order_acq_rel)) {
@@ -208,7 +202,6 @@ void ThreadSafeFunction::finalize() {
208
202
closing_.store (true , std::memory_order_release);
209
203
210
204
const auto onFinalize = [self = shared_from_this ()] {
211
- // Invoke user finalizer and unregister the handle from the global map.
212
205
if (self->threadFinalizeCb_ ) {
213
206
self->threadFinalizeCb_ (
214
207
self->env_ , self->threadFinalizeData_ , self->context_ );
@@ -249,12 +242,14 @@ void ThreadSafeFunction::processQueue() {
249
242
// Execute JS callback if we have data and aren't aborted
250
243
if (queuedData && !aborted_.load (std::memory_order_relaxed)) {
251
244
if (callJsCb_) {
245
+ // Prefer the user-provided callJsCb_ (Node-API compatible)
252
246
napi_value fn{nullptr };
253
247
if (jsFuncRef_) {
254
248
napi_get_reference_value (env_, jsFuncRef_, &fn);
255
249
}
256
250
callJsCb_ (env_, fn, context_, queuedData);
257
251
} else if (jsFuncRef_) {
252
+ // Fallback: call JS function directly with no args
258
253
napi_value fn{nullptr };
259
254
if (napi_get_reference_value (env_, jsFuncRef_, &fn) == napi_ok) {
260
255
napi_value recv{nullptr };
@@ -271,14 +266,13 @@ void ThreadSafeFunction::processQueue() {
271
266
}
272
267
}
273
268
274
- [[nodiscard]] bool ThreadSafeFunction::isClosingOrAborted () const noexcept {
269
+ bool ThreadSafeFunction::isClosingOrAborted () const noexcept {
275
270
return aborted_.load (std::memory_order_relaxed) ||
276
271
closing_.load (std::memory_order_relaxed);
277
272
}
278
273
279
- [[nodiscard]] bool ThreadSafeFunction::shouldFinalize () const noexcept {
274
+ bool ThreadSafeFunction::shouldFinalize () const noexcept {
280
275
return threadCount_.load (std::memory_order_acquire) == 0 &&
281
276
!closing_.load (std::memory_order_acquire);
282
277
}
283
-
284
- } // namespace callstack::nodeapihost
278
+ } // namespace callstack::nodeapihost
0 commit comments