@gjsify/napi 0.64.1 → 0.64.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/napi",
3
- "version": "0.64.1",
3
+ "version": "0.64.4",
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",
@@ -97,7 +97,7 @@
97
97
  },
98
98
  "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/napi/napi#readme",
99
99
  "optionalDependencies": {
100
- "@gjsify/napi-darwin-arm64": "0.64.1",
101
- "@gjsify/napi-linux-x64": "0.64.1"
100
+ "@gjsify/napi-darwin-arm64": "0.64.4",
101
+ "@gjsify/napi-linux-x64": "0.64.4"
102
102
  }
103
103
  }
package/src/cc/common.h CHANGED
@@ -57,6 +57,7 @@
57
57
  #include <js/Exception.h>
58
58
  #include <js/GCAPI.h>
59
59
  #include <js/GlobalObject.h>
60
+ #include <js/MemoryFunctions.h>
60
61
  #include <js/PropertyAndElement.h>
61
62
  #include <js/RootingAPI.h>
62
63
  #include <js/String.h>
@@ -423,6 +424,11 @@ struct napi_env__ {
423
424
  // Foreign-object InstanceState WeakMap (§5d fallback tier), lazily
424
425
  // created. Reset in teardown().
425
426
  JS::PersistentRooted<JSObject*> wrap_map;
427
+ // Net external memory the addon has reported
428
+ // (napi_adjust_external_memory). Mirrors exactly what is booked against
429
+ // `global` via JS::AddAssociatedMemory, so teardown can hand that amount
430
+ // back — see env.cc.
431
+ int64_t external_memory = 0;
426
432
 
427
433
  // §5b ref lists (sentinel heads; two-list teardown order per
428
434
  // js_native_api_v8.h:121-130).
@@ -466,7 +472,8 @@ struct napi_env__ {
466
472
  // 3. RefTracker::FinalizeAll(finalizing_reflist) then (reflist);
467
473
  // 4. instance-data finalizer;
468
474
  // 5. flip can_call_into_js/torn_down (subsequent napi entry → status);
469
- // 6. drop every root: remove the extra-roots tracer, reset
475
+ // 6. return the still-booked external memory, then drop every root:
476
+ // remove the extra-roots tracer, reset
470
477
  // PersistentRooteds, clear arena/scopes/weak set — all BEFORE
471
478
  // JS_DestroyContext runs (a live root past that point is the UAF
472
479
  // class). Callback bundles are NOT freed here: live function
package/src/cc/env.cc CHANGED
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // @gjsify/napi — env lifecycle, handle arena, GC tracer, handle scopes.
2
+ // @gjsify/napi — env lifecycle, handle arena, GC tracer, handle scopes,
3
+ // external-memory accounting.
3
4
  //
4
5
  // Reference: refs/node/src/js_native_api_v8.{h,cc} (Node.js contributors,
5
6
  // MIT) for the napi_env/handle-scope semantics; tracer pattern adapted from
@@ -162,6 +163,20 @@ void napi_env__::teardown() {
162
163
  // context, refs/gjs/gjs/context.cpp:457.)
163
164
  JS_RemoveExtraGCRootsTracer(cx, gjsify_napi::trace_env, this);
164
165
  exports.reset();
166
+ // Hand back whatever external memory is still booked against the global
167
+ // BEFORE that root goes: an Add without its Remove leaves the zone's
168
+ // malloc-bytes counter inflated for the rest of the process. Finalizers
169
+ // ran in steps 2-4, so everything they returned is already subtracted;
170
+ // what is left is what the addon never gave back.
171
+ if (external_memory > 0 && global.get() != nullptr) {
172
+ g_debug("gjsify-napi: returning %" G_GINT64_FORMAT
173
+ " bytes of external memory at env teardown (%s)",
174
+ external_memory, filename.c_str());
175
+ JS::RemoveAssociatedMemory(global.get(),
176
+ static_cast<size_t>(external_memory),
177
+ JS::MemoryUse::Embedding1);
178
+ }
179
+ external_memory = 0;
165
180
  global.reset();
166
181
  wrap_map.reset();
167
182
  arena.clearAll();
@@ -260,3 +275,75 @@ napi_status NAPI_CDECL napi_escape_handle(napi_env env,
260
275
  *result = reinterpret_cast<napi_value>(rec.escape_slot);
261
276
  return gjsify_napi::clear_last_error(env);
262
277
  }
278
+
279
+ // ---- external memory (js_native_api_v8.cc:3043-3052 semantics) ----
280
+
281
+ // Node hands the delta straight to V8's
282
+ // AdjustAmountOfExternalAllocatedMemory, which feeds the GC's allocation-
283
+ // pressure heuristic and returns the new total. mozjs's equivalent pair,
284
+ // JS::Add/RemoveAssociatedMemory, books the amount against an OBJECT — and
285
+ // this Node signature carries none. The realm global is the one object
286
+ // guaranteed to outlive every allocation the addon reports (it dies with the
287
+ // context, and teardown hands the balance back), so the env's whole external
288
+ // total hangs off it under MemoryUse::Embedding1. GJS itself never uses that
289
+ // enumerator (it books Cairo, GObjectInstanceStruct and GdkPixbuf), so the
290
+ // slot is ours alone and a mismatched Remove cannot corrupt GJS's own
291
+ // accounting.
292
+ //
293
+ // The clamp at zero is OURS, and the reason is mozjs, not Node parity:
294
+ // RemoveAssociatedMemory must never give back more than was booked for a
295
+ // given (object, use) pair — that asserts in a debug mozjs and silently
296
+ // wrecks the zone's malloc-bytes accounting in a release one. So the running
297
+ // balance lives in the env and every delta is applied to it, never passed
298
+ // through raw.
299
+ //
300
+ // Node does NOT tolerate an over-subtract either, it just fails harder:
301
+ // V8's Heap::UpdateExternalMemory keeps the total in a uint64_t and guards it
302
+ // with CHECK_GE(total_before, -delta) (refs/node/deps/v8/src/heap/heap.cc),
303
+ // a release-mode check — an addon that returns more than it booked ABORTS
304
+ // node. Node's total is therefore never negative, which is why a single
305
+ // non-negative balance is the Node-faithful shape; reporting a clamped zero
306
+ // instead of aborting is the deliberate, strictly softer deviation.
307
+ //
308
+ // The INT64_MAX saturation is likewise ours and likewise softer: V8 CHECKs
309
+ // that a single delta is under --external-memory-max-reasonable-size (32 GB
310
+ // by default, api.cc AdjustAmountOfExternalAllocatedMemoryImpl) and aborts
311
+ // past it. Saturating keeps a nonsensical delta from wrapping the counter.
312
+ //
313
+ // Basic env: callable from a synchronous finalizer, i.e. during GC. No JS
314
+ // runs and no exception state is touched, so CHECK_ENV is the whole gate, as
315
+ // in Node. After teardown the global root is gone and only the counter moves,
316
+ // so a late call still answers instead of touching a dropped root.
317
+ napi_status NAPI_CDECL napi_adjust_external_memory(node_api_basic_env basic_env,
318
+ int64_t change_in_bytes,
319
+ int64_t* adjusted_value) {
320
+ napi_env env = const_cast<napi_env>(basic_env);
321
+ GJSIFY_NAPI_CHECK_ENV(env);
322
+ GJSIFY_NAPI_CHECK_ARG(env, adjusted_value);
323
+
324
+ const int64_t booked = env->external_memory;
325
+ int64_t target;
326
+ if (change_in_bytes > 0 && booked > INT64_MAX - change_in_bytes) {
327
+ target = INT64_MAX;
328
+ } else {
329
+ target = booked + change_in_bytes;
330
+ if (target < 0) {
331
+ target = 0;
332
+ }
333
+ }
334
+
335
+ JSObject* owner = env->global.get();
336
+ if (owner != nullptr && target != booked) {
337
+ if (target > booked) {
338
+ JS::AddAssociatedMemory(owner, static_cast<size_t>(target - booked),
339
+ JS::MemoryUse::Embedding1);
340
+ } else {
341
+ JS::RemoveAssociatedMemory(owner,
342
+ static_cast<size_t>(booked - target),
343
+ JS::MemoryUse::Embedding1);
344
+ }
345
+ }
346
+ env->external_memory = target;
347
+ *adjusted_value = target;
348
+ return gjsify_napi::clear_last_error(env);
349
+ }
package/src/cc/stubs.cc CHANGED
@@ -10,7 +10,7 @@
10
10
  // napi_*/node_api_* references always resolve — reaching past the
11
11
  // implemented surface fails loud with napi_generic_failure + last_error
12
12
  // (plan section 2 posture), never with an unresolved-symbol load failure and
13
- // never with a crash. 10 stubs.
13
+ // never with a crash. 9 stubs.
14
14
 
15
15
  #include "common.h"
16
16
 
@@ -52,12 +52,6 @@ napi_status NAPI_CDECL napi_create_external_arraybuffer(napi_env env, void* exte
52
52
  GJSIFY_NAPI_STUB(env);
53
53
  }
54
54
 
55
- napi_status NAPI_CDECL napi_adjust_external_memory(node_api_basic_env env, int64_t change_in_bytes, int64_t* adjusted_value) {
56
- (void)change_in_bytes;
57
- (void)adjusted_value;
58
- GJSIFY_NAPI_STUB(env);
59
- }
60
-
61
55
  napi_status NAPI_CDECL napi_create_bigint_words(napi_env env, int sign_bit, size_t word_count, const uint64_t* words, napi_value* result) {
62
56
  (void)sign_bit;
63
57
  (void)word_count;