@gjsify/node-gi 0.64.0 → 0.64.2

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/calls.cc CHANGED
@@ -1242,6 +1242,57 @@ static GIFunctionInfo* ResolveInstanceMethod(GIRepository* repo, GType gtype, GI
1242
1242
  return func;
1243
1243
  }
1244
1244
 
1245
+ // Resolved instance methods, keyed by the instance's concrete GType and the name
1246
+ // as JS spelled it. The resolution above is the expensive half of a GI call —
1247
+ // dup the default repository, walk the GType parents for the nearest
1248
+ // GIObjectInfo, then search own + interface + inherited method tables twice (the
1249
+ // literal name, then the snake_case alias) — and it repeats IDENTICALLY on every
1250
+ // call, because a typelib's method tables do not change at runtime. MEASURED on
1251
+ // this machine (gjs 1.88.1 / GLib 2.88.3): `set_property` on a registerClass
1252
+ // subclass cost 8224 ns through callMethod against 2815 ns for the same
1253
+ // g_object_set_property through setProperty — 5.4 µs of pure re-resolution per
1254
+ // call, which is most of what a property write on the real-GObject engine spends.
1255
+ //
1256
+ // Only a HIT is cached. A miss must stay uncached: namespaces load lazily, so a
1257
+ // name that resolves to nothing now can resolve after the typelib that declares
1258
+ // it is required — caching the miss would make load order change behaviour.
1259
+ //
1260
+ // The cache OWNS the ref it holds (GIFunctionInfos live as long as the process
1261
+ // anyway, and the entry count is bounded by the methods a program actually
1262
+ // calls); callers get a borrowed pointer and must not unref it. Written only
1263
+ // from the JS thread, like every other entry point here.
1264
+ static std::map<std::pair<GType, std::string>, GIFunctionInfo*> g_instanceMethodCache;
1265
+
1266
+ // The resolution of ResolveInstanceMethod, memoized. Returns a BORROWED pointer
1267
+ // (never unref) or nullptr.
1268
+ static GIFunctionInfo* ResolveInstanceMethodCached(GType gtype, const std::string& method) {
1269
+ const std::pair<GType, std::string> key(gtype, method);
1270
+ auto it = g_instanceMethodCache.find(key);
1271
+ if (it != g_instanceMethodCache.end()) return it->second;
1272
+
1273
+ GIRepository* repo = DupDefaultRepository();
1274
+ GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1275
+ GIFunctionInfo* func = nullptr;
1276
+ if (objInfo != nullptr) {
1277
+ func = ResolveInstanceMethod(repo, gtype, objInfo, method);
1278
+ gi_base_info_unref(objInfo);
1279
+ }
1280
+ g_object_unref(repo);
1281
+ if (func != nullptr) g_instanceMethodCache.emplace(key, func);
1282
+ return func;
1283
+ }
1284
+
1285
+ // Whether `gtype` has introspection info at all — the distinction callMethod
1286
+ // reports as "no introspection info for X" before it even looks for a method.
1287
+ static bool HasObjectInfo(GType gtype) {
1288
+ GIRepository* repo = DupDefaultRepository();
1289
+ GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1290
+ const bool found = objInfo != nullptr;
1291
+ if (objInfo != nullptr) gi_base_info_unref(objInfo);
1292
+ g_object_unref(repo);
1293
+ return found;
1294
+ }
1295
+
1245
1296
  // callMethod(handle, methodName, args?: unknown[]) -> unknown
1246
1297
  // Resolve an instance method by walking the instance's GIObjectInfo (own +
1247
1298
  // implemented-interface methods at each level, then up the parent chain), then
@@ -1262,20 +1313,14 @@ Napi::Value CallMethod(const Napi::CallbackInfo& info) {
1262
1313
  : Napi::Array::New(env, 0);
1263
1314
 
1264
1315
  GType gtype = G_OBJECT_TYPE(obj);
1265
- GIRepository* repo = DupDefaultRepository();
1316
+ GIFunctionInfo* func = ResolveInstanceMethodCached(gtype, method);
1266
1317
 
1267
- GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1268
- if (objInfo == nullptr) {
1269
- g_object_unref(repo);
1318
+ if (func == nullptr && !HasObjectInfo(gtype)) {
1270
1319
  Napi::Error::New(env, std::string("no introspection info for ") + G_OBJECT_TYPE_NAME(obj))
1271
1320
  .ThrowAsJavaScriptException();
1272
1321
  return env.Null();
1273
1322
  }
1274
1323
 
1275
- GIFunctionInfo* func = ResolveInstanceMethod(repo, gtype, objInfo, method);
1276
- gi_base_info_unref(objInfo);
1277
- g_object_unref(repo);
1278
-
1279
1324
  if (func == nullptr) {
1280
1325
  // On a terminating env the method-name read (info[1].Utf8Value) degrades to
1281
1326
  // "" (a swallowed napi failure), so resolution misses and we land here — but
@@ -1290,15 +1335,13 @@ Napi::Value CallMethod(const Napi::CallbackInfo& info) {
1290
1335
  return env.Null();
1291
1336
  }
1292
1337
  if (!gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func))) {
1293
- gi_base_info_unref(func);
1294
1338
  Napi::TypeError::New(env, method + " is not an instance method").ThrowAsJavaScriptException();
1295
1339
  return env.Null();
1296
1340
  }
1297
1341
 
1298
- Napi::Value result =
1299
- InvokeFunctionInfo(env, func, obj, args, std::string(G_OBJECT_TYPE_NAME(obj)) + "." + method);
1300
- gi_base_info_unref(func);
1301
- return result;
1342
+ // The cache owns `func`; this is a borrowed pointer.
1343
+ return InvokeFunctionInfo(env, func, obj, args,
1344
+ std::string(G_OBJECT_TYPE_NAME(obj)) + "." + method);
1302
1345
  }
1303
1346
 
1304
1347
  // hasMethod(handle, methodName) -> boolean
@@ -1321,20 +1364,10 @@ Napi::Value HasMethod(const Napi::CallbackInfo& info) {
1321
1364
  if (obj == nullptr) return env.Null();
1322
1365
  std::string method = info[1].As<Napi::String>().Utf8Value();
1323
1366
 
1324
- GType gtype = G_OBJECT_TYPE(obj);
1325
- GIRepository* repo = DupDefaultRepository();
1326
- GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1327
- if (objInfo == nullptr) {
1328
- g_object_unref(repo);
1329
- return Napi::Boolean::New(env, false);
1330
- }
1331
- GIFunctionInfo* func = ResolveInstanceMethod(repo, gtype, objInfo, method);
1332
- gi_base_info_unref(objInfo);
1333
- g_object_unref(repo);
1367
+ GIFunctionInfo* func = ResolveInstanceMethodCached(G_OBJECT_TYPE(obj), method);
1334
1368
  if (func == nullptr) return Napi::Boolean::New(env, false);
1335
- const bool invocable = gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func));
1336
- gi_base_info_unref(func);
1337
- return Napi::Boolean::New(env, invocable);
1369
+ return Napi::Boolean::New(env,
1370
+ gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func)));
1338
1371
  }
1339
1372
 
1340
1373
  // hasClassMethod(namespace, typeName, methodName) -> boolean
package/src/class.cc CHANGED
@@ -44,6 +44,8 @@ static GType UnwrapGType(Napi::Env env, Napi::Value v) {
44
44
  static GType TypeNameToGType(const std::string& t) {
45
45
  if (t == "string" || t == "utf8") return G_TYPE_STRING;
46
46
  if (t == "boolean" || t == "bool") return G_TYPE_BOOLEAN;
47
+ if (t == "char") return G_TYPE_CHAR;
48
+ if (t == "uchar") return G_TYPE_UCHAR;
47
49
  if (t == "int") return G_TYPE_INT;
48
50
  if (t == "uint") return G_TYPE_UINT;
49
51
  if (t == "int64") return G_TYPE_INT64;
@@ -95,6 +97,19 @@ static GParamSpec* BuildParamSpec(Napi::Env env, Napi::Object spec, std::string*
95
97
  : (def.IsNumber() ? NodeGiToBool(def) : FALSE);
96
98
  return g_param_spec_boolean(nm, nm, nm, d, flags);
97
99
  }
100
+ if (type == "char") {
101
+ return g_param_spec_char(nm, nm, nm, hasMin ? static_cast<gint8>(mn) : G_MININT8,
102
+ hasMax ? static_cast<gint8>(mx) : G_MAXINT8,
103
+ def.IsNumber() ? static_cast<gint8>(def.As<Napi::Number>().Int32Value()) : 0,
104
+ flags);
105
+ }
106
+ if (type == "uchar") {
107
+ return g_param_spec_uchar(nm, nm, nm, hasMin ? static_cast<guint8>(mn) : 0,
108
+ hasMax ? static_cast<guint8>(mx) : G_MAXUINT8,
109
+ def.IsNumber() ? static_cast<guint8>(def.As<Napi::Number>().Uint32Value())
110
+ : 0,
111
+ flags);
112
+ }
98
113
  if (type == "int") {
99
114
  return g_param_spec_int(nm, nm, nm, hasMin ? static_cast<gint>(mn) : G_MININT,
100
115
  hasMax ? static_cast<gint>(mx) : G_MAXINT,
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>
@@ -280,6 +281,180 @@ inline uint64_t JsValueToUint64(Napi::Value v) {
280
281
  return n.IsEmpty() ? 0 : static_cast<uint64_t>(n.Int64Value());
281
282
  }
282
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, kListElement, kHashElement };
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
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")) +
393
+ " (type " + t + ")";
394
+ }
395
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
396
+ }
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
+
413
+ // IN array elements and GValue int/uint, GJS-exact: these WRAP instead of refusing.
414
+ // An array element goes through js_value_to_c_strict (refs/gjs/gi/arg.cpp:543), which
415
+ // is ToInt32/ToUint32 truncated to the tag for 8/16/32 bits and ToInt64/ToUint64 —
416
+ // modulo 2^64 — for 64 bits; a BigInt takes ToBigInt64/ToBigUint64 (also modulo 2^64)
417
+ // on a 64-bit tag and is a TypeError on a narrower one. kPending: that TypeError is
418
+ // already pending, the caller must stop without throwing a second error.
419
+ inline uint64_t NodeGiWrapToUint64(double d) {
420
+ if (!std::isfinite(d)) return 0;
421
+ double m = std::fmod(std::trunc(d), 18446744073709551616.0);
422
+ return m < 0 ? uint64_t{0} - static_cast<uint64_t>(-m) : static_cast<uint64_t>(m);
423
+ }
424
+ inline NodeGiIntResult NodeGiJsToIntegerWrap(Napi::Value v, GITypeTag tag, GIArgument* out) {
425
+ const bool is64 = tag == GI_TYPE_TAG_INT64 || tag == GI_TYPE_TAG_UINT64;
426
+ if (is64 && v.IsBigInt()) {
427
+ bool lossless = false;
428
+ out->v_uint64 = v.As<Napi::BigInt>().Uint64Value(&lossless);
429
+ return NodeGiIntResult::kOk;
430
+ }
431
+ Napi::Number n = v.ToNumber();
432
+ if (n.IsEmpty()) return NodeGiIntResult::kPending;
433
+ switch (tag) {
434
+ case GI_TYPE_TAG_INT8: out->v_int8 = static_cast<int8_t>(n.Int32Value()); break;
435
+ case GI_TYPE_TAG_UINT8: out->v_uint8 = static_cast<uint8_t>(n.Uint32Value()); break;
436
+ case GI_TYPE_TAG_INT16: out->v_int16 = static_cast<int16_t>(n.Int32Value()); break;
437
+ case GI_TYPE_TAG_UINT16: out->v_uint16 = static_cast<uint16_t>(n.Uint32Value()); break;
438
+ case GI_TYPE_TAG_INT32: out->v_int32 = n.Int32Value(); break;
439
+ case GI_TYPE_TAG_UINT32: out->v_uint32 = n.Uint32Value(); break;
440
+ case GI_TYPE_TAG_INT64:
441
+ case GI_TYPE_TAG_UINT64: out->v_uint64 = NodeGiWrapToUint64(n.DoubleValue()); break;
442
+ default: break;
443
+ }
444
+ return NodeGiIntResult::kOk;
445
+ }
446
+
447
+ // GValue char/uchar/int64/uint64: refused out of range with gjs_value_to_g_value's own
448
+ // text (refs/gjs/gi/value.cpp:602-690, throw_expect_type), a plain Error. `what` is the
449
+ // "expected" noun gjs prints; the offending value is its JS ToString.
450
+ inline void NodeGiThrowGValueOutOfRange(Napi::Env env, Napi::Value v, const char* what) {
451
+ Napi::String s = v.ToString();
452
+ std::string shown = s.IsEmpty() ? std::string() : s.Utf8Value();
453
+ Napi::Error::New(env, std::string("Wrong type ") + (v.IsBigInt() ? "bigint" : "number") + "; " +
454
+ what + " expected. But it's out of range: " + shown)
455
+ .ThrowAsJavaScriptException();
456
+ }
457
+
283
458
  // OUT (WarnIfUnsafe{Int,Uint}64): GJS ALWAYS returns a JS Number for a 64-bit int
284
459
  // (never a BigInt) and emits this g_warning when the value falls outside the range
285
460
  // a double represents exactly (|v| > 2^53 - 1 == Number.MAX_SAFE_INTEGER), because
@@ -391,7 +566,8 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
391
566
  CreatedClosures* closures = nullptr,
392
567
  CreatedBytes* bytes = nullptr,
393
568
  CreatedValues* values = nullptr,
394
- const char* argName = nullptr);
569
+ const char* argName = nullptr,
570
+ NodeGiIntContext intContext = NodeGiIntContext::kArgument);
395
571
 
396
572
  // ---- IN container building -----------------------------------------
397
573
  //
package/src/marshal.cc CHANGED
@@ -516,7 +516,8 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
516
516
  CreatedClosures* closures,
517
517
  CreatedBytes* bytes,
518
518
  CreatedValues* values,
519
- const char* argName) {
519
+ const char* argName,
520
+ NodeGiIntContext intContext) {
520
521
  if (v.IsEmpty()) {
521
522
  // Residue of a swallowed napi failure (a fallible Get()/coercion upstream
522
523
  // failed on a terminating env, or a throwing getter left the exception
@@ -536,16 +537,24 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
536
537
  GITypeTag tag = gi_type_info_get_tag(type);
537
538
  switch (tag) {
538
539
  case GI_TYPE_TAG_BOOLEAN: out->v_boolean = NodeGiToBool(v); return true;
539
- case GI_TYPE_TAG_INT8: out->v_int8 = static_cast<int8_t>(NodeGiToInt32(v)); return true;
540
- case GI_TYPE_TAG_UINT8: out->v_uint8 = static_cast<uint8_t>(NodeGiToUint32(v)); return true;
541
- case GI_TYPE_TAG_INT16: out->v_int16 = static_cast<int16_t>(NodeGiToInt32(v)); return true;
542
- case GI_TYPE_TAG_UINT16: out->v_uint16 = static_cast<uint16_t>(NodeGiToUint32(v)); return true;
543
- case GI_TYPE_TAG_INT32: out->v_int32 = NodeGiToInt32(v); return true;
544
- case GI_TYPE_TAG_UINT32: out->v_uint32 = NodeGiToUint32(v); return true;
545
- // 64-bit: accept a BigInt losslessly (else a Number, truncated) — never let a
546
- // BigInt reach ToNumber() (fatal abort). See JsValueTo{Int,Uint}64 in common.h.
547
- case GI_TYPE_TAG_INT64: out->v_int64 = JsValueToInt64(v); return true;
548
- 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
+ }
549
558
  case GI_TYPE_TAG_FLOAT: out->v_float = static_cast<float>(NodeGiToDouble(v)); return true;
550
559
  case GI_TYPE_TAG_DOUBLE: out->v_double = NodeGiToDouble(v); return true;
551
560
  case GI_TYPE_TAG_UTF8:
@@ -1051,6 +1060,8 @@ static bool IsSupportedElementType(GITypeInfo* elem, std::string* why, bool byVa
1051
1060
  case GI_TYPE_TAG_UTF8:
1052
1061
  case GI_TYPE_TAG_FILENAME:
1053
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;
1054
1065
  case GI_TYPE_TAG_INTERFACE: {
1055
1066
  GIBaseInfo* iface = gi_type_info_get_interface(elem);
1056
1067
  bool ok = iface != nullptr && (GI_IS_OBJECT_INFO(iface) || GI_IS_INTERFACE_INFO(iface));
@@ -1169,7 +1180,8 @@ size_t CElementSize(GITypeInfo* elem) {
1169
1180
  case GI_TYPE_TAG_INT16:
1170
1181
  case GI_TYPE_TAG_UINT16: return 2;
1171
1182
  case GI_TYPE_TAG_INT32:
1172
- case GI_TYPE_TAG_UINT32: return 4;
1183
+ case GI_TYPE_TAG_UINT32:
1184
+ case GI_TYPE_TAG_UNICHAR: return 4;
1173
1185
  case GI_TYPE_TAG_INT64:
1174
1186
  case GI_TYPE_TAG_UINT64: return 8;
1175
1187
  case GI_TYPE_TAG_FLOAT: return sizeof(gfloat);
@@ -1592,21 +1604,40 @@ Napi::Value ReadOutOrReturn(Napi::Env env, GICallableInfo* callable, GITypeInfo*
1592
1604
  // the element, so it gets a ref (objects) or an independent copy (boxed) instead.
1593
1605
  // Throws + returns false on an unsupported element.
1594
1606
  static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v, GIArgument* a,
1595
- GITransfer elemTransfer) {
1607
+ GITransfer elemTransfer, NodeGiIntContext intContext = NodeGiIntContext::kListElement,
1608
+ bool wrapIntegers = false) {
1596
1609
  memset(a, 0, sizeof(*a));
1597
1610
  switch (gi_type_info_get_tag(elem)) {
1598
1611
  // Scalar coercions via the terminate-safe helpers (common.h): a swallowed
1599
1612
  // napi failure mid worker.terminate() must degrade to a zero, not cascade
1600
1613
  // into Error::New(nullptr)'s fatal sites.
1601
1614
  case GI_TYPE_TAG_BOOLEAN: a->v_boolean = NodeGiToBool(v); return true;
1602
- case GI_TYPE_TAG_INT8: a->v_int8 = static_cast<gint8>(NodeGiToInt32(v)); return true;
1603
- case GI_TYPE_TAG_UINT8: a->v_uint8 = static_cast<guint8>(NodeGiToUint32(v)); return true;
1604
- case GI_TYPE_TAG_INT16: a->v_int16 = static_cast<gint16>(NodeGiToInt32(v)); return true;
1605
- case GI_TYPE_TAG_UINT16: a->v_uint16 = static_cast<guint16>(NodeGiToUint32(v)); return true;
1606
- case GI_TYPE_TAG_INT32: a->v_int32 = NodeGiToInt32(v); return true;
1607
- case GI_TYPE_TAG_UINT32: a->v_uint32 = NodeGiToUint32(v); return true;
1608
- case GI_TYPE_TAG_INT64: a->v_int64 = JsValueToInt64(v); return true;
1609
- case GI_TYPE_TAG_UINT64: a->v_uint64 = JsValueToUint64(v); return true;
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.
1617
+ case GI_TYPE_TAG_INT8:
1618
+ case GI_TYPE_TAG_UINT8:
1619
+ case GI_TYPE_TAG_INT16:
1620
+ case GI_TYPE_TAG_UINT16:
1621
+ case GI_TYPE_TAG_INT32:
1622
+ case GI_TYPE_TAG_UINT32:
1623
+ case GI_TYPE_TAG_INT64:
1624
+ case GI_TYPE_TAG_UINT64:
1625
+ if (wrapIntegers)
1626
+ return NodeGiJsToIntegerWrap(v, gi_type_info_get_tag(elem), a) == NodeGiIntResult::kOk;
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;
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;
1610
1641
  case GI_TYPE_TAG_FLOAT: a->v_float = static_cast<gfloat>(NodeGiToDouble(v)); return true;
1611
1642
  case GI_TYPE_TAG_DOUBLE: a->v_double = NodeGiToDouble(v); return true;
1612
1643
  case GI_TYPE_TAG_GTYPE: {
@@ -1814,7 +1845,10 @@ bool JsToAnswerArgument(Napi::Env env, Napi::Value v, GITypeInfo* ti, GITransfer
1814
1845
  }
1815
1846
  std::string held;
1816
1847
  return JsToGIArgument(env, v, ti, out, &held, transfer, nullptr, nullptr, nullptr, nullptr,
1817
- argName);
1848
+ argName,
1849
+ argName != nullptr && strcmp(argName, "return value") == 0
1850
+ ? NodeGiIntContext::kReturnValue
1851
+ : NodeGiIntContext::kArgument);
1818
1852
  }
1819
1853
 
1820
1854
  } // namespace
@@ -2240,6 +2274,20 @@ static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfe
2240
2274
  return true;
2241
2275
  }
2242
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
+
2243
2291
  if (!v.IsArray()) {
2244
2292
  gi_base_info_unref(elem);
2245
2293
  Napi::TypeError::New(env, "expected an array for the array argument")
@@ -2273,7 +2321,8 @@ static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfe
2273
2321
  continue;
2274
2322
  }
2275
2323
  GIArgument a;
2276
- ok = ElementToGIArgument(env, elem, arr.Get(static_cast<uint32_t>(i)), &a, elemTransfer);
2324
+ ok = ElementToGIArgument(env, elem, arr.Get(static_cast<uint32_t>(i)), &a, elemTransfer,
2325
+ NodeGiIntContext::kListElement, /*wrapIntegers=*/true);
2277
2326
  if (!ok) break;
2278
2327
  // Copy the element's storage bytes (LE: the low elemSize bytes of the union
2279
2328
  // alias the active field — v_string/v_pointer for pointers, the scalar
@@ -2420,7 +2469,8 @@ static bool JsToGHashIn(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransf
2420
2469
  } else {
2421
2470
  GIArgument ka;
2422
2471
  Napi::Value keyNum = Napi::Number::New(env, static_cast<double>(g_ascii_strtoll(ks.c_str(), nullptr, 10)));
2423
- ok = ElementToGIArgument(env, kt, keyNum, &ka, GI_TRANSFER_NOTHING);
2472
+ ok = ElementToGIArgument(env, kt, keyNum, &ka, GI_TRANSFER_NOTHING,
2473
+ NodeGiIntContext::kHashElement, /*wrapIntegers=*/true);
2424
2474
  if (!ok) break;
2425
2475
  kp = gi_type_info_hash_pointer_from_argument(kt, &ka);
2426
2476
  }
@@ -2431,7 +2481,7 @@ static bool JsToGHashIn(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransf
2431
2481
  Napi::Value val = obj.Get(key);
2432
2482
  gpointer vp = nullptr;
2433
2483
  GIArgument va;
2434
- ok = ElementToGIArgument(env, vt, val, &va, elemTransfer);
2484
+ ok = ElementToGIArgument(env, vt, val, &va, elemTransfer, NodeGiIntContext::kHashElement);
2435
2485
  if (!ok) {
2436
2486
  g_free(kp);
2437
2487
  break;
@@ -2973,7 +3023,8 @@ Napi::Value SetBoxedField(const Napi::CallbackInfo& info) {
2973
3023
  GIArgument arg;
2974
3024
  memset(&arg, 0, sizeof(arg));
2975
3025
  std::string heldString;
2976
- if (JsToGIArgument(env, value, ftype, &arg, &heldString, GI_TRANSFER_NOTHING, nullptr)) {
3026
+ if (JsToGIArgument(env, value, ftype, &arg, &heldString, GI_TRANSFER_NOTHING, nullptr,
3027
+ nullptr, nullptr, nullptr, name.c_str(), NodeGiIntContext::kField)) {
2977
3028
  if (!gi_field_info_set_field(field, bh->ptr, &arg)) {
2978
3029
  Napi::Error::New(env, std::string("Writing field ") + typeName + "." + name + " is not supported")
2979
3030
  .ThrowAsJavaScriptException();
package/src/object.cc CHANGED
@@ -444,17 +444,53 @@ bool JsToGValue(Napi::Env env, Napi::Value js, GValue* v) {
444
444
  // napi failure mid worker.terminate() must degrade to a zero, not cascade
445
445
  // into Error::New(nullptr)'s fatal sites (the terminate-mid-`newObject` path).
446
446
  case G_TYPE_BOOLEAN: g_value_set_boolean(v, NodeGiToBool(js)); return true;
447
- case G_TYPE_CHAR: g_value_set_schar(v, static_cast<gint8>(NodeGiToInt32(js))); return true;
448
- case G_TYPE_UCHAR: g_value_set_uchar(v, static_cast<guchar>(NodeGiToUint32(js))); return true;
449
- case G_TYPE_INT: g_value_set_int(v, NodeGiToInt32(js)); return true;
450
- case G_TYPE_UINT: g_value_set_uint(v, NodeGiToUint32(js)); return true;
451
- // 64-bit (glong/gulong are 64-bit on LP64): accept a BigInt losslessly, else a
452
- // truncated Number — never let a BigInt reach ToNumber(). Shared with the GI
453
- // scalar marshaller via JsValueTo{Int,Uint}64 (common.h).
447
+ // char/uchar/int64/uint64 refuse an out-of-range value and int/uint wrap, as gjs's
448
+ // gjs_value_to_g_value does (value.cpp:602-690) — the same rule the constructor
449
+ // properties and signal arguments reach. A BigInt on a 32-bit tag is ToNumber's
450
+ // TypeError, left pending.
451
+ case G_TYPE_CHAR:
452
+ case G_TYPE_UCHAR:
453
+ case G_TYPE_INT64:
454
+ case G_TYPE_UINT64: {
455
+ GITypeTag tag = ft == G_TYPE_CHAR ? GI_TYPE_TAG_INT8
456
+ : ft == G_TYPE_UCHAR ? GI_TYPE_TAG_UINT8
457
+ : ft == G_TYPE_INT64 ? GI_TYPE_TAG_INT64
458
+ : GI_TYPE_TAG_UINT64;
459
+ GIArgument a;
460
+ memset(&a, 0, sizeof(a));
461
+ switch (NodeGiJsToInteger(js, tag, &a, nullptr)) {
462
+ case NodeGiIntResult::kPending: return false;
463
+ case NodeGiIntResult::kOutOfRange:
464
+ NodeGiThrowGValueOutOfRange(env, js,
465
+ ft == G_TYPE_CHAR ? "char"
466
+ : ft == G_TYPE_UCHAR ? "unsigned char"
467
+ : ft == G_TYPE_INT64 ? "64-bit integer"
468
+ : "unsigned 64-bit integer");
469
+ return false;
470
+ case NodeGiIntResult::kOk: break;
471
+ }
472
+ if (ft == G_TYPE_CHAR) g_value_set_schar(v, a.v_int8);
473
+ else if (ft == G_TYPE_UCHAR) g_value_set_uchar(v, a.v_uint8);
474
+ else if (ft == G_TYPE_INT64) g_value_set_int64(v, a.v_int64);
475
+ else g_value_set_uint64(v, a.v_uint64);
476
+ return true;
477
+ }
478
+ case G_TYPE_INT:
479
+ case G_TYPE_UINT: {
480
+ GIArgument a;
481
+ memset(&a, 0, sizeof(a));
482
+ if (NodeGiJsToIntegerWrap(js, ft == G_TYPE_INT ? GI_TYPE_TAG_INT32 : GI_TYPE_TAG_UINT32,
483
+ &a) == NodeGiIntResult::kPending)
484
+ return false;
485
+ if (ft == G_TYPE_INT) g_value_set_int(v, a.v_int32);
486
+ else g_value_set_uint(v, a.v_uint32);
487
+ return true;
488
+ }
489
+ // glong/gulong are 64-bit on LP64: accept a BigInt losslessly, else a truncated
490
+ // Number — never let a BigInt reach ToNumber(). Shared with the GI scalar marshaller
491
+ // via JsValueTo{Int,Uint}64 (common.h). gjs has no G_TYPE_LONG case here at all.
454
492
  case G_TYPE_LONG: g_value_set_long(v, static_cast<glong>(JsValueToInt64(js))); return true;
455
493
  case G_TYPE_ULONG: g_value_set_ulong(v, static_cast<gulong>(JsValueToUint64(js))); return true;
456
- case G_TYPE_INT64: g_value_set_int64(v, JsValueToInt64(js)); return true;
457
- case G_TYPE_UINT64: g_value_set_uint64(v, JsValueToUint64(js)); return true;
458
494
  case G_TYPE_FLOAT: g_value_set_float(v, static_cast<float>(NodeGiToDouble(js))); return true;
459
495
  case G_TYPE_DOUBLE: g_value_set_double(v, NodeGiToDouble(js)); return true;
460
496
  case G_TYPE_ENUM: g_value_set_enum(v, NodeGiToInt32(js)); return true;
@@ -611,7 +647,16 @@ Napi::Value ConstructGObject(Napi::Env env, GType gtype, Napi::Object props,
611
647
  }
612
648
  g_value_init(&values[i], pspec->value_type);
613
649
  initialised = i + 1;
614
- if (!JsToGValue(env, props.Get(nameStorage[i]), &values[i])) {
650
+ Napi::Value propValue = props.Get(nameStorage[i]);
651
+ if (!propValue.IsEmpty() && propValue.IsUndefined()) {
652
+ // gjs refuses `undefined` for every property type (object.cpp:2090).
653
+ Napi::Error::New(env, "Invalid value 'undefined' for property " + std::string(pspec->name) +
654
+ " in object initializer.")
655
+ .ThrowAsJavaScriptException();
656
+ ok = false;
657
+ break;
658
+ }
659
+ if (!JsToGValue(env, propValue, &values[i])) {
615
660
  ok = false;
616
661
  break;
617
662
  }
@@ -734,6 +779,30 @@ Napi::Value SetProperty(const Napi::CallbackInfo& info) {
734
779
  Napi::TypeError::New(env, "no such property '" + name + "'").ThrowAsJavaScriptException();
735
780
  return env.Undefined();
736
781
  }
782
+ // gjs refuses an out-of-range value for these property types on assignment
783
+ // (object.cpp, with its own text); JsToGValue below now refuses them too, with the
784
+ // gjs_value_to_g_value text.
785
+ GITypeTag intTag = GI_TYPE_TAG_VOID;
786
+ switch (G_TYPE_FUNDAMENTAL(pspec->value_type)) {
787
+ case G_TYPE_CHAR: intTag = GI_TYPE_TAG_INT8; break;
788
+ case G_TYPE_UCHAR: intTag = GI_TYPE_TAG_UINT8; break;
789
+ case G_TYPE_UINT: intTag = GI_TYPE_TAG_UINT32; break;
790
+ case G_TYPE_INT64: intTag = GI_TYPE_TAG_INT64; break;
791
+ case G_TYPE_UINT64: intTag = GI_TYPE_TAG_UINT64; break;
792
+ default: break;
793
+ }
794
+ if (intTag != GI_TYPE_TAG_VOID) {
795
+ GIArgument probe;
796
+ std::string shown;
797
+ NodeGiIntResult r = NodeGiJsToInteger(info[2], intTag, &probe, &shown);
798
+ if (r == NodeGiIntResult::kPending) return env.Undefined();
799
+ if (r == NodeGiIntResult::kOutOfRange) {
800
+ Napi::Error::New(env, "value " + shown + " is out of range for " + pspec->name +
801
+ " (type " + NodeGiIntTagName(intTag) + ")")
802
+ .ThrowAsJavaScriptException();
803
+ return env.Undefined();
804
+ }
805
+ }
737
806
  GValue v = G_VALUE_INIT;
738
807
  g_value_init(&v, pspec->value_type);
739
808
  if (JsToGValue(env, info[2], &v)) {