@gjsify/node-gi 0.63.0 → 0.64.1

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
@@ -35,6 +35,7 @@
35
35
  #endif
36
36
 
37
37
  #ifndef _WIN32
38
+ #include <cmath>
38
39
  #include <dlfcn.h> // dlopen/dlsym the GtkWidgetClass template API (no GTK link)
39
40
  #endif
40
41
  #include <napi.h>
@@ -83,11 +84,6 @@ GIRepository* DupDefaultRepository();
83
84
  // Init, freed by the instance-data finalizer at env teardown.
84
85
  struct NodeGiEnvData {
85
86
  napi_ref errorBuilder = nullptr;
86
- // The CLASS the builder above constructs — L1's `GLib.Error`. Registered with
87
- // the builder (one call, so the two cannot drift apart) and read the other way
88
- // round: a GError-typed IN arg uses it to recognise an L1 error and rebuild a
89
- // real GError from its fields. Per-env for the same reason as errorBuilder.
90
- napi_ref errorClass = nullptr;
91
87
  // L1-registered resolver for Gtk.Template `<signal handler="…">` callbacks: given
92
88
  // (instanceHandle, handlerName) it returns the instance's bound JS method (or
93
89
  // undefined). Stored per-env for the same reason as errorBuilder (a napi_ref is
@@ -160,6 +156,13 @@ extern const napi_type_tag kGObjectHandleTag;
160
156
  Napi::Value MakeBoxedHandle(Napi::Env env, gpointer ptr, GType gtype, bool owns,
161
157
  GIBaseInfo* info = nullptr, bool rawOwned = false);
162
158
  Napi::Value WrapVariant(Napi::Env env, GVariant* var, GITransfer transfer);
159
+ // Wrap a `GError*` as the boxed handle L1's `wrapGError` turns into the error
160
+ // object. ONE helper for both directions a GError reaches JS by — a GError-typed
161
+ // return/OUT and the error a failed invoke THREW — because those used to produce
162
+ // two different JS shapes, and sharing the wrap is what keeps them from drifting
163
+ // apart again. Transfer follows the boxed rule: transfer-everything is adopted,
164
+ // transfer-none is copied, so the handle always owns what it frees.
165
+ Napi::Value MakeGErrorHandle(Napi::Env env, GError* error, GITransfer transfer);
163
166
  BoxedHandle* TryGetBoxedHandle(Napi::Value v);
164
167
  bool TryGetBoxedPtr(Napi::Value v, gpointer* out);
165
168
  Napi::Value MakeGTypeHandle(Napi::Env env, GType gtype);
@@ -278,6 +281,162 @@ inline uint64_t JsValueToUint64(Napi::Value v) {
278
281
  return n.IsEmpty() ? 0 : static_cast<uint64_t>(n.Int64Value());
279
282
  }
280
283
 
284
+ // IN integers, GJS-exact (refs/gjs/gi/js-value-inl.h, arg-inl.h): an integer tag
285
+ // REFUSES an out-of-range value instead of wrapping it. The rule is not uniform and is
286
+ // copied as is, never made stricter: 8/16-bit run ToInt32 first (so it wraps mod 2^32)
287
+ // and then compare against the tag range; int32 is not checked at all; uint32 and the
288
+ // 64-bit tags refuse what falls outside their range. undefined and a non-finite Number
289
+ // become 0. `shown` receives the text GJS prints for the offending value.
290
+ // kPending: a BigInt/Symbol hit ToNumber, the TypeError is already pending — the caller
291
+ // must stop without throwing a second error.
292
+ enum class NodeGiIntResult { kOk, kOutOfRange, kPending };
293
+ inline NodeGiIntResult NodeGiJsToInteger(Napi::Value v, GITypeTag tag, GIArgument* out,
294
+ std::string* shown) {
295
+ const bool is64 = tag == GI_TYPE_TAG_INT64 || tag == GI_TYPE_TAG_UINT64;
296
+ if (is64 && v.IsBigInt()) {
297
+ bool lossless = false;
298
+ if (tag == GI_TYPE_TAG_INT64) {
299
+ out->v_int64 = v.As<Napi::BigInt>().Int64Value(&lossless);
300
+ } else {
301
+ out->v_uint64 = v.As<Napi::BigInt>().Uint64Value(&lossless);
302
+ }
303
+ if (!lossless && shown != nullptr) *shown = "0";
304
+ return lossless ? NodeGiIntResult::kOk : NodeGiIntResult::kOutOfRange;
305
+ }
306
+ Napi::Number n = v.ToNumber();
307
+ if (n.IsEmpty()) return NodeGiIntResult::kPending;
308
+ double d = n.DoubleValue();
309
+ switch (tag) {
310
+ case GI_TYPE_TAG_INT8:
311
+ case GI_TYPE_TAG_UINT8:
312
+ case GI_TYPE_TAG_INT16:
313
+ case GI_TYPE_TAG_UINT16: {
314
+ int32_t i = n.Int32Value();
315
+ int32_t lo = tag == GI_TYPE_TAG_INT8 ? INT8_MIN : tag == GI_TYPE_TAG_INT16 ? INT16_MIN : 0;
316
+ int32_t hi = tag == GI_TYPE_TAG_INT8 ? INT8_MAX : tag == GI_TYPE_TAG_UINT8 ? UINT8_MAX
317
+ : tag == GI_TYPE_TAG_INT16 ? INT16_MAX : UINT16_MAX;
318
+ if (shown != nullptr) *shown = std::to_string(i);
319
+ if (i < lo || i > hi) return NodeGiIntResult::kOutOfRange;
320
+ if (tag == GI_TYPE_TAG_INT8) out->v_int8 = static_cast<int8_t>(i);
321
+ else if (tag == GI_TYPE_TAG_UINT8) out->v_uint8 = static_cast<uint8_t>(i);
322
+ else if (tag == GI_TYPE_TAG_INT16) out->v_int16 = static_cast<int16_t>(i);
323
+ else out->v_uint16 = static_cast<uint16_t>(i);
324
+ return NodeGiIntResult::kOk;
325
+ }
326
+ case GI_TYPE_TAG_INT32:
327
+ out->v_int32 = n.Int32Value();
328
+ return NodeGiIntResult::kOk;
329
+ case GI_TYPE_TAG_UINT32: {
330
+ bool zero = v.IsUndefined() || (v.IsNumber() && !std::isfinite(d));
331
+ if (zero) {
332
+ out->v_uint32 = 0;
333
+ return NodeGiIntResult::kOk;
334
+ }
335
+ if (shown != nullptr) *shown = std::to_string(d);
336
+ if (std::isnan(d) || d < 0 || d > static_cast<double>(UINT32_MAX))
337
+ return NodeGiIntResult::kOutOfRange;
338
+ out->v_uint32 = static_cast<uint32_t>(d);
339
+ return NodeGiIntResult::kOk;
340
+ }
341
+ case GI_TYPE_TAG_INT64: {
342
+ double t = std::isfinite(d) ? std::trunc(d) : 0;
343
+ if (t < -9223372036854775808.0 || t >= 9223372036854775808.0) {
344
+ if (shown != nullptr)
345
+ *shown = std::to_string(t < 0 ? INT64_MIN : INT64_MAX);
346
+ return NodeGiIntResult::kOutOfRange;
347
+ }
348
+ out->v_int64 = static_cast<int64_t>(t);
349
+ return NodeGiIntResult::kOk;
350
+ }
351
+ case GI_TYPE_TAG_UINT64: {
352
+ double t = std::isfinite(d) ? std::trunc(d) : 0;
353
+ if (t < 0 || t >= 18446744073709551616.0) {
354
+ if (shown != nullptr)
355
+ *shown = std::to_string(t < 0 ? uint64_t{0} : UINT64_MAX);
356
+ return NodeGiIntResult::kOutOfRange;
357
+ }
358
+ out->v_uint64 = static_cast<uint64_t>(t);
359
+ return NodeGiIntResult::kOk;
360
+ }
361
+ default:
362
+ return NodeGiIntResult::kOk;
363
+ }
364
+ }
365
+
366
+ enum class NodeGiIntContext { kArgument, kField, kReturnValue };
367
+ inline const char* NodeGiIntTagName(GITypeTag tag) {
368
+ switch (tag) {
369
+ case GI_TYPE_TAG_INT8: return "int8";
370
+ case GI_TYPE_TAG_UINT8: return "uint8";
371
+ case GI_TYPE_TAG_INT16: return "int16";
372
+ case GI_TYPE_TAG_UINT16: return "uint16";
373
+ case GI_TYPE_TAG_INT32: return "int32";
374
+ case GI_TYPE_TAG_UINT32: return "uint32";
375
+ case GI_TYPE_TAG_INT64: return "int64";
376
+ default: return "uint64";
377
+ }
378
+ }
379
+ // GJS's three texts (arg-cache.cpp / arg.cpp), plain Error.
380
+ inline void NodeGiThrowIntOutOfRange(Napi::Env env, GITypeTag tag, NodeGiIntContext ctx,
381
+ const char* name, const std::string& shown) {
382
+ std::string t = NodeGiIntTagName(tag);
383
+ std::string msg;
384
+ if (ctx == NodeGiIntContext::kArgument) {
385
+ msg = std::string("Argument ") + (name != nullptr ? name : "") + ": value is out of range for " + t;
386
+ } else {
387
+ msg = "value " + shown + " is out of range for " +
388
+ (ctx == NodeGiIntContext::kField ? std::string("Field '") + (name ? name : "") + "'"
389
+ : std::string("Return value")) +
390
+ " (type " + t + ")";
391
+ }
392
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
393
+ }
394
+
395
+ // IN array elements and GValue int/uint, GJS-exact: these WRAP instead of refusing.
396
+ // An array element goes through js_value_to_c_strict (refs/gjs/gi/arg.cpp:543), which
397
+ // is ToInt32/ToUint32 truncated to the tag for 8/16/32 bits and ToInt64/ToUint64 —
398
+ // modulo 2^64 — for 64 bits; a BigInt takes ToBigInt64/ToBigUint64 (also modulo 2^64)
399
+ // on a 64-bit tag and is a TypeError on a narrower one. kPending: that TypeError is
400
+ // already pending, the caller must stop without throwing a second error.
401
+ inline uint64_t NodeGiWrapToUint64(double d) {
402
+ if (!std::isfinite(d)) return 0;
403
+ double m = std::fmod(std::trunc(d), 18446744073709551616.0);
404
+ return m < 0 ? uint64_t{0} - static_cast<uint64_t>(-m) : static_cast<uint64_t>(m);
405
+ }
406
+ inline NodeGiIntResult NodeGiJsToIntegerWrap(Napi::Value v, GITypeTag tag, GIArgument* out) {
407
+ const bool is64 = tag == GI_TYPE_TAG_INT64 || tag == GI_TYPE_TAG_UINT64;
408
+ if (is64 && v.IsBigInt()) {
409
+ bool lossless = false;
410
+ out->v_uint64 = v.As<Napi::BigInt>().Uint64Value(&lossless);
411
+ return NodeGiIntResult::kOk;
412
+ }
413
+ Napi::Number n = v.ToNumber();
414
+ if (n.IsEmpty()) return NodeGiIntResult::kPending;
415
+ switch (tag) {
416
+ case GI_TYPE_TAG_INT8: out->v_int8 = static_cast<int8_t>(n.Int32Value()); break;
417
+ case GI_TYPE_TAG_UINT8: out->v_uint8 = static_cast<uint8_t>(n.Uint32Value()); break;
418
+ case GI_TYPE_TAG_INT16: out->v_int16 = static_cast<int16_t>(n.Int32Value()); break;
419
+ case GI_TYPE_TAG_UINT16: out->v_uint16 = static_cast<uint16_t>(n.Uint32Value()); break;
420
+ case GI_TYPE_TAG_INT32: out->v_int32 = n.Int32Value(); break;
421
+ case GI_TYPE_TAG_UINT32: out->v_uint32 = n.Uint32Value(); break;
422
+ case GI_TYPE_TAG_INT64:
423
+ case GI_TYPE_TAG_UINT64: out->v_uint64 = NodeGiWrapToUint64(n.DoubleValue()); break;
424
+ default: break;
425
+ }
426
+ return NodeGiIntResult::kOk;
427
+ }
428
+
429
+ // GValue char/uchar/int64/uint64: refused out of range with gjs_value_to_g_value's own
430
+ // text (refs/gjs/gi/value.cpp:602-690, throw_expect_type), a plain Error. `what` is the
431
+ // "expected" noun gjs prints; the offending value is its JS ToString.
432
+ inline void NodeGiThrowGValueOutOfRange(Napi::Env env, Napi::Value v, const char* what) {
433
+ Napi::String s = v.ToString();
434
+ std::string shown = s.IsEmpty() ? std::string() : s.Utf8Value();
435
+ Napi::Error::New(env, std::string("Wrong type ") + (v.IsBigInt() ? "bigint" : "number") + "; " +
436
+ what + " expected. But it's out of range: " + shown)
437
+ .ThrowAsJavaScriptException();
438
+ }
439
+
281
440
  // OUT (WarnIfUnsafe{Int,Uint}64): GJS ALWAYS returns a JS Number for a 64-bit int
282
441
  // (never a BigInt) and emits this g_warning when the value falls outside the range
283
442
  // a double represents exactly (|v| > 2^53 - 1 == Number.MAX_SAFE_INTEGER), because
@@ -317,13 +476,13 @@ struct CreatedClosures {
317
476
 
318
477
  // ---- C objects this layer SYNTHESISES for one IN argument --------------------
319
478
  //
320
- // Three IN arms build a throw-away C object out of a plain JS value, because the
321
- // JS side has no handle to hand over: a Uint8Array for a `GLib.Bytes` arg, a
322
- // number/string for a `GObject.Value` arg, an L1 `GLib.Error` for a `GError` arg.
323
- // All three carry the SAME lifetime rule, which is why they share one type
324
- // instead of a third near-identical struct: transfer-none → the callee borrowed
325
- // (or copied) it, so we release it after the invoke; transfer-full → the callee
326
- // adopted it, so it is released only when the invoke never ran.
479
+ // Two IN arms build a throw-away C object out of a plain JS value, because the JS
480
+ // side has no handle to hand over: a Uint8Array for a `GLib.Bytes` arg, a
481
+ // number/string for a `GObject.Value` arg. Both carry the SAME lifetime rule,
482
+ // which is why they share one type instead of a second near-identical struct:
483
+ // transfer-none → the callee borrowed (or copied) it, so we release it after the
484
+ // invoke; transfer-full → the callee adopted it, so it is released only when the
485
+ // invoke never ran.
327
486
  template <typename T>
328
487
  struct CreatedIn {
329
488
  std::vector<T*> transferNone; // release after the invoke (callee borrowed/copied)
@@ -353,17 +512,6 @@ using CreatedBytes = CreatedIn<GBytes>;
353
512
  // boxed-handle path and is passed through untouched.
354
513
  using CreatedValues = CreatedIn<GValue>;
355
514
 
356
- // An L1 `GLib.Error` (what a `catch` hands you, and what `new GLib.Error(…)`
357
- // builds) has no `GError*` behind it — it is a JS class, not a boxed handle — so
358
- // a `GError`-typed IN arg gets a fresh `g_error_new_literal` built from its
359
- // domain/code/message. Without this, the hand-back direction the OUT direction
360
- // exists for is only open to errors that came OUT of GI in the first place:
361
- // `GLib.propagate_error(caught)` and `Gst.Message.new_error(src, caught, dbg)`
362
- // both refused where gjs accepts, which is the shape an application actually
363
- // writes. A marshalled GError (a boxed handle) still routes through
364
- // TransferBoxedIn and is not copied here.
365
- using CreatedErrors = CreatedIn<GError>;
366
-
367
515
  // Release a GValue this layer boxed: unset the held value (dropping the ref an
368
516
  // object/boxed/string GValue took) and free the struct. A plain `g_free` would
369
517
  // leak whatever the GValue owns.
@@ -390,9 +538,6 @@ GClosure* NodeGiMakeGenericJsClosure(Napi::Env env, Napi::Value fn);
390
538
  // `values` (optional): enables the JS-value→GValue IN-arg boxing above; nullptr
391
539
  // (the default) keeps the previous behaviour (plain value → TypeError) on paths
392
540
  // that cannot release a created GValue.
393
- // `errors` (optional): enables the L1-GLib.Error→GError IN-arg build above;
394
- // nullptr (the default) keeps a marshalled boxed handle working and refuses the
395
- // L1 class, on paths that cannot release a created GError.
396
541
  // `argName` (optional): the introspected argument name, used ONLY in error
397
542
  // messages so a refusal reads like gjs's ("Expected type string for argument
398
543
  // 'name' but got type number"); nullptr degrades to a message without the name.
@@ -403,8 +548,8 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
403
548
  CreatedClosures* closures = nullptr,
404
549
  CreatedBytes* bytes = nullptr,
405
550
  CreatedValues* values = nullptr,
406
- CreatedErrors* errors = nullptr,
407
- const char* argName = nullptr);
551
+ const char* argName = nullptr,
552
+ NodeGiIntContext intContext = NodeGiIntContext::kArgument);
408
553
 
409
554
  // ---- IN container building -----------------------------------------
410
555
  //
@@ -671,6 +816,7 @@ Napi::Value FindInfo(const Napi::CallbackInfo& info);
671
816
  Napi::Value GetConstantValue(const Napi::CallbackInfo& info);
672
817
  Napi::Value GetEnumValues(const Napi::CallbackInfo& info);
673
818
  Napi::Value GetErrorDomain(const Napi::CallbackInfo& info);
819
+ Napi::Value ErrorDomainInfo(const Napi::CallbackInfo& info);
674
820
  Napi::Value SetErrorBuilder(const Napi::CallbackInfo& info);
675
821
  Napi::Value PrependSearchPath(const Napi::CallbackInfo& info);
676
822
  Napi::Value PrependLibraryPath(const Napi::CallbackInfo& info);
package/src/marshal.cc CHANGED
@@ -143,6 +143,22 @@ static Napi::Value WrapBoxed(Napi::Env env, gpointer ptr, GIBaseInfo* structInfo
143
143
  return MakeBoxedHandle(env, handlePtr, handleGType, owns, info);
144
144
  }
145
145
 
146
+ // See common.h. Wrapped through the GLib.Error STRUCT info rather than by GType
147
+ // alone, so `domain`/`code`/`message` resolve as fields — which is how L1 reads
148
+ // them off the GError*.
149
+ Napi::Value MakeGErrorHandle(Napi::Env env, GError* error, GITransfer transfer) {
150
+ if (error == nullptr) return env.Null();
151
+ GIRepository* repo = DupDefaultRepository();
152
+ // GLib is loaded by any namespace require; require defensively anyway so a bare
153
+ // engine call still resolves the struct info (idempotent, cheap).
154
+ gi_repository_require(repo, "GLib", "2.0", static_cast<GIRepositoryLoadFlags>(0), nullptr);
155
+ GIBaseInfo* errInfo = gi_repository_find_by_name(repo, "GLib", "Error");
156
+ Napi::Value result = WrapBoxed(env, error, errInfo, transfer);
157
+ if (errInfo != nullptr) gi_base_info_unref(errInfo);
158
+ g_object_unref(repo);
159
+ return result;
160
+ }
161
+
146
162
  // Read a boxed handle (ptr + boxed GType) if `v` is one (tag-checked; no deref of
147
163
  // ptr). Returns nullptr when `v` is not a node-gi boxed handle. The returned handle
148
164
  // is owned by the External — never free it. Callers that need the GType (boxed
@@ -472,47 +488,6 @@ static void ThrowExpectedGLibError(Napi::Env env, const char* argName, const std
472
488
  .ThrowAsJavaScriptException();
473
489
  }
474
490
 
475
- // Whether `v` is an instance of the L1 `GLib.Error` class — the shape a `catch`
476
- // around a failed GI invoke hands you, and what `new GLib.Error(domain, code,
477
- // message)` builds. Recognition is `instanceof` against the class L1 registered
478
- // alongside its error builder, never duck-typing: a plain object carrying a
479
- // `code` must still be refused.
480
- static bool IsL1GLibError(Napi::Env env, Napi::Value v) {
481
- if (!v.IsObject()) return false;
482
- NodeGiEnvData* d = EnvData(env);
483
- if (d == nullptr || d->errorClass == nullptr) return false;
484
- napi_value cls = nullptr;
485
- if (napi_get_reference_value(env, d->errorClass, &cls) != napi_ok || cls == nullptr) return false;
486
- Napi::Value clsValue(env, cls);
487
- return clsValue.IsFunction() && v.As<Napi::Object>().InstanceOf(clsValue.As<Napi::Function>());
488
- }
489
-
490
- // Build a real GError out of an L1 `GLib.Error`, which carries no `GError*` of
491
- // its own. Throws + returns nullptr when its domain is unusable.
492
- //
493
- // `.domainQuark` is the numeric GQuark the engine read off the original GError;
494
- // `.domain` is its NAME (the node-gi divergence documented in
495
- // docs/node-gi-gjs-surface.md), which is what an error constructed in JS from a
496
- // quark name carries. Either resolves to the same quark; a value with neither is
497
- // not a usable GError — quark 0 would produce an error no `matches()` can ever
498
- // answer for, so it is refused rather than marshalled into a silent mismatch.
499
- static GError* ErrorFromL1GLibError(Napi::Env env, Napi::Value v, const char* argName) {
500
- Napi::Object err = v.As<Napi::Object>();
501
- Napi::Value quark = err.Get("domainQuark");
502
- Napi::Value domain = err.Get("domain");
503
- GQuark gq = 0;
504
- if (quark.IsNumber()) gq = static_cast<GQuark>(NodeGiToUint32(quark));
505
- else if (domain.IsString())
506
- gq = g_quark_from_string(domain.As<Napi::String>().Utf8Value().c_str());
507
- if (gq == 0) {
508
- ThrowExpectedGLibError(env, argName, "a GLib.Error with no domain");
509
- return nullptr;
510
- }
511
- Napi::Value message = err.Get("message");
512
- std::string text = message.IsString() ? message.As<Napi::String>().Utf8Value() : std::string();
513
- return g_error_new_literal(gq, NodeGiToInt32(err.Get("code")), text.c_str());
514
- }
515
-
516
491
  // "Gtk.Box" for an introspected GType, g_type_name() otherwise (a registerClass
517
492
  // subtype has no GI info of its own; its C name is still exact). The gjs twin is
518
493
  // GIWrapperBase::format_name() (refs/gjs/gi/wrapperutils.h).
@@ -541,8 +516,8 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
541
516
  CreatedClosures* closures,
542
517
  CreatedBytes* bytes,
543
518
  CreatedValues* values,
544
- CreatedErrors* errors,
545
- const char* argName) {
519
+ const char* argName,
520
+ NodeGiIntContext intContext) {
546
521
  if (v.IsEmpty()) {
547
522
  // Residue of a swallowed napi failure (a fallible Get()/coercion upstream
548
523
  // failed on a terminating env, or a throwing getter left the exception
@@ -562,16 +537,24 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
562
537
  GITypeTag tag = gi_type_info_get_tag(type);
563
538
  switch (tag) {
564
539
  case GI_TYPE_TAG_BOOLEAN: out->v_boolean = NodeGiToBool(v); return true;
565
- case GI_TYPE_TAG_INT8: out->v_int8 = static_cast<int8_t>(NodeGiToInt32(v)); return true;
566
- case GI_TYPE_TAG_UINT8: out->v_uint8 = static_cast<uint8_t>(NodeGiToUint32(v)); return true;
567
- case GI_TYPE_TAG_INT16: out->v_int16 = static_cast<int16_t>(NodeGiToInt32(v)); return true;
568
- case GI_TYPE_TAG_UINT16: out->v_uint16 = static_cast<uint16_t>(NodeGiToUint32(v)); return true;
569
- case GI_TYPE_TAG_INT32: out->v_int32 = NodeGiToInt32(v); return true;
570
- case GI_TYPE_TAG_UINT32: out->v_uint32 = NodeGiToUint32(v); return true;
571
- // 64-bit: accept a BigInt losslessly (else a Number, truncated) — never let a
572
- // BigInt reach ToNumber() (fatal abort). See JsValueTo{Int,Uint}64 in common.h.
573
- case GI_TYPE_TAG_INT64: out->v_int64 = JsValueToInt64(v); return true;
574
- case GI_TYPE_TAG_UINT64: out->v_uint64 = JsValueToUint64(v); return true;
540
+ case GI_TYPE_TAG_INT8:
541
+ case GI_TYPE_TAG_UINT8:
542
+ case GI_TYPE_TAG_INT16:
543
+ case GI_TYPE_TAG_UINT16:
544
+ case GI_TYPE_TAG_INT32:
545
+ case GI_TYPE_TAG_UINT32:
546
+ case GI_TYPE_TAG_INT64:
547
+ case GI_TYPE_TAG_UINT64: {
548
+ std::string shown;
549
+ switch (NodeGiJsToInteger(v, tag, out, &shown)) {
550
+ case NodeGiIntResult::kOk: return true;
551
+ case NodeGiIntResult::kPending: return false;
552
+ case NodeGiIntResult::kOutOfRange:
553
+ NodeGiThrowIntOutOfRange(env, tag, intContext, argName, shown);
554
+ return false;
555
+ }
556
+ return false;
557
+ }
575
558
  case GI_TYPE_TAG_FLOAT: out->v_float = static_cast<float>(NodeGiToDouble(v)); return true;
576
559
  case GI_TYPE_TAG_DOUBLE: out->v_double = NodeGiToDouble(v); return true;
577
560
  case GI_TYPE_TAG_UTF8:
@@ -829,43 +812,35 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
829
812
  out->v_pointer = nullptr;
830
813
  return true;
831
814
  }
832
- // Shape 1: the boxed handle a GError-typed return/OUT produced. Ownership is
833
- // the ordinary boxed one and TransferBoxedIn supplies it: transfer-none is a
834
- // borrow, transfer-full hands the callee an independent g_error_copy
835
- // (g_propagate_error ADOPTS and frees its `src`) so the JS handle's finalizer
836
- // cannot double-free it. Measured against gjs: after `GLib.propagate_error(e)`
837
- // the original `e` is still readable there too.
815
+ // There is exactly ONE shape to accept: the boxed handle every GError reaching
816
+ // JS carries, whether it was marshalled out, copied, constructed or thrown
817
+ // (L1's wrapGError puts the same handle on all four). The second arm this used
818
+ // to need — rebuilding a GError from an L1 Error subclass's JS fields — is gone
819
+ // with the subclass itself.
820
+ //
821
+ // Ownership is the ordinary boxed one and TransferBoxedIn supplies it:
822
+ // transfer-none is a borrow, transfer-full hands the callee an independent
823
+ // g_error_copy (g_propagate_error ADOPTS and frees its `src`) so the JS
824
+ // handle's finalizer cannot double-free it. Measured against gjs: after
825
+ // `GLib.propagate_error(e)` the original `e` is still readable there too.
838
826
  //
839
827
  // Type-safety is UNCONDITIONAL here, unlike the struct/boxed arm's "compare
840
828
  // only when both sides carry a registered GType": the expected type is known
841
829
  // (G_TYPE_ERROR, always), so a handle without it is a wrong pointer — and a
842
830
  // wrong pointer in a GError slot is a wild dereference inside GLib.
843
831
  BoxedHandle* h = TryGetBoxedHandle(v);
844
- if (h != nullptr) {
845
- if (h->gtype == G_TYPE_INVALID || !g_type_is_a(h->gtype, G_TYPE_ERROR)) {
846
- ThrowExpectedGLibError(env, argName,
847
- h->gtype != G_TYPE_INVALID ? g_type_name(h->gtype)
848
- : InformalValueTypeName(v));
849
- return false;
850
- }
851
- out->v_pointer = TransferBoxedIn(h, transfer);
852
- return true;
832
+ if (h == nullptr) {
833
+ ThrowExpectedGLibError(env, argName, InformalValueTypeName(v));
834
+ return false;
853
835
  }
854
- // Shape 2: the L1 `GLib.Error` JS class — a caught error, or one built with
855
- // `new GLib.Error(…)`. It carries no GError*, so build one and release it per
856
- // transfer (CreatedErrors, the rule GBytes and GValue already follow). gjs has
857
- // ONE object for both shapes and accepts either; refusing this one left the
858
- // hand-back the OUT direction exists for closed to exactly the errors an
859
- // application has in hand.
860
- if (errors != nullptr && IsL1GLibError(env, v)) {
861
- GError* built = ErrorFromL1GLibError(env, v, argName);
862
- if (built == nullptr) return false; // ErrorFromL1GLibError already threw
863
- out->v_pointer = built;
864
- errors->Record(built, transfer);
865
- return true;
836
+ if (h->gtype == G_TYPE_INVALID || !g_type_is_a(h->gtype, G_TYPE_ERROR)) {
837
+ ThrowExpectedGLibError(
838
+ env, argName,
839
+ h->gtype != G_TYPE_INVALID ? g_type_name(h->gtype) : InformalValueTypeName(v));
840
+ return false;
866
841
  }
867
- ThrowExpectedGLibError(env, argName, InformalValueTypeName(v));
868
- return false;
842
+ out->v_pointer = TransferBoxedIn(h, transfer);
843
+ return true;
869
844
  }
870
845
  default:
871
846
  Napi::TypeError::New(
@@ -995,23 +970,10 @@ Napi::Value GIArgumentToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
995
970
  }
996
971
  case GI_TYPE_TAG_ERROR: {
997
972
  // A GError-typed value (a `GLib.Error` return like Gtk.GLArea.get_error(),
998
- // or a GLib.Error.new_literal construct): GJS surfaces it as a GLib.Error
999
- // boxed (G_TYPE_ERROR) with `.domain`/`.code`/`.message` field access +
1000
- // `.matches()`/`.copy()` methods. Wrap through the GLib.Error struct info
1001
- // so the boxed handle resolves fields and methods; a NULL GError (the
1002
- // no-error case) stays null. WrapBoxed handles transfer: a transfer-none
1003
- // borrow is g_boxed_copy'd into an owned copy, transfer-full is adopted.
1004
- GError* gerr = static_cast<GError*>(arg->v_pointer);
1005
- if (gerr == nullptr) return env.Null();
1006
- GIRepository* repo = DupDefaultRepository();
1007
- // GLib is loaded by any namespace require; require defensively anyway so a
1008
- // bare engine call still resolves the struct info (idempotent, cheap).
1009
- gi_repository_require(repo, "GLib", "2.0", static_cast<GIRepositoryLoadFlags>(0), nullptr);
1010
- GIBaseInfo* errInfo = gi_repository_find_by_name(repo, "GLib", "Error");
1011
- Napi::Value result = WrapBoxed(env, gerr, errInfo, transfer);
1012
- if (errInfo != nullptr) gi_base_info_unref(errInfo);
1013
- g_object_unref(repo);
1014
- return result;
973
+ // or a GLib.Error.new_literal construct). The same handle a THROWN error
974
+ // carries, so L1 builds the same object from both — a NULL GError (the
975
+ // no-error case) stays null.
976
+ return MakeGErrorHandle(env, static_cast<GError*>(arg->v_pointer), transfer);
1015
977
  }
1016
978
  default:
1017
979
  Napi::TypeError::New(env, "Unsupported return type tag " +
@@ -1639,21 +1601,36 @@ Napi::Value ReadOutOrReturn(Napi::Env env, GICallableInfo* callable, GITypeInfo*
1639
1601
  // the element, so it gets a ref (objects) or an independent copy (boxed) instead.
1640
1602
  // Throws + returns false on an unsupported element.
1641
1603
  static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v, GIArgument* a,
1642
- GITransfer elemTransfer) {
1604
+ GITransfer elemTransfer, bool wrapIntegers = false) {
1643
1605
  memset(a, 0, sizeof(*a));
1644
1606
  switch (gi_type_info_get_tag(elem)) {
1645
1607
  // Scalar coercions via the terminate-safe helpers (common.h): a swallowed
1646
1608
  // napi failure mid worker.terminate() must degrade to a zero, not cascade
1647
1609
  // into Error::New(nullptr)'s fatal sites.
1648
1610
  case GI_TYPE_TAG_BOOLEAN: a->v_boolean = NodeGiToBool(v); return true;
1649
- case GI_TYPE_TAG_INT8: a->v_int8 = static_cast<gint8>(NodeGiToInt32(v)); return true;
1650
- case GI_TYPE_TAG_UINT8: a->v_uint8 = static_cast<guint8>(NodeGiToUint32(v)); return true;
1651
- case GI_TYPE_TAG_INT16: a->v_int16 = static_cast<gint16>(NodeGiToInt32(v)); return true;
1652
- case GI_TYPE_TAG_UINT16: a->v_uint16 = static_cast<guint16>(NodeGiToUint32(v)); return true;
1653
- case GI_TYPE_TAG_INT32: a->v_int32 = NodeGiToInt32(v); return true;
1654
- case GI_TYPE_TAG_UINT32: a->v_uint32 = NodeGiToUint32(v); return true;
1655
- case GI_TYPE_TAG_INT64: a->v_int64 = JsValueToInt64(v); return true;
1656
- case GI_TYPE_TAG_UINT64: a->v_uint64 = JsValueToUint64(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.
1614
+ case GI_TYPE_TAG_INT8:
1615
+ case GI_TYPE_TAG_UINT8:
1616
+ case GI_TYPE_TAG_INT16:
1617
+ case GI_TYPE_TAG_UINT16:
1618
+ case GI_TYPE_TAG_INT32:
1619
+ case GI_TYPE_TAG_UINT32:
1620
+ case GI_TYPE_TAG_INT64:
1621
+ case GI_TYPE_TAG_UINT64:
1622
+ if (wrapIntegers)
1623
+ 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;
1633
+ }
1657
1634
  case GI_TYPE_TAG_FLOAT: a->v_float = static_cast<gfloat>(NodeGiToDouble(v)); return true;
1658
1635
  case GI_TYPE_TAG_DOUBLE: a->v_double = NodeGiToDouble(v); return true;
1659
1636
  case GI_TYPE_TAG_GTYPE: {
@@ -1861,7 +1838,10 @@ bool JsToAnswerArgument(Napi::Env env, Napi::Value v, GITypeInfo* ti, GITransfer
1861
1838
  }
1862
1839
  std::string held;
1863
1840
  return JsToGIArgument(env, v, ti, out, &held, transfer, nullptr, nullptr, nullptr, nullptr,
1864
- nullptr, argName);
1841
+ argName,
1842
+ argName != nullptr && strcmp(argName, "return value") == 0
1843
+ ? NodeGiIntContext::kReturnValue
1844
+ : NodeGiIntContext::kArgument);
1865
1845
  }
1866
1846
 
1867
1847
  } // namespace
@@ -2320,7 +2300,8 @@ static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfe
2320
2300
  continue;
2321
2301
  }
2322
2302
  GIArgument a;
2323
- ok = ElementToGIArgument(env, elem, arr.Get(static_cast<uint32_t>(i)), &a, elemTransfer);
2303
+ ok = ElementToGIArgument(env, elem, arr.Get(static_cast<uint32_t>(i)), &a, elemTransfer,
2304
+ /*wrapIntegers=*/true);
2324
2305
  if (!ok) break;
2325
2306
  // Copy the element's storage bytes (LE: the low elemSize bytes of the union
2326
2307
  // alias the active field — v_string/v_pointer for pointers, the scalar
@@ -3020,7 +3001,8 @@ Napi::Value SetBoxedField(const Napi::CallbackInfo& info) {
3020
3001
  GIArgument arg;
3021
3002
  memset(&arg, 0, sizeof(arg));
3022
3003
  std::string heldString;
3023
- if (JsToGIArgument(env, value, ftype, &arg, &heldString, GI_TRANSFER_NOTHING, nullptr)) {
3004
+ if (JsToGIArgument(env, value, ftype, &arg, &heldString, GI_TRANSFER_NOTHING, nullptr,
3005
+ nullptr, nullptr, nullptr, name.c_str(), NodeGiIntContext::kField)) {
3024
3006
  if (!gi_field_info_set_field(field, bh->ptr, &arg)) {
3025
3007
  Napi::Error::New(env, std::string("Writing field ") + typeName + "." + name + " is not supported")
3026
3008
  .ThrowAsJavaScriptException();