@gjsify/node-gi 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/src/common.h CHANGED
@@ -363,7 +363,7 @@ inline NodeGiIntResult NodeGiJsToInteger(Napi::Value v, GITypeTag tag, GIArgumen
363
363
  }
364
364
  }
365
365
 
366
- enum class NodeGiIntContext { kArgument, kField, kReturnValue };
366
+ enum class NodeGiIntContext { kArgument, kField, kReturnValue, kListElement, kHashElement };
367
367
  inline const char* NodeGiIntTagName(GITypeTag tag) {
368
368
  switch (tag) {
369
369
  case GI_TYPE_TAG_INT8: return "int8";
@@ -385,13 +385,31 @@ inline void NodeGiThrowIntOutOfRange(Napi::Env env, GITypeTag tag, NodeGiIntCont
385
385
  msg = std::string("Argument ") + (name != nullptr ? name : "") + ": value is out of range for " + t;
386
386
  } else {
387
387
  msg = "value " + shown + " is out of range for " +
388
- (ctx == NodeGiIntContext::kField ? std::string("Field '") + (name ? name : "") + "'"
389
- : std::string("Return value")) +
388
+ (ctx == NodeGiIntContext::kField
389
+ ? std::string("Field '") + (name ? name : "") + "'"
390
+ : ctx == NodeGiIntContext::kListElement ? std::string("List element")
391
+ : ctx == NodeGiIntContext::kHashElement ? std::string("Hash element")
392
+ : std::string("Return value")) +
390
393
  " (type " + t + ")";
391
394
  }
392
395
  Napi::Error::New(env, msg).ThrowAsJavaScriptException();
393
396
  }
394
397
 
398
+ // The text gjs_debug_value (refs/gjs/gi/../jsapi-util-string.cpp) prints for the offending
399
+ // value in a list/hash element error: a Number goes through C++ ostream (%g, 6 digits), a
400
+ // BigInt is shown as its signed 64-bit value with a "(modulo 2^64)" suffix.
401
+ inline std::string NodeGiDebugValue(Napi::Value v) {
402
+ if (v.IsBigInt()) {
403
+ bool lossless = false;
404
+ return std::to_string(v.As<Napi::BigInt>().Int64Value(&lossless)) + "n (modulo 2^64)";
405
+ }
406
+ Napi::Number n = v.ToNumber();
407
+ if (n.IsEmpty()) return std::string();
408
+ char buf[G_ASCII_DTOSTR_BUF_SIZE];
409
+ g_ascii_formatd(buf, sizeof(buf), "%g", n.DoubleValue()); // locale-independent, like ostream
410
+ return buf;
411
+ }
412
+
395
413
  // IN array elements and GValue int/uint, GJS-exact: these WRAP instead of refusing.
396
414
  // An array element goes through js_value_to_c_strict (refs/gjs/gi/arg.cpp:543), which
397
415
  // is ToInt32/ToUint32 truncated to the tag for 8/16/32 bits and ToInt64/ToUint64 —
package/src/marshal.cc CHANGED
@@ -1060,6 +1060,8 @@ static bool IsSupportedElementType(GITypeInfo* elem, std::string* why, bool byVa
1060
1060
  case GI_TYPE_TAG_UTF8:
1061
1061
  case GI_TYPE_TAG_FILENAME:
1062
1062
  return true;
1063
+ // A gunichar is a 4-byte cell; only a C array writes one (no list/hash node).
1064
+ case GI_TYPE_TAG_UNICHAR: return cArrayCell;
1063
1065
  case GI_TYPE_TAG_INTERFACE: {
1064
1066
  GIBaseInfo* iface = gi_type_info_get_interface(elem);
1065
1067
  bool ok = iface != nullptr && (GI_IS_OBJECT_INFO(iface) || GI_IS_INTERFACE_INFO(iface));
@@ -1178,7 +1180,8 @@ size_t CElementSize(GITypeInfo* elem) {
1178
1180
  case GI_TYPE_TAG_INT16:
1179
1181
  case GI_TYPE_TAG_UINT16: return 2;
1180
1182
  case GI_TYPE_TAG_INT32:
1181
- case GI_TYPE_TAG_UINT32: return 4;
1183
+ case GI_TYPE_TAG_UINT32:
1184
+ case GI_TYPE_TAG_UNICHAR: return 4;
1182
1185
  case GI_TYPE_TAG_INT64:
1183
1186
  case GI_TYPE_TAG_UINT64: return 8;
1184
1187
  case GI_TYPE_TAG_FLOAT: return sizeof(gfloat);
@@ -1601,16 +1604,16 @@ Napi::Value ReadOutOrReturn(Napi::Env env, GICallableInfo* callable, GITypeInfo*
1601
1604
  // the element, so it gets a ref (objects) or an independent copy (boxed) instead.
1602
1605
  // Throws + returns false on an unsupported element.
1603
1606
  static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v, GIArgument* a,
1604
- GITransfer elemTransfer, bool wrapIntegers = false) {
1607
+ GITransfer elemTransfer, NodeGiIntContext intContext = NodeGiIntContext::kListElement,
1608
+ bool wrapIntegers = false) {
1605
1609
  memset(a, 0, sizeof(*a));
1606
1610
  switch (gi_type_info_get_tag(elem)) {
1607
1611
  // Scalar coercions via the terminate-safe helpers (common.h): a swallowed
1608
1612
  // napi failure mid worker.terminate() must degrade to a zero, not cascade
1609
1613
  // into Error::New(nullptr)'s fatal sites.
1610
1614
  case GI_TYPE_TAG_BOOLEAN: a->v_boolean = NodeGiToBool(v); return true;
1611
- // Integers WRAP here, in an array (arg.cpp gjs_array_to_auto_array) — see
1612
- // NodeGiJsToIntegerWrap (common.h). A GList/GHashTable element keeps the older
1613
- // coercion: gjs's rule for those is not probed yet.
1615
+ // Integers WRAP in an array (arg.cpp gjs_array_to_auto_array, NodeGiJsToIntegerWrap in
1616
+ // common.h) and are REFUSED out of range in a GList/GHashTable node.
1614
1617
  case GI_TYPE_TAG_INT8:
1615
1618
  case GI_TYPE_TAG_UINT8:
1616
1619
  case GI_TYPE_TAG_INT16:
@@ -1621,16 +1624,20 @@ static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v,
1621
1624
  case GI_TYPE_TAG_UINT64:
1622
1625
  if (wrapIntegers)
1623
1626
  return NodeGiJsToIntegerWrap(v, gi_type_info_get_tag(elem), a) == NodeGiIntResult::kOk;
1624
- switch (gi_type_info_get_tag(elem)) {
1625
- case GI_TYPE_TAG_INT8: a->v_int8 = static_cast<gint8>(NodeGiToInt32(v)); return true;
1626
- case GI_TYPE_TAG_UINT8: a->v_uint8 = static_cast<guint8>(NodeGiToUint32(v)); return true;
1627
- case GI_TYPE_TAG_INT16: a->v_int16 = static_cast<gint16>(NodeGiToInt32(v)); return true;
1628
- case GI_TYPE_TAG_UINT16: a->v_uint16 = static_cast<guint16>(NodeGiToUint32(v)); return true;
1629
- case GI_TYPE_TAG_INT32: a->v_int32 = NodeGiToInt32(v); return true;
1630
- case GI_TYPE_TAG_UINT32: a->v_uint32 = NodeGiToUint32(v); return true;
1631
- case GI_TYPE_TAG_INT64: a->v_int64 = JsValueToInt64(v); return true;
1632
- default: a->v_uint64 = JsValueToUint64(v); return true;
1627
+ // A list/hash node is checked, not wrapped (arg.cpp gjs_value_to_basic_gi_argument
1628
+ // -> gjs_arg_set_from_js_value): out of range is a plain Error.
1629
+ switch (NodeGiJsToInteger(v, gi_type_info_get_tag(elem), a, nullptr)) {
1630
+ case NodeGiIntResult::kPending: return false;
1631
+ case NodeGiIntResult::kOutOfRange:
1632
+ NodeGiThrowIntOutOfRange(env, gi_type_info_get_tag(elem), intContext, nullptr,
1633
+ NodeGiDebugValue(v));
1634
+ return false;
1635
+ case NodeGiIntResult::kOk: return true;
1633
1636
  }
1637
+ return true;
1638
+ // gjs_array_to_auto_array<char32_t> wraps like uint32 (arg.cpp js_value_to_c_strict).
1639
+ case GI_TYPE_TAG_UNICHAR:
1640
+ return NodeGiJsToIntegerWrap(v, GI_TYPE_TAG_UINT32, a) == NodeGiIntResult::kOk;
1634
1641
  case GI_TYPE_TAG_FLOAT: a->v_float = static_cast<gfloat>(NodeGiToDouble(v)); return true;
1635
1642
  case GI_TYPE_TAG_DOUBLE: a->v_double = NodeGiToDouble(v); return true;
1636
1643
  case GI_TYPE_TAG_GTYPE: {
@@ -2267,6 +2274,20 @@ static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfe
2267
2274
  return true;
2268
2275
  }
2269
2276
 
2277
+ // A JS string as a gunichar C array → its code points (arg.cpp:584, gjs_string_to_ucs4).
2278
+ if (at == GI_ARRAY_TYPE_C && etag == GI_TYPE_TAG_UNICHAR && v.IsString()) {
2279
+ std::string s = v.As<Napi::String>().Utf8Value();
2280
+ glong n = 0;
2281
+ gunichar* ucs = g_utf8_to_ucs4_fast(s.c_str(), static_cast<glong>(s.size()), &n);
2282
+ void* buf = g_malloc0(elemSize * (n + (zt ? 1 : 0)));
2283
+ if (n > 0) memcpy(buf, ucs, elemSize * n);
2284
+ g_free(ucs);
2285
+ *outPtr = buf;
2286
+ *outCount = static_cast<long>(n);
2287
+ gi_base_info_unref(elem);
2288
+ return true;
2289
+ }
2290
+
2270
2291
  if (!v.IsArray()) {
2271
2292
  gi_base_info_unref(elem);
2272
2293
  Napi::TypeError::New(env, "expected an array for the array argument")
@@ -2301,7 +2322,7 @@ static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfe
2301
2322
  }
2302
2323
  GIArgument a;
2303
2324
  ok = ElementToGIArgument(env, elem, arr.Get(static_cast<uint32_t>(i)), &a, elemTransfer,
2304
- /*wrapIntegers=*/true);
2325
+ NodeGiIntContext::kListElement, /*wrapIntegers=*/true);
2305
2326
  if (!ok) break;
2306
2327
  // Copy the element's storage bytes (LE: the low elemSize bytes of the union
2307
2328
  // alias the active field — v_string/v_pointer for pointers, the scalar
@@ -2448,7 +2469,8 @@ static bool JsToGHashIn(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransf
2448
2469
  } else {
2449
2470
  GIArgument ka;
2450
2471
  Napi::Value keyNum = Napi::Number::New(env, static_cast<double>(g_ascii_strtoll(ks.c_str(), nullptr, 10)));
2451
- ok = ElementToGIArgument(env, kt, keyNum, &ka, GI_TRANSFER_NOTHING);
2472
+ ok = ElementToGIArgument(env, kt, keyNum, &ka, GI_TRANSFER_NOTHING,
2473
+ NodeGiIntContext::kHashElement, /*wrapIntegers=*/true);
2452
2474
  if (!ok) break;
2453
2475
  kp = gi_type_info_hash_pointer_from_argument(kt, &ka);
2454
2476
  }
@@ -2459,7 +2481,7 @@ static bool JsToGHashIn(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransf
2459
2481
  Napi::Value val = obj.Get(key);
2460
2482
  gpointer vp = nullptr;
2461
2483
  GIArgument va;
2462
- ok = ElementToGIArgument(env, vt, val, &va, elemTransfer);
2484
+ ok = ElementToGIArgument(env, vt, val, &va, elemTransfer, NodeGiIntContext::kHashElement);
2463
2485
  if (!ok) {
2464
2486
  g_free(kp);
2465
2487
  break;
package/src/signals.cc CHANGED
@@ -7,22 +7,36 @@ namespace nodegi {
7
7
 
8
8
  // ---- signals (milestone 1) ----
9
9
  //
10
- // A GClosure that wraps a JS callback. The callback is held by a strong
11
- // napi_ref; the closure's finalize notifier drops it. The generic marshal
12
- // converts the signal's GValue params to JS — the EMITTER instance (param 0)
13
- // first, then the signal's own params (1..n), matching GJS — and the JS return
14
- // into the signal return GValue.
10
+ // A GClosure that wraps a JS callback. The closure's finalize notifier drops the
11
+ // napi_ref. The generic marshal converts the signal's GValue params to JS — the
12
+ // EMITTER instance (param 0) first, then the signal's own params (1..n), matching
13
+ // GJS — and the JS return into the signal return GValue.
15
14
  //
16
- // Narrowed-leak semantics (with the toggle-ref bridge): a handler that does NOT
17
- // close over its own object is now collectable once C is the sole owner
18
- // (toggle-down → weak → GC). A handler that DOES close over its object forms a
19
- // GObject -> GClosure -> napi_ref -> callback -> wrapper -> handle cycle the GC
20
- // cannot break across C, so it keeps the object alive until disconnect — which is
21
- // the GJS-faithful contract (a connected self-referential handler is a reason to
22
- // stay alive). disconnect drops the closure's napi_ref (JsClosureFinalize),
23
- // breaking the cycle so the next GC collects the object. The callback ref stays
24
- // STRONG while connected (a connected handler must fire). Verified by the
25
- // signal-cycle case in test/gc-identity.test.mjs.
15
+ // REF STRENGTH IS THE WHOLE CONTRACT, and it differs per closure KIND, exactly as
16
+ // in GJS (refs/gjs/gi/closure.h: `create_marshaled` roots, `create_for_signal`
17
+ // does not):
18
+ //
19
+ // * GENERIC closure (a JS function marshalled as a GClosure IN-arg, invoked by
20
+ // arbitrary C) — STRONG. Nothing on the JS side owns it; the ref IS the owner.
21
+ // * SIGNAL closure (`.connect()`) and TEMPLATE-CALLBACK closure — WEAK. A
22
+ // handler that captures its own emitter otherwise forms a
23
+ // GObject → GClosure → napi_ref → callback → wrapper → toggle ref → GObject
24
+ // cycle that spans C, and no GC can break it: `obj.connect('x', () => obj.y)`
25
+ // made `obj` immortal. GJS has the identical cycle on paper and collects it,
26
+ // because its signal closures keep the callable UNROOTED and trace it from
27
+ // the owning wrapper (ObjectInstance::m_closures / trace_impl), which puts
28
+ // the whole cycle inside one heap the GC can reason about.
29
+ //
30
+ // node-gi has no tracing hook, so the equivalent edge is built in JS: index.js
31
+ // retains the callback in a WeakMap keyed by the canonical handle — the only
32
+ // ephemeron V8 offers over a non-extensible External, and the same shape
33
+ // `associate_closure` has. The handle is reachable exactly when GJS would trace
34
+ // the closure (JS holds the wrapper, or C owns the object and toggle.cc roots
35
+ // it), so a connected handler still fires for as long as anything can emit to it,
36
+ // and the self-referential cycle collects. A collected callback leaves the weak
37
+ // ref empty and the marshal returns early (it already tolerates a null value).
38
+ // Measured against gjs 1.88.1 with a FinalizationRegistry witness; verified by the
39
+ // signal-cycle cases in test/gc-identity.test.mjs.
26
40
 
27
41
  struct JsClosureData {
28
42
  napi_env env;
@@ -191,8 +205,11 @@ static void JsGenericClosureMarshal(GClosure* closure, GValue* return_value, gui
191
205
 
192
206
  // JS function → a floating marshaled GClosure (the engine's twin of GJS's
193
207
  // Gjs::Closure::create_marshaled for "boxed" closure args — refs/gjs/gi/
194
- // arg-cache.cpp GClosureInTransferNone::in). The closure holds a strong ref to
195
- // the function; JsClosureFinalize drops it when the closure is invalidated /
208
+ // arg-cache.cpp GClosureInTransferNone::in). The closure holds a STRONG ref to
209
+ // the function — deliberately, and unlike the signal/template closures above:
210
+ // `create_marshaled` defaults `root = true` (refs/gjs/gi/closure.h), because no
211
+ // JS-side owner exists for a closure handed to arbitrary C. JsClosureFinalize
212
+ // drops it when the closure is invalidated /
196
213
  // finalized (the callee released its last ref, or the invoke-site released the
197
214
  // only ref because the callee never kept one). Ref/sink + release policy lives at
198
215
  // the marshalling site (marshal.cc / calls.cc CreatedClosures).
@@ -231,9 +248,11 @@ Napi::Value ConnectSignal(const Napi::CallbackInfo& info) {
231
248
  return env.Null();
232
249
  }
233
250
 
251
+ // WEAK (refcount 0) — the keep-alive is the JS-side handle→callback edge
252
+ // index.js installs; see the ref-strength contract at the top of this file.
234
253
  JsClosureData* jc = g_new0(JsClosureData, 1);
235
254
  jc->env = env;
236
- napi_create_reference(env, info[2], 1, &jc->callback);
255
+ napi_create_reference(env, info[2], 0, &jc->callback);
237
256
 
238
257
  GClosure* closure = g_closure_new_simple(sizeof(GClosure), jc);
239
258
  g_closure_set_marshal(closure, JsClosureMarshal);
@@ -468,13 +487,17 @@ static GClosure* NodeGiScopeCreateClosure(void* selfScope, void* builder,
468
487
  }
469
488
 
470
489
  // Wrap the resolved (instance-bound, arg-wrapping) JS function in a JsClosure —
471
- // the same closure type a `.connect()` produces. The closure owns a strong ref to
472
- // the function; g_object_watch_closure ties its lifetime to the widget (the
473
- // handler `this`) and invalidates it when the widget is finalized, exactly like
490
+ // the same closure type a `.connect()` produces, and WEAK for the same reason:
491
+ // the resolver's dispatcher captures the widget handle, so a strong ref made
492
+ // every templated widget immortal. gi.js's resolveTemplateCallback retains the
493
+ // dispatcher on the widget handle before returning it, which is what GJS's Gtk
494
+ // override does with `Gi.associateClosure(lifetimeObject, …)`
495
+ // (refs/gjs/modules/core/overrides/Gtk.js). g_object_watch_closure additionally
496
+ // ties the closure to the widget and invalidates it on finalize, exactly like
474
497
  // GtkBuilderCScope's own create_closure.
475
498
  JsClosureData* jc = g_new0(JsClosureData, 1);
476
499
  jc->env = rawEnv;
477
- napi_create_reference(rawEnv, resolved, 1, &jc->callback);
500
+ napi_create_reference(rawEnv, resolved, 0, &jc->callback);
478
501
  GClosure* closure = g_closure_new_simple(sizeof(GClosure), jc);
479
502
  g_closure_set_marshal(closure, JsClosureMarshal);
480
503
  g_closure_add_finalize_notifier(closure, jc, JsClosureFinalize);