@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/src/cc/env.cc ADDED
@@ -0,0 +1,262 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/napi — env lifecycle, handle arena, GC tracer, handle scopes.
3
+ //
4
+ // Reference: refs/node/src/js_native_api_v8.{h,cc} (Node.js contributors,
5
+ // MIT) for the napi_env/handle-scope semantics; tracer pattern adapted from
6
+ // GJS's own context tracer (refs/gjs/gjs/context.cpp:343-353,689 —
7
+ // JS_AddExtraGCRootsTracer + JS::TraceEdge; GNOME contributors, MIT/LGPLv2+).
8
+
9
+ #include "common.h"
10
+
11
+ namespace gjsify_napi {
12
+
13
+ // ---- HandleArena ----
14
+
15
+ JS::Heap<JS::Value>* HandleArena::slotAt(size_t index) {
16
+ return &chunks_[index / kArenaChunkSlots]->slots[index % kArenaChunkSlots];
17
+ }
18
+
19
+ JS::Heap<JS::Value>* HandleArena::push(const JS::Value& value) {
20
+ if (top_ == chunks_.size() * kArenaChunkSlots) {
21
+ chunks_.push_back(std::make_unique<ArenaChunk>());
22
+ }
23
+ JS::Heap<JS::Value>* slot = slotAt(top_);
24
+ *slot = value; // JS::Heap assignment runs the post barrier
25
+ top_++;
26
+ return slot;
27
+ }
28
+
29
+ void HandleArena::popTo(size_t mark) {
30
+ for (size_t i = mark; i < top_; i++) {
31
+ // Immediate release, matching V8 handle-scope close semantics; the
32
+ // JS::Heap assignment runs the pre barrier for the vacated value.
33
+ *slotAt(i) = JS::UndefinedValue();
34
+ }
35
+ top_ = mark;
36
+ }
37
+
38
+ void HandleArena::trace(JSTracer* trc) {
39
+ for (size_t i = 0; i < top_; i++) {
40
+ // Marks AND relocates: a moving GC updates the slot in place, so
41
+ // every outstanding napi_value pointer stays valid.
42
+ JS::TraceEdge(trc, slotAt(i), "gjsify-napi arena slot");
43
+ }
44
+ }
45
+
46
+ void HandleArena::clearAll() {
47
+ popTo(0);
48
+ chunks_.clear();
49
+ }
50
+
51
+ // ---- env tracer ----
52
+
53
+ // Walk a RefTracker list, invoking the virtual Trace (Reference traces its
54
+ // strong slot; weak slots are deliberately NOT traced — the sweep callback
55
+ // owns them).
56
+ static void trace_ref_list(JSTracer* trc, const RefTracker* head) {
57
+ for (RefTracker* t = head->next(); t != nullptr; t = t->next()) {
58
+ t->Trace(trc);
59
+ }
60
+ }
61
+
62
+ void trace_env(JSTracer* trc, void* data) {
63
+ auto* env = static_cast<napi_env>(data);
64
+ env->arena.trace(trc);
65
+ trace_ref_list(trc, &env->reflist);
66
+ trace_ref_list(trc, &env->finalizing_reflist);
67
+ }
68
+
69
+ napi_value arena_push(napi_env env, const JS::Value& value) {
70
+ return reinterpret_cast<napi_value>(env->arena.push(value));
71
+ }
72
+
73
+ } // namespace gjsify_napi
74
+
75
+ // ---- napi_env lifecycle ----
76
+
77
+ napi_env__::napi_env__(JSContext* cx_in, GjsContext* gjs, int32_t api_version,
78
+ void* handle, std::string file)
79
+ : cx(cx_in),
80
+ gjs_context(gjs),
81
+ module_api_version(api_version),
82
+ dl_handle(handle),
83
+ filename(std::move(file)),
84
+ exports(cx_in),
85
+ global(cx_in, JS::CurrentGlobalOrNull(cx_in)),
86
+ // MUST be ctor-initialized with the context: a default-constructed
87
+ // PersistentRooted is NOT registered with the runtime — assigning to
88
+ // it later stores the pointer UNROOTED and the map dies at the next
89
+ // GC (found by the P0.3 gate: SetWeakMapEntry crashed in the dead
90
+ // map's zone-tracked allocator).
91
+ wrap_map(cx_in) {
92
+ JS_AddExtraGCRootsTracer(cx, gjsify_napi::trace_env, this);
93
+ }
94
+
95
+ napi_env__::~napi_env__() {
96
+ teardown();
97
+ // Free any async_work the addon never deleted (its completion already ran,
98
+ // or it was created-but-never-queued). Self-deleted works are already
99
+ // unlinked, so this cannot double-free. Runs at context dispose, no JS.
100
+ gjsify_napi::destroy_env_async_works(this);
101
+ // Bundle memory is freed HERE, not in teardown(): function objects
102
+ // created by this env keep the bundle pointer in their reserved slot,
103
+ // and JS may still call them after an early teardown — the trampoline
104
+ // bails on env->torn_down, dereferencing only the still-live bundle.
105
+ // The destructor runs at context dispose, when no JS runs anymore.
106
+ for (gjsify_napi::CallbackBundle* bundle : bundles) {
107
+ delete bundle;
108
+ }
109
+ bundles.clear();
110
+ }
111
+
112
+ void napi_env__::teardown() {
113
+ // §5e order — the node-gi SIGSEGV lesson. Steps 1-4 run with JS fully
114
+ // callable (finalizers may call napi and create values, so the arena,
115
+ // tracer and can_call_into_js MUST stay live until step 5).
116
+ if (tearing_down || torn_down) {
117
+ return;
118
+ }
119
+ tearing_down = true;
120
+
121
+ // 1. env cleanup hooks, LIFO (napi_add_env_cleanup_hook — the
122
+ // better-sqlite3 Addon::Cleanup slot).
123
+ while (!cleanup_hooks.empty()) {
124
+ CleanupHook hook = cleanup_hooks.back();
125
+ cleanup_hooks.pop_back();
126
+ hook.fn(hook.arg);
127
+ }
128
+
129
+ // 1b. finalize threadsafe functions (§7): a release/abort issued by a
130
+ // cleanup hook above scheduled a JS-thread finalize dispatch that can
131
+ // never run — the main context stops iterating once GjsContext
132
+ // dispose began — so it is executed synchronously here, while JS is
133
+ // still fully callable. Never-released tsfns are force-closed too.
134
+ gjsify_napi::finalize_env_tsfns(this);
135
+
136
+ // 1c. drain pending async_work completions (§?): a work queued in the last
137
+ // tick before dispose has a g_idle completion that can no longer fire —
138
+ // run it here, JS still callable, exactly like the tsfn finalize above.
139
+ gjsify_napi::drain_env_async_works(this);
140
+
141
+ // 2. drain finalizers already queued by earlier GC deaths.
142
+ gjsify_napi::drain_finalizer_queue(this);
143
+
144
+ // 3. finalize the ref lists: finalizing refs FIRST (their finalizers
145
+ // may still delete plain refs — js_native_api_v8.h:121-130), then
146
+ // plain refs. Instance data is linked in finalizing_reflist like
147
+ // Node's TrackedFinalizer, so it finalizes here too. Deaths queued
148
+ // by GC during these callbacks are drained afterwards.
149
+ gjsify_napi::RefTracker::FinalizeAll(&finalizing_reflist);
150
+ gjsify_napi::RefTracker::FinalizeAll(&reflist);
151
+ gjsify_napi::drain_finalizer_queue(this);
152
+
153
+ // 4. (instance data handled in step 3 — see TrackedFinalizer::Finalize.)
154
+
155
+ // 5. block further calls into JS through this env.
156
+ can_call_into_js = false;
157
+ torn_down = true;
158
+
159
+ // 6. drop every root this env owns — all before JS_DestroyContext;
160
+ // a live PersistentRooted/JS::Heap past that point is the UAF class.
161
+ // (GJS removes its own tracer the same way before destroying the
162
+ // context, refs/gjs/gjs/context.cpp:457.)
163
+ JS_RemoveExtraGCRootsTracer(cx, gjsify_napi::trace_env, this);
164
+ exports.reset();
165
+ global.reset();
166
+ wrap_map.reset();
167
+ arena.clearAll();
168
+ scopes.clear();
169
+ weak_refs.clear();
170
+ pending_finalizers.clear();
171
+ }
172
+
173
+ // ---- handle scopes (js_native_api_v8.cc:2927-2953 semantics) ----
174
+
175
+ napi_status NAPI_CDECL napi_open_handle_scope(napi_env env,
176
+ napi_handle_scope* result) {
177
+ GJSIFY_NAPI_CHECK_ENV(env);
178
+ GJSIFY_NAPI_CHECK_ARG(env, result);
179
+ env->scopes.push_back({env->arena.top()});
180
+ // Encode the 1-based depth: no allocation, close validates LIFO order.
181
+ *result = reinterpret_cast<napi_handle_scope>(
182
+ static_cast<uintptr_t>(env->scopes.size()));
183
+ return gjsify_napi::clear_last_error(env);
184
+ }
185
+
186
+ namespace gjsify_napi {
187
+
188
+ // Shared close path: LIFO validation + escapable-type matching (stricter
189
+ // than Node's count-only check — a type-confused close is caught, correct
190
+ // matched-pair usage is unaffected).
191
+ static napi_status close_scope(napi_env env, uintptr_t depth,
192
+ bool expect_escapable) {
193
+ if (env->scopes.empty() || depth != env->scopes.size() ||
194
+ env->scopes.back().escapable != expect_escapable) {
195
+ return set_last_error(env, napi_handle_scope_mismatch);
196
+ }
197
+ env->arena.popTo(env->scopes.back().mark);
198
+ env->scopes.pop_back();
199
+ return clear_last_error(env);
200
+ }
201
+
202
+ } // namespace gjsify_napi
203
+
204
+ napi_status NAPI_CDECL napi_close_handle_scope(napi_env env,
205
+ napi_handle_scope scope) {
206
+ GJSIFY_NAPI_CHECK_ENV(env);
207
+ GJSIFY_NAPI_CHECK_ARG(env, scope);
208
+ return gjsify_napi::close_scope(env, reinterpret_cast<uintptr_t>(scope),
209
+ /* expect_escapable = */ false);
210
+ }
211
+
212
+ // ---- escapable handle scopes (js_native_api_v8.cc:2955-3002 semantics) ----
213
+
214
+ napi_status NAPI_CDECL napi_open_escapable_handle_scope(
215
+ napi_env env, napi_escapable_handle_scope* result) {
216
+ GJSIFY_NAPI_CHECK_ENV(env);
217
+ GJSIFY_NAPI_CHECK_ARG(env, result);
218
+ // Reserve the escape slot in the PARENT region first; the scope's mark
219
+ // then sits ABOVE it, so closing releases everything but the slot.
220
+ JS::Heap<JS::Value>* escape_slot = env->arena.push(JS::UndefinedValue());
221
+ gjsify_napi::HandleScopeRec rec;
222
+ rec.mark = env->arena.top();
223
+ rec.escapable = true;
224
+ rec.escape_slot = escape_slot;
225
+ env->scopes.push_back(rec);
226
+ *result = reinterpret_cast<napi_escapable_handle_scope>(
227
+ static_cast<uintptr_t>(env->scopes.size()));
228
+ return gjsify_napi::clear_last_error(env);
229
+ }
230
+
231
+ napi_status NAPI_CDECL napi_close_escapable_handle_scope(
232
+ napi_env env, napi_escapable_handle_scope scope) {
233
+ GJSIFY_NAPI_CHECK_ENV(env);
234
+ GJSIFY_NAPI_CHECK_ARG(env, scope);
235
+ return gjsify_napi::close_scope(env, reinterpret_cast<uintptr_t>(scope),
236
+ /* expect_escapable = */ true);
237
+ }
238
+
239
+ napi_status NAPI_CDECL napi_escape_handle(napi_env env,
240
+ napi_escapable_handle_scope scope,
241
+ napi_value escapee,
242
+ napi_value* result) {
243
+ GJSIFY_NAPI_CHECK_ENV(env);
244
+ GJSIFY_NAPI_CHECK_ARG(env, scope);
245
+ GJSIFY_NAPI_CHECK_ARG(env, escapee);
246
+ GJSIFY_NAPI_CHECK_ARG(env, result);
247
+ const size_t depth = reinterpret_cast<uintptr_t>(scope);
248
+ if (depth == 0 || depth > env->scopes.size()) {
249
+ return gjsify_napi::set_last_error(env, napi_handle_scope_mismatch);
250
+ }
251
+ gjsify_napi::HandleScopeRec& rec = env->scopes[depth - 1];
252
+ if (!rec.escapable) {
253
+ return gjsify_napi::set_last_error(env, napi_handle_scope_mismatch);
254
+ }
255
+ if (rec.escape_called) {
256
+ return gjsify_napi::set_last_error(env, napi_escape_called_twice);
257
+ }
258
+ *rec.escape_slot = gjsify_napi::napi_value_to_js(escapee);
259
+ rec.escape_called = true;
260
+ *result = reinterpret_cast<napi_value>(rec.escape_slot);
261
+ return gjsify_napi::clear_last_error(env);
262
+ }
@@ -0,0 +1,300 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/napi — napi_status bookkeeping + exception mapping (§6).
3
+ //
4
+ // Design: SpiderMonkey's own pending-exception slot IS the env exception
5
+ // state — no env-side mirror, no TryCatch dance. V8-Node needs the mirror
6
+ // because v8::TryCatch swallows the engine state at each napi boundary and
7
+ // rethrows at CallIntoModule exit; on SM a pending exception set on the
8
+ // JSContext simply stays there across C calls until cleared.
9
+ //
10
+ // Reference: refs/node/src/js_native_api_v8.cc (Node.js contributors, MIT) —
11
+ // error_messages table (cc:889-914, copied verbatim), napi_get_last_error_info
12
+ // (cc:916-939), napi_throw (cc:2219-2229), napi_is_exception_pending
13
+ // (cc:3064-3072), napi_get_and_clear_last_exception (cc:3074-3090).
14
+
15
+ #include "common.h"
16
+
17
+ #include <cstdio>
18
+ #include <cstring>
19
+
20
+ namespace gjsify_napi {
21
+
22
+ napi_status set_last_error(napi_env env, napi_status status,
23
+ uint32_t engine_error_code, void* engine_reserved) {
24
+ env->last_error.error_code = status;
25
+ env->last_error.engine_error_code = engine_error_code;
26
+ env->last_error.engine_reserved = engine_reserved;
27
+ env->last_error.error_message = nullptr; // filled lazily on request
28
+ return status;
29
+ }
30
+
31
+ napi_status clear_last_error(napi_env env) {
32
+ return set_last_error(env, napi_ok);
33
+ }
34
+
35
+ } // namespace gjsify_napi
36
+
37
+ // Copied verbatim from refs/node/src/js_native_api_v8.cc (error_messages).
38
+ // Warning: keep in-sync with the napi_status enum.
39
+ static const char* error_messages[] = {
40
+ nullptr,
41
+ "Invalid argument",
42
+ "An object was expected",
43
+ "A string was expected",
44
+ "A string or symbol was expected",
45
+ "A function was expected",
46
+ "A number was expected",
47
+ "A boolean was expected",
48
+ "An array was expected",
49
+ "Unknown failure",
50
+ "An exception is pending",
51
+ "The async work item was cancelled",
52
+ "napi_escape_handle already called on scope",
53
+ "Invalid handle scope usage",
54
+ "Invalid callback scope usage",
55
+ "Thread-safe function queue is full",
56
+ "Thread-safe function handle is closing",
57
+ "A bigint was expected",
58
+ "A date was expected",
59
+ "An arraybuffer was expected",
60
+ "A detachable arraybuffer was expected",
61
+ "Main thread would deadlock",
62
+ "External buffers are not allowed",
63
+ "Cannot run JavaScript",
64
+ };
65
+
66
+ napi_status NAPI_CDECL napi_get_last_error_info(
67
+ node_api_basic_env basic_env, const napi_extended_error_info** result) {
68
+ napi_env env = const_cast<napi_env>(basic_env);
69
+ GJSIFY_NAPI_CHECK_ENV(env);
70
+ GJSIFY_NAPI_CHECK_ARG(env, result);
71
+
72
+ // Must reference the LAST message in napi_status (no napi_status_last in
73
+ // the ABI — mirror of js_native_api_v8.cc:925-930).
74
+ constexpr int last_status = napi_cannot_run_js;
75
+ static_assert(sizeof(error_messages) / sizeof(*error_messages) ==
76
+ last_status + 1,
77
+ "Count of error messages must match count of error values");
78
+
79
+ const napi_status code = env->last_error.error_code;
80
+ env->last_error.error_message =
81
+ (code >= 0 && code <= last_status) ? error_messages[code] : nullptr;
82
+ if (code == napi_ok) {
83
+ gjsify_napi::clear_last_error(env);
84
+ }
85
+ *result = &env->last_error;
86
+ return napi_ok;
87
+ }
88
+
89
+ napi_status NAPI_CDECL napi_throw(napi_env env, napi_value error) {
90
+ GJSIFY_NAPI_PREAMBLE(env);
91
+ GJSIFY_NAPI_CHECK_ARG(env, error);
92
+ // ANY value may be thrown, including null/undefined (the contract
93
+ // better-sqlite3 depends on — never normalize, never abort).
94
+ JS::RootedValue v(env->cx, gjsify_napi::napi_value_to_js(error));
95
+ JS_SetPendingException(env->cx, v); // ExceptionStackBehavior::Capture
96
+ return napi_ok; // preamble already cleared last_error
97
+ }
98
+
99
+ napi_status NAPI_CDECL napi_is_exception_pending(napi_env env, bool* result) {
100
+ // No preamble — must work WHILE an exception is pending.
101
+ GJSIFY_NAPI_CHECK_ENV(env);
102
+ GJSIFY_NAPI_CHECK_ARG(env, result);
103
+ *result = JS_IsExceptionPending(env->cx);
104
+ return gjsify_napi::clear_last_error(env);
105
+ }
106
+
107
+ napi_status NAPI_CDECL napi_get_and_clear_last_exception(napi_env env,
108
+ napi_value* result) {
109
+ // No preamble — must work WHILE an exception is pending.
110
+ GJSIFY_NAPI_CHECK_ENV(env);
111
+ GJSIFY_NAPI_CHECK_ARG(env, result);
112
+ if (!JS_IsExceptionPending(env->cx)) {
113
+ *result = gjsify_napi::arena_push(env, JS::UndefinedValue());
114
+ } else {
115
+ JS::RootedValue exc(env->cx);
116
+ JS_GetPendingException(env->cx, &exc);
117
+ JS_ClearPendingException(env->cx);
118
+ *result = gjsify_napi::arena_push(env, exc);
119
+ }
120
+ return gjsify_napi::clear_last_error(env);
121
+ }
122
+
123
+ // ---- error construction (§6) ----
124
+ //
125
+ // The create_* family has NO preamble and must work WHILE an exception is
126
+ // pending (better-sqlite3 builds SqliteErrors on error paths). JS::CreateError
127
+ // (js/ErrorReport.h:553) constructs the error object directly — no JS runs,
128
+ // so pending state is never disturbed.
129
+
130
+ namespace gjsify_napi {
131
+
132
+ static napi_status create_error_object(napi_env env, JSExnType type,
133
+ napi_value code, napi_value msg,
134
+ napi_value* result) {
135
+ GJSIFY_NAPI_CHECK_ENV(env);
136
+ GJSIFY_NAPI_CHECK_ARG(env, msg);
137
+ GJSIFY_NAPI_CHECK_ARG(env, result);
138
+ JS::Value msg_v = napi_value_to_js(msg);
139
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(env, msg_v.isString(),
140
+ napi_string_expected);
141
+ // `code` must be a string when given (set_error_code, cc:2231-2260).
142
+ JS::Value code_v = JS::UndefinedValue();
143
+ if (code != nullptr) {
144
+ code_v = napi_value_to_js(code);
145
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(env, code_v.isString(),
146
+ napi_string_expected);
147
+ }
148
+
149
+ JS::RootedString msg_str(env->cx, msg_v.toString());
150
+ JS::RootedObject stack(env->cx);
151
+ // fileName must be a real (empty) string — ErrorObject::create
152
+ // dereferences it (null handle segfaults, mozjs-140 finding).
153
+ JS::RootedString file_name(env->cx, JS_GetEmptyString(env->cx));
154
+ JS::Rooted<mozilla::Maybe<JS::Value>> cause(env->cx, mozilla::Nothing());
155
+ JS::RootedValue error(env->cx);
156
+ if (!JS::CreateError(env->cx, type, stack, file_name, 0,
157
+ JS::ColumnNumberOneOrigin(), nullptr, msg_str, cause,
158
+ &error)) {
159
+ // OOM: don't clobber a pending exception the caller may hold.
160
+ return set_last_error(env, napi_generic_failure);
161
+ }
162
+ if (code != nullptr) {
163
+ JS::RootedObject error_obj(env->cx, &error.toObject());
164
+ JS::RootedValue code_rooted(env->cx, code_v);
165
+ // Plain data property on a fresh error object — no JS runs
166
+ // (assignment-equivalent attributes: enumerable+writable+configurable).
167
+ if (!JS_DefineProperty(env->cx, error_obj, "code", code_rooted,
168
+ JSPROP_ENUMERATE)) {
169
+ return set_last_error(env, napi_generic_failure);
170
+ }
171
+ }
172
+ *result = arena_push(env, error);
173
+ return clear_last_error(env);
174
+ }
175
+
176
+ // throw_* helpers build the message/code from C strings (lossy UTF-8) and
177
+ // set the pending exception; PREAMBLE applies (throwing over a pending
178
+ // exception is an error, matching Node).
179
+ static napi_status throw_error_with_code(napi_env env, JSExnType type,
180
+ const char* code, const char* msg) {
181
+ GJSIFY_NAPI_PREAMBLE(env);
182
+ GJSIFY_NAPI_CHECK_ARG(env, msg);
183
+ JSString* msg_str = new_string_utf8_lossy(env->cx, msg, strlen(msg));
184
+ if (msg_str == nullptr) {
185
+ return set_last_error(env, napi_generic_failure);
186
+ }
187
+ napi_value msg_v = arena_push(env, JS::StringValue(msg_str));
188
+ napi_value code_v = nullptr;
189
+ if (code != nullptr) {
190
+ JSString* code_str = new_string_utf8_lossy(env->cx, code, strlen(code));
191
+ if (code_str == nullptr) {
192
+ return set_last_error(env, napi_generic_failure);
193
+ }
194
+ code_v = arena_push(env, JS::StringValue(code_str));
195
+ }
196
+ napi_value error = nullptr;
197
+ napi_status status = create_error_object(env, type, code_v, msg_v, &error);
198
+ if (status != napi_ok) {
199
+ return status;
200
+ }
201
+ JS::RootedValue error_v(env->cx, napi_value_to_js(error));
202
+ JS_SetPendingException(env->cx, error_v);
203
+ return napi_ok;
204
+ }
205
+
206
+ } // namespace gjsify_napi
207
+
208
+ napi_status NAPI_CDECL napi_create_error(napi_env env, napi_value code,
209
+ napi_value msg, napi_value* result) {
210
+ return gjsify_napi::create_error_object(env, JSEXN_ERR, code, msg, result);
211
+ }
212
+
213
+ napi_status NAPI_CDECL napi_create_type_error(napi_env env, napi_value code,
214
+ napi_value msg,
215
+ napi_value* result) {
216
+ return gjsify_napi::create_error_object(env, JSEXN_TYPEERR, code, msg,
217
+ result);
218
+ }
219
+
220
+ napi_status NAPI_CDECL napi_create_range_error(napi_env env, napi_value code,
221
+ napi_value msg,
222
+ napi_value* result) {
223
+ return gjsify_napi::create_error_object(env, JSEXN_RANGEERR, code, msg,
224
+ result);
225
+ }
226
+
227
+ napi_status NAPI_CDECL node_api_create_syntax_error(napi_env env,
228
+ napi_value code,
229
+ napi_value msg,
230
+ napi_value* result) {
231
+ return gjsify_napi::create_error_object(env, JSEXN_SYNTAXERR, code, msg,
232
+ result);
233
+ }
234
+
235
+ napi_status NAPI_CDECL napi_throw_error(napi_env env, const char* code,
236
+ const char* msg) {
237
+ return gjsify_napi::throw_error_with_code(env, JSEXN_ERR, code, msg);
238
+ }
239
+
240
+ napi_status NAPI_CDECL napi_throw_type_error(napi_env env, const char* code,
241
+ const char* msg) {
242
+ return gjsify_napi::throw_error_with_code(env, JSEXN_TYPEERR, code, msg);
243
+ }
244
+
245
+ napi_status NAPI_CDECL napi_throw_range_error(napi_env env, const char* code,
246
+ const char* msg) {
247
+ return gjsify_napi::throw_error_with_code(env, JSEXN_RANGEERR, code, msg);
248
+ }
249
+
250
+ napi_status NAPI_CDECL node_api_throw_syntax_error(napi_env env,
251
+ const char* code,
252
+ const char* msg) {
253
+ return gjsify_napi::throw_error_with_code(env, JSEXN_SYNTAXERR, code, msg);
254
+ }
255
+
256
+ napi_status NAPI_CDECL napi_is_error(napi_env env, napi_value value,
257
+ bool* result) {
258
+ // No preamble — must work while an exception is pending.
259
+ GJSIFY_NAPI_CHECK_ENV(env);
260
+ GJSIFY_NAPI_CHECK_ARG(env, value);
261
+ GJSIFY_NAPI_CHECK_ARG(env, result);
262
+ JS::Value v = gjsify_napi::napi_value_to_js(value);
263
+ if (!v.isObject()) {
264
+ *result = false;
265
+ return gjsify_napi::clear_last_error(env);
266
+ }
267
+ JS::RootedObject obj(env->cx, &v.toObject());
268
+ js::ESClass cls = js::ESClass::Other;
269
+ if (!JS::GetBuiltinClass(env->cx, obj, &cls)) {
270
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
271
+ }
272
+ *result = (cls == js::ESClass::Error); // IsNativeError analog
273
+ return gjsify_napi::clear_last_error(env);
274
+ }
275
+
276
+ napi_status NAPI_CDECL napi_fatal_exception(napi_env env, napi_value err) {
277
+ GJSIFY_NAPI_CHECK_ENV(env);
278
+ GJSIFY_NAPI_CHECK_ARG(env, err);
279
+ // Node triggers 'uncaughtException' (default: process exit). We have no
280
+ // process-level handler chain in a GJS host — report loudly to stderr
281
+ // and keep the host alive (documented deviation; a GUI app should not
282
+ // die from an addon's background error).
283
+ JS::RootedValue v(env->cx, gjsify_napi::napi_value_to_js(err));
284
+ JSString* str = JS::ToString(env->cx, v);
285
+ if (str != nullptr) {
286
+ JS::UniqueChars utf8 = JS_EncodeStringToUTF8(
287
+ env->cx, JS::RootedString(env->cx, str));
288
+ fprintf(stderr, "[gjsify-napi] napi_fatal_exception: %s\n",
289
+ utf8 ? utf8.get() : "<unprintable>");
290
+ } else {
291
+ JS_ClearPendingException(env->cx); // ToString threw — swallow
292
+ fprintf(stderr,
293
+ "[gjsify-napi] napi_fatal_exception: <unstringifiable>\n");
294
+ }
295
+ fflush(stderr);
296
+ return gjsify_napi::clear_last_error(env);
297
+ }
298
+
299
+ // The not-yet-implemented remainder of the ABI lives in stubs.cc (loud
300
+ // napi_generic_failure + last_error for every declared symbol).