@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
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @gjsify/napi — asynchronous work items (napi_create/queue/cancel/delete_
|
|
3
|
+
// async_work). This is what unblocks the whole node-addon-api async ecosystem
|
|
4
|
+
// (node-sqlite3, bcrypt, …): every Napi::AsyncWorker is these four functions.
|
|
5
|
+
//
|
|
6
|
+
// Reference: refs/node/src/node_api.cc uvimpl::Work (Node.js contributors,
|
|
7
|
+
// MIT) — the normative execute-on-worker / complete-on-loop semantics mirrored
|
|
8
|
+
// here. Node realizes the two phases as node::ThreadPoolWork on libuv: a WORKER
|
|
9
|
+
// thread runs `execute(env, data)` (NO JS access — the ABI contract), then the
|
|
10
|
+
// event loop runs `complete(env, status, data)` on the main/JS thread under a
|
|
11
|
+
// HandleScope + CallbackScope.
|
|
12
|
+
//
|
|
13
|
+
// THE HOST REALIZATION — GThreadPool worker + GMainContext dispatch:
|
|
14
|
+
// GJS owns exactly one JSContext on one thread, but a napi worker phase enters
|
|
15
|
+
// no JS, so it can run on a real pooled thread. This shim gives each env a
|
|
16
|
+
// GThreadPool (libuv-thread-pool analog, sized after UV_THREADPOOL_SIZE):
|
|
17
|
+
// - `napi_queue_async_work` pushes the work onto the pool and RETURNS (true
|
|
18
|
+
// concurrency — the loop no longer blocks during `execute`, the one
|
|
19
|
+
// divergence the earlier inline model documented). A pooled WORKER thread
|
|
20
|
+
// runs `execute(env, data)`; it enters no JSAPI and touches no GObject (the
|
|
21
|
+
// ABI contract), so it races nothing on the JS thread.
|
|
22
|
+
// - on completion the worker marshals `complete` back to the JS thread via a
|
|
23
|
+
// g_idle GSource attached to the env's GMainContext — EXACTLY the machinery
|
|
24
|
+
// tsfn.cc's cross-thread dispatch uses. `g_source_attach` is thread-safe
|
|
25
|
+
// (takes the context lock, wakes its eventfd, runs no JSAPI), and the source
|
|
26
|
+
// only ever DISPATCHES on the thread iterating the context (the JS thread).
|
|
27
|
+
// `complete` runs there under a fresh handle scope, guarded by
|
|
28
|
+
// can_call_into_js, pending exception logged-and-cleared — the "no JS frame
|
|
29
|
+
// below a loop-dispatched callback" posture shared with the finalizer
|
|
30
|
+
// pipeline and tsfn. The completion fires ASYNCHRONOUSLY, a loop turn after
|
|
31
|
+
// queue returns, exactly as callers depend on.
|
|
32
|
+
//
|
|
33
|
+
// THE MUTEX: `state` + `complete_source` + `complete_status` are touched by the
|
|
34
|
+
// JS thread (create/queue/cancel/dispatch/teardown) AND a pool worker, so a
|
|
35
|
+
// per-work GMutex serializes them. It is NEVER held across `execute` (long,
|
|
36
|
+
// unbounded) nor across `complete`/any JS call (may run arbitrary JS →
|
|
37
|
+
// self-delete) — the tsfn "lock never spans call_js" discipline. The
|
|
38
|
+
// JS↔worker race that decides cancel lives entirely inside that lock: whoever
|
|
39
|
+
// claims the work first (worker → kExecuting, cancel → kCancelled) wins, so
|
|
40
|
+
// cancel is EBUSY iff the worker already started (Node's uv_cancel semantics).
|
|
41
|
+
//
|
|
42
|
+
// An AsyncWork holds NO JS values (execute/complete/data are plain C pointers;
|
|
43
|
+
// any napi_ref the addon needs lives on the env's ref list, traced there), so —
|
|
44
|
+
// unlike tsfn/Reference — it needs no GC root and no weak-sweep interaction.
|
|
45
|
+
// Its only lifetime hazards are the pending GSource and the worker; both are
|
|
46
|
+
// handled the tsfn way (source owned until dispatch/teardown; worker joined at
|
|
47
|
+
// teardown before any root drops).
|
|
48
|
+
//
|
|
49
|
+
// Lifetime / ownership (Node parity — addons commonly delete the work INSIDE
|
|
50
|
+
// complete, js_native_api "Don't access work after complete" rule):
|
|
51
|
+
// - create registers the work on env->async_works (teardown reaches it);
|
|
52
|
+
// - the complete dispatch marks the work kComplete, drops the source, then
|
|
53
|
+
// runs complete — which MAY call napi_delete_async_work(work) (self-delete,
|
|
54
|
+
// unlinks + frees) so the struct is NEVER touched afterwards;
|
|
55
|
+
// - a work the addon never deletes stays linked and is freed at env
|
|
56
|
+
// destruction (destroy_env_async_works) — Node leaks that same struct (its
|
|
57
|
+
// Work is addon-owned), so the mem leg runs leak-check=no.
|
|
58
|
+
//
|
|
59
|
+
// Teardown (§5e crash-class boundary): at env teardown the pool is drained
|
|
60
|
+
// deterministically — g_thread_pool_free(pool, immediate=FALSE, wait=TRUE) lets
|
|
61
|
+
// every in-flight/queued execute finish and JOINS every worker (a happens-before
|
|
62
|
+
// barrier: the worker's writes are then visible on the JS thread, and no worker
|
|
63
|
+
// can touch a work or the env again). Only THEN are the still-pending completions
|
|
64
|
+
// run synchronously, JS still callable (drain_env_async_works) — the same
|
|
65
|
+
// discipline finalize_env_tsfns uses, because the deferred g_idle can no longer
|
|
66
|
+
// fire once GjsContext dispose began. An async_work must not outlive the env; a
|
|
67
|
+
// worker must not touch a torn-down env.
|
|
68
|
+
|
|
69
|
+
#include "common.h"
|
|
70
|
+
|
|
71
|
+
#include <algorithm>
|
|
72
|
+
|
|
73
|
+
// Global scope: the struct tag is fixed by the vendored
|
|
74
|
+
// `typedef struct napi_async_work__* napi_async_work`.
|
|
75
|
+
struct napi_async_work__ {
|
|
76
|
+
// Lifecycle state. Transitions cross the JS thread ↔ pool worker boundary,
|
|
77
|
+
// so all reads/writes are under `mutex` (except at teardown, after the pool
|
|
78
|
+
// join has re-established single-threadedness).
|
|
79
|
+
enum class State : uint8_t {
|
|
80
|
+
kCreated, // created, not yet queued (no uv request → cancel EINVAL)
|
|
81
|
+
kQueued, // pushed to the pool; no worker has started execute
|
|
82
|
+
kExecuting, // a worker is running execute (cancel now → EBUSY)
|
|
83
|
+
kCancelled, // cancelled while queued; the worker will skip execute
|
|
84
|
+
kCompletePending, // execute ran (or was skipped); a complete dispatch is armed
|
|
85
|
+
kComplete, // complete has been dispatched (may already be freed)
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
napi_env env = nullptr; // owning env (outlives the work)
|
|
89
|
+
napi_async_execute_callback execute = nullptr; // worker-phase, NO JS
|
|
90
|
+
napi_async_complete_callback complete = nullptr; // main-phase, may run JS
|
|
91
|
+
void* data = nullptr; // addon-owned payload
|
|
92
|
+
|
|
93
|
+
// Guards state + complete_source + complete_status across JS thread/worker.
|
|
94
|
+
GMutex mutex;
|
|
95
|
+
State state = State::kCreated;
|
|
96
|
+
// Set by the worker (napi_ok) or by the worker for a cancelled item
|
|
97
|
+
// (napi_cancelled); read by the JS-thread dispatch. Guarded by mutex.
|
|
98
|
+
napi_status complete_status = napi_ok;
|
|
99
|
+
// The env's dispatch surface (default GMainContext, ref held) — captured on
|
|
100
|
+
// the JS thread at create so the worker never calls g_main_context_default().
|
|
101
|
+
GMainContext* main_context = nullptr;
|
|
102
|
+
// The pending main-loop complete dispatch (creation ref held until the
|
|
103
|
+
// dispatch runs or teardown/free destroys it — tsfn dispatch_source
|
|
104
|
+
// discipline). Armed by the worker, dropped on the JS thread.
|
|
105
|
+
GSource* complete_source = nullptr;
|
|
106
|
+
|
|
107
|
+
napi_async_work__() { g_mutex_init(&mutex); }
|
|
108
|
+
|
|
109
|
+
~napi_async_work__() {
|
|
110
|
+
// Defensive: a live source here would fire into freed memory. In every
|
|
111
|
+
// normal path complete_source is already null (the dispatch, free or
|
|
112
|
+
// teardown cleared it) — this only bites a pathological free. By the
|
|
113
|
+
// time a work is freed, its worker is done (joined at teardown, or the
|
|
114
|
+
// work reached kComplete), so no thread races this.
|
|
115
|
+
if (complete_source != nullptr) {
|
|
116
|
+
g_source_destroy(complete_source);
|
|
117
|
+
g_source_unref(complete_source);
|
|
118
|
+
complete_source = nullptr;
|
|
119
|
+
}
|
|
120
|
+
if (main_context != nullptr) {
|
|
121
|
+
g_main_context_unref(main_context);
|
|
122
|
+
}
|
|
123
|
+
g_mutex_clear(&mutex);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
namespace gjsify_napi {
|
|
128
|
+
namespace {
|
|
129
|
+
|
|
130
|
+
gboolean complete_dispatch_cb(gpointer data);
|
|
131
|
+
|
|
132
|
+
// libuv's UV_THREADPOOL_SIZE (default 4, clamped [1, 1024]). TestCancel relies
|
|
133
|
+
// on the pool being SMALLER than MAX_CANCEL_THREADS (6): it saturates the pool
|
|
134
|
+
// with sleeping works so the cancel target stays queued. Reading the same env
|
|
135
|
+
// var libuv reads keeps the Node reference and the shim in lockstep.
|
|
136
|
+
int async_pool_max_threads() {
|
|
137
|
+
int n = 4;
|
|
138
|
+
const char* size = g_getenv("UV_THREADPOOL_SIZE");
|
|
139
|
+
if (size != nullptr) {
|
|
140
|
+
const gint64 parsed = g_ascii_strtoll(size, nullptr, 10);
|
|
141
|
+
if (parsed > 0) {
|
|
142
|
+
n = static_cast<int>(parsed);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (n < 1) n = 1;
|
|
146
|
+
if (n > 1024) n = 1024;
|
|
147
|
+
return n;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Arm the JS-thread complete dispatch. Caller holds work->mutex. Safe from the
|
|
151
|
+
// worker thread: pure GLib, no JSAPI (see file header) — g_source_attach takes
|
|
152
|
+
// the context lock and wakes it; the source dispatches only on the JS thread.
|
|
153
|
+
void schedule_complete_locked(napi_async_work work) {
|
|
154
|
+
if (work->complete_source != nullptr) {
|
|
155
|
+
return; // already armed — never double-schedule a single work
|
|
156
|
+
}
|
|
157
|
+
GSource* src = g_idle_source_new();
|
|
158
|
+
// uv_async parity: the loop checks its event queue every turn, so run the
|
|
159
|
+
// completion promptly rather than at idle-starvation priority.
|
|
160
|
+
g_source_set_priority(src, G_PRIORITY_DEFAULT);
|
|
161
|
+
g_source_set_name(src, "gjsify-napi async_work complete");
|
|
162
|
+
g_source_set_callback(src, complete_dispatch_cb, work, nullptr);
|
|
163
|
+
work->complete_source = src; // keep the creation ref until dispatch/teardown
|
|
164
|
+
g_source_attach(src, work->main_context);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// The pool worker phase (Node's uvimpl::Work::ExecuteCallback on a libuv worker
|
|
168
|
+
// thread). Runs OFF the JS thread; enters no JSAPI, touches no GObject. Claims
|
|
169
|
+
// the work under the lock (kQueued → kExecuting, so a concurrent cancel now
|
|
170
|
+
// returns EBUSY) or, if cancel won the race (kCancelled), skips execute. The
|
|
171
|
+
// lock is released across `execute` (unbounded) and re-taken only to record the
|
|
172
|
+
// status + arm the JS-thread completion.
|
|
173
|
+
void pool_worker(gpointer data, gpointer /* user_data */) {
|
|
174
|
+
auto work = static_cast<napi_async_work>(data);
|
|
175
|
+
napi_env env = work->env;
|
|
176
|
+
|
|
177
|
+
g_mutex_lock(&work->mutex);
|
|
178
|
+
if (work->state == napi_async_work__::State::kCancelled) {
|
|
179
|
+
// Cancel won the race before any worker started this item (Node's
|
|
180
|
+
// uv_cancel-on-a-queued-work → the after-cb runs with UV_ECANCELED).
|
|
181
|
+
work->complete_status = napi_cancelled;
|
|
182
|
+
work->state = napi_async_work__::State::kCompletePending;
|
|
183
|
+
schedule_complete_locked(work);
|
|
184
|
+
g_mutex_unlock(&work->mutex);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
work->state = napi_async_work__::State::kExecuting;
|
|
188
|
+
napi_async_execute_callback execute = work->execute;
|
|
189
|
+
void* payload = work->data;
|
|
190
|
+
g_mutex_unlock(&work->mutex); // execute must run with NO lock held
|
|
191
|
+
|
|
192
|
+
if (execute != nullptr) {
|
|
193
|
+
execute(env, payload); // worker phase — NO JS, NO GObject (ABI contract)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
g_mutex_lock(&work->mutex);
|
|
197
|
+
work->complete_status = napi_ok;
|
|
198
|
+
work->state = napi_async_work__::State::kCompletePending;
|
|
199
|
+
schedule_complete_locked(work);
|
|
200
|
+
g_mutex_unlock(&work->mutex);
|
|
201
|
+
// Do NOT touch `work` after unlock: the JS-thread dispatch may already have
|
|
202
|
+
// run complete and self-deleted it.
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Run the completion callback with JS callable (Node AfterThreadPoolWork:
|
|
206
|
+
// HandleScope + CallbackScope + CallbackIntoModule<true>). NO lock held. A null
|
|
207
|
+
// complete is a no-op (Node returns early). The env==torn-down path still calls
|
|
208
|
+
// complete so the addon can free `data`, but opens no handle scope and enters no
|
|
209
|
+
// JS — the EmptyQueueAndDelete posture shared with tsfn. WARNING: complete may
|
|
210
|
+
// delete the work; the caller must not touch it afterwards.
|
|
211
|
+
void run_complete(napi_async_work work, napi_status status) {
|
|
212
|
+
napi_env env = work->env;
|
|
213
|
+
napi_async_complete_callback complete = work->complete;
|
|
214
|
+
void* payload = work->data;
|
|
215
|
+
if (complete == nullptr) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const bool can_call =
|
|
219
|
+
env != nullptr && env->can_call_into_js && !env->torn_down;
|
|
220
|
+
if (!can_call) {
|
|
221
|
+
complete(env, status, payload);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
napi_handle_scope scope = nullptr;
|
|
225
|
+
napi_open_handle_scope(env, &scope);
|
|
226
|
+
complete(env, status, payload);
|
|
227
|
+
log_and_clear_pending_exception(env, "async-work complete callback");
|
|
228
|
+
napi_close_handle_scope(env, scope);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// The JS-thread complete dispatch (Node's AfterThreadPoolWork on the loop).
|
|
232
|
+
// Under the lock: drop the source, snapshot the status, mark kComplete. Then
|
|
233
|
+
// RELEASE the lock and run the callback exactly once — the lock is never held
|
|
234
|
+
// across JS (self-delete would clear a held mutex), and after complete the work
|
|
235
|
+
// may be gone, so it is never touched again.
|
|
236
|
+
gboolean complete_dispatch_cb(gpointer data) {
|
|
237
|
+
auto work = static_cast<napi_async_work>(data);
|
|
238
|
+
g_mutex_lock(&work->mutex);
|
|
239
|
+
// Drop our creation ref; the context's ref dies with SOURCE_REMOVE. Null it
|
|
240
|
+
// first so a self-delete inside complete does not re-free the source.
|
|
241
|
+
if (work->complete_source != nullptr) {
|
|
242
|
+
g_source_unref(work->complete_source);
|
|
243
|
+
work->complete_source = nullptr;
|
|
244
|
+
}
|
|
245
|
+
const napi_status status = work->complete_status;
|
|
246
|
+
work->state = napi_async_work__::State::kComplete;
|
|
247
|
+
g_mutex_unlock(&work->mutex);
|
|
248
|
+
run_complete(work, status); // may delete `work` — do not touch it after
|
|
249
|
+
return G_SOURCE_REMOVE;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Unlink a work from its env's registry (no-op if absent).
|
|
253
|
+
void unregister_async_work(napi_env env, napi_async_work work) {
|
|
254
|
+
auto& list = env->async_works;
|
|
255
|
+
list.erase(std::remove(list.begin(), list.end(), work), list.end());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Lazily create the env's worker pool. Refuses once teardown has begun so an
|
|
259
|
+
// async_work can never outlive the env (a pool recreated after the teardown
|
|
260
|
+
// drain would run a worker into a disposing context). Exclusive pool: threads
|
|
261
|
+
// are dedicated + created eagerly, so g_thread_pool_free joins them
|
|
262
|
+
// deterministically (clean valgrind, no lingering shared idle threads).
|
|
263
|
+
GThreadPool* ensure_env_pool(napi_env env) {
|
|
264
|
+
if (env->tearing_down || env->torn_down) {
|
|
265
|
+
return nullptr;
|
|
266
|
+
}
|
|
267
|
+
if (env->async_pool == nullptr) {
|
|
268
|
+
env->async_pool = g_thread_pool_new(pool_worker, nullptr,
|
|
269
|
+
async_pool_max_threads(),
|
|
270
|
+
/* exclusive = */ TRUE, nullptr);
|
|
271
|
+
}
|
|
272
|
+
return env->async_pool;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
} // namespace
|
|
276
|
+
|
|
277
|
+
// Env teardown step 1c (env.cc), the §5e crash-class boundary. FIRST join the
|
|
278
|
+
// pool: g_thread_pool_free(immediate=FALSE, wait=TRUE) runs every queued/in-
|
|
279
|
+
// flight execute to completion and joins all workers — after it returns the
|
|
280
|
+
// process is single-threaded again, every worker's writes are visible here, and
|
|
281
|
+
// nothing off-thread can touch a work or the (tearing-down) env. THEN run each
|
|
282
|
+
// still-pending completion synchronously, JS callable, because the deferred
|
|
283
|
+
// g_idle can no longer fire once GjsContext dispose began (mirror
|
|
284
|
+
// finalize_env_tsfns). No lock is needed below the join.
|
|
285
|
+
void drain_env_async_works(napi_env env) {
|
|
286
|
+
if (env->async_pool != nullptr) {
|
|
287
|
+
// wait=TRUE: block here until in-flight execute finishes + workers join.
|
|
288
|
+
// This is what keeps a worker from touching a torn-down env.
|
|
289
|
+
g_thread_pool_free(env->async_pool, /* immediate = */ FALSE,
|
|
290
|
+
/* wait = */ TRUE);
|
|
291
|
+
env->async_pool = nullptr;
|
|
292
|
+
}
|
|
293
|
+
for (;;) {
|
|
294
|
+
napi_async_work work = nullptr;
|
|
295
|
+
for (napi_async_work w : env->async_works) {
|
|
296
|
+
// Post-join: workers are all gone, so plain reads are safe. Every
|
|
297
|
+
// work is now kCompletePending, kComplete (dispatch already ran) or
|
|
298
|
+
// kCreated (never queued); only the first still needs completing.
|
|
299
|
+
if (w->state == napi_async_work__::State::kCompletePending) {
|
|
300
|
+
work = w;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (work == nullptr) {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
const napi_status status = work->complete_status;
|
|
308
|
+
if (work->complete_source != nullptr) {
|
|
309
|
+
g_source_destroy(work->complete_source);
|
|
310
|
+
g_source_unref(work->complete_source);
|
|
311
|
+
work->complete_source = nullptr;
|
|
312
|
+
}
|
|
313
|
+
// Mark BEFORE running: complete may self-delete (unlinks + frees), and a
|
|
314
|
+
// work it does NOT delete must not be revisited by the next scan.
|
|
315
|
+
work->state = napi_async_work__::State::kComplete;
|
|
316
|
+
run_complete(work, status); // may delete `work` — do not touch after
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Env destruction (~napi_env__, after teardown, no JS runs anymore): free every
|
|
321
|
+
// remaining work. Self-deleted works were already unlinked, so nothing here can
|
|
322
|
+
// double-free; a work the addon never deleted is freed now (Node leaks its
|
|
323
|
+
// equivalent — the struct is addon-owned there). The pool is already gone
|
|
324
|
+
// (drained at teardown); the guard is defensive for a partial-init env.
|
|
325
|
+
void destroy_env_async_works(napi_env env) {
|
|
326
|
+
if (env->async_pool != nullptr) {
|
|
327
|
+
g_thread_pool_free(env->async_pool, /* immediate = */ TRUE,
|
|
328
|
+
/* wait = */ TRUE);
|
|
329
|
+
env->async_pool = nullptr;
|
|
330
|
+
}
|
|
331
|
+
for (napi_async_work work : env->async_works) {
|
|
332
|
+
delete work;
|
|
333
|
+
}
|
|
334
|
+
env->async_works.clear();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
} // namespace gjsify_napi
|
|
338
|
+
|
|
339
|
+
// ---- ABI ----
|
|
340
|
+
|
|
341
|
+
// Node napi_create_async_work: allocate the work, capture the callbacks + data,
|
|
342
|
+
// register it on the env. async_resource / async_resource_name are async-hooks
|
|
343
|
+
// naming with no GJS equivalent (as with tsfn's async_resource) — validated,
|
|
344
|
+
// then ignored.
|
|
345
|
+
napi_status NAPI_CDECL napi_create_async_work(
|
|
346
|
+
napi_env env, napi_value async_resource, napi_value async_resource_name,
|
|
347
|
+
napi_async_execute_callback execute, napi_async_complete_callback complete,
|
|
348
|
+
void* data, napi_async_work* result) {
|
|
349
|
+
(void)async_resource;
|
|
350
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
351
|
+
GJSIFY_NAPI_CHECK_ARG(env, async_resource_name);
|
|
352
|
+
GJSIFY_NAPI_CHECK_ARG(env, execute);
|
|
353
|
+
GJSIFY_NAPI_CHECK_ARG(env, result);
|
|
354
|
+
|
|
355
|
+
auto work = new napi_async_work__();
|
|
356
|
+
work->env = env;
|
|
357
|
+
work->execute = execute;
|
|
358
|
+
work->complete = complete;
|
|
359
|
+
work->data = data;
|
|
360
|
+
// The GJS host loop iterates the GLOBAL default main context (the same
|
|
361
|
+
// surface the §5c finalizer drain + tsfn dispatch target). Ref it on the JS
|
|
362
|
+
// thread so the worker reads a stable, owned pointer.
|
|
363
|
+
work->main_context = g_main_context_ref(g_main_context_default());
|
|
364
|
+
env->async_works.push_back(work);
|
|
365
|
+
|
|
366
|
+
*result = work;
|
|
367
|
+
return gjsify_napi::clear_last_error(env);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Node napi_delete_async_work: free the work. Node parity — addons delete inside
|
|
371
|
+
// complete, so this must tolerate a still-registered OR already-unlinked work.
|
|
372
|
+
// Unregister (no-op if the complete dispatch already removed it) then free.
|
|
373
|
+
napi_status NAPI_CDECL napi_delete_async_work(napi_env env,
|
|
374
|
+
napi_async_work work) {
|
|
375
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
376
|
+
GJSIFY_NAPI_CHECK_ARG(env, work);
|
|
377
|
+
gjsify_napi::unregister_async_work(env, work);
|
|
378
|
+
delete work; // ~napi_async_work__ destroys any lingering source
|
|
379
|
+
return gjsify_napi::clear_last_error(env);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Node napi_queue_async_work: hand the work to the env's worker pool. A pooled
|
|
383
|
+
// thread runs `execute` (NO JS — the ABI contract), then marshals `complete`
|
|
384
|
+
// back to the JS thread on the next loop turn (see file header). Re-queuing a
|
|
385
|
+
// work, or queuing after teardown began, is a caller error → generic_failure.
|
|
386
|
+
napi_status NAPI_CDECL napi_queue_async_work(node_api_basic_env basic_env,
|
|
387
|
+
napi_async_work work) {
|
|
388
|
+
napi_env env = const_cast<napi_env>(basic_env);
|
|
389
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
390
|
+
GJSIFY_NAPI_CHECK_ARG(env, work);
|
|
391
|
+
|
|
392
|
+
GThreadPool* pool = gjsify_napi::ensure_env_pool(env);
|
|
393
|
+
if (pool == nullptr) {
|
|
394
|
+
// Teardown began (or the pool failed to allocate) — cannot accept work.
|
|
395
|
+
return gjsify_napi::set_last_error(env, napi_generic_failure);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
g_mutex_lock(&work->mutex);
|
|
399
|
+
if (work->state != napi_async_work__::State::kCreated) {
|
|
400
|
+
// Already queued / completed — re-queuing a work is a caller error.
|
|
401
|
+
g_mutex_unlock(&work->mutex);
|
|
402
|
+
return gjsify_napi::set_last_error(env, napi_generic_failure);
|
|
403
|
+
}
|
|
404
|
+
work->state = napi_async_work__::State::kQueued;
|
|
405
|
+
g_mutex_unlock(&work->mutex);
|
|
406
|
+
|
|
407
|
+
// Enqueue onto the pool. For an exclusive pool the worker threads already
|
|
408
|
+
// exist, so this only appends to the async queue (never spawns) — the GError
|
|
409
|
+
// out-param would only report a thread-create failure, which cannot occur
|
|
410
|
+
// here; the host owns thread provisioning (as Node owns libuv's).
|
|
411
|
+
g_thread_pool_push(pool, work, nullptr);
|
|
412
|
+
return gjsify_napi::clear_last_error(env);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Node napi_cancel_async_work: cancel a not-yet-STARTED work. Semantics mirror
|
|
416
|
+
// Node's uv_cancel exactly:
|
|
417
|
+
// - kQueued (in the pool, no worker has claimed it) → napi_ok; the worker will
|
|
418
|
+
// later dequeue it, skip execute, and complete with napi_cancelled (uv's
|
|
419
|
+
// queued-work-removed → UV_ECANCELED after-callback);
|
|
420
|
+
// - kCreated (never queued → no uv request) → generic_failure (uv EINVAL);
|
|
421
|
+
// - kExecuting / kCompletePending / kComplete (already running or done) →
|
|
422
|
+
// generic_failure (uv EBUSY).
|
|
423
|
+
// The JS-thread cancel races the worker's claim under the SAME lock, so exactly
|
|
424
|
+
// one of them transitions kQueued: cancel loses (EBUSY) iff the worker already
|
|
425
|
+
// flipped kExecuting.
|
|
426
|
+
napi_status NAPI_CDECL napi_cancel_async_work(node_api_basic_env basic_env,
|
|
427
|
+
napi_async_work work) {
|
|
428
|
+
napi_env env = const_cast<napi_env>(basic_env);
|
|
429
|
+
GJSIFY_NAPI_CHECK_ENV(env);
|
|
430
|
+
GJSIFY_NAPI_CHECK_ARG(env, work);
|
|
431
|
+
g_mutex_lock(&work->mutex);
|
|
432
|
+
if (work->state != napi_async_work__::State::kQueued) {
|
|
433
|
+
g_mutex_unlock(&work->mutex);
|
|
434
|
+
return gjsify_napi::set_last_error(env, napi_generic_failure);
|
|
435
|
+
}
|
|
436
|
+
work->state = napi_async_work__::State::kCancelled;
|
|
437
|
+
g_mutex_unlock(&work->mutex);
|
|
438
|
+
return gjsify_napi::clear_last_error(env);
|
|
439
|
+
}
|