@gjsify/napi 0.22.0 → 0.25.1
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 +72 -3
- package/package.json +7 -3
- package/prebuilds/darwin-arm64/GjsifyNapi-1.0.gir +19 -0
- package/prebuilds/darwin-arm64/GjsifyNapi-1.0.typelib +0 -0
- package/prebuilds/darwin-arm64/libgjsifynapi.dylib +0 -0
- package/prebuilds/{linux-x86_64 → linux-x64}/libgjsifynapi.so +0 -0
- package/src/cc/module.cc +12 -6
- package/src/cc/tsfn.cc +329 -49
- /package/prebuilds/{linux-x86_64 → linux-x64}/GjsifyNapi-1.0.gir +0 -0
- /package/prebuilds/{linux-x86_64 → linux-x64}/GjsifyNapi-1.0.typelib +0 -0
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ gjsify install @gjsify/napi
|
|
|
42
42
|
|
|
43
43
|
Published on the `@gjsify/*` release train. The tarball ships the shim
|
|
44
44
|
**prebuilt** for the platforms in [Platforms](#platforms) below —
|
|
45
|
-
`prebuilds/linux-
|
|
45
|
+
`prebuilds/linux-x64/` and `prebuilds/darwin-arm64/`, each a `.so`/`.dylib`
|
|
46
46
|
plus its `.gir` and `.typelib`, rebuilt from the released source by the
|
|
47
47
|
`napi-prebuild-*` jobs in `.github/workflows/release.yml` — so an install brings
|
|
48
48
|
the shim itself, not just a build recipe. Installing through the `gjsify` CLI
|
|
@@ -120,7 +120,7 @@ both:
|
|
|
120
120
|
|
|
121
121
|
| Platform | Prebuild dir | Status |
|
|
122
122
|
| --- | --- | --- |
|
|
123
|
-
| Linux
|
|
123
|
+
| Linux x64 | `prebuilds/linux-x64/` | Supported (full gate + conformance + consumer CI) |
|
|
124
124
|
| macOS arm64 | `prebuilds/darwin-arm64/` | Supported (build + load + P0.x value-model + **P1 tsfn** gates); the earlier tsfn exit-segfault (a UAF — the tsfn was freed while foreign claimants still held claims) is **fixed** (join-before-free, both abort + teardown paths); conformance/consumer/valgrind widening deferred |
|
|
125
125
|
| Windows x64 | — | **Attempted, blocked at gjs-on-Windows** (shim-side portability done; see below) |
|
|
126
126
|
|
|
@@ -151,7 +151,7 @@ meson setup build . # or: gjsify run init:meson
|
|
|
151
151
|
meson compile -C build # builds libgjsifynapi.so + GjsifyNapi-1.0.{gir,typelib}
|
|
152
152
|
|
|
153
153
|
# stage the prebuild the loader/gates resolve from:
|
|
154
|
-
gjsify run build:prebuilds # meson build + copy into prebuilds/linux-
|
|
154
|
+
gjsify run build:prebuilds # meson build + copy into prebuilds/linux-x64/
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
## Test
|
|
@@ -167,6 +167,8 @@ gjsify run test:gate # smoke: load a minimal addon under gjs
|
|
|
167
167
|
gjsify run test:gate:p01 # env + value model + scopes
|
|
168
168
|
gjsify run test:gate:p02 # js_native_api core
|
|
169
169
|
gjsify run test:gate:p03 # wrap + lifetime
|
|
170
|
+
gjsify run test:gate:p1 # threadsafe functions from real foreign pthreads
|
|
171
|
+
gjsify run test:gate:tsfn-teardown # tsfn env-teardown claim attribution (measured)
|
|
170
172
|
gjsify run test:mem # valgrind leak/UAF leg for the gates
|
|
171
173
|
|
|
172
174
|
gjsify run test:conformance # golden-diff vs Node over node's js-native-api addons
|
|
@@ -190,6 +192,73 @@ reason (an unlisted failure fails the harness; a listed program that starts
|
|
|
190
192
|
passing also fails — stale entry). `test:conformance:mem` / `test:consumer:mem`
|
|
191
193
|
are the valgrind variants.
|
|
192
194
|
|
|
195
|
+
## Diagnostics
|
|
196
|
+
|
|
197
|
+
Loading an addon prints **nothing**. The shim's internal diagnostics — the
|
|
198
|
+
symbol-promotion notes and the §3d P0.0 teardown probe (`GjsContext weak notify
|
|
199
|
+
fired…` / `N env(s) torn down before JS_DestroyContext`) — are `g_debug()`
|
|
200
|
+
messages on GLib's standard debug channel, so they are silent unless you ask for
|
|
201
|
+
them:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
G_MESSAGES_DEBUG=all GI_TYPELIB_PATH=build LD_LIBRARY_PATH=build gjs your-script.js
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Note GLib writes `DEBUG`-level records to **stdout** (only `WARNING` and above
|
|
208
|
+
go to stderr), so capture with `2>&1` — which is why `test:gate:p03`, the one
|
|
209
|
+
leg that asserts the teardown-ordering probe, both sets `G_MESSAGES_DEBUG=all`
|
|
210
|
+
and merges the streams.
|
|
211
|
+
|
|
212
|
+
Anything the shim reports **unconditionally** is a real problem, not a trace: a
|
|
213
|
+
`g_warning`, or the `napi_fatal_error` / unhandled-exception reports. Do not
|
|
214
|
+
silence those.
|
|
215
|
+
|
|
216
|
+
### Threadsafe functions still holding claims at env teardown
|
|
217
|
+
|
|
218
|
+
The warnings an addon can provoke. Both come from `finalize_env_tsfns`
|
|
219
|
+
(`src/cc/tsfn.cc`) and both mean the same contract violation — **a claim that was
|
|
220
|
+
never handed back** — differing only in what the shim could establish about who
|
|
221
|
+
holds it.
|
|
222
|
+
|
|
223
|
+
At env teardown the shim closes every still-registered threadsafe function, runs
|
|
224
|
+
its JS-side finalization (thread finalizer, `env == NULL` queue drain, function
|
|
225
|
+
ref) and then frees it — but **only if no claim is outstanding**. It never frees
|
|
226
|
+
under a live claim: that is the use-after-free that crashed macOS at exit (#809).
|
|
227
|
+
When claims remain it does what Node's own `MaybeDelete()` does — releases what
|
|
228
|
+
it can and hands the control block to whichever thread returns the last claim,
|
|
229
|
+
which then frees it. If no thread ever does, the control block (a few hundred
|
|
230
|
+
bytes) leaks for the process lifetime. Node behaves identically.
|
|
231
|
+
|
|
232
|
+
To decide what is worth waiting for, each claim is attributed to an owner from
|
|
233
|
+
facts the shim can observe (`napi_acquire_threadsafe_function` takes a claim on a
|
|
234
|
+
known thread; a `napi_call_threadsafe_function` proves — per the ABI contract —
|
|
235
|
+
that its caller holds one). `initial_thread_count` claims start unattributed,
|
|
236
|
+
because the addon hands them out through memory the shim never sees.
|
|
237
|
+
|
|
238
|
+
* **`teardown join timed out after 2s with N claim(s) still held by foreign
|
|
239
|
+
thread(s)`** — a thread other than the JS thread demonstrably holds a claim and
|
|
240
|
+
then neither called again (a `napi_closing` return consumes the claim) nor
|
|
241
|
+
released for a full 2 s.
|
|
242
|
+
* **`closed at env teardown with N claim(s) outstanding that nothing can hand
|
|
243
|
+
back`** — the remainder: claims held by the JS thread itself (which is the
|
|
244
|
+
thread running the teardown, so they cannot drop) plus claims no thread was
|
|
245
|
+
ever observed to hold. The message reports the two counts separately rather
|
|
246
|
+
than guessing; an unattributed claim is named as unattributed.
|
|
247
|
+
|
|
248
|
+
**Release every claim before teardown** and neither fires (an
|
|
249
|
+
`napi_add_env_cleanup_hook` that calls `napi_release_threadsafe_function(…,
|
|
250
|
+
napi_tsfn_abort)` is the standard shape; it is what `@gjsify/node-gi` does, and
|
|
251
|
+
why it never trips this).
|
|
252
|
+
|
|
253
|
+
The 2 s deadline is not the problem, and only the first case can reach it: a
|
|
254
|
+
genuinely slow drain — eight producer threads each dropping their claim on the
|
|
255
|
+
next push — measures **tens of µs**, four orders of magnitude inside it. A claim
|
|
256
|
+
nothing can hand back is not joined at all, so it costs nothing.
|
|
257
|
+
`test/tsfn-teardown-gate.mjs` (`gjsify run test:gate:tsfn-teardown`) measures the
|
|
258
|
+
teardown of every claim-owner shape on each run and is the regression guard for
|
|
259
|
+
exactly that; `G_MESSAGES_DEBUG=all` prints the measured join time and the
|
|
260
|
+
disposition on every teardown that had claims to deal with.
|
|
261
|
+
|
|
193
262
|
## The mozjs-140 pin
|
|
194
263
|
|
|
195
264
|
The pin is intrinsic and by design: the **shim** links GJS's SpiderMonkey (built
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/napi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"description": "Node-API (N-API) host for GJS — implements the napi C ABI over SpiderMonkey (mozjs-140) so GJS can load unmodified compiled .node addons. The forward mirror of @gjsify/node-gi (GI in Node); this is N-API in GJS. Vala/C++ shim as .so+.gir+.typelib prebuild, loaded via imports.gi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/esm/index.js",
|
|
@@ -22,9 +22,12 @@
|
|
|
22
22
|
],
|
|
23
23
|
"gjsify": {
|
|
24
24
|
"platforms": [
|
|
25
|
-
"linux-
|
|
25
|
+
"linux-x64",
|
|
26
26
|
"darwin-arm64"
|
|
27
27
|
],
|
|
28
|
+
"platformsUncommitted": {
|
|
29
|
+
"darwin-arm64": "napi.yml's `macos` job builds, load-tests and uploads it as a workflow artifact for a release to ship; no job commits it back into this repo"
|
|
30
|
+
},
|
|
28
31
|
"prebuilds": "prebuilds",
|
|
29
32
|
"runtimes": {
|
|
30
33
|
"gjs": "polyfill",
|
|
@@ -55,7 +58,7 @@
|
|
|
55
58
|
"init:meson": "meson setup build .",
|
|
56
59
|
"init:meson:wipe": "gjsify run init:meson --wipe",
|
|
57
60
|
"build:meson": "gjsify run init:meson && meson compile -C build",
|
|
58
|
-
"build:prebuilds": "gjsify run build:meson &&
|
|
61
|
+
"build:prebuilds": "gjsify run build:meson && node ../../../scripts/stage-prebuild.mjs .",
|
|
59
62
|
"build:test-addons": "for d in test/*-addon; do (cd \"$d\" && npm exec -- node-gyp rebuild) || exit 1; done",
|
|
60
63
|
"build:stubs": "python3 scripts/gen-stubs.py",
|
|
61
64
|
"test:gate": "GI_TYPELIB_PATH=build LD_LIBRARY_PATH=build gjs test/load-gate.js test/hello-addon/build/Release/hello.node",
|
|
@@ -63,6 +66,7 @@
|
|
|
63
66
|
"test:gate:p02": "GI_TYPELIB_PATH=build LD_LIBRARY_PATH=build gjs test/p02-gate.js",
|
|
64
67
|
"test:gate:p03": "sh test/run-p03-gate.sh",
|
|
65
68
|
"test:gate:p1": "GI_TYPELIB_PATH=build LD_LIBRARY_PATH=build gjs test/p1-tsfn-gate.js",
|
|
69
|
+
"test:gate:tsfn-teardown": "node test/tsfn-teardown-gate.mjs",
|
|
66
70
|
"test:nodegi": "node test/nodegi-gate.mjs",
|
|
67
71
|
"test:nodegi:mem": "node test/nodegi-gate.mjs --valgrind",
|
|
68
72
|
"test:mem": "sh test/p03-mem.sh",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<?xml version="1.0"?>
|
|
2
|
+
<!-- GjsifyNapi-1.0.gir generated by valac 0.56.19, do not modify. -->
|
|
3
|
+
<repository version="1.2" xmlns="http://www.gtk.org/introspection/core/1.0" xmlns:c="http://www.gtk.org/introspection/c/1.0" xmlns:doc="http://www.gtk.org/introspection/doc/1.0" xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
|
|
4
|
+
<package name="gjsifynapi"/>
|
|
5
|
+
<c:include name="gjsifynapi.h"/>
|
|
6
|
+
<doc:format name="unknown"/>
|
|
7
|
+
<namespace name="GjsifyNapi" version="1.0" shared-library="libgjsifynapi.dylib" c:prefix="GjsifyNapi" c:identifier-prefixes="GjsifyNapi" c:symbol-prefixes="gjsify_napi">
|
|
8
|
+
<function name="init" c:identifier="gjsify_napi_init">
|
|
9
|
+
<return-value transfer-ownership="full">
|
|
10
|
+
<type name="gboolean" c:type="gboolean"/>
|
|
11
|
+
</return-value>
|
|
12
|
+
</function>
|
|
13
|
+
<function name="is_available" c:identifier="gjsify_napi_is_available">
|
|
14
|
+
<return-value transfer-ownership="full">
|
|
15
|
+
<type name="gboolean" c:type="gboolean"/>
|
|
16
|
+
</return-value>
|
|
17
|
+
</function>
|
|
18
|
+
</namespace>
|
|
19
|
+
</repository>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/cc/module.cc
CHANGED
|
@@ -200,15 +200,22 @@ bool throw_load_error(JSContext* cx, const std::string& message) {
|
|
|
200
200
|
// §3d env-teardown seam + P0.0 probe. Fires while the JSContext is still
|
|
201
201
|
// alive (before the final GC + JS_DestroyContext) — exactly when Node-style
|
|
202
202
|
// env teardown must run (§5e).
|
|
203
|
+
//
|
|
204
|
+
// The two P0.0 probe lines are DIAGNOSTICS, not events a consumer must see:
|
|
205
|
+
// merely loading an addon must leave stderr clean. They therefore go through
|
|
206
|
+
// g_debug() — the same gated channel the rest of this file's non-error
|
|
207
|
+
// diagnostics already use (the symbol-promotion notes below) — so GLib's
|
|
208
|
+
// standard `G_MESSAGES_DEBUG=all` (or the log domain) turns them back on for
|
|
209
|
+
// the gates that assert the teardown ordering. Real problems keep using
|
|
210
|
+
// g_warning and stay unconditional.
|
|
203
211
|
void on_gjs_context_dispose(gpointer data, GObject* /* where_the_object_was */) {
|
|
204
212
|
auto* cx = static_cast<JSContext*>(data);
|
|
205
213
|
// Probe: JSRuntime-level and context-state calls must still succeed here.
|
|
206
214
|
JSRuntime* rt = JS_GetRuntime(cx);
|
|
207
215
|
const bool pending = JS_IsExceptionPending(cx);
|
|
208
|
-
|
|
209
|
-
"[gjsify-napi] P0.0 TEARDOWN PROBE: GjsContext weak notify fired; "
|
|
216
|
+
g_debug("[gjsify-napi] P0.0 TEARDOWN PROBE: GjsContext weak notify fired; "
|
|
210
217
|
"JS_GetRuntime=%p exception_pending=%d — JSContext alive at the "
|
|
211
|
-
"env-teardown seam
|
|
218
|
+
"env-teardown seam",
|
|
212
219
|
static_cast<void*>(rt), pending ? 1 : 0);
|
|
213
220
|
size_t count = all_envs().size();
|
|
214
221
|
// Teardown may run finalizers that trigger GC — the weak sweep callback
|
|
@@ -228,9 +235,8 @@ void on_gjs_context_dispose(gpointer data, GObject* /* where_the_object_was */)
|
|
|
228
235
|
// JS_DestroyContext (refs/gjs/gjs/context.cpp:437,471).
|
|
229
236
|
JS_RemoveWeakPointerZonesCallback(cx, gjsify_napi::weak_sweep_callback);
|
|
230
237
|
loader_installed = false;
|
|
231
|
-
|
|
232
|
-
"
|
|
233
|
-
"JS_DestroyContext\n",
|
|
238
|
+
g_debug("[gjsify-napi] P0.0 TEARDOWN PROBE: %zu env(s) torn down before "
|
|
239
|
+
"JS_DestroyContext",
|
|
234
240
|
count);
|
|
235
241
|
}
|
|
236
242
|
|
package/src/cc/tsfn.cc
CHANGED
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
// thread toggles that need this same mutex — the node-gi drain-lock
|
|
29
29
|
// lesson, toggle.cc DrainTsfnCb).
|
|
30
30
|
//
|
|
31
|
-
// Lifetime: the struct is freed
|
|
32
|
-
//
|
|
33
|
-
// thread_count has reached 0 (a natural close, OR an abort whose remaining
|
|
31
|
+
// Lifetime: the struct is freed by the LAST claimant and NEVER while a claim is
|
|
32
|
+
// still outstanding. On the JS thread that means the dispatch finalizes only
|
|
33
|
+
// once thread_count has reached 0 (a natural close, OR an abort whose remaining
|
|
34
34
|
// claimants have each drained their claim via a claim-consuming napi_closing
|
|
35
35
|
// push/release, every one of which re-arms the dispatch as it drops the last
|
|
36
36
|
// claim). This is the tsfn analog of async_work's join-before-free
|
|
@@ -43,14 +43,44 @@
|
|
|
43
43
|
// but macOS's pthread + allocator + exit-time dylib unload turn into a crash.
|
|
44
44
|
// The remaining fallback is env teardown (finalize_env_tsfns: a release(abort)
|
|
45
45
|
// issued by an env cleanup hook schedules a dispatch that would never run, the
|
|
46
|
-
// main context stops iterating once GjsContext dispose began)
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
46
|
+
// main context stops iterating once GjsContext dispose began) — there the JS
|
|
47
|
+
// thread runs the finalization itself and then DETACHES, handing ownership of
|
|
48
|
+
// the struct to whichever claimant drops the last claim (Node's MaybeDelete;
|
|
49
|
+
// see the comment above finalize_env_tsfns). ABI contract, as in Node: a thread
|
|
50
|
+
// must hold a claim (initial_thread_count / napi_acquire) while calling, and
|
|
51
|
+
// must make no call after its release — a claimless call racing the final free
|
|
52
|
+
// is UB in every host.
|
|
53
|
+
//
|
|
54
|
+
// CLAIM ATTRIBUTION — who holds the outstanding claims?
|
|
55
|
+
// `thread_count` is the ABI-visible count; on its own it cannot answer "can any
|
|
56
|
+
// of these still be handed back?", which is exactly the question env teardown
|
|
57
|
+
// has to decide. So the count is also carried as a PARTITION by owner:
|
|
58
|
+
// js_claims + foreign_claims + unattributed_claims == thread_count
|
|
59
|
+
// maintained under the same mutex, from facts the shim can observe rather than
|
|
60
|
+
// guess:
|
|
61
|
+
// • napi_acquire_threadsafe_function — the calling thread demonstrably takes a
|
|
62
|
+
// claim: exact attribution (js vs foreign by GThread identity).
|
|
63
|
+
// • napi_call_threadsafe_function — per the ABI contract the caller MUST hold
|
|
64
|
+
// a claim, so a call from thread T proves T owns one. The first call from
|
|
65
|
+
// each distinct thread therefore credits ONE initial (unattributed) claim to
|
|
66
|
+
// T; `attributed_threads` keeps that one-per-thread (crediting per call
|
|
67
|
+
// would over-attribute a single claim many times over).
|
|
68
|
+
// • napi_release_threadsafe_function / a claim-consuming napi_closing push —
|
|
69
|
+
// retires one claim from the owner's bucket.
|
|
70
|
+
// initial_thread_count claims start UNATTRIBUTED: they are handed out by the
|
|
71
|
+
// addon through memory the shim never sees, so nothing is assumed about them
|
|
72
|
+
// until a thread proves ownership by using the function. Guessing here is what
|
|
73
|
+
// #833 deliberately refused to do, and the partition keeps that honesty — an
|
|
74
|
+
// unattributed claim is reported as unattributed, never as "ours".
|
|
75
|
+
// Cost: `attribute_claim_locked` early-outs on `unattributed_claims == 0`
|
|
76
|
+
// (which is the steady state after every participating thread's first call), so
|
|
77
|
+
// the hot push path pays one predicted-not-taken branch on a word in the same
|
|
78
|
+
// cache line as thread_count — no g_thread_self(), no scan, no allocation.
|
|
50
79
|
|
|
51
80
|
#include "common.h"
|
|
52
81
|
|
|
53
82
|
#include <deque>
|
|
83
|
+
#include <vector>
|
|
54
84
|
|
|
55
85
|
// ---- the tsfn object ----
|
|
56
86
|
//
|
|
@@ -66,12 +96,25 @@ struct napi_threadsafe_function__ {
|
|
|
66
96
|
napi_finalize finalize_cb = nullptr;
|
|
67
97
|
void* finalize_data = nullptr;
|
|
68
98
|
size_t max_queue_size = 0; // 0 = unbounded
|
|
99
|
+
// The thread that created this tsfn — the JS thread, and therefore the
|
|
100
|
+
// thread that will later run finalize_env_tsfns. A claim owned by it cannot
|
|
101
|
+
// be handed back from inside that teardown, by construction.
|
|
102
|
+
GThread* js_thread = nullptr;
|
|
69
103
|
|
|
70
104
|
// Mutable state — guarded by mutex (any thread):
|
|
71
105
|
GMutex mutex;
|
|
72
106
|
GCond cond; // blocking-push space + waiter drain
|
|
73
107
|
std::deque<void*> queue;
|
|
74
108
|
size_t thread_count = 0;
|
|
109
|
+
// Owner partition of thread_count (see "CLAIM ATTRIBUTION" in the header):
|
|
110
|
+
// js_claims + foreign_claims + unattributed_claims == thread_count.
|
|
111
|
+
size_t js_claims = 0;
|
|
112
|
+
size_t foreign_claims = 0;
|
|
113
|
+
size_t unattributed_claims = 0;
|
|
114
|
+
// Threads already credited with one of the initial claims. Bounded by
|
|
115
|
+
// initial_thread_count (each entry costs one unattributed claim) and
|
|
116
|
+
// released the moment the last one is credited.
|
|
117
|
+
std::vector<GThread*> attributed_threads;
|
|
75
118
|
size_t waiters = 0; // threads inside the blocking-push wait
|
|
76
119
|
// No new pushes accepted: release(abort), the dispatch's natural-close
|
|
77
120
|
// decision, or env teardown. Node's `is_closing`.
|
|
@@ -80,6 +123,14 @@ struct napi_threadsafe_function__ {
|
|
|
80
123
|
// guard — at most ONE pending wake, exactly uv_async semantics.
|
|
81
124
|
GSource* dispatch_source = nullptr;
|
|
82
125
|
bool referenced = true; // napi_unref bookkeeping (see note below)
|
|
126
|
+
// finalize_env_tsfns is running this tsfn's JS-side finalization; a
|
|
127
|
+
// dispatch that somehow reaches the loop meanwhile must not free under it.
|
|
128
|
+
bool finalizing = false;
|
|
129
|
+
// Env teardown finished the JS-side finalization while claims were still
|
|
130
|
+
// outstanding and handed ownership of this struct to the last claimant
|
|
131
|
+
// (Node's MaybeDelete). The env and the main context are gone: whoever
|
|
132
|
+
// drops the last claim deletes directly instead of arming a dispatch.
|
|
133
|
+
bool detached = false;
|
|
83
134
|
|
|
84
135
|
napi_threadsafe_function__() {
|
|
85
136
|
g_mutex_init(&mutex);
|
|
@@ -118,6 +169,87 @@ void schedule_dispatch_locked(napi_threadsafe_function tsfn) {
|
|
|
118
169
|
g_source_attach(src, tsfn->main_context);
|
|
119
170
|
}
|
|
120
171
|
|
|
172
|
+
// ---- claim attribution (see "CLAIM ATTRIBUTION" in the file header) ----
|
|
173
|
+
//
|
|
174
|
+
// All three run under tsfn->mutex and only move counters between the buckets of
|
|
175
|
+
// the owner partition; they never touch thread_count itself, so they cannot
|
|
176
|
+
// perturb the ABI-visible claim count.
|
|
177
|
+
|
|
178
|
+
// A call from `self` PROVES (ABI contract) that `self` holds a claim. Credit it
|
|
179
|
+
// one of the still-unattributed initial claims — once per thread, or a single
|
|
180
|
+
// claim would be attributed to every thread that ever pushes.
|
|
181
|
+
//
|
|
182
|
+
// Hot path: this is called on every napi_call_threadsafe_function, so the
|
|
183
|
+
// early-out is the point — once every participating thread has pushed once,
|
|
184
|
+
// `unattributed_claims` is 0 forever after and the whole function is one branch.
|
|
185
|
+
inline void attribute_claim_locked(napi_threadsafe_function tsfn) {
|
|
186
|
+
if (tsfn->unattributed_claims == 0) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
GThread* self = g_thread_self();
|
|
190
|
+
for (GThread* seen : tsfn->attributed_threads) {
|
|
191
|
+
if (seen == self) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
tsfn->attributed_threads.push_back(self);
|
|
196
|
+
tsfn->unattributed_claims--;
|
|
197
|
+
if (self == tsfn->js_thread) {
|
|
198
|
+
tsfn->js_claims++;
|
|
199
|
+
} else {
|
|
200
|
+
tsfn->foreign_claims++;
|
|
201
|
+
}
|
|
202
|
+
if (tsfn->unattributed_claims == 0) {
|
|
203
|
+
// Nothing left to credit — drop the bookkeeping and the branch above
|
|
204
|
+
// becomes the only cost for the rest of this tsfn's life.
|
|
205
|
+
tsfn->attributed_threads.clear();
|
|
206
|
+
tsfn->attributed_threads.shrink_to_fit();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// napi_acquire_threadsafe_function: the calling thread takes a NEW claim, so
|
|
211
|
+
// this attribution is exact rather than inferred. Caller already did
|
|
212
|
+
// thread_count++.
|
|
213
|
+
inline void take_claim_locked(napi_threadsafe_function tsfn) {
|
|
214
|
+
if (g_thread_self() == tsfn->js_thread) {
|
|
215
|
+
tsfn->js_claims++;
|
|
216
|
+
} else {
|
|
217
|
+
tsfn->foreign_claims++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A release, or a claim-consuming napi_closing push, retires one claim. Caller
|
|
222
|
+
// already did thread_count--; this keeps the partition summing to it. Prefer the
|
|
223
|
+
// caller's own bucket, then the unattributed pool (the caller proved it held a
|
|
224
|
+
// claim, so an unattributed one was its), then whatever is left — the partition
|
|
225
|
+
// must never drift from thread_count even if an addon violates the contract.
|
|
226
|
+
inline void retire_claim_locked(napi_threadsafe_function tsfn) {
|
|
227
|
+
if (tsfn->js_claims == 0 && tsfn->unattributed_claims == 0) {
|
|
228
|
+
// Steady state for a fully-attributed foreign fan-out: no need to ask
|
|
229
|
+
// which thread we are.
|
|
230
|
+
if (tsfn->foreign_claims > 0) {
|
|
231
|
+
tsfn->foreign_claims--;
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (g_thread_self() == tsfn->js_thread) {
|
|
236
|
+
if (tsfn->js_claims > 0) {
|
|
237
|
+
tsfn->js_claims--;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
} else if (tsfn->foreign_claims > 0) {
|
|
241
|
+
tsfn->foreign_claims--;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (tsfn->unattributed_claims > 0) {
|
|
245
|
+
tsfn->unattributed_claims--;
|
|
246
|
+
} else if (tsfn->js_claims > 0) {
|
|
247
|
+
tsfn->js_claims--;
|
|
248
|
+
} else if (tsfn->foreign_claims > 0) {
|
|
249
|
+
tsfn->foreign_claims--;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
121
253
|
// Node CallJs (node_api.cc): the default call_js when the creator passed none.
|
|
122
254
|
void default_call_js(napi_env env, napi_value cb, void* /* context */,
|
|
123
255
|
void* /* data */) {
|
|
@@ -158,12 +290,14 @@ void invoke_call_js(napi_threadsafe_function tsfn, void* item) {
|
|
|
158
290
|
napi_close_handle_scope(env, scope);
|
|
159
291
|
}
|
|
160
292
|
|
|
161
|
-
// JS-thread finalization (Node
|
|
162
|
-
//
|
|
163
|
-
// leftover queue items with env == NULL so their data can be freed, drop
|
|
164
|
-
// function ref, unregister from the env
|
|
165
|
-
//
|
|
166
|
-
|
|
293
|
+
// JS-thread finalization WITHOUT the free (Node Finalize → EmptyQueue +
|
|
294
|
+
// finalize_cb + ReleaseResources): run the thread finalizer with JS available,
|
|
295
|
+
// drain leftover queue items with env == NULL so their data can be freed, drop
|
|
296
|
+
// the function ref, unregister from the env. Caller must have set `closing` and
|
|
297
|
+
// destroyed any pending dispatch source under the lock. Everything that needs a
|
|
298
|
+
// live env happens here; what remains afterwards is only GLib-owned memory,
|
|
299
|
+
// which is why the struct itself can outlive the env (see finalize_env_tsfns).
|
|
300
|
+
void finalize_tsfn_resources(napi_threadsafe_function tsfn) {
|
|
167
301
|
napi_env env = tsfn->env;
|
|
168
302
|
const bool can_call =
|
|
169
303
|
env != nullptr && env->can_call_into_js && !env->torn_down;
|
|
@@ -204,6 +338,12 @@ void finalize_tsfn(napi_threadsafe_function tsfn) {
|
|
|
204
338
|
}
|
|
205
339
|
}
|
|
206
340
|
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// The full JS-thread finalization (Node CloseHandlesAndMaybeDelete → Finalize →
|
|
344
|
+
// EmptyQueueAndDelete). ONLY legal with thread_count == 0 — see the file header.
|
|
345
|
+
void finalize_tsfn(napi_threadsafe_function tsfn) {
|
|
346
|
+
finalize_tsfn_resources(tsfn);
|
|
207
347
|
delete tsfn;
|
|
208
348
|
}
|
|
209
349
|
|
|
@@ -222,6 +362,14 @@ gboolean dispatch_cb(gpointer data) {
|
|
|
222
362
|
g_source_unref(tsfn->dispatch_source);
|
|
223
363
|
tsfn->dispatch_source = nullptr;
|
|
224
364
|
}
|
|
365
|
+
// Env teardown owns this tsfn: it is either mid-finalization on this very
|
|
366
|
+
// thread (a nested loop iteration from inside finalize_cb would otherwise
|
|
367
|
+
// free the struct under it) or already detached to its last claimant.
|
|
368
|
+
// Either way this dispatch has nothing left to do.
|
|
369
|
+
if (tsfn->finalizing || tsfn->detached) {
|
|
370
|
+
g_mutex_unlock(&tsfn->mutex);
|
|
371
|
+
return G_SOURCE_REMOVE;
|
|
372
|
+
}
|
|
225
373
|
unsigned iterations_left = kMaxDispatchIterations;
|
|
226
374
|
for (;;) {
|
|
227
375
|
// Finalize (free) ONLY once thread_count == 0 — no foreign thread can
|
|
@@ -274,57 +422,158 @@ gboolean dispatch_cb(gpointer data) {
|
|
|
274
422
|
|
|
275
423
|
} // namespace
|
|
276
424
|
|
|
277
|
-
// Env teardown step 1b (env.cc):
|
|
278
|
-
// release(abort) node-gi issues from its env cleanup hook schedules a
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
425
|
+
// Env teardown step 1b (env.cc): close every tsfn still registered. The
|
|
426
|
+
// release(abort) node-gi issues from its env cleanup hook schedules a dispatch
|
|
427
|
+
// that can never run — GjsContext dispose has begun, the main context will not
|
|
428
|
+
// be iterated again — so the deferred finalization is executed HERE,
|
|
429
|
+
// synchronously, while JS is still fully callable (§5e steps 1-4 discipline).
|
|
430
|
+
//
|
|
431
|
+
// Three steps, in this order:
|
|
432
|
+
//
|
|
433
|
+
// 1. JOIN the claims that CAN still be handed back. `foreign_claims` is the
|
|
434
|
+
// subset of thread_count that some OTHER thread has demonstrably taken
|
|
435
|
+
// (§CLAIM ATTRIBUTION in the file header) — each of those drains on its
|
|
436
|
+
// owner's next napi_closing push or release, so waiting for it is what lets
|
|
437
|
+
// the struct be freed here instead of leaked. Claims held by THIS thread,
|
|
438
|
+
// and claims no thread has ever been observed to hold, are NOT waited on:
|
|
439
|
+
// the first cannot drop while this thread is blocked in the wait, and for
|
|
440
|
+
// the second there is no evidence any thread will ever come back for it.
|
|
441
|
+
// That distinction is the whole point — before it, an unreturnable claim
|
|
442
|
+
// cost a deterministic full-deadline stall on a legal (if sloppy) shape.
|
|
443
|
+
// 2. Run the JS-side finalization (thread finalizer, env==NULL queue drain,
|
|
444
|
+
// function ref, env deregistration) — everything that needs a live env.
|
|
445
|
+
// 3. Free the struct if no claim is left; otherwise DETACH it.
|
|
446
|
+
//
|
|
447
|
+
// DETACH is Node's own posture, not an invention here: node_api.cc's
|
|
448
|
+
// `MaybeDelete()` bails out with `if (thread_count > 0) { ReleaseResources();
|
|
449
|
+
// return; }` — "we need to keep it alive for other threads that still have
|
|
450
|
+
// pointers to it until they release them" — and the last claimant's Release()/
|
|
451
|
+
// Push() runs `delete this` instead. So Node force-CLOSES at env teardown but
|
|
452
|
+
// never force-FREES under a live claim, and neither do we: `detached` tells the
|
|
453
|
+
// last claimant to delete directly rather than arm a dispatch on a main context
|
|
454
|
+
// that is gone. The consequence for a claim nobody ever hands back is a leaked
|
|
455
|
+
// control block at process exit — again exactly Node's, and strictly better than
|
|
456
|
+
// the force-free it replaces, which reopened the pre-#809 use-after-free window
|
|
457
|
+
// (benign on Linux/glibc, a hard crash at exit on macOS).
|
|
458
|
+
//
|
|
459
|
+
// THE WARNINGS ARE NOT NOISE — DO NOT GATE OR REMOVE THEM. They mark a real
|
|
460
|
+
// contract violation with a real consequence, and they are what keeps the
|
|
461
|
+
// attribution honest: a claim this function cannot attribute is REPORTED as
|
|
462
|
+
// unattributed, never quietly assumed to be ours.
|
|
463
|
+
// • join timed out → a foreign thread proved it holds a claim and then neither
|
|
464
|
+
// called again nor released for a full 2 s.
|
|
465
|
+
// • claims left that nothing can hand back → the JS thread's own claims plus
|
|
466
|
+
// any never-attributed initial claim; the struct leaks.
|
|
467
|
+
// A clean teardown (every claim released, or every foreign claim drained inside
|
|
468
|
+
// the deadline) says nothing at all.
|
|
469
|
+
//
|
|
470
|
+
// Measured (linux-x64, gjs 1.88, tsfn-addon driven into env teardown with claims
|
|
471
|
+
// outstanding — 3 runs each, wall time of the whole gjs process):
|
|
472
|
+
// • every claim owned by a FOREIGN thread that drains on its next napi_closing
|
|
473
|
+
// push (the shutdown-conformance shape): the join completes in tens of µs,
|
|
474
|
+
// four orders of magnitude inside the 2 s deadline — the deadline is NOT too
|
|
475
|
+
// tight and widening it would buy nothing.
|
|
476
|
+
// • a claim nothing hands back (the creating thread's own initial claim, or an
|
|
477
|
+
// unattributed one): BEFORE, a deterministic 2048 / 2090 / 2137 ms stall,
|
|
478
|
+
// then warn + force-free. AFTER, it is not joined at all — see
|
|
479
|
+
// test/tsfn-teardown-gate.mjs, which measures both shapes on every run.
|
|
295
480
|
void finalize_env_tsfns(napi_env env) {
|
|
296
481
|
while (!env->tsfns.empty()) {
|
|
297
482
|
napi_threadsafe_function tsfn = env->tsfns.back();
|
|
483
|
+
|
|
298
484
|
g_mutex_lock(&tsfn->mutex);
|
|
299
485
|
// Stop accepting pushes + wake blocking pushers so they observe it.
|
|
300
486
|
tsfn->closing = true;
|
|
487
|
+
tsfn->finalizing = true;
|
|
301
488
|
g_cond_broadcast(&tsfn->cond);
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
489
|
+
// A dispatch armed before dispose can no longer run — drop it now so it
|
|
490
|
+
// cannot fire into the finalization below.
|
|
491
|
+
if (tsfn->dispatch_source != nullptr) {
|
|
492
|
+
g_source_destroy(tsfn->dispatch_source);
|
|
493
|
+
g_source_unref(tsfn->dispatch_source);
|
|
494
|
+
tsfn->dispatch_source = nullptr;
|
|
495
|
+
}
|
|
496
|
+
// Step 1 — join only the claims a foreign thread demonstrably holds.
|
|
497
|
+
const size_t foreign_at_entry = tsfn->foreign_claims;
|
|
498
|
+
const gint64 join_started = g_get_monotonic_time();
|
|
499
|
+
const gint64 deadline = join_started + 2 * G_TIME_SPAN_SECOND;
|
|
306
500
|
bool joined = true;
|
|
307
|
-
while (tsfn->
|
|
501
|
+
while (tsfn->foreign_claims > 0) {
|
|
308
502
|
if (!g_cond_wait_until(&tsfn->cond, &tsfn->mutex, deadline)) {
|
|
309
503
|
joined = false;
|
|
310
504
|
break;
|
|
311
505
|
}
|
|
312
506
|
}
|
|
507
|
+
const gint64 join_us = g_get_monotonic_time() - join_started;
|
|
508
|
+
const size_t stuck_foreign = tsfn->foreign_claims;
|
|
509
|
+
g_mutex_unlock(&tsfn->mutex);
|
|
510
|
+
|
|
313
511
|
if (!joined) {
|
|
314
512
|
g_warning("gjsify-napi: threadsafe-function teardown join timed out "
|
|
315
|
-
"with %zu claim(s) still
|
|
316
|
-
"
|
|
317
|
-
|
|
513
|
+
"after 2s with %zu claim(s) still held by foreign "
|
|
514
|
+
"thread(s) that neither called nor released; the function "
|
|
515
|
+
"is closed and its JS resources are freed, but its control "
|
|
516
|
+
"block is handed to whichever thread hands the last claim "
|
|
517
|
+
"back — and leaks if none ever does. Release every claim "
|
|
518
|
+
"before env teardown (an napi_add_env_cleanup_hook calling "
|
|
519
|
+
"napi_release_threadsafe_function(…, napi_tsfn_abort) is "
|
|
520
|
+
"the standard shape)",
|
|
521
|
+
stuck_foreign);
|
|
522
|
+
} else if (foreign_at_entry > 0) {
|
|
523
|
+
g_debug("gjsify-napi: threadsafe-function teardown joined %zu "
|
|
524
|
+
"foreign claim(s) in %" G_GINT64_FORMAT " us "
|
|
525
|
+
"(deadline 2000000 us)",
|
|
526
|
+
foreign_at_entry, join_us);
|
|
318
527
|
}
|
|
319
|
-
|
|
320
|
-
//
|
|
528
|
+
|
|
529
|
+
// Step 2 — everything that needs a live env. Runs JS (the thread
|
|
530
|
+
// finalizer), so it must NOT hold the lock, and `detached` must not be
|
|
531
|
+
// set yet: a claimant that drops the last claim meanwhile has to leave
|
|
532
|
+
// the struct alone until we are done with it.
|
|
533
|
+
finalize_tsfn_resources(tsfn); // removes it from env->tsfns
|
|
534
|
+
|
|
535
|
+
// Step 3 — free, or detach to the last claimant.
|
|
536
|
+
g_mutex_lock(&tsfn->mutex);
|
|
537
|
+
tsfn->finalizing = false;
|
|
538
|
+
// A claimant that reached 0 during step 2 may have armed a dispatch that
|
|
539
|
+
// can no longer run (dispose has begun) — destroy it before the free.
|
|
321
540
|
if (tsfn->dispatch_source != nullptr) {
|
|
322
541
|
g_source_destroy(tsfn->dispatch_source);
|
|
323
542
|
g_source_unref(tsfn->dispatch_source);
|
|
324
543
|
tsfn->dispatch_source = nullptr;
|
|
325
544
|
}
|
|
545
|
+
if (tsfn->thread_count == 0) {
|
|
546
|
+
g_mutex_unlock(&tsfn->mutex);
|
|
547
|
+
delete tsfn;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const size_t own = tsfn->js_claims;
|
|
551
|
+
const size_t unattributed = tsfn->unattributed_claims;
|
|
552
|
+
const size_t outstanding = tsfn->thread_count;
|
|
553
|
+
tsfn->detached = true;
|
|
554
|
+
tsfn->env = nullptr; // the env is about to stop being callable
|
|
326
555
|
g_mutex_unlock(&tsfn->mutex);
|
|
327
|
-
|
|
556
|
+
|
|
557
|
+
if (own > 0 || unattributed > 0) {
|
|
558
|
+
g_warning("gjsify-napi: threadsafe-function closed at env teardown "
|
|
559
|
+
"with %zu of %zu outstanding claim(s) that nothing can "
|
|
560
|
+
"hand back: %zu held by the thread running this teardown, "
|
|
561
|
+
"%zu never attributed to any thread (typically an "
|
|
562
|
+
"initial_thread_count claim the creating thread never "
|
|
563
|
+
"released). The function is closed and its JS resources "
|
|
564
|
+
"are freed; its control block is handed to the last "
|
|
565
|
+
"claimant and therefore leaks for the process lifetime. "
|
|
566
|
+
"Release every claim before env teardown (an "
|
|
567
|
+
"napi_add_env_cleanup_hook calling "
|
|
568
|
+
"napi_release_threadsafe_function(…, napi_tsfn_abort) is "
|
|
569
|
+
"the standard shape)",
|
|
570
|
+
own + unattributed, outstanding, own, unattributed);
|
|
571
|
+
} else {
|
|
572
|
+
g_debug("gjsify-napi: threadsafe-function detached at env teardown "
|
|
573
|
+
"with %zu foreign claim(s) outstanding; the last claimant "
|
|
574
|
+
"frees it",
|
|
575
|
+
outstanding);
|
|
576
|
+
}
|
|
328
577
|
}
|
|
329
578
|
}
|
|
330
579
|
|
|
@@ -375,6 +624,10 @@ napi_status NAPI_CDECL napi_create_threadsafe_function(
|
|
|
375
624
|
tsfn->finalize_data = thread_finalize_data;
|
|
376
625
|
tsfn->max_queue_size = max_queue_size;
|
|
377
626
|
tsfn->thread_count = initial_thread_count;
|
|
627
|
+
// The addon hands the initial claims out through memory the shim never
|
|
628
|
+
// sees, so none of them is attributed until a thread proves it holds one.
|
|
629
|
+
tsfn->unattributed_claims = initial_thread_count;
|
|
630
|
+
tsfn->js_thread = g_thread_self();
|
|
378
631
|
env->tsfns.push_back(tsfn);
|
|
379
632
|
|
|
380
633
|
*result = tsfn;
|
|
@@ -391,6 +644,10 @@ napi_status NAPI_CDECL napi_call_threadsafe_function(
|
|
|
391
644
|
return napi_invalid_arg;
|
|
392
645
|
}
|
|
393
646
|
g_mutex_lock(&func->mutex);
|
|
647
|
+
// This call PROVES the caller holds a claim (ABI contract) — credit it one
|
|
648
|
+
// of the still-unattributed initial claims. Early-outs to a single branch
|
|
649
|
+
// once every participating thread has been seen; see the file header.
|
|
650
|
+
gjsify_napi::attribute_claim_locked(func);
|
|
394
651
|
while (func->max_queue_size > 0 &&
|
|
395
652
|
func->queue.size() >= func->max_queue_size && !func->closing) {
|
|
396
653
|
if (is_blocking == napi_tsfn_nonblocking) {
|
|
@@ -411,13 +668,24 @@ napi_status NAPI_CDECL napi_call_threadsafe_function(
|
|
|
411
668
|
return napi_invalid_arg;
|
|
412
669
|
}
|
|
413
670
|
func->thread_count--;
|
|
414
|
-
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
671
|
+
gjsify_napi::retire_claim_locked(func);
|
|
672
|
+
// This closing push just consumed the caller's claim. If it was the LAST
|
|
673
|
+
// one AND env teardown already detached the struct, we own it now: free
|
|
674
|
+
// directly (no dispatch — the main context is gone). Otherwise arm the
|
|
675
|
+
// JS-thread dispatch so the deferred finalize runs now that no thread can
|
|
676
|
+
// re-enter (the free half of the abort-path fix — dispatch_cb frees only
|
|
677
|
+
// at thread_count == 0; coalesces if pending).
|
|
678
|
+
if (func->thread_count == 0 && func->detached) {
|
|
679
|
+
g_mutex_unlock(&func->mutex);
|
|
680
|
+
delete func;
|
|
681
|
+
return napi_closing;
|
|
682
|
+
}
|
|
419
683
|
if (func->thread_count == 0) {
|
|
420
684
|
gjsify_napi::schedule_dispatch_locked(func);
|
|
685
|
+
}
|
|
686
|
+
// Wake a teardown join (finalize_env_tsfns): it waits on the FOREIGN
|
|
687
|
+
// claims, which can reach 0 well before thread_count does.
|
|
688
|
+
if (func->thread_count == 0 || func->foreign_claims == 0) {
|
|
421
689
|
g_cond_broadcast(&func->cond);
|
|
422
690
|
}
|
|
423
691
|
g_mutex_unlock(&func->mutex);
|
|
@@ -443,10 +711,19 @@ napi_status NAPI_CDECL napi_release_threadsafe_function(
|
|
|
443
711
|
return napi_invalid_arg;
|
|
444
712
|
}
|
|
445
713
|
func->thread_count--;
|
|
714
|
+
gjsify_napi::retire_claim_locked(func);
|
|
446
715
|
if (mode == napi_tsfn_abort && !func->closing) {
|
|
447
716
|
func->closing = true;
|
|
448
717
|
g_cond_broadcast(&func->cond); // wake blocking pushers
|
|
449
718
|
}
|
|
719
|
+
// Env teardown already ran the finalization and handed the struct to its
|
|
720
|
+
// last claimant — that is us, so free it directly (the main context is gone,
|
|
721
|
+
// so a dispatch would never run).
|
|
722
|
+
if (func->thread_count == 0 && func->detached) {
|
|
723
|
+
g_mutex_unlock(&func->mutex);
|
|
724
|
+
delete func;
|
|
725
|
+
return napi_ok;
|
|
726
|
+
}
|
|
450
727
|
// Arm the JS-thread dispatch when this drops the last claim (natural close,
|
|
451
728
|
// OR the deferred abort-path finalize once every claimant has drained) or
|
|
452
729
|
// when an abort just closed the tsfn (so a still-populated queue reaches its
|
|
@@ -455,8 +732,9 @@ napi_status NAPI_CDECL napi_release_threadsafe_function(
|
|
|
455
732
|
if (func->thread_count == 0 || mode == napi_tsfn_abort) {
|
|
456
733
|
gjsify_napi::schedule_dispatch_locked(func);
|
|
457
734
|
}
|
|
458
|
-
|
|
459
|
-
|
|
735
|
+
// Wake a teardown join (finalize_env_tsfns): it waits on the FOREIGN claims,
|
|
736
|
+
// which can reach 0 well before thread_count does.
|
|
737
|
+
if (func->thread_count == 0 || func->foreign_claims == 0) {
|
|
460
738
|
g_cond_broadcast(&func->cond);
|
|
461
739
|
}
|
|
462
740
|
g_mutex_unlock(&func->mutex);
|
|
@@ -527,6 +805,8 @@ napi_status NAPI_CDECL napi_acquire_threadsafe_function(
|
|
|
527
805
|
return napi_closing;
|
|
528
806
|
}
|
|
529
807
|
func->thread_count++;
|
|
808
|
+
// The one claim whose owner is known exactly rather than inferred.
|
|
809
|
+
gjsify_napi::take_claim_locked(func);
|
|
530
810
|
g_mutex_unlock(&func->mutex);
|
|
531
811
|
return napi_ok;
|
|
532
812
|
}
|
|
File without changes
|
|
File without changes
|