@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,772 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/napi — buffers, arraybuffers, typed arrays, dataviews, promises,
3
+ // dates, bigint uint64 (P0.4).
4
+ //
5
+ // Reference: refs/node/src/{js_native_api_v8.cc,node_api.cc} (Node.js
6
+ // contributors, MIT) for per-function argument checks + status codes; realized
7
+ // over JSAPI.
8
+ //
9
+ // §5f — THE moving-GC data-pointer trap (the "works in the demo, corrupts in
10
+ // production" bug): SpiderMonkey stores SMALL ArrayBuffer contents INLINE in
11
+ // the movable GC cell, so a naive data pointer silently goes stale when a GC
12
+ // relocates the owning object — while the buffer is still alive. V8 never does
13
+ // this (out-of-line backing stores), so addons assume stability. Two
14
+ // mitigations, applied here:
15
+ // 1. Buffers WE create (napi_create_buffer(_copy), napi_create_arraybuffer)
16
+ // always get malloc'd, OUT-OF-LINE contents via
17
+ // JS::NewArrayBufferWithContents — inherently stable across GC.
18
+ // 2. For FOREIGN views (created in JS, possibly inline), every
19
+ // napi_get_*_info pins the contents out-of-line with
20
+ // JS::EnsureNonInlineArrayBufferOrView (js/experimental/TypedData.h:308,
21
+ // confirmed present in mozjs-140) BEFORE handing out any pointer.
22
+ // Zero-copy external buffers ride JS::NewExternalArrayBuffer; the SM free
23
+ // callback must not call GC-capable JSAPI (js/ArrayBuffer.h), so the user's
24
+ // napi_finalize runs later on the loop via the §5b/§5c Reference death path
25
+ // (Node runs Buffer finalizers on the loop too, not in the GC free callback).
26
+
27
+ #include "common.h"
28
+
29
+ #include <js/ArrayBuffer.h>
30
+ #include <js/Date.h>
31
+ #include <js/Promise.h>
32
+ #include <js/ScalarType.h>
33
+ #include <js/experimental/TypedData.h>
34
+
35
+ #include <mozilla/UniquePtr.h>
36
+
37
+ #include <cstring>
38
+ #include <utility>
39
+
40
+ namespace gjsify_napi {
41
+
42
+ // ---- shared helpers ----
43
+
44
+ // Re-parent a freshly created Uint8Array to globalThis.Buffer.prototype when
45
+ // gjsify's Buffer (a Uint8Array subclass) is installed, so JS-side
46
+ // Buffer.isBuffer holds on the result. No-op (plain Uint8Array) when Buffer is
47
+ // absent. Never fails the caller — a hiccup just leaves the plain prototype.
48
+ static void adopt_buffer_prototype(napi_env env, JS::HandleObject view) {
49
+ if (env->global == nullptr) {
50
+ return;
51
+ }
52
+ JS::RootedObject g(env->cx, env->global);
53
+ JS::RootedValue buffer_ctor(env->cx);
54
+ if (!JS_GetProperty(env->cx, g, "Buffer", &buffer_ctor)) {
55
+ JS_ClearPendingException(env->cx);
56
+ return;
57
+ }
58
+ if (!buffer_ctor.isObject()) {
59
+ return;
60
+ }
61
+ JS::RootedObject ctor(env->cx, &buffer_ctor.toObject());
62
+ JS::RootedValue proto_v(env->cx);
63
+ if (!JS_GetProperty(env->cx, ctor, "prototype", &proto_v)) {
64
+ JS_ClearPendingException(env->cx);
65
+ return;
66
+ }
67
+ if (!proto_v.isObject()) {
68
+ return;
69
+ }
70
+ JS::RootedObject proto(env->cx, &proto_v.toObject());
71
+ if (!JS_SetPrototype(env->cx, view, proto)) {
72
+ JS_ClearPendingException(env->cx);
73
+ }
74
+ }
75
+
76
+ // Create an ArrayBuffer whose contents are malloc'd and OUT-OF-LINE (§5f
77
+ // stability), zero-filled (v8::ArrayBuffer::New parity). Returns nullptr on
78
+ // OOM (caller sets last_error). *out_data receives the stable backing pointer
79
+ // (nullptr for a zero-length buffer). The explicit-void*+CallerMustFreeMemory
80
+ // overload makes ownership unambiguous: transferred iff a buffer is returned.
81
+ static JSObject* new_stable_arraybuffer(napi_env env, size_t length,
82
+ void** out_data) {
83
+ void* contents = nullptr;
84
+ if (length > 0) {
85
+ contents = js_calloc(length);
86
+ if (contents == nullptr) {
87
+ return nullptr;
88
+ }
89
+ }
90
+ JSObject* ab = JS::NewArrayBufferWithContents(
91
+ env->cx, length, contents,
92
+ JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory);
93
+ if (ab == nullptr) {
94
+ js_free(contents); // ownership NOT transferred on failure
95
+ return nullptr;
96
+ }
97
+ if (out_data != nullptr) {
98
+ *out_data = contents;
99
+ }
100
+ return ab;
101
+ }
102
+
103
+ // Wrap a (rooted) stable-contents ArrayBuffer as a Node Buffer: a full-length
104
+ // Uint8Array view with globalThis.Buffer.prototype.
105
+ static napi_status finish_buffer(napi_env env, JS::HandleObject ab,
106
+ void* backing, void** out_data,
107
+ napi_value* result) {
108
+ JSObject* view = JS_NewUint8ArrayWithBuffer(env->cx, ab, 0, -1);
109
+ if (view == nullptr) {
110
+ return set_last_error(env, napi_generic_failure);
111
+ }
112
+ JS::RootedObject view_r(env->cx, view);
113
+ adopt_buffer_prototype(env, view_r);
114
+ if (out_data != nullptr) {
115
+ *out_data = backing;
116
+ }
117
+ *result = arena_push(env, JS::ObjectValue(*view_r));
118
+ return napi_ok;
119
+ }
120
+
121
+ // The external-buffer free callback: MUST NOT call GC-capable JSAPI and may run
122
+ // on any thread (js/ArrayBuffer.h). It frees NOTHING — the user's data is owned
123
+ // by the user and released by their napi_finalize on the loop (see the file
124
+ // header). SM merely drops its reference to the contents when the buffer dies.
125
+ static void external_buffer_noop_free(void* /* contents */, void* /* user */) {}
126
+
127
+ // JS::Scalar::Type → napi_typedarray_type (js_native_api_v8.cc get-info order).
128
+ static napi_typedarray_type scalar_to_napi(JS::Scalar::Type type) {
129
+ switch (type) {
130
+ case JS::Scalar::Int8:
131
+ return napi_int8_array;
132
+ case JS::Scalar::Uint8:
133
+ return napi_uint8_array;
134
+ case JS::Scalar::Uint8Clamped:
135
+ return napi_uint8_clamped_array;
136
+ case JS::Scalar::Int16:
137
+ return napi_int16_array;
138
+ case JS::Scalar::Uint16:
139
+ return napi_uint16_array;
140
+ case JS::Scalar::Int32:
141
+ return napi_int32_array;
142
+ case JS::Scalar::Uint32:
143
+ return napi_uint32_array;
144
+ case JS::Scalar::Float32:
145
+ return napi_float32_array;
146
+ case JS::Scalar::Float64:
147
+ return napi_float64_array;
148
+ case JS::Scalar::BigInt64:
149
+ return napi_bigint64_array;
150
+ case JS::Scalar::BigUint64:
151
+ return napi_biguint64_array;
152
+ case JS::Scalar::Float16:
153
+ return napi_float16_array;
154
+ default:
155
+ return napi_uint8_array; // unreachable for a real typed array
156
+ }
157
+ }
158
+
159
+ } // namespace gjsify_napi
160
+
161
+ // ---- node::Buffer slice (node_api.cc:1055-1160) ----
162
+
163
+ napi_status NAPI_CDECL napi_create_buffer(napi_env env, size_t length,
164
+ void** data, napi_value* result) {
165
+ GJSIFY_NAPI_PREAMBLE(env);
166
+ GJSIFY_NAPI_CHECK_ARG(env, result);
167
+ void* backing = nullptr;
168
+ JS::RootedObject ab(env->cx,
169
+ gjsify_napi::new_stable_arraybuffer(env, length,
170
+ &backing));
171
+ if (!ab) {
172
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
173
+ }
174
+ return gjsify_napi::finish_buffer(env, ab, backing, data, result);
175
+ }
176
+
177
+ napi_status NAPI_CDECL napi_create_buffer_copy(napi_env env, size_t length,
178
+ const void* data,
179
+ void** result_data,
180
+ napi_value* result) {
181
+ GJSIFY_NAPI_PREAMBLE(env);
182
+ GJSIFY_NAPI_CHECK_ARG(env, result);
183
+ void* backing = nullptr;
184
+ JS::RootedObject ab(env->cx,
185
+ gjsify_napi::new_stable_arraybuffer(env, length,
186
+ &backing));
187
+ if (!ab) {
188
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
189
+ }
190
+ if (length > 0 && data != nullptr) {
191
+ memcpy(backing, data, length); // independent copy (own backing)
192
+ }
193
+ return gjsify_napi::finish_buffer(env, ab, backing, result_data, result);
194
+ }
195
+
196
+ napi_status NAPI_CDECL napi_create_external_buffer(
197
+ napi_env env, size_t length, void* data,
198
+ node_api_basic_finalize basic_finalize_cb, void* finalize_hint,
199
+ napi_value* result) {
200
+ GJSIFY_NAPI_PREAMBLE(env);
201
+ GJSIFY_NAPI_CHECK_ARG(env, result);
202
+ auto finalize_cb = reinterpret_cast<napi_finalize>(basic_finalize_cb);
203
+ // Zero-copy external ArrayBuffer over the user's data. The SM deleter frees
204
+ // nothing (external_buffer_noop_free) — the user's finalize_cb owns the
205
+ // data and runs on the loop via the Reference death path below.
206
+ mozilla::UniquePtr<void, JS::BufferContentsDeleter> contents(
207
+ data, JS::BufferContentsDeleter(
208
+ gjsify_napi::external_buffer_noop_free, nullptr));
209
+ JS::RootedObject ab(
210
+ env->cx,
211
+ JS::NewExternalArrayBuffer(env->cx, length, std::move(contents)));
212
+ if (!ab) {
213
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
214
+ }
215
+ JSObject* view = JS_NewUint8ArrayWithBuffer(env->cx, ab, 0, -1);
216
+ if (view == nullptr) {
217
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
218
+ }
219
+ JS::RootedObject view_r(env->cx, view);
220
+ gjsify_napi::adopt_buffer_prototype(env, view_r);
221
+ if (finalize_cb != nullptr) {
222
+ // kRuntime weak refcount-0 ReferenceWithFinalizer on the Buffer object
223
+ // — the uniform §5b weak-sweep + §5c drain runs the user callback on
224
+ // the loop when the buffer becomes unreachable.
225
+ JS::RootedValue view_v(env->cx, JS::ObjectValue(*view_r));
226
+ gjsify_napi::Reference::New(env, view_v, 0,
227
+ gjsify_napi::ReferenceOwnership::kRuntime,
228
+ finalize_cb, data, finalize_hint);
229
+ }
230
+ *result = gjsify_napi::arena_push(env, JS::ObjectValue(*view_r));
231
+ return napi_ok;
232
+ }
233
+
234
+ napi_status NAPI_CDECL napi_is_buffer(napi_env env, napi_value value,
235
+ bool* result) {
236
+ GJSIFY_NAPI_CHECK_ENV(env);
237
+ GJSIFY_NAPI_CHECK_ARG(env, value);
238
+ GJSIFY_NAPI_CHECK_ARG(env, result);
239
+ JS::Value v = gjsify_napi::napi_value_to_js(value);
240
+ // node::Buffer::HasInstance == IsUint8Array (gjsify's Buffer is a Uint8Array
241
+ // subclass, so both real Buffers and plain Uint8Arrays qualify, matching
242
+ // Node). GetArrayBufferViewType's IsArrayBufferView precondition is met by
243
+ // the short-circuited JS_IsTypedArrayObject check.
244
+ *result = v.isObject() && JS_IsTypedArrayObject(&v.toObject()) &&
245
+ JS_GetArrayBufferViewType(&v.toObject()) == JS::Scalar::Uint8;
246
+ return gjsify_napi::clear_last_error(env);
247
+ }
248
+
249
+ napi_status NAPI_CDECL napi_get_buffer_info(napi_env env, napi_value value,
250
+ void** data, size_t* length) {
251
+ GJSIFY_NAPI_CHECK_ENV(env);
252
+ GJSIFY_NAPI_CHECK_ARG(env, value);
253
+ JS::Value v = gjsify_napi::napi_value_to_js(value);
254
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(
255
+ env,
256
+ v.isObject() && JS_IsTypedArrayObject(&v.toObject()) &&
257
+ JS_GetArrayBufferViewType(&v.toObject()) == JS::Scalar::Uint8,
258
+ napi_invalid_arg);
259
+ JS::RootedObject obj(env->cx, &v.toObject());
260
+ // §5f: pin the contents out-of-line before handing out the pointer.
261
+ if (!JS::EnsureNonInlineArrayBufferOrView(env->cx, obj)) {
262
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
263
+ }
264
+ if (length != nullptr) {
265
+ *length = JS_GetArrayBufferViewByteLength(obj);
266
+ }
267
+ if (data != nullptr) {
268
+ JS::AutoCheckCannotGC nogc;
269
+ bool shared = false;
270
+ *data = JS_GetArrayBufferViewData(obj, &shared, nogc);
271
+ }
272
+ return gjsify_napi::clear_last_error(env);
273
+ }
274
+
275
+ // ---- ArrayBuffer (js_native_api_v8.cc) ----
276
+
277
+ napi_status NAPI_CDECL napi_is_arraybuffer(napi_env env, napi_value value,
278
+ bool* result) {
279
+ GJSIFY_NAPI_CHECK_ENV(env);
280
+ GJSIFY_NAPI_CHECK_ARG(env, value);
281
+ GJSIFY_NAPI_CHECK_ARG(env, result);
282
+ JS::Value v = gjsify_napi::napi_value_to_js(value);
283
+ *result = v.isObject() && JS::IsArrayBufferObject(&v.toObject());
284
+ return gjsify_napi::clear_last_error(env);
285
+ }
286
+
287
+ napi_status NAPI_CDECL napi_create_arraybuffer(napi_env env, size_t byte_length,
288
+ void** data,
289
+ napi_value* result) {
290
+ GJSIFY_NAPI_PREAMBLE(env);
291
+ GJSIFY_NAPI_CHECK_ARG(env, result);
292
+ void* backing = nullptr;
293
+ JS::RootedObject ab(env->cx,
294
+ gjsify_napi::new_stable_arraybuffer(env, byte_length,
295
+ &backing));
296
+ if (!ab) {
297
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
298
+ }
299
+ if (data != nullptr) {
300
+ *data = backing;
301
+ }
302
+ *result = gjsify_napi::arena_push(env, JS::ObjectValue(*ab));
303
+ return napi_ok;
304
+ }
305
+
306
+ napi_status NAPI_CDECL napi_get_arraybuffer_info(napi_env env,
307
+ napi_value arraybuffer,
308
+ void** data,
309
+ size_t* byte_length) {
310
+ GJSIFY_NAPI_CHECK_ENV(env);
311
+ GJSIFY_NAPI_CHECK_ARG(env, arraybuffer);
312
+ JS::Value v = gjsify_napi::napi_value_to_js(arraybuffer);
313
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(
314
+ env, v.isObject() && JS::IsArrayBufferObject(&v.toObject()),
315
+ napi_invalid_arg);
316
+ JS::RootedObject obj(env->cx, &v.toObject());
317
+ // §5f: pin out-of-line so the returned pointer survives a moving GC.
318
+ if (!JS::EnsureNonInlineArrayBufferOrView(env->cx, obj)) {
319
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
320
+ }
321
+ size_t len = 0;
322
+ bool shared = false;
323
+ uint8_t* ptr = nullptr;
324
+ JS::GetArrayBufferLengthAndData(obj, &len, &shared, &ptr);
325
+ if (data != nullptr) {
326
+ *data = ptr;
327
+ }
328
+ if (byte_length != nullptr) {
329
+ *byte_length = len;
330
+ }
331
+ return gjsify_napi::clear_last_error(env);
332
+ }
333
+
334
+ napi_status NAPI_CDECL napi_detach_arraybuffer(napi_env env,
335
+ napi_value arraybuffer) {
336
+ GJSIFY_NAPI_CHECK_ENV(env);
337
+ GJSIFY_NAPI_CHECK_ARG(env, arraybuffer);
338
+ JS::Value v = gjsify_napi::napi_value_to_js(arraybuffer);
339
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(
340
+ env, v.isObject() && JS::IsArrayBufferObject(&v.toObject()),
341
+ napi_arraybuffer_expected);
342
+ JS::RootedObject obj(env->cx, &v.toObject());
343
+ // A defined [[ArrayBufferDetachKey]] slot = not detachable (WASM/asm.js) —
344
+ // Node's it->IsDetachable() check → napi_detachable_arraybuffer_expected.
345
+ bool has_detach_key = false;
346
+ if (!JS::HasDefinedArrayBufferDetachKey(env->cx, obj, &has_detach_key)) {
347
+ return gjsify_napi::set_last_error(
348
+ env, JS_IsExceptionPending(env->cx) ? napi_pending_exception
349
+ : napi_generic_failure);
350
+ }
351
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(env, !has_detach_key,
352
+ napi_detachable_arraybuffer_expected);
353
+ if (!JS::DetachArrayBuffer(env->cx, obj)) {
354
+ return gjsify_napi::set_last_error(
355
+ env, JS_IsExceptionPending(env->cx) ? napi_pending_exception
356
+ : napi_generic_failure);
357
+ }
358
+ return gjsify_napi::clear_last_error(env);
359
+ }
360
+
361
+ napi_status NAPI_CDECL napi_is_detached_arraybuffer(napi_env env,
362
+ napi_value arraybuffer,
363
+ bool* result) {
364
+ GJSIFY_NAPI_CHECK_ENV(env);
365
+ GJSIFY_NAPI_CHECK_ARG(env, arraybuffer);
366
+ GJSIFY_NAPI_CHECK_ARG(env, result);
367
+ JS::Value v = gjsify_napi::napi_value_to_js(arraybuffer);
368
+ *result = v.isObject() && JS::IsArrayBufferObject(&v.toObject()) &&
369
+ JS::IsDetachedArrayBufferObject(&v.toObject());
370
+ return gjsify_napi::clear_last_error(env);
371
+ }
372
+
373
+ // ---- typed arrays (js_native_api_v8.cc) ----
374
+
375
+ napi_status NAPI_CDECL napi_is_typedarray(napi_env env, napi_value value,
376
+ bool* result) {
377
+ GJSIFY_NAPI_CHECK_ENV(env);
378
+ GJSIFY_NAPI_CHECK_ARG(env, value);
379
+ GJSIFY_NAPI_CHECK_ARG(env, result);
380
+ JS::Value v = gjsify_napi::napi_value_to_js(value);
381
+ *result = v.isObject() && JS_IsTypedArrayObject(&v.toObject());
382
+ return gjsify_napi::clear_last_error(env);
383
+ }
384
+
385
+ napi_status NAPI_CDECL napi_create_typedarray(napi_env env,
386
+ napi_typedarray_type type,
387
+ size_t length,
388
+ napi_value arraybuffer,
389
+ size_t byte_offset,
390
+ napi_value* result) {
391
+ GJSIFY_NAPI_PREAMBLE(env);
392
+ GJSIFY_NAPI_CHECK_ARG(env, arraybuffer);
393
+ GJSIFY_NAPI_CHECK_ARG(env, result);
394
+ JS::Value v = gjsify_napi::napi_value_to_js(arraybuffer);
395
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(
396
+ env, v.isObject() && JS::IsArrayBufferObject(&v.toObject()),
397
+ napi_invalid_arg);
398
+ JS::RootedObject buffer(env->cx, &v.toObject());
399
+ const int64_t len = static_cast<int64_t>(length);
400
+ JSObject* ta = nullptr;
401
+ switch (type) {
402
+ case napi_int8_array:
403
+ ta = JS_NewInt8ArrayWithBuffer(env->cx, buffer, byte_offset, len);
404
+ break;
405
+ case napi_uint8_array:
406
+ ta = JS_NewUint8ArrayWithBuffer(env->cx, buffer, byte_offset, len);
407
+ break;
408
+ case napi_uint8_clamped_array:
409
+ ta = JS_NewUint8ClampedArrayWithBuffer(env->cx, buffer, byte_offset,
410
+ len);
411
+ break;
412
+ case napi_int16_array:
413
+ ta = JS_NewInt16ArrayWithBuffer(env->cx, buffer, byte_offset, len);
414
+ break;
415
+ case napi_uint16_array:
416
+ ta = JS_NewUint16ArrayWithBuffer(env->cx, buffer, byte_offset, len);
417
+ break;
418
+ case napi_int32_array:
419
+ ta = JS_NewInt32ArrayWithBuffer(env->cx, buffer, byte_offset, len);
420
+ break;
421
+ case napi_uint32_array:
422
+ ta = JS_NewUint32ArrayWithBuffer(env->cx, buffer, byte_offset, len);
423
+ break;
424
+ case napi_float32_array:
425
+ ta = JS_NewFloat32ArrayWithBuffer(env->cx, buffer, byte_offset, len);
426
+ break;
427
+ case napi_float64_array:
428
+ ta = JS_NewFloat64ArrayWithBuffer(env->cx, buffer, byte_offset, len);
429
+ break;
430
+ case napi_bigint64_array:
431
+ ta = JS_NewBigInt64ArrayWithBuffer(env->cx, buffer, byte_offset,
432
+ len);
433
+ break;
434
+ case napi_biguint64_array:
435
+ ta = JS_NewBigUint64ArrayWithBuffer(env->cx, buffer, byte_offset,
436
+ len);
437
+ break;
438
+ case napi_float16_array:
439
+ ta = JS_NewFloat16ArrayWithBuffer(env->cx, buffer, byte_offset, len);
440
+ break;
441
+ default:
442
+ return gjsify_napi::set_last_error(env, napi_invalid_arg);
443
+ }
444
+ if (ta == nullptr) {
445
+ // Out-of-range offset/length throws a RangeError (Node's
446
+ // CREATE_TYPED_ARRAY bounds check maps the same way).
447
+ return gjsify_napi::set_last_error(
448
+ env, JS_IsExceptionPending(env->cx) ? napi_pending_exception
449
+ : napi_generic_failure);
450
+ }
451
+ *result = gjsify_napi::arena_push(env, JS::ObjectValue(*ta));
452
+ return napi_ok;
453
+ }
454
+
455
+ napi_status NAPI_CDECL napi_get_typedarray_info(napi_env env,
456
+ napi_value typedarray,
457
+ napi_typedarray_type* type,
458
+ size_t* length, void** data,
459
+ napi_value* arraybuffer,
460
+ size_t* byte_offset) {
461
+ GJSIFY_NAPI_CHECK_ENV(env);
462
+ GJSIFY_NAPI_CHECK_ARG(env, typedarray);
463
+ JS::Value v = gjsify_napi::napi_value_to_js(typedarray);
464
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(
465
+ env, v.isObject() && JS_IsTypedArrayObject(&v.toObject()),
466
+ napi_invalid_arg);
467
+ JS::RootedObject obj(env->cx, &v.toObject());
468
+ // §5f: pin out-of-line before returning a pointer into the data.
469
+ if (!JS::EnsureNonInlineArrayBufferOrView(env->cx, obj)) {
470
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
471
+ }
472
+ if (type != nullptr) {
473
+ *type = gjsify_napi::scalar_to_napi(JS_GetArrayBufferViewType(obj));
474
+ }
475
+ if (length != nullptr) {
476
+ *length = JS_GetTypedArrayLength(obj);
477
+ }
478
+ if (byte_offset != nullptr) {
479
+ *byte_offset = JS_GetArrayBufferViewByteOffset(obj);
480
+ }
481
+ if (data != nullptr || arraybuffer != nullptr) {
482
+ // Materializing the buffer object may allocate/GC, so do it BEFORE the
483
+ // no-GC data read (the out-of-line pointer is stable across it).
484
+ bool shared = false;
485
+ JS::RootedObject ab(
486
+ env->cx, JS_GetArrayBufferViewBuffer(env->cx, obj, &shared));
487
+ if (!ab) {
488
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
489
+ }
490
+ if (arraybuffer != nullptr) {
491
+ *arraybuffer = gjsify_napi::arena_push(env, JS::ObjectValue(*ab));
492
+ }
493
+ if (data != nullptr) {
494
+ JS::AutoCheckCannotGC nogc;
495
+ // The view data pointer already includes the byte offset (= Node's
496
+ // buffer->Data() + array->ByteOffset()).
497
+ *data = JS_GetArrayBufferViewData(obj, &shared, nogc);
498
+ }
499
+ }
500
+ return gjsify_napi::clear_last_error(env);
501
+ }
502
+
503
+ // ---- DataView (js_native_api_v8.cc) ----
504
+
505
+ napi_status NAPI_CDECL napi_create_dataview(napi_env env, size_t byte_length,
506
+ napi_value arraybuffer,
507
+ size_t byte_offset,
508
+ napi_value* result) {
509
+ GJSIFY_NAPI_PREAMBLE(env);
510
+ GJSIFY_NAPI_CHECK_ARG(env, arraybuffer);
511
+ GJSIFY_NAPI_CHECK_ARG(env, result);
512
+ JS::Value v = gjsify_napi::napi_value_to_js(arraybuffer);
513
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(
514
+ env, v.isObject() && JS::IsArrayBufferObject(&v.toObject()),
515
+ napi_invalid_arg);
516
+ JS::RootedObject buffer(env->cx, &v.toObject());
517
+ // Node throws ERR_NAPI_INVALID_DATAVIEW_ARGS on an out-of-range window.
518
+ if (byte_length + byte_offset > JS::GetArrayBufferByteLength(buffer)) {
519
+ napi_throw_range_error(
520
+ env, "ERR_NAPI_INVALID_DATAVIEW_ARGS",
521
+ "byte_offset + byte_length should be less than or equal to the "
522
+ "size in bytes of the array passed in");
523
+ return gjsify_napi::set_last_error(env, napi_pending_exception);
524
+ }
525
+ JSObject* dv = JS_NewDataView(env->cx, buffer, byte_offset, byte_length);
526
+ if (dv == nullptr) {
527
+ return gjsify_napi::set_last_error(
528
+ env, JS_IsExceptionPending(env->cx) ? napi_pending_exception
529
+ : napi_generic_failure);
530
+ }
531
+ *result = gjsify_napi::arena_push(env, JS::ObjectValue(*dv));
532
+ return napi_ok;
533
+ }
534
+
535
+ napi_status NAPI_CDECL napi_is_dataview(napi_env env, napi_value value,
536
+ bool* result) {
537
+ GJSIFY_NAPI_CHECK_ENV(env);
538
+ GJSIFY_NAPI_CHECK_ARG(env, value);
539
+ GJSIFY_NAPI_CHECK_ARG(env, result);
540
+ JS::Value v = gjsify_napi::napi_value_to_js(value);
541
+ // A DataView is an ArrayBufferView that is not a typed array.
542
+ *result = v.isObject() && JS_IsArrayBufferViewObject(&v.toObject()) &&
543
+ !JS_IsTypedArrayObject(&v.toObject());
544
+ return gjsify_napi::clear_last_error(env);
545
+ }
546
+
547
+ napi_status NAPI_CDECL napi_get_dataview_info(napi_env env, napi_value dataview,
548
+ size_t* bytelength, void** data,
549
+ napi_value* arraybuffer,
550
+ size_t* byte_offset) {
551
+ GJSIFY_NAPI_CHECK_ENV(env);
552
+ GJSIFY_NAPI_CHECK_ARG(env, dataview);
553
+ JS::Value v = gjsify_napi::napi_value_to_js(dataview);
554
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(
555
+ env,
556
+ v.isObject() && JS_IsArrayBufferViewObject(&v.toObject()) &&
557
+ !JS_IsTypedArrayObject(&v.toObject()),
558
+ napi_invalid_arg);
559
+ JS::RootedObject obj(env->cx, &v.toObject());
560
+ if (!JS::EnsureNonInlineArrayBufferOrView(env->cx, obj)) {
561
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
562
+ }
563
+ if (bytelength != nullptr) {
564
+ *bytelength = JS_GetArrayBufferViewByteLength(obj);
565
+ }
566
+ if (byte_offset != nullptr) {
567
+ *byte_offset = JS_GetArrayBufferViewByteOffset(obj);
568
+ }
569
+ if (data != nullptr || arraybuffer != nullptr) {
570
+ bool shared = false;
571
+ JS::RootedObject ab(
572
+ env->cx, JS_GetArrayBufferViewBuffer(env->cx, obj, &shared));
573
+ if (!ab) {
574
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
575
+ }
576
+ if (arraybuffer != nullptr) {
577
+ *arraybuffer = gjsify_napi::arena_push(env, JS::ObjectValue(*ab));
578
+ }
579
+ if (data != nullptr) {
580
+ JS::AutoCheckCannotGC nogc;
581
+ *data = JS_GetArrayBufferViewData(obj, &shared, nogc);
582
+ }
583
+ }
584
+ return gjsify_napi::clear_last_error(env);
585
+ }
586
+
587
+ // ---- Promises (js_native_api_v8.cc) ----
588
+ //
589
+ // SpiderMonkey has no Promise::Resolver object: JS::NewPromiseObject(cx,
590
+ // nullptr) creates a promise settled ONLY via JS::ResolvePromise/RejectPromise
591
+ // on the promise itself. The napi_deferred is therefore a strong Reference that
592
+ // keeps the promise alive until it is concluded; kRuntime so a never-concluded
593
+ // deferred drops its root at env teardown (before JS_DestroyContext) rather
594
+ // than leaking a live root past context dispose (the UAF class).
595
+ //
596
+ // A settle is NOT observable synchronously in the same native call — the
597
+ // reaction jobs run on GJS's microtask/job queue, which the caller must drain
598
+ // (an await or a main-context turn) before the resolved value/reason is seen.
599
+
600
+ napi_status NAPI_CDECL napi_create_promise(napi_env env, napi_deferred* deferred,
601
+ napi_value* promise) {
602
+ GJSIFY_NAPI_PREAMBLE(env);
603
+ GJSIFY_NAPI_CHECK_ARG(env, deferred);
604
+ GJSIFY_NAPI_CHECK_ARG(env, promise);
605
+ JS::RootedObject promise_obj(env->cx,
606
+ JS::NewPromiseObject(env->cx, nullptr));
607
+ if (!promise_obj) {
608
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
609
+ }
610
+ JS::RootedValue promise_v(env->cx, JS::ObjectValue(*promise_obj));
611
+ gjsify_napi::Reference* ref = gjsify_napi::Reference::New(
612
+ env, promise_v, 1, gjsify_napi::ReferenceOwnership::kRuntime);
613
+ *deferred = reinterpret_cast<napi_deferred>(ref);
614
+ *promise = gjsify_napi::arena_push(env, promise_v);
615
+ return napi_ok;
616
+ }
617
+
618
+ namespace gjsify_napi {
619
+
620
+ static napi_status conclude_deferred(napi_env env, napi_deferred deferred,
621
+ napi_value result, bool resolve) {
622
+ GJSIFY_NAPI_PREAMBLE(env);
623
+ GJSIFY_NAPI_CHECK_ARG(env, deferred);
624
+ GJSIFY_NAPI_CHECK_ARG(env, result);
625
+ auto* ref = reinterpret_cast<Reference*>(deferred);
626
+ JS::RootedValue promise_v(env->cx);
627
+ const bool have = ref->GetValue(&promise_v); // strong: always live
628
+ JS::RootedObject promise(
629
+ env->cx, have && promise_v.isObject() ? &promise_v.toObject()
630
+ : nullptr);
631
+ JS::RootedValue resolution(env->cx, napi_value_to_js(result));
632
+ bool ok = promise &&
633
+ (resolve ? JS::ResolvePromise(env->cx, promise, resolution)
634
+ : JS::RejectPromise(env->cx, promise, resolution));
635
+ // Node deletes the deferred's persistent regardless of settle success; the
636
+ // delete unlinks the kRuntime Reference from the env list (so teardown's
637
+ // FinalizeAll won't revisit it).
638
+ delete ref;
639
+ if (!ok) {
640
+ return set_last_error(env, JS_IsExceptionPending(env->cx)
641
+ ? napi_pending_exception
642
+ : napi_generic_failure);
643
+ }
644
+ return napi_ok; // preamble already cleared last_error
645
+ }
646
+
647
+ } // namespace gjsify_napi
648
+
649
+ napi_status NAPI_CDECL napi_resolve_deferred(napi_env env,
650
+ napi_deferred deferred,
651
+ napi_value resolution) {
652
+ return gjsify_napi::conclude_deferred(env, deferred, resolution,
653
+ /* resolve = */ true);
654
+ }
655
+
656
+ napi_status NAPI_CDECL napi_reject_deferred(napi_env env, napi_deferred deferred,
657
+ napi_value rejection) {
658
+ return gjsify_napi::conclude_deferred(env, deferred, rejection,
659
+ /* resolve = */ false);
660
+ }
661
+
662
+ napi_status NAPI_CDECL napi_is_promise(napi_env env, napi_value value,
663
+ bool* is_promise) {
664
+ GJSIFY_NAPI_CHECK_ENV(env);
665
+ GJSIFY_NAPI_CHECK_ARG(env, value);
666
+ GJSIFY_NAPI_CHECK_ARG(env, is_promise);
667
+ JS::Value v = gjsify_napi::napi_value_to_js(value);
668
+ if (v.isObject()) {
669
+ JS::RootedObject obj(env->cx, &v.toObject());
670
+ *is_promise = JS::IsPromiseObject(obj);
671
+ } else {
672
+ *is_promise = false;
673
+ }
674
+ return gjsify_napi::clear_last_error(env);
675
+ }
676
+
677
+ // ---- Dates (js_native_api_v8.cc) ----
678
+
679
+ napi_status NAPI_CDECL napi_create_date(napi_env env, double time,
680
+ napi_value* result) {
681
+ GJSIFY_NAPI_PREAMBLE(env);
682
+ GJSIFY_NAPI_CHECK_ARG(env, result);
683
+ JSObject* date = JS::NewDateObject(env->cx, JS::TimeClip(time));
684
+ if (date == nullptr) {
685
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
686
+ }
687
+ *result = gjsify_napi::arena_push(env, JS::ObjectValue(*date));
688
+ return napi_ok;
689
+ }
690
+
691
+ napi_status NAPI_CDECL napi_is_date(napi_env env, napi_value value,
692
+ bool* is_date) {
693
+ GJSIFY_NAPI_CHECK_ENV(env);
694
+ GJSIFY_NAPI_CHECK_ARG(env, value);
695
+ GJSIFY_NAPI_CHECK_ARG(env, is_date);
696
+ JS::Value v = gjsify_napi::napi_value_to_js(value);
697
+ bool d = false;
698
+ if (v.isObject()) {
699
+ JS::RootedObject obj(env->cx, &v.toObject());
700
+ if (!JS::ObjectIsDate(env->cx, obj, &d)) {
701
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
702
+ }
703
+ }
704
+ *is_date = d;
705
+ return gjsify_napi::clear_last_error(env);
706
+ }
707
+
708
+ napi_status NAPI_CDECL napi_get_date_value(napi_env env, napi_value value,
709
+ double* result) {
710
+ GJSIFY_NAPI_PREAMBLE(env);
711
+ GJSIFY_NAPI_CHECK_ARG(env, value);
712
+ GJSIFY_NAPI_CHECK_ARG(env, result);
713
+ JS::Value v = gjsify_napi::napi_value_to_js(value);
714
+ JS::RootedObject obj(env->cx, v.isObject() ? &v.toObject() : nullptr);
715
+ bool is_date = false;
716
+ if (obj) {
717
+ if (!JS::ObjectIsDate(env->cx, obj, &is_date)) {
718
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
719
+ }
720
+ }
721
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(env, is_date, napi_date_expected);
722
+ // mozjs-140 exposes no public [[DateValue]] getter — Date.prototype.valueOf
723
+ // returns the same time value as v8::Date::ValueOf (NaN for an invalid
724
+ // date).
725
+ JS::RootedValue rval(env->cx);
726
+ if (!JS_CallFunctionName(env->cx, obj, "valueOf",
727
+ JS::HandleValueArray::empty(), &rval)) {
728
+ return gjsify_napi::set_last_error(
729
+ env, JS_IsExceptionPending(env->cx) ? napi_pending_exception
730
+ : napi_generic_failure);
731
+ }
732
+ *result = rval.toNumber();
733
+ return napi_ok;
734
+ }
735
+
736
+ // ---- BigInt uint64 (js_native_api_v8.cc; public JS::BigInt* wrappers) ----
737
+
738
+ napi_status NAPI_CDECL napi_create_bigint_uint64(napi_env env, uint64_t value,
739
+ napi_value* result) {
740
+ GJSIFY_NAPI_CHECK_ENV(env);
741
+ GJSIFY_NAPI_CHECK_ARG(env, result);
742
+ // Public wrapper: NumberToBigInt<uint64_t> dispatches to
743
+ // JS::detail::BigIntFromUint64 (js/BigInt.h:35,54-60).
744
+ JS::BigInt* bi = JS::NumberToBigInt(env->cx, value);
745
+ if (bi == nullptr) {
746
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
747
+ }
748
+ *result = gjsify_napi::arena_push(env, JS::BigIntValue(bi));
749
+ return gjsify_napi::clear_last_error(env);
750
+ }
751
+
752
+ napi_status NAPI_CDECL napi_get_value_bigint_uint64(napi_env env,
753
+ napi_value value,
754
+ uint64_t* result,
755
+ bool* lossless) {
756
+ GJSIFY_NAPI_CHECK_ENV(env);
757
+ GJSIFY_NAPI_CHECK_ARG(env, value);
758
+ GJSIFY_NAPI_CHECK_ARG(env, result);
759
+ GJSIFY_NAPI_CHECK_ARG(env, lossless);
760
+ JS::Value v = gjsify_napi::napi_value_to_js(value);
761
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(env, v.isBigInt(), napi_bigint_expected);
762
+ JS::BigInt* bi = v.toBigInt();
763
+ // BigIntFits<uint64_t> → BigIntIsUint64 (js/BigInt.h). Out of range =
764
+ // v8::BigInt::Uint64Value two's-complement truncation, lossless=false.
765
+ if (JS::BigIntFits(bi, result)) {
766
+ *lossless = true;
767
+ } else {
768
+ *lossless = false;
769
+ *result = JS::ToBigUint64(bi);
770
+ }
771
+ return gjsify_napi::clear_last_error(env);
772
+ }