@gjsify/napi 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -0
- package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/index.js +1 -0
- package/lib/types/index.d.ts +25 -0
- package/meson.build +126 -0
- package/package.json +100 -0
- package/prebuilds/linux-x86_64/GjsifyNapi-1.0.gir +19 -0
- package/prebuilds/linux-x86_64/GjsifyNapi-1.0.typelib +0 -0
- package/prebuilds/linux-x86_64/libgjsifynapi.so +0 -0
- package/src/cc/arraybuffer.cc +772 -0
- package/src/cc/async_work.cc +439 -0
- package/src/cc/common.h +519 -0
- package/src/cc/env.cc +262 -0
- package/src/cc/error.cc +300 -0
- package/src/cc/function.cc +358 -0
- package/src/cc/module.cc +625 -0
- package/src/cc/napi.def +22 -0
- package/src/cc/napi.export.map +16 -0
- package/src/cc/napi.exported-symbols.txt +15 -0
- package/src/cc/object.cc +728 -0
- package/src/cc/ref.cc +510 -0
- package/src/cc/stubs.cc +100 -0
- package/src/cc/tsfn.cc +602 -0
- package/src/cc/value.cc +656 -0
- package/src/cc/wrap.cc +321 -0
- package/src/napi-headers/js_native_api.h +639 -0
- package/src/napi-headers/js_native_api_types.h +249 -0
- package/src/napi-headers/node_api.h +270 -0
- package/src/napi-headers/node_api_types.h +63 -0
- package/src/vala/gjs-1.0.vapi +6 -0
- package/src/vala/mozjs-140.vapi +6 -0
- package/src/vala/napi.vala +49 -0
package/src/cc/tsfn.cc
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @gjsify/napi — threadsafe functions + napi_make_callback (Phase 1).
|
|
3
|
+
//
|
|
4
|
+
// Reference: refs/node/src/node_api.cc v8impl::ThreadSafeFunction (Node.js
|
|
5
|
+
// contributors, MIT) — Push/Release/DispatchOne/CloseHandlesAndMaybeDelete/
|
|
6
|
+
// EmptyQueueAndDelete are the normative semantics mirrored here. Node realizes
|
|
7
|
+
// the cross-thread wake as a uv_async on the event loop; this host realizes it
|
|
8
|
+
// as an idle GSource attached to the GJS main context.
|
|
9
|
+
//
|
|
10
|
+
// THE CRUX — foreign thread → single-threaded JSContext:
|
|
11
|
+
// napi_call_threadsafe_function is documented callable from ANY thread
|
|
12
|
+
// (GIO/GStreamer workers in node-gi's case), but the JSContext is owned by the
|
|
13
|
+
// GJS main thread and must never be entered from another thread. The safe
|
|
14
|
+
// bridge is GSource attachment:
|
|
15
|
+
// - `g_source_attach` is explicitly thread-safe (it takes the GMainContext
|
|
16
|
+
// lock and wakes the context's eventfd), and it runs NO JSAPI — so a
|
|
17
|
+
// foreign thread may schedule a dispatch even while the main thread is
|
|
18
|
+
// mid-GC or mid-dispatch; the source only ever DISPATCHES on the thread
|
|
19
|
+
// iterating the context (the GJS main/JS thread).
|
|
20
|
+
// - `g_main_context_invoke_full` is deliberately NOT used: when the calling
|
|
21
|
+
// thread owns the context it invokes the callback SYNCHRONOUSLY — a
|
|
22
|
+
// same-thread napi_call_threadsafe_function (e.g. from a napi finalizer
|
|
23
|
+
// during the §5c drain) would re-enter JS in place instead of deferring
|
|
24
|
+
// to a loop turn, breaking the tsfn's async contract (and node-gi's
|
|
25
|
+
// "the finalizer only schedules" crash-mode-1 discipline).
|
|
26
|
+
// - the tsfn's own GMutex guards all mutable state; it is NEVER held across
|
|
27
|
+
// call_js (the callback may run arbitrary JS → GObject dispose → foreign-
|
|
28
|
+
// thread toggles that need this same mutex — the node-gi drain-lock
|
|
29
|
+
// lesson, toggle.cc DrainTsfnCb).
|
|
30
|
+
//
|
|
31
|
+
// Lifetime: the struct is freed on the JS thread only, and NEVER while a
|
|
32
|
+
// foreign thread still holds a claim — the dispatch finalizes only once
|
|
33
|
+
// thread_count has reached 0 (a natural close, OR an abort whose remaining
|
|
34
|
+
// claimants have each drained their claim via a claim-consuming napi_closing
|
|
35
|
+
// push/release, every one of which re-arms the dispatch as it drops the last
|
|
36
|
+
// claim). This is the tsfn analog of async_work's join-before-free
|
|
37
|
+
// (g_thread_pool_free wait=TRUE): thread_count == 0 under the mutex means every
|
|
38
|
+
// claimant's last struct access happened-before this dispatch re-acquired the
|
|
39
|
+
// lock, so no foreign thread can touch freed tsfn memory. The alternative — the
|
|
40
|
+
// old "free the instant closing is observed" abort path — freed the struct
|
|
41
|
+
// while claimants were still about to lock the (now cleared) mutex: a
|
|
42
|
+
// use-after-free Linux/glibc tolerates (destroy is a no-op, freed bytes survive)
|
|
43
|
+
// but macOS's pthread + allocator + exit-time dylib unload turn into a crash.
|
|
44
|
+
// The remaining fallback is env teardown (finalize_env_tsfns: a release(abort)
|
|
45
|
+
// issued by an env cleanup hook schedules a dispatch that would never run, the
|
|
46
|
+
// main context stops iterating once GjsContext dispose began). ABI contract, as
|
|
47
|
+
// in Node: a thread must hold a claim (initial_thread_count / napi_acquire)
|
|
48
|
+
// while calling, and must make no call after its release — a claimless call
|
|
49
|
+
// racing the final free is UB in every host.
|
|
50
|
+
|
|
51
|
+
#include "common.h"
|
|
52
|
+
|
|
53
|
+
#include <deque>
|
|
54
|
+
|
|
55
|
+
// ---- the tsfn object ----
|
|
56
|
+
//
|
|
57
|
+
// Global scope: the struct tag is fixed by the vendored
|
|
58
|
+
// `typedef struct napi_threadsafe_function__* napi_threadsafe_function`.
|
|
59
|
+
struct napi_threadsafe_function__ {
|
|
60
|
+
// Immutable after creation (JS thread):
|
|
61
|
+
napi_env env = nullptr;
|
|
62
|
+
GMainContext* main_context = nullptr; // ref held; released in finalize
|
|
63
|
+
void* context = nullptr;
|
|
64
|
+
napi_threadsafe_function_call_js call_js = nullptr; // never null
|
|
65
|
+
napi_ref func_ref = nullptr; // strong ref on the JS function (nullable)
|
|
66
|
+
napi_finalize finalize_cb = nullptr;
|
|
67
|
+
void* finalize_data = nullptr;
|
|
68
|
+
size_t max_queue_size = 0; // 0 = unbounded
|
|
69
|
+
|
|
70
|
+
// Mutable state — guarded by mutex (any thread):
|
|
71
|
+
GMutex mutex;
|
|
72
|
+
GCond cond; // blocking-push space + waiter drain
|
|
73
|
+
std::deque<void*> queue;
|
|
74
|
+
size_t thread_count = 0;
|
|
75
|
+
size_t waiters = 0; // threads inside the blocking-push wait
|
|
76
|
+
// No new pushes accepted: release(abort), the dispatch's natural-close
|
|
77
|
+
// decision, or env teardown. Node's `is_closing`.
|
|
78
|
+
bool closing = false;
|
|
79
|
+
// A dispatch idle is attached and pending (owned ref); the coalescing
|
|
80
|
+
// guard — at most ONE pending wake, exactly uv_async semantics.
|
|
81
|
+
GSource* dispatch_source = nullptr;
|
|
82
|
+
bool referenced = true; // napi_unref bookkeeping (see note below)
|
|
83
|
+
|
|
84
|
+
napi_threadsafe_function__() {
|
|
85
|
+
g_mutex_init(&mutex);
|
|
86
|
+
g_cond_init(&cond);
|
|
87
|
+
}
|
|
88
|
+
~napi_threadsafe_function__() {
|
|
89
|
+
g_cond_clear(&cond);
|
|
90
|
+
g_mutex_clear(&mutex);
|
|
91
|
+
if (main_context != nullptr) {
|
|
92
|
+
g_main_context_unref(main_context);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
namespace gjsify_napi {
|
|
98
|
+
namespace {
|
|
99
|
+
|
|
100
|
+
// Node's Dispatch() iteration cap (event-loop starvation guard).
|
|
101
|
+
constexpr unsigned kMaxDispatchIterations = 1000;
|
|
102
|
+
|
|
103
|
+
gboolean dispatch_cb(gpointer data);
|
|
104
|
+
|
|
105
|
+
// Arm the JS-thread dispatch. Caller holds tsfn->mutex. Safe from any thread
|
|
106
|
+
// and from within GC sweeps: pure GLib, no JSAPI (see file header).
|
|
107
|
+
void schedule_dispatch_locked(napi_threadsafe_function tsfn) {
|
|
108
|
+
if (tsfn->dispatch_source != nullptr) {
|
|
109
|
+
return; // a wake is already pending — coalesce
|
|
110
|
+
}
|
|
111
|
+
GSource* src = g_idle_source_new();
|
|
112
|
+
// uv_async parity: process the wake promptly (an async is checked every
|
|
113
|
+
// loop iteration in Node), not at idle-starvation priority.
|
|
114
|
+
g_source_set_priority(src, G_PRIORITY_DEFAULT);
|
|
115
|
+
g_source_set_name(src, "gjsify-napi tsfn dispatch");
|
|
116
|
+
g_source_set_callback(src, dispatch_cb, tsfn, nullptr);
|
|
117
|
+
tsfn->dispatch_source = src; // keep our creation ref until dispatch/teardown
|
|
118
|
+
g_source_attach(src, tsfn->main_context);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Node CallJs (node_api.cc): the default call_js when the creator passed none.
|
|
122
|
+
void default_call_js(napi_env env, napi_value cb, void* /* context */,
|
|
123
|
+
void* /* data */) {
|
|
124
|
+
if (env == nullptr || cb == nullptr) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
napi_value recv = nullptr;
|
|
128
|
+
if (napi_get_undefined(env, &recv) != napi_ok) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
napi_call_function(env, recv, cb, 0, nullptr, nullptr);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Run call_js for one queue item on the JS thread. NO tsfn lock held. A fresh
|
|
135
|
+
// handle scope wraps the call (Node invokes call_js inside a HandleScope +
|
|
136
|
+
// CallbackScope); a pending exception left by the callback is logged and
|
|
137
|
+
// cleared — there is no JS frame below a loop-dispatched invocation, and a
|
|
138
|
+
// lingering pending exception would poison unrelated later JSAPI calls (the
|
|
139
|
+
// finalizer-pipeline posture, ref.cc).
|
|
140
|
+
void invoke_call_js(napi_threadsafe_function tsfn, void* item) {
|
|
141
|
+
napi_env env = tsfn->env;
|
|
142
|
+
const bool can_call =
|
|
143
|
+
env != nullptr && env->can_call_into_js && !env->torn_down;
|
|
144
|
+
if (!can_call) {
|
|
145
|
+
// Node EmptyQueueAndDelete posture: env == NULL tells the callback
|
|
146
|
+
// "free `data`, run no JS".
|
|
147
|
+
tsfn->call_js(nullptr, nullptr, tsfn->context, item);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
napi_handle_scope scope = nullptr;
|
|
151
|
+
napi_open_handle_scope(env, &scope);
|
|
152
|
+
napi_value js_cb = nullptr;
|
|
153
|
+
if (tsfn->func_ref != nullptr) {
|
|
154
|
+
napi_get_reference_value(env, tsfn->func_ref, &js_cb);
|
|
155
|
+
}
|
|
156
|
+
tsfn->call_js(env, js_cb, tsfn->context, item);
|
|
157
|
+
log_and_clear_pending_exception(env, "threadsafe-function callback");
|
|
158
|
+
napi_close_handle_scope(env, scope);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// JS-thread finalization (Node CloseHandlesAndMaybeDelete → Finalize →
|
|
162
|
+
// EmptyQueueAndDelete): run the thread finalizer with JS available, drain
|
|
163
|
+
// leftover queue items with env == NULL so their data can be freed, drop the
|
|
164
|
+
// function ref, unregister from the env, free the struct. Caller must have
|
|
165
|
+
// set `closing` and destroyed any pending dispatch source under the lock.
|
|
166
|
+
void finalize_tsfn(napi_threadsafe_function tsfn) {
|
|
167
|
+
napi_env env = tsfn->env;
|
|
168
|
+
const bool can_call =
|
|
169
|
+
env != nullptr && env->can_call_into_js && !env->torn_down;
|
|
170
|
+
|
|
171
|
+
// Wait out blocking pushers still inside g_cond_wait: they hold no claim
|
|
172
|
+
// on the memory beyond the wait itself, and freeing the cond under a
|
|
173
|
+
// waiter is UB. `closing` is already set, so awakened waiters exit fast.
|
|
174
|
+
g_mutex_lock(&tsfn->mutex);
|
|
175
|
+
g_cond_broadcast(&tsfn->cond);
|
|
176
|
+
while (tsfn->waiters > 0) {
|
|
177
|
+
g_cond_wait(&tsfn->cond, &tsfn->mutex);
|
|
178
|
+
}
|
|
179
|
+
g_mutex_unlock(&tsfn->mutex);
|
|
180
|
+
|
|
181
|
+
if (tsfn->finalize_cb != nullptr && can_call) {
|
|
182
|
+
napi_handle_scope scope = nullptr;
|
|
183
|
+
napi_open_handle_scope(env, &scope);
|
|
184
|
+
tsfn->finalize_cb(env, tsfn->finalize_data, tsfn->context);
|
|
185
|
+
log_and_clear_pending_exception(env, "threadsafe-function finalizer");
|
|
186
|
+
napi_close_handle_scope(env, scope);
|
|
187
|
+
}
|
|
188
|
+
// Leftover items (abort path): give the callback its free-data-only call.
|
|
189
|
+
while (!tsfn->queue.empty()) {
|
|
190
|
+
void* item = tsfn->queue.front();
|
|
191
|
+
tsfn->queue.pop_front();
|
|
192
|
+
tsfn->call_js(nullptr, nullptr, tsfn->context, item);
|
|
193
|
+
}
|
|
194
|
+
if (tsfn->func_ref != nullptr && env != nullptr && !env->torn_down) {
|
|
195
|
+
napi_delete_reference(env, tsfn->func_ref);
|
|
196
|
+
tsfn->func_ref = nullptr;
|
|
197
|
+
}
|
|
198
|
+
if (env != nullptr) {
|
|
199
|
+
auto& list = env->tsfns;
|
|
200
|
+
for (auto it = list.begin(); it != list.end(); ++it) {
|
|
201
|
+
if (*it == tsfn) {
|
|
202
|
+
list.erase(it);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
delete tsfn;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The JS-thread dispatch (Node AsyncCb → Dispatch → DispatchOne loop). Pops
|
|
211
|
+
// ONE item under the lock, RELEASES the lock, invokes call_js, re-checks —
|
|
212
|
+
// the lock is never held across JS (see file header). Decides + performs
|
|
213
|
+
// finalization ONLY at thread_count == 0 — a natural close (last claim gone +
|
|
214
|
+
// queue drained) or an abort whose remaining claimants have all drained. While
|
|
215
|
+
// an abort is closing but claims remain, it defers (join-before-free); the
|
|
216
|
+
// claim-consuming pushes/releases re-arm it when the last claim drops.
|
|
217
|
+
gboolean dispatch_cb(gpointer data) {
|
|
218
|
+
auto tsfn = static_cast<napi_threadsafe_function>(data);
|
|
219
|
+
g_mutex_lock(&tsfn->mutex);
|
|
220
|
+
if (tsfn->dispatch_source != nullptr) {
|
|
221
|
+
// Drop our creation ref; the context's ref dies with SOURCE_REMOVE.
|
|
222
|
+
g_source_unref(tsfn->dispatch_source);
|
|
223
|
+
tsfn->dispatch_source = nullptr;
|
|
224
|
+
}
|
|
225
|
+
unsigned iterations_left = kMaxDispatchIterations;
|
|
226
|
+
for (;;) {
|
|
227
|
+
// Finalize (free) ONLY once thread_count == 0 — no foreign thread can
|
|
228
|
+
// still be inside, or about to enter, napi_call_threadsafe_function
|
|
229
|
+
// holding a claim (§5e join-before-free; see the file header). A natural
|
|
230
|
+
// close needs the queue drained too; an abort (closing) then drops any
|
|
231
|
+
// leftover items via finalize_tsfn's env==NULL drain. Freeing while a
|
|
232
|
+
// claim is outstanding is the abort-path UAF — the closing pushes/
|
|
233
|
+
// releases below re-arm this dispatch when the LAST claim drops.
|
|
234
|
+
if (tsfn->thread_count == 0 &&
|
|
235
|
+
(tsfn->closing || tsfn->queue.empty())) {
|
|
236
|
+
tsfn->closing = true;
|
|
237
|
+
// A same-thread release() from inside call_js may have re-armed a
|
|
238
|
+
// wake meanwhile — destroy it, or it would fire into freed memory.
|
|
239
|
+
if (tsfn->dispatch_source != nullptr) {
|
|
240
|
+
g_source_destroy(tsfn->dispatch_source);
|
|
241
|
+
g_source_unref(tsfn->dispatch_source);
|
|
242
|
+
tsfn->dispatch_source = nullptr;
|
|
243
|
+
}
|
|
244
|
+
g_mutex_unlock(&tsfn->mutex);
|
|
245
|
+
finalize_tsfn(tsfn);
|
|
246
|
+
return G_SOURCE_REMOVE;
|
|
247
|
+
}
|
|
248
|
+
// Closing (abort) but claims are still outstanding: stop delivering and
|
|
249
|
+
// yield. Each claimant's claim-consuming napi_closing push/release
|
|
250
|
+
// re-arms this dispatch when it drops the last claim (thread_count → 0),
|
|
251
|
+
// and only THEN is the struct freed — never under a live claim.
|
|
252
|
+
if (tsfn->closing) {
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (tsfn->queue.empty()) {
|
|
256
|
+
break; // idle: claims alive, nothing queued
|
|
257
|
+
}
|
|
258
|
+
if (iterations_left-- == 0) {
|
|
259
|
+
// Starvation guard (Node kMaxIterationCount): yield to the loop,
|
|
260
|
+
// re-arm for the remainder.
|
|
261
|
+
schedule_dispatch_locked(tsfn);
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
void* item = tsfn->queue.front();
|
|
265
|
+
tsfn->queue.pop_front();
|
|
266
|
+
g_cond_broadcast(&tsfn->cond); // space freed for blocking pushers
|
|
267
|
+
g_mutex_unlock(&tsfn->mutex);
|
|
268
|
+
invoke_call_js(tsfn, item); // arbitrary JS — no lock held
|
|
269
|
+
g_mutex_lock(&tsfn->mutex);
|
|
270
|
+
}
|
|
271
|
+
g_mutex_unlock(&tsfn->mutex);
|
|
272
|
+
return G_SOURCE_REMOVE;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
} // namespace
|
|
276
|
+
|
|
277
|
+
// Env teardown step 1b (env.cc): finalize every tsfn still registered. The
|
|
278
|
+
// release(abort) node-gi issues from its env cleanup hook schedules a
|
|
279
|
+
// dispatch that can never run — GjsContext dispose has begun, the main
|
|
280
|
+
// context will not be iterated again — so the deferred finalize is executed
|
|
281
|
+
// HERE, synchronously, while JS is still fully callable (§5e steps 1-4
|
|
282
|
+
// discipline). A tsfn whose claims were never released (the shutdown
|
|
283
|
+
// conformance test's 32 producers loop napi_call_threadsafe_function until
|
|
284
|
+
// napi_closing, then exit — no release ever) is JOINED before it is freed:
|
|
285
|
+
// set `closing`, then wait for thread_count to reach 0 — every claimant drains
|
|
286
|
+
// its claim (its next call returns napi_closing) before the free, so no foreign
|
|
287
|
+
// thread touches freed memory. This is the teardown-path arm of the same
|
|
288
|
+
// thread_count==0 join-before-free the dispatch uses (async_work realizes it
|
|
289
|
+
// with g_thread_pool_free wait=TRUE; tsfn cannot join threads it does not own,
|
|
290
|
+
// so it waits for the voluntary drain). Without it, a producer's next
|
|
291
|
+
// napi_closing push would lock a g_mutex_clear'd mutex and re-arm a dispatch on
|
|
292
|
+
// a freed GMainContext — the abort-path UAF, reached here. A deadline caps a
|
|
293
|
+
// claimant that never drains (a claim-contract violation — UB in Node too),
|
|
294
|
+
// falling back to the pre-fix force-free rather than hanging teardown.
|
|
295
|
+
void finalize_env_tsfns(napi_env env) {
|
|
296
|
+
while (!env->tsfns.empty()) {
|
|
297
|
+
napi_threadsafe_function tsfn = env->tsfns.back();
|
|
298
|
+
g_mutex_lock(&tsfn->mutex);
|
|
299
|
+
// Stop accepting pushes + wake blocking pushers so they observe it.
|
|
300
|
+
tsfn->closing = true;
|
|
301
|
+
g_cond_broadcast(&tsfn->cond);
|
|
302
|
+
// Join: wait for every outstanding claim to drain (each claim-consuming
|
|
303
|
+
// napi_closing push/release drops thread_count and broadcasts at 0).
|
|
304
|
+
const gint64 deadline =
|
|
305
|
+
g_get_monotonic_time() + 2 * G_TIME_SPAN_SECOND;
|
|
306
|
+
bool joined = true;
|
|
307
|
+
while (tsfn->thread_count > 0) {
|
|
308
|
+
if (!g_cond_wait_until(&tsfn->cond, &tsfn->mutex, deadline)) {
|
|
309
|
+
joined = false;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (!joined) {
|
|
314
|
+
g_warning("gjsify-napi: threadsafe-function teardown join timed out "
|
|
315
|
+
"with %zu claim(s) still outstanding; freeing anyway (a "
|
|
316
|
+
"foreign thread holds a tsfn claim without draining it)",
|
|
317
|
+
tsfn->thread_count);
|
|
318
|
+
}
|
|
319
|
+
// A claimant that reached 0 may have armed a dispatch that can no longer
|
|
320
|
+
// run (dispose has begun) — destroy it before the free.
|
|
321
|
+
if (tsfn->dispatch_source != nullptr) {
|
|
322
|
+
g_source_destroy(tsfn->dispatch_source);
|
|
323
|
+
g_source_unref(tsfn->dispatch_source);
|
|
324
|
+
tsfn->dispatch_source = nullptr;
|
|
325
|
+
}
|
|
326
|
+
g_mutex_unlock(&tsfn->mutex);
|
|
327
|
+
finalize_tsfn(tsfn); // removes it from env->tsfns
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
} // namespace gjsify_napi
|
|
332
|
+
|
|
333
|
+
// ---- ABI ----
|
|
334
|
+
|
|
335
|
+
napi_status NAPI_CDECL napi_create_threadsafe_function(
|
|
336
|
+
napi_env env, napi_value func, napi_value async_resource,
|
|
337
|
+
napi_value async_resource_name, size_t max_queue_size,
|
|
338
|
+
size_t initial_thread_count, void* thread_finalize_data,
|
|
339
|
+
napi_finalize thread_finalize_cb, void* context,
|
|
340
|
+
napi_threadsafe_function_call_js call_js_cb,
|
|
341
|
+
napi_threadsafe_function* result) {
|
|
342
|
+
(void)async_resource; // no async-hooks under GJS
|
|
343
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
344
|
+
GJSIFY_NAPI_CHECK_ARG(env, async_resource_name);
|
|
345
|
+
GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(env, initial_thread_count > 0,
|
|
346
|
+
napi_invalid_arg);
|
|
347
|
+
GJSIFY_NAPI_CHECK_ARG(env, result);
|
|
348
|
+
|
|
349
|
+
napi_ref func_ref = nullptr;
|
|
350
|
+
if (func == nullptr) {
|
|
351
|
+
// js_callback-less tsfn: call_js_cb IS the callback (node_api.cc).
|
|
352
|
+
GJSIFY_NAPI_CHECK_ARG(env, call_js_cb);
|
|
353
|
+
} else {
|
|
354
|
+
JS::Value fn_v = gjsify_napi::napi_value_to_js(func);
|
|
355
|
+
GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(
|
|
356
|
+
env, fn_v.isObject() && JS::IsCallable(&fn_v.toObject()),
|
|
357
|
+
napi_function_expected);
|
|
358
|
+
napi_status st = napi_create_reference(env, func, 1, &func_ref);
|
|
359
|
+
if (st != napi_ok) {
|
|
360
|
+
return st;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
auto tsfn = new napi_threadsafe_function__();
|
|
365
|
+
tsfn->env = env;
|
|
366
|
+
// The GJS host loop iterates the GLOBAL default main context (the same
|
|
367
|
+
// context the §5c finalizer drain targets via g_idle_add) — that context
|
|
368
|
+
// is the JS thread's dispatch surface.
|
|
369
|
+
tsfn->main_context = g_main_context_ref(g_main_context_default());
|
|
370
|
+
tsfn->context = context;
|
|
371
|
+
tsfn->call_js = call_js_cb != nullptr ? call_js_cb
|
|
372
|
+
: gjsify_napi::default_call_js;
|
|
373
|
+
tsfn->func_ref = func_ref;
|
|
374
|
+
tsfn->finalize_cb = thread_finalize_cb;
|
|
375
|
+
tsfn->finalize_data = thread_finalize_data;
|
|
376
|
+
tsfn->max_queue_size = max_queue_size;
|
|
377
|
+
tsfn->thread_count = initial_thread_count;
|
|
378
|
+
env->tsfns.push_back(tsfn);
|
|
379
|
+
|
|
380
|
+
*result = tsfn;
|
|
381
|
+
return gjsify_napi::clear_last_error(env);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Callable from ANY thread — env-less, touches no last_error. Mirrors Node's
|
|
385
|
+
// ThreadSafeFunction::Push byte-for-byte, including the auto-release on a
|
|
386
|
+
// closing push (a napi_closing return CONSUMES the caller's claim).
|
|
387
|
+
napi_status NAPI_CDECL napi_call_threadsafe_function(
|
|
388
|
+
napi_threadsafe_function func, void* data,
|
|
389
|
+
napi_threadsafe_function_call_mode is_blocking) {
|
|
390
|
+
if (func == nullptr) {
|
|
391
|
+
return napi_invalid_arg;
|
|
392
|
+
}
|
|
393
|
+
g_mutex_lock(&func->mutex);
|
|
394
|
+
while (func->max_queue_size > 0 &&
|
|
395
|
+
func->queue.size() >= func->max_queue_size && !func->closing) {
|
|
396
|
+
if (is_blocking == napi_tsfn_nonblocking) {
|
|
397
|
+
g_mutex_unlock(&func->mutex);
|
|
398
|
+
return napi_queue_full;
|
|
399
|
+
}
|
|
400
|
+
func->waiters++;
|
|
401
|
+
g_cond_wait(&func->cond, &func->mutex);
|
|
402
|
+
func->waiters--;
|
|
403
|
+
if (func->waiters == 0) {
|
|
404
|
+
// A finalizer may be waiting for the waiter set to drain.
|
|
405
|
+
g_cond_broadcast(&func->cond);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (func->closing) {
|
|
409
|
+
if (func->thread_count == 0) {
|
|
410
|
+
g_mutex_unlock(&func->mutex);
|
|
411
|
+
return napi_invalid_arg;
|
|
412
|
+
}
|
|
413
|
+
func->thread_count--;
|
|
414
|
+
// This closing push just consumed the caller's claim; if it was the
|
|
415
|
+
// LAST one, arm the JS-thread dispatch so the deferred finalize runs now
|
|
416
|
+
// that no thread can re-enter (the free half of the abort-path fix —
|
|
417
|
+
// dispatch_cb frees only at thread_count == 0; coalesces if pending) AND
|
|
418
|
+
// wake a teardown JOIN (finalize_env_tsfns) waiting on the last claim.
|
|
419
|
+
if (func->thread_count == 0) {
|
|
420
|
+
gjsify_napi::schedule_dispatch_locked(func);
|
|
421
|
+
g_cond_broadcast(&func->cond);
|
|
422
|
+
}
|
|
423
|
+
g_mutex_unlock(&func->mutex);
|
|
424
|
+
return napi_closing;
|
|
425
|
+
}
|
|
426
|
+
func->queue.push_back(data);
|
|
427
|
+
gjsify_napi::schedule_dispatch_locked(func);
|
|
428
|
+
g_mutex_unlock(&func->mutex);
|
|
429
|
+
return napi_ok;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Callable from ANY thread (Node Release): drop one claim; abort flips
|
|
433
|
+
// `closing` (no further pushes; queued items get the env-NULL drain). The
|
|
434
|
+
// finalize itself always runs deferred on the JS thread via the dispatch.
|
|
435
|
+
napi_status NAPI_CDECL napi_release_threadsafe_function(
|
|
436
|
+
napi_threadsafe_function func, napi_threadsafe_function_release_mode mode) {
|
|
437
|
+
if (func == nullptr) {
|
|
438
|
+
return napi_invalid_arg;
|
|
439
|
+
}
|
|
440
|
+
g_mutex_lock(&func->mutex);
|
|
441
|
+
if (func->thread_count == 0) {
|
|
442
|
+
g_mutex_unlock(&func->mutex);
|
|
443
|
+
return napi_invalid_arg;
|
|
444
|
+
}
|
|
445
|
+
func->thread_count--;
|
|
446
|
+
if (mode == napi_tsfn_abort && !func->closing) {
|
|
447
|
+
func->closing = true;
|
|
448
|
+
g_cond_broadcast(&func->cond); // wake blocking pushers
|
|
449
|
+
}
|
|
450
|
+
// Arm the JS-thread dispatch when this drops the last claim (natural close,
|
|
451
|
+
// OR the deferred abort-path finalize once every claimant has drained) or
|
|
452
|
+
// when an abort just closed the tsfn (so a still-populated queue reaches its
|
|
453
|
+
// env==NULL drain + finalize). Not gated on !closing: a last-claim release
|
|
454
|
+
// AFTER a prior abort must still arm the finalize. Coalesces if pending.
|
|
455
|
+
if (func->thread_count == 0 || mode == napi_tsfn_abort) {
|
|
456
|
+
gjsify_napi::schedule_dispatch_locked(func);
|
|
457
|
+
}
|
|
458
|
+
if (func->thread_count == 0) {
|
|
459
|
+
// Wake a teardown JOIN (finalize_env_tsfns) waiting on the last claim.
|
|
460
|
+
g_cond_broadcast(&func->cond);
|
|
461
|
+
}
|
|
462
|
+
g_mutex_unlock(&func->mutex);
|
|
463
|
+
return napi_ok;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Keep-event-loop-alive bookkeeping. Under the GJS host there is nothing to
|
|
467
|
+
// unref: an attached idle GSource does not keep gjs alive (process lifetime
|
|
468
|
+
// is the module evaluation + whatever main loop the app itself runs), which
|
|
469
|
+
// is already the semantic an UNREF'D Node tsfn has — so this records the
|
|
470
|
+
// flag and succeeds. JS-thread-only API, like Node.
|
|
471
|
+
napi_status NAPI_CDECL napi_unref_threadsafe_function(
|
|
472
|
+
node_api_basic_env basic_env, napi_threadsafe_function func) {
|
|
473
|
+
napi_env env = const_cast<napi_env>(basic_env);
|
|
474
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
475
|
+
GJSIFY_NAPI_CHECK_ARG(env, func);
|
|
476
|
+
g_mutex_lock(&func->mutex);
|
|
477
|
+
func->referenced = false;
|
|
478
|
+
g_mutex_unlock(&func->mutex);
|
|
479
|
+
return gjsify_napi::clear_last_error(env);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// The async-context parameter is an async-hooks concept with no GJS
|
|
483
|
+
// equivalent; the call itself is a plain call. Microtask parity holds the
|
|
484
|
+
// GJS way: promise jobs queued by the callback drain from GJS's own
|
|
485
|
+
// promise-job source as soon as this stack unwinds to the main loop — the
|
|
486
|
+
// same boundary semantics GJS gives its own C→JS closures (and the posture
|
|
487
|
+
// node-gi already codes against, src/loop.cc "cross-runtime microtask
|
|
488
|
+
// checkpoint": SpiderMonkey/GJS drains when the last JS frame exits).
|
|
489
|
+
napi_status NAPI_CDECL napi_make_callback(napi_env env,
|
|
490
|
+
napi_async_context async_context,
|
|
491
|
+
napi_value recv, napi_value func,
|
|
492
|
+
size_t argc, const napi_value* argv,
|
|
493
|
+
napi_value* result) {
|
|
494
|
+
(void)async_context;
|
|
495
|
+
GJSIFY_NAPI_PREAMBLE(env);
|
|
496
|
+
GJSIFY_NAPI_CHECK_ARG(env, recv);
|
|
497
|
+
// Node coerces the receiver via CHECK_TO_OBJECT → napi_object_expected.
|
|
498
|
+
GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(
|
|
499
|
+
env, gjsify_napi::napi_value_to_js(recv).isObject(),
|
|
500
|
+
napi_object_expected);
|
|
501
|
+
return napi_call_function(env, recv, func, argc, argv, result);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---- threadsafe-function accessors (were loud stubs; the struct already
|
|
505
|
+
// tracks context + thread_count + referenced, so these are exact) ----
|
|
506
|
+
|
|
507
|
+
// Env-less, touches no last_error — mirrors Node's GetContext.
|
|
508
|
+
napi_status NAPI_CDECL napi_get_threadsafe_function_context(
|
|
509
|
+
napi_threadsafe_function func, void** result) {
|
|
510
|
+
if (func == nullptr || result == nullptr) {
|
|
511
|
+
return napi_invalid_arg;
|
|
512
|
+
}
|
|
513
|
+
*result = func->context;
|
|
514
|
+
return napi_ok;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Callable from ANY thread (Node Acquire): a closing tsfn refuses new claims;
|
|
518
|
+
// otherwise take one. Env-less, no last_error, like call/release.
|
|
519
|
+
napi_status NAPI_CDECL napi_acquire_threadsafe_function(
|
|
520
|
+
napi_threadsafe_function func) {
|
|
521
|
+
if (func == nullptr) {
|
|
522
|
+
return napi_invalid_arg;
|
|
523
|
+
}
|
|
524
|
+
g_mutex_lock(&func->mutex);
|
|
525
|
+
if (func->closing) {
|
|
526
|
+
g_mutex_unlock(&func->mutex);
|
|
527
|
+
return napi_closing;
|
|
528
|
+
}
|
|
529
|
+
func->thread_count++;
|
|
530
|
+
g_mutex_unlock(&func->mutex);
|
|
531
|
+
return napi_ok;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// The ref counterpart of napi_unref_threadsafe_function. As with unref, there
|
|
535
|
+
// is nothing to keep alive under the GJS host (an idle GSource does not keep
|
|
536
|
+
// gjs running), so this records the flag and succeeds. JS-thread-only.
|
|
537
|
+
napi_status NAPI_CDECL napi_ref_threadsafe_function(
|
|
538
|
+
node_api_basic_env basic_env, napi_threadsafe_function func) {
|
|
539
|
+
napi_env env = const_cast<napi_env>(basic_env);
|
|
540
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
541
|
+
GJSIFY_NAPI_CHECK_ARG(env, func);
|
|
542
|
+
g_mutex_lock(&func->mutex);
|
|
543
|
+
func->referenced = true;
|
|
544
|
+
g_mutex_unlock(&func->mutex);
|
|
545
|
+
return gjsify_napi::clear_last_error(env);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ---- async context + callback scope (node_api.h) ----
|
|
549
|
+
//
|
|
550
|
+
// These are async-hooks bookkeeping. GJS has no async_hooks, so an async
|
|
551
|
+
// context is a benign opaque token and a callback scope is a no-op push/pop —
|
|
552
|
+
// the callback still runs and SpiderMonkey drains its microtasks when the JS
|
|
553
|
+
// stack unwinds (see napi_make_callback's note). This is the correct minimal
|
|
554
|
+
// implementation for a host without async_hooks/uv, NOT a stub: node's
|
|
555
|
+
// make_callback / callback_scope tests need napi_async_init to SUCCEED and
|
|
556
|
+
// return a usable context, and open/close_callback_scope to bracket a call.
|
|
557
|
+
|
|
558
|
+
struct napi_async_context__ {
|
|
559
|
+
napi_env env;
|
|
560
|
+
};
|
|
561
|
+
struct napi_callback_scope__ {
|
|
562
|
+
napi_env env;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
napi_status NAPI_CDECL napi_async_init(napi_env env, napi_value async_resource,
|
|
566
|
+
napi_value async_resource_name,
|
|
567
|
+
napi_async_context* result) {
|
|
568
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
569
|
+
GJSIFY_NAPI_CHECK_ARG(env, async_resource_name);
|
|
570
|
+
GJSIFY_NAPI_CHECK_ARG(env, result);
|
|
571
|
+
(void)async_resource; // resource object is an async-hooks concept
|
|
572
|
+
*result = new napi_async_context__{env};
|
|
573
|
+
return gjsify_napi::clear_last_error(env);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
napi_status NAPI_CDECL napi_async_destroy(napi_env env,
|
|
577
|
+
napi_async_context async_context) {
|
|
578
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
579
|
+
GJSIFY_NAPI_CHECK_ARG(env, async_context);
|
|
580
|
+
delete async_context;
|
|
581
|
+
return gjsify_napi::clear_last_error(env);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
napi_status NAPI_CDECL napi_open_callback_scope(napi_env env,
|
|
585
|
+
napi_value resource_object,
|
|
586
|
+
napi_async_context context,
|
|
587
|
+
napi_callback_scope* result) {
|
|
588
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
589
|
+
GJSIFY_NAPI_CHECK_ARG(env, result);
|
|
590
|
+
(void)resource_object;
|
|
591
|
+
(void)context;
|
|
592
|
+
*result = new napi_callback_scope__{env};
|
|
593
|
+
return gjsify_napi::clear_last_error(env);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
napi_status NAPI_CDECL napi_close_callback_scope(napi_env env,
|
|
597
|
+
napi_callback_scope scope) {
|
|
598
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
599
|
+
GJSIFY_NAPI_CHECK_ARG(env, scope);
|
|
600
|
+
delete scope;
|
|
601
|
+
return gjsify_napi::clear_last_error(env);
|
|
602
|
+
}
|