@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.
@@ -0,0 +1,519 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/napi — Node-API (N-API) host over GJS's SpiderMonkey (mozjs-140).
3
+ //
4
+ // Reference: refs/node/src/js_native_api_v8.{h,cc} + node_api.cc (Node.js
5
+ // contributors, MIT) — the normative Node-API reference implementation whose
6
+ // observable semantics this shim mirrors over JSAPI. GJS's own production
7
+ // patterns are the mechanism reference: extra-GC-roots tracer
8
+ // (refs/gjs/gjs/context.cpp), natives-with-reserved-slots
9
+ // (refs/gjs/gjs/jsapi-dynamic-class.cpp), public context accessors
10
+ // (refs/gjs/gjs/context.h). GNOME contributors, MIT/LGPLv2+.
11
+ //
12
+ // Cross-TU layout (node-gi common.h idiom): this header owns every
13
+ // cross-TU declaration in `namespace gjsify_napi` plus the `napi_env__`
14
+ // definition (global scope — the tag is declared by the vendored
15
+ // js_native_api_types.h). Functional slices:
16
+ // env.cc — arena, tracer, handle scopes, env lifecycle/teardown
17
+ // value.cc — minimal value surface (undefined/null/object/string/typeof)
18
+ // function.cc — JSNative trampoline, napi_create_function, napi_get_cb_info
19
+ // error.cc — status bookkeeping, throw/pending mapping, stubs
20
+ // module.cc — gjsify_napi_install, loadAddon, dlopen + registration,
21
+ // napi_get_version, napi_fatal_error, teardown seam
22
+
23
+ #ifndef GJSIFY_NAPI_SRC_CC_COMMON_H_
24
+ #define GJSIFY_NAPI_SRC_CC_COMMON_H_
25
+
26
+ // Surface every declaration up to the max version this host reports
27
+ // (napi_get_version = 10) when compiling the shim itself.
28
+ #define NAPI_VERSION 10
29
+
30
+ #include "js_native_api.h" // vendored, src/napi-headers/
31
+ #include "node_api.h" // vendored, src/napi-headers/
32
+
33
+ #include <glib-object.h>
34
+ #include <glib.h>
35
+
36
+ #include <gjs/gjs.h> // umbrella: gjs/context.h forbids direct inclusion
37
+
38
+ #include <jsapi.h>
39
+ #include <jsfriendapi.h>
40
+
41
+ #include <js/Array.h>
42
+ #include <js/BigInt.h>
43
+ #include <js/CallAndConstruct.h>
44
+ #include <js/CallArgs.h>
45
+ #include <js/CharacterEncoding.h>
46
+ #include <js/Class.h>
47
+ #include <js/CompilationAndEvaluation.h>
48
+ #include <js/Conversions.h>
49
+ #include <js/Equality.h>
50
+ #include <js/GCVector.h>
51
+ #include <js/Id.h>
52
+ #include <js/Object.h>
53
+ #include <js/PropertyDescriptor.h>
54
+ #include <js/SourceText.h>
55
+ #include <js/Context.h>
56
+ #include <js/ErrorReport.h>
57
+ #include <js/Exception.h>
58
+ #include <js/GCAPI.h>
59
+ #include <js/GlobalObject.h>
60
+ #include <js/PropertyAndElement.h>
61
+ #include <js/RootingAPI.h>
62
+ #include <js/String.h>
63
+ #include <js/Symbol.h>
64
+ #include <js/TracingAPI.h>
65
+ #include <js/Value.h>
66
+
67
+ #include <cstdint>
68
+ #include <memory>
69
+ #include <string>
70
+ #include <vector>
71
+
72
+ namespace gjsify_napi {
73
+
74
+ // NODE_API_SUPPORTED_VERSION_MAX / NODE_API_DEFAULT_MODULE_API_VERSION
75
+ // (refs/node/src/node_version.h:103-108).
76
+ constexpr int32_t kNapiVersionMax = 10;
77
+ constexpr int32_t kNapiDefaultModuleApiVersion = 8;
78
+
79
+ // ---- handle arena (§5a) ----
80
+ //
81
+ // `napi_value` = pointer to a `JS::Heap<JS::Value>` slot in a per-env chunked
82
+ // arena. Chunks are fixed-size heap blocks that never move or realloc, so a
83
+ // slot address stays stable for the lifetime of its handle scope — the
84
+ // property V8's handle blocks give Node's `napi_value`. `JS::Heap` provides
85
+ // the pre/post barriers a heap-stored GC pointer needs under SpiderMonkey's
86
+ // generational GC; the per-env extra-GC-roots tracer (env.cc) marks *and
87
+ // relocates* live slots, so outstanding `napi_value` pointers survive moving
88
+ // GC. NOT `JS::Rooted` (stack-only, LIFO) and NOT a raw `JS::Value` (goes
89
+ // stale across nursery evacuation) — those cannot back a C ABI.
90
+
91
+ constexpr size_t kArenaChunkSlots = 256;
92
+
93
+ struct ArenaChunk {
94
+ JS::Heap<JS::Value> slots[kArenaChunkSlots];
95
+ };
96
+
97
+ class HandleArena {
98
+ public:
99
+ // Store `value` in the next slot; returns the stable slot address.
100
+ JS::Heap<JS::Value>* push(const JS::Value& value);
101
+ size_t top() const { return top_; }
102
+ // Release slots [mark, top): clear them (write barrier via JS::Heap
103
+ // assignment) and rewind. Chunk storage is retained for reuse.
104
+ void popTo(size_t mark);
105
+ // Trace live slots [0, top) — called from the env's extra-roots tracer.
106
+ void trace(JSTracer* trc);
107
+ // Env teardown: clear every live slot and free the chunks. Must run
108
+ // before JS_DestroyContext (a live JS::Heap past that point is UAF).
109
+ void clearAll();
110
+
111
+ private:
112
+ JS::Heap<JS::Value>* slotAt(size_t index);
113
+ std::vector<std::unique_ptr<ArenaChunk>> chunks_;
114
+ size_t top_ = 0;
115
+ };
116
+
117
+ // A handle scope is a saved arena top; scopes close LIFO
118
+ // (napi_handle_scope_mismatch otherwise), matching
119
+ // js_native_api_v8.cc:2927-2953. The napi_handle_scope /
120
+ // napi_escapable_handle_scope pointer encodes the 1-based scope-stack depth
121
+ // (no heap allocation, trivially validated). Escapable scopes (cc:2955-3002)
122
+ // reserve ONE slot in the PARENT region at open (Bun's trick,
123
+ // refs/bun/src/jsc/bindings/napi_handle_scope.cpp:24-27,66-75);
124
+ // napi_escape_handle copies the escapee into that slot once.
125
+ struct HandleScopeRec {
126
+ size_t mark;
127
+ bool escapable = false;
128
+ bool escape_called = false;
129
+ // The parent-region slot (address stable — chunks never move); only
130
+ // set for escapable scopes. Lives BELOW mark, so close leaves it alive.
131
+ JS::Heap<JS::Value>* escape_slot = nullptr;
132
+ };
133
+
134
+ // ---- callbacks (§8) ----
135
+
136
+ // Heap bundle behind every napi-created function; owned by the env, freed at
137
+ // env destruction (Phase-0 simplification of V8-Node's weak-ref-on-function
138
+ // early free, js_native_api_v8.cc:398-402). Stored in reserved slot 0 of the
139
+ // js::NewFunctionWithReserved native as a JS::PrivateValue.
140
+ struct CallbackBundle {
141
+ napi_env env;
142
+ napi_callback cb;
143
+ void* data;
144
+ // napi_define_class constructor: constructing creates the instance with
145
+ // the fast-tier instance JSClass (reserved slots) instead of a plain
146
+ // object (§5d).
147
+ bool construct_as_class = false;
148
+ };
149
+
150
+ // ---- §5b references + finalizer pipeline ----
151
+
152
+ enum class ReferenceOwnership : uint8_t {
153
+ kRuntime, // the runtime deletes the reference after its finalizer ran
154
+ kUserland, // userland must delete it via napi_delete_reference
155
+ };
156
+
157
+ // Intrusive doubly-linked base for everything finalized at env teardown —
158
+ // mirror of js_native_api_v8.h:101-130 RefTracker. Env holds two sentinel
159
+ // heads: `finalizing_reflist` (user finalizer present) and `reflist`
160
+ // (plain), finalized in that order at teardown (the v8.h:121-130 comment
161
+ // explains why finalizing refs go first: their finalizers may still use
162
+ // plain refs).
163
+ class RefTracker {
164
+ public:
165
+ RefTracker() = default;
166
+ virtual ~RefTracker() { Unlink(); }
167
+ // Finalization: unlinks + (subclass) runs the user callback. May be
168
+ // called at most once; implementations must Unlink() first.
169
+ virtual void Finalize() {}
170
+ void Link(RefTracker* list_head);
171
+ void Unlink();
172
+ static void FinalizeAll(RefTracker* list_head);
173
+ // Trace hook for the env's extra-roots tracer (Reference overrides).
174
+ virtual void Trace(JSTracer* trc) {}
175
+ RefTracker* next() const { return next_; }
176
+
177
+ protected:
178
+ RefTracker* next_ = nullptr;
179
+ RefTracker* prev_ = nullptr;
180
+ };
181
+
182
+ // A queued napi_finalize callback that is not attached to a JS value:
183
+ // node_api_post_finalizer + napi_set_instance_data (§5d instance data).
184
+ // Finalize() runs the callback (JS callable, fresh handle scope) and
185
+ // deletes itself.
186
+ class TrackedFinalizer : public RefTracker {
187
+ public:
188
+ TrackedFinalizer(napi_env env, napi_finalize cb, void* data, void* hint);
189
+ ~TrackedFinalizer() override;
190
+ void Finalize() override;
191
+ void* data() const { return data_; }
192
+
193
+ private:
194
+ napi_env env_;
195
+ napi_finalize cb_;
196
+ void* data_;
197
+ void* hint_;
198
+ };
199
+
200
+ // napi_ref (§5b): strong/weak/refcounted, v10 primitive semantics. State
201
+ // machine mirrors js_native_api_v8.cc:690-757 (Ref/Unref/SetWeak):
202
+ // refcount 0→1 = leave the weak set + become traced-strong; 1→0 = SetWeak;
203
+ // SetWeak on a non-weakable value releases immediately (primitives at
204
+ // refcount 0 read back as empty — the v10 semantic better-sqlite3 relies
205
+ // on). SpiderMonkey mechanics:
206
+ // - strong state: value lives in a JS::Heap<JS::Value> traced (marks AND
207
+ // relocates) from the env's extra-roots tracer;
208
+ // - weak state: the object lives in a JS::Heap<JSObject*> that is NOT
209
+ // traced; the weak sweep callback updates it via
210
+ // JS_UpdateWeakPointerAfterGC — moved ⇒ pointer updated, dead ⇒ nulled
211
+ // (GCAPI.h contract; GJS jsapi-util-root.h update_after_gc pattern);
212
+ // - both flips stack-root the object across the transition (GJS
213
+ // switch_to_rooted/switch_to_unrooted pattern: never leave the thing in
214
+ // neither slot); JS::Heap assignments run the pre/post write barriers,
215
+ // JS::Heap::get() runs the read barrier (gray-unmarking on
216
+ // resurrection) — js/RootingAPI.h Heap<T> barrier contract.
217
+ class Reference : public RefTracker {
218
+ public:
219
+ static Reference* New(napi_env env, JS::HandleValue value,
220
+ uint32_t initial_refcount,
221
+ ReferenceOwnership ownership,
222
+ napi_finalize cb = nullptr, void* data = nullptr,
223
+ void* hint = nullptr);
224
+ ~Reference() override;
225
+
226
+ uint32_t Ref();
227
+ uint32_t Unref();
228
+ uint32_t RefCount() const { return refcount_; }
229
+ ReferenceOwnership Ownership() const { return ownership_; }
230
+ void* Data() const { return data_; }
231
+ bool HasFinalizer() const { return cb_ != nullptr; }
232
+ // false when released/dead (napi_get_reference_value → NULL).
233
+ bool GetValue(JS::MutableHandleValue out);
234
+ // Weak sweep: update weak_obj_; on death → released + dequeued from the
235
+ // weak set by the caller; returns true when the referent died.
236
+ bool UpdateAfterGC(JSTracer* trc);
237
+ void Finalize() override;
238
+ void Trace(JSTracer* trc) override;
239
+ void RemoveFromWeakSet();
240
+
241
+ napi_env env() const { return env_; }
242
+
243
+ private:
244
+ Reference(napi_env env, JS::HandleValue value, uint32_t initial_refcount,
245
+ ReferenceOwnership ownership, napi_finalize cb, void* data,
246
+ void* hint);
247
+ void SetWeakOrRelease(); // 1→0 transition (or weakable init at 0)
248
+
249
+ enum class State : uint8_t { kStrong, kWeak, kReleased };
250
+ napi_env env_;
251
+ State state_;
252
+ uint32_t refcount_;
253
+ ReferenceOwnership ownership_;
254
+ bool can_be_weak_; // value.isObject(); symbols = primitives (§5b ledger)
255
+ napi_finalize cb_;
256
+ void* data_;
257
+ void* hint_;
258
+ JS::Heap<JS::Value> strong_; // valid iff state == kStrong
259
+ JS::Heap<JSObject*> weak_obj_; // valid iff state == kWeak
260
+ };
261
+
262
+ // Finalizer pipeline (§5c): GC callbacks only QUEUE; ONE idle drain
263
+ // (finalization_scheduled guard) runs finalizers with JS callable, each
264
+ // under a fresh handle scope, exceptions logged-and-cleared between
265
+ // finalizers. A tight synchronous script never iterates the main context,
266
+ // so it observes finalizers only at env teardown (documented; matches
267
+ // Node's SetImmediate needing a loop turn). ref.cc.
268
+ void enqueue_finalizer(napi_env env, RefTracker* tracker);
269
+ void dequeue_finalizer(napi_env env, RefTracker* tracker);
270
+ void drain_finalizer_queue(napi_env env);
271
+ // The per-process weak sweep callback (JSWeakPointerZonesCallback);
272
+ // registered once at install, walks every env's weak set. ref.cc.
273
+ void weak_sweep_callback(JSTracer* trc, void* data);
274
+ // Process-wide env registry (module.cc owns the storage).
275
+ std::vector<napi_env>& env_registry();
276
+ // Ask module.cc to arm the one idle drain source (no-op if armed).
277
+ void schedule_finalizer_drain();
278
+
279
+ // Log + clear a pending exception on env->cx (shared by the finalizer
280
+ // pipeline and the tsfn/make_callback boundary — the "no JS frame below us"
281
+ // posture). No-op when nothing is pending. ref.cc.
282
+ void log_and_clear_pending_exception(napi_env env, const char* where);
283
+
284
+ // ---- §7 threadsafe functions (tsfn.cc) ----
285
+
286
+ // Env teardown step 1b: synchronously finalize every tsfn still registered on
287
+ // the env (their deferred JS-thread finalize dispatch can no longer run once
288
+ // GjsContext dispose has begun). Runs with JS fully callable.
289
+ void finalize_env_tsfns(napi_env env);
290
+
291
+ // ---- §? asynchronous work items (async_work.cc) ----
292
+
293
+ // Env teardown step 1c (§5e crash-class boundary): drain the worker pool
294
+ // deterministically — g_thread_pool_free(wait=TRUE) lets in-flight execute
295
+ // finish and JOINS every worker, so none touches the tearing-down env — then
296
+ // run every still-pending completion synchronously, JS callable (the deferred
297
+ // g_idle can no longer fire once GjsContext dispose has begun — mirror
298
+ // finalize_env_tsfns).
299
+ void drain_env_async_works(napi_env env);
300
+ // Env destruction: free every async_work the addon never deleted (Node leaks
301
+ // its equivalent — the struct is addon-owned). Runs after teardown, no JS.
302
+ void destroy_env_async_works(napi_env env);
303
+
304
+ // ---- §5d instance state: fast tier + WeakMap tier ----
305
+ //
306
+ // Instances of napi_define_class classes and napi_create_external externals
307
+ // carry ONE reserved slot: undefined or JS::PrivateValue(InstanceState*).
308
+ // The InstanceState is freed by a FOREGROUND finalize hook that frees
309
+ // memory only (legal even for nursery deaths — finalizable classes are
310
+ // nursery-allocatable in 140; JSFinalizeOp runs with JS::GCContext*,
311
+ // js/Class.h:301, and must not call GC-capable JSAPI). Foreign objects get
312
+ // the same InstanceState via a lazily-created env WeakMap (js/WeakMap.h)
313
+ // whose value is a hostdata_class carrier object. Pattern: GJS
314
+ // cwrapper.h reserved-slot storage + JSCLASS_FOREGROUND_FINALIZE like
315
+ // GJS's GObject wrapper (gi/object.cpp:3404-3407).
316
+ constexpr size_t kInstanceSlotState = 0;
317
+
318
+ struct InstanceState {
319
+ Reference* wrap = nullptr; // §5d uniform wrap lifecycle
320
+ bool has_tag = false;
321
+ napi_type_tag tag = {0, 0};
322
+ bool is_external = false;
323
+ void* external_data = nullptr;
324
+ };
325
+
326
+ extern const JSClass instance_class; // napi_define_class instances
327
+ extern const JSClass external_class; // napi_create_external
328
+ extern const JSClass hostdata_class; // WeakMap-tier value carrier
329
+
330
+ // Look up (or create) the InstanceState for any object; *out == nullptr
331
+ // when absent and !create. wrap.cc.
332
+ napi_status get_instance_state(napi_env env, JS::HandleObject obj,
333
+ bool create, InstanceState** out);
334
+
335
+ // Shared property-descriptor applier (object.cc) — the body of
336
+ // napi_define_properties, reused by napi_define_class for both the
337
+ // prototype (non-static) and constructor (napi_static) targets.
338
+ napi_status apply_property_descriptor(napi_env env, JS::HandleObject target,
339
+ const napi_property_descriptor& p);
340
+
341
+ // Property id from a descriptor's utf8name XOR name (napi_name_expected
342
+ // otherwise); name must be a string or symbol. (object.cc)
343
+ napi_status property_key_to_id(napi_env env, const char* utf8name,
344
+ napi_value name, JS::MutableHandleId id);
345
+
346
+ // Lossy UTF-8 → JSString (invalid sequences → U+FFFD, matching V8's
347
+ // NewFromUtf8; SpiderMonkey's JS_NewStringCopyUTF8N throws on malformed
348
+ // input, so this falls back to LossyUTF8CharsToNewTwoByteCharsZ). Returns
349
+ // nullptr on OOM with the exception cleared. (value.cc)
350
+ JSString* new_string_utf8_lossy(JSContext* cx, const char* str, size_t len);
351
+
352
+ // Bundle-function factory shared by napi_create_function,
353
+ // napi_define_class and the accessor/method descriptors. (function.cc)
354
+ JSObject* new_bundle_function(napi_env env, const char* utf8name,
355
+ size_t length, napi_callback cb, void* data,
356
+ unsigned flags, bool construct_as_class);
357
+
358
+ // Stack-allocated per trampoline invocation; napi_callback_info points here.
359
+ struct CallbackInfo {
360
+ napi_value* argv; // arena slots, one per provided argument
361
+ size_t argc;
362
+ napi_value this_arg; // arena slot
363
+ napi_value new_target; // nullptr when not constructing (P0.2)
364
+ void* data; // bundle->data
365
+ };
366
+
367
+ // The shared JSNative all napi-created functions dispatch through.
368
+ bool function_trampoline(JSContext* cx, unsigned argc, JS::Value* vp);
369
+
370
+ // ---- status bookkeeping (§6, error.cc) ----
371
+
372
+ napi_status set_last_error(napi_env env, napi_status status,
373
+ uint32_t engine_error_code = 0,
374
+ void* engine_reserved = nullptr);
375
+ napi_status clear_last_error(napi_env env);
376
+
377
+ // ---- value boxing (env.cc) ----
378
+
379
+ // The single choke point boxing a JSAPI value into an env arena slot.
380
+ napi_value arena_push(napi_env env, const JS::Value& value);
381
+
382
+ // Deref a napi_value back to the engine value.
383
+ inline JS::Value napi_value_to_js(napi_value value) {
384
+ return reinterpret_cast<JS::Heap<JS::Value>*>(value)->get();
385
+ }
386
+
387
+ // The env's extra-GC-roots trace hook (env.cc).
388
+ void trace_env(JSTracer* trc, void* data);
389
+
390
+ } // namespace gjsify_napi
391
+
392
+ // ---- napi_env (§5e, minimal P0.0 shape) ----
393
+ //
394
+ // Global scope: the struct tag is fixed by the vendored
395
+ // `typedef struct napi_env__* napi_env`.
396
+ struct napi_env__ {
397
+ JSContext* cx;
398
+ GjsContext* gjs_context;
399
+ // Per-addon Node-API version (§3c discovery + clamp); gates §5b/§6
400
+ // behavior from P0.1 on.
401
+ int32_t module_api_version;
402
+ gjsify_napi::HandleArena arena;
403
+ std::vector<gjsify_napi::HandleScopeRec> scopes;
404
+ napi_extended_error_info last_error = {nullptr, nullptr, 0, napi_ok};
405
+ // dlopen handle of the addon; kept for the process lifetime (Node never
406
+ // dlcloses registered addons either).
407
+ void* dl_handle = nullptr;
408
+ // file:// URL of the addon (node_api_get_module_file_name, P0.5).
409
+ std::string filename;
410
+ // false during/after teardown — NAPI_PREAMBLE gate.
411
+ bool can_call_into_js = true;
412
+ bool torn_down = false;
413
+ bool tearing_down = false;
414
+ bool draining_finalizers = false;
415
+ // Bundle ownership (§8): freed at destruction.
416
+ std::vector<gjsify_napi::CallbackBundle*> bundles;
417
+ // The addon's exports object, rooted for the exports cache. Reset in
418
+ // teardown() — a live PersistentRooted past JS_DestroyContext is UAF.
419
+ JS::PersistentRooted<JSObject*> exports;
420
+ // The realm global captured at env creation (napi_get_global — Node's
421
+ // context()->Global() analog). Reset in teardown().
422
+ JS::PersistentRooted<JSObject*> global;
423
+ // Foreign-object InstanceState WeakMap (§5d fallback tier), lazily
424
+ // created. Reset in teardown().
425
+ JS::PersistentRooted<JSObject*> wrap_map;
426
+
427
+ // §5b ref lists (sentinel heads; two-list teardown order per
428
+ // js_native_api_v8.h:121-130).
429
+ gjsify_napi::RefTracker reflist;
430
+ gjsify_napi::RefTracker finalizing_reflist;
431
+ // Weak set: references currently in the kWeak state, walked by the
432
+ // sweep callback.
433
+ std::vector<gjsify_napi::Reference*> weak_refs;
434
+ // §5c pending finalizers (FIFO), drained on idle or at teardown.
435
+ std::vector<gjsify_napi::RefTracker*> pending_finalizers;
436
+ // §5d instance data (TrackedFinalizer; overwrite deletes un-finalized).
437
+ gjsify_napi::TrackedFinalizer* instance_data = nullptr;
438
+ // §7 live threadsafe functions (JS-thread-only access: registered at
439
+ // create, unregistered by the JS-thread finalize; teardown force-
440
+ // finalizes leftovers — tsfn.cc).
441
+ std::vector<napi_threadsafe_function> tsfns;
442
+ // Live async_work items (registered at create on the JS thread, unlinked by
443
+ // the completion dispatch's self-delete; teardown drains pending
444
+ // completions, the destructor frees leftovers — async_work.cc).
445
+ std::vector<napi_async_work> async_works;
446
+ // Per-env worker pool backing napi_queue_async_work (libuv-thread-pool
447
+ // analog). Lazily created on first queue; drained + freed deterministically
448
+ // at teardown (g_thread_pool_free wait=TRUE) BEFORE any root is dropped, so
449
+ // no worker outlives the env. nullptr until the first queue — async_work.cc.
450
+ GThreadPool* async_pool = nullptr;
451
+ // Env cleanup hooks, run LIFO as teardown step 1.
452
+ struct CleanupHook {
453
+ void(NAPI_CDECL* fn)(void* arg);
454
+ void* arg;
455
+ };
456
+ std::vector<CleanupHook> cleanup_hooks;
457
+
458
+ napi_env__(JSContext* cx, GjsContext* gjs, int32_t api_version,
459
+ void* handle, std::string file);
460
+ ~napi_env__();
461
+
462
+ // §5e teardown order (the node-gi SIGSEGV lesson — full sequence):
463
+ // 1. run env cleanup hooks LIFO (JS fully available);
464
+ // 2. drain pending finalizers (finalizers may call napi and create
465
+ // values — arena, tracer and can_call_into_js MUST still be live);
466
+ // 3. RefTracker::FinalizeAll(finalizing_reflist) then (reflist);
467
+ // 4. instance-data finalizer;
468
+ // 5. flip can_call_into_js/torn_down (subsequent napi entry → status);
469
+ // 6. drop every root: remove the extra-roots tracer, reset
470
+ // PersistentRooteds, clear arena/scopes/weak set — all BEFORE
471
+ // JS_DestroyContext runs (a live root past that point is the UAF
472
+ // class). Callback bundles are NOT freed here: live function
473
+ // objects still reference them and the trampoline guards on
474
+ // torn_down; the destructor frees them at context dispose.
475
+ void teardown();
476
+ };
477
+
478
+ namespace gjsify_napi {
479
+
480
+ // ---- status check macros (mirroring js_native_api_v8.h:233-243) ----
481
+
482
+ #define GJSIFY_NAPI_CHECK_ENV(env) \
483
+ do { \
484
+ if ((env) == nullptr) { \
485
+ return napi_invalid_arg; \
486
+ } \
487
+ } while (0)
488
+
489
+ #define GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(env, condition, status) \
490
+ do { \
491
+ if (!(condition)) { \
492
+ return gjsify_napi::set_last_error((env), (status)); \
493
+ } \
494
+ } while (0)
495
+
496
+ #define GJSIFY_NAPI_CHECK_ARG(env, arg) \
497
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE((env), ((arg) != nullptr), napi_invalid_arg)
498
+
499
+ // The NAPI_PREAMBLE partition (§6): only for functions that can run JS.
500
+ // Pending exception ⇒ napi_pending_exception immediately; torn-down env ⇒
501
+ // napi_cannot_run_js (module_api_version >= 10) / napi_pending_exception
502
+ // (< 10); then clear last_error. Pure value/scope ops must NOT use this —
503
+ // they keep working while an exception is pending.
504
+ #define GJSIFY_NAPI_PREAMBLE(env) \
505
+ GJSIFY_NAPI_CHECK_ENV(env); \
506
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE( \
507
+ (env), !JS_IsExceptionPending((env)->cx), napi_pending_exception); \
508
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE( \
509
+ (env), (env)->can_call_into_js, \
510
+ ((env)->module_api_version >= 10 ? napi_cannot_run_js \
511
+ : napi_pending_exception)); \
512
+ gjsify_napi::clear_last_error(env)
513
+
514
+ } // namespace gjsify_napi
515
+
516
+ // The C bridge the Vala `GjsifyNapi.init()` body calls (module.cc).
517
+ extern "C" gboolean gjsify_napi_install(void);
518
+
519
+ #endif // GJSIFY_NAPI_SRC_CC_COMMON_H_