@gjsify/node-gi 0.44.0 → 0.46.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.
package/src/calls.cc CHANGED
@@ -331,9 +331,8 @@ static NodeGiCallback* CreateCallback(Napi::Env env, Napi::Function fn, GICallab
331
331
 
332
332
  // Whether `type` is a supported OUT/INOUT marshalling type for this milestone:
333
333
  // fundamentals (numbers/bool), strings (utf8/filename), GObject/interface +
334
- // enums/flags, struct/boxed/union (wrapped as boxed handles on return), and the
335
- // containers (arrays, GList/GSList/GHashTable). GError (its own roadmap PR) is
336
- // deferred: a clear error rather than silent mis-handling. *why receives a short
334
+ // enums/flags, struct/boxed/union (wrapped as boxed handles on return), GError,
335
+ // and the containers (arrays, GList/GSList/GHashTable). *why receives a short
337
336
  // type label on refusal. Declared in common.h — shared with the vfunc chain-up
338
337
  // path (class.cc CallParentVfunc), which routes each OUT/INOUT vfunc arg the same
339
338
  // way this function-invoke path does.
@@ -365,6 +364,24 @@ bool IsSupportedOutType(GITypeInfo* type, std::string* why) {
365
364
  if (iface != nullptr) gi_base_info_unref(iface);
366
365
  return ok;
367
366
  }
367
+ case GI_TYPE_TAG_ERROR:
368
+ // A plain `GError**` OUT slot — `Gst.Message.parse_error()` and the other bus
369
+ // accessors of the GStreamer message APIs, `GLib.set_error_literal`,
370
+ // `GLib.propagate_error`. Swept over the installed typelibs, EVERY GError
371
+ // OUT/INOUT parameter is caller_allocates=false + transfer=EVERYTHING, so
372
+ // this tag never reaches the caller-allocates branch above and the callee
373
+ // always writes a GError the caller then owns.
374
+ //
375
+ // Ownership needs nothing new: ReadOutOrReturn falls through to
376
+ // GIArgumentToJs's GI_TYPE_TAG_ERROR arm (marshal.cc), and G_TYPE_ERROR is a
377
+ // BOXED type — glib gobject/gboxed.c,
378
+ // `G_DEFINE_BOXED_TYPE(GError, g_error, g_error_copy, g_error_free)` — so the
379
+ // handle adopts a transfer-full pointer and its finalizer g_boxed_free's it
380
+ // (= g_error_free) exactly like any other EVERYTHING boxed OUT. A callee that
381
+ // leaves the slot untouched (`parse_info()` on an ERROR message) reads back as
382
+ // `null` and not an empty error, because `slots` is value-initialised and that
383
+ // arm null-guards the pointer — matching gjs.
384
+ return true;
368
385
  case GI_TYPE_TAG_ARRAY:
369
386
  case GI_TYPE_TAG_GLIST:
370
387
  case GI_TYPE_TAG_GSLIST:
@@ -372,7 +389,7 @@ bool IsSupportedOutType(GITypeInfo* type, std::string* why) {
372
389
  // Container OUT/return — arrays (incl. GStrv / byte arrays), GList/GSList,
373
390
  // and string-keyed GHashTable. Element-type support is checked here so an
374
391
  // exotic combo defers cleanly before the invoke.
375
- return IsSupportedContainerType(type, why);
392
+ return IsSupportedContainerType(type, why, ContainerUse::kReadBack);
376
393
  default:
377
394
  if (why != nullptr)
378
395
  *why = "type tag " + std::to_string(static_cast<int>(gi_type_info_get_tag(type)));
@@ -581,8 +598,10 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
581
598
  // belong to the callee (dropped only when the callee never ran).
582
599
  CreatedBytes createdBytes;
583
600
  CreatedValues createdValues;
601
+ CreatedErrors createdErrors;
584
602
  bool ok = true;
585
603
  size_t jsCursor = 0;
604
+ std::vector<LengthWrite> lengthWrites;
586
605
  for (unsigned int i = 0; i < n_args && ok; i++) {
587
606
  if (skip[i]) {
588
607
  // A length arg that is OUT/INOUT: the callee writes the array length into
@@ -710,7 +729,7 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
710
729
  // that call was removed from the suite and must not come back.
711
730
  Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
712
731
  jsCursor++;
713
- if (!IsSupportedContainerType(ti, &why)) {
732
+ if (!IsSupportedContainerType(ti, &why, ContainerUse::kReadBack)) {
714
733
  Napi::TypeError::New(env, displayName + ": INOUT " + why +
715
734
  " parameters are not yet supported")
716
735
  .ThrowAsJavaScriptException();
@@ -758,7 +777,8 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
758
777
  jsCursor++;
759
778
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
760
779
  if (JsToGIArgument(env, v, ti, &slots[i], &held[i], tr, &ownedInStrings, nullptr,
761
- nullptr, nullptr, gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)))) {
780
+ nullptr, nullptr, &createdErrors,
781
+ gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)))) {
762
782
  in_args[inPos[i]].v_pointer = &slots[i];
763
783
  out_args[outPos[i]].v_pointer = &slots[i];
764
784
  } else {
@@ -825,7 +845,7 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
825
845
  if (tg == GI_TYPE_TAG_ARRAY || tg == GI_TYPE_TAG_GLIST || tg == GI_TYPE_TAG_GSLIST ||
826
846
  tg == GI_TYPE_TAG_GHASH) {
827
847
  std::string why;
828
- if (!IsSupportedContainerType(ti, &why)) {
848
+ if (!IsSupportedContainerType(ti, &why, ContainerUse::kIn)) {
829
849
  Napi::TypeError::New(env, displayName + ": IN " + why +
830
850
  " parameters are not yet supported")
831
851
  .ThrowAsJavaScriptException();
@@ -842,11 +862,21 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
842
862
  unsigned int L = 0;
843
863
  if (gi_type_info_get_array_length_index(ti, &L) && L < n_args &&
844
864
  dirs[L] == GI_DIRECTION_IN && inPos[L] >= 0) {
845
- GIArgInfo* la = gi_callable_info_get_arg(callable, L);
846
- GITypeInfo* lt = gi_arg_info_get_type_info(la);
847
- WriteLengthValue(lt, &in_args[inPos[L]], ccount);
848
- gi_base_info_unref(lt);
849
- gi_base_info_unref(la);
865
+ long other = 0;
866
+ if (!RecordLengthWrite(&lengthWrites, L, ccount, &other)) {
867
+ Napi::TypeError::New(
868
+ env, displayName + ": two arrays share length argument " +
869
+ std::to_string(L) + " but have different lengths (" +
870
+ std::to_string(other) + " and " + std::to_string(ccount) + ")")
871
+ .ThrowAsJavaScriptException();
872
+ ok = false;
873
+ } else {
874
+ GIArgInfo* la = gi_callable_info_get_arg(callable, L);
875
+ GITypeInfo* lt = gi_arg_info_get_type_info(la);
876
+ WriteLengthValue(lt, &in_args[inPos[L]], ccount);
877
+ gi_base_info_unref(lt);
878
+ gi_base_info_unref(la);
879
+ }
850
880
  }
851
881
  }
852
882
  }
@@ -854,7 +884,7 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
854
884
  } else {
855
885
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
856
886
  ok = JsToGIArgument(env, v, ti, &in_args[inPos[i]], &held[i], tr, &ownedInStrings,
857
- &createdClosures, &createdBytes, &createdValues,
887
+ &createdClosures, &createdBytes, &createdValues, &createdErrors,
858
888
  gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)));
859
889
  }
860
890
  }
@@ -876,6 +906,9 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
876
906
  // Likewise every GValue boxed from a plain JS value — never adopted.
877
907
  for (GValue* gv : createdValues.transferNone) NodeGiFreeBoxedGValue(gv);
878
908
  for (GValue* gv : createdValues.transferFull) NodeGiFreeBoxedGValue(gv);
909
+ // Likewise every GError built from an L1 GLib.Error — never adopted.
910
+ for (GError* e : createdErrors.transferNone) g_error_free(e);
911
+ for (GError* e : createdErrors.transferFull) g_error_free(e);
879
912
  // Likewise the transfer-full-instance ref: the callee never consumed it.
880
913
  if (instanceRefTaken) g_object_unref(static_cast<GObject*>(instance));
881
914
  for (gpointer s : ownedInStrings) g_free(s); // never reached the callee (#658)
@@ -920,6 +953,11 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
920
953
  // box is ours to free — success AND error paths, mirroring the GBytes rule.
921
954
  // Transfer-full GValues were adopted by the callee — no release.
922
955
  for (GValue* gv : createdValues.transferNone) NodeGiFreeBoxedGValue(gv);
956
+ // Transfer-none GError IN-args built from an L1 GLib.Error: the callee copied
957
+ // what it kept (gst_message_new_error copies the GError into the message), so
958
+ // the build is ours to free — success AND error paths, mirroring the two rules
959
+ // above. Transfer-full GErrors were adopted by the callee — no release.
960
+ for (GError* e : createdErrors.transferNone) g_error_free(e);
923
961
  if (!success) {
924
962
  for (const InContainer& c : inContainers) FreeInContainer(c);
925
963
  // A failed invoke did not adopt the transfer-full IN/INOUT strings we g_strdup'd
@@ -1362,7 +1400,15 @@ Napi::Value ClassMethodArity(const Napi::CallbackInfo& info) {
1362
1400
  // CHAIN, where gjs_define_static_methods put GObjectClass's methods on
1363
1401
  // GObject.Object's constructor and Gtk.Button inherits them. Returns a new ref or
1364
1402
  // nullptr; same shape as ResolveMethodByName's parent walk above.
1365
- static GIFunctionInfo* ResolveClassStructMethod(GIObjectInfo* objInfo, const char* name) {
1403
+ //
1404
+ // `declaringGType` receives the GType whose class struct DECLARED the method —
1405
+ // G_TYPE_OBJECT for `list_properties` — and is left untouched on a miss. That is the
1406
+ // type a receiver has to be compatible with, exactly as in gjs, where the method is
1407
+ // one function on GObject.Object's constructor and every class inherits the same one:
1408
+ // measured, `Gtk.ListItem.list_properties.call(Gtk.Widget)` answers GtkWidget's 36
1409
+ // there. Checking against the type the name was READ from would refuse that.
1410
+ static GIFunctionInfo* ResolveClassStructMethod(GIObjectInfo* objInfo, const char* name,
1411
+ GType* declaringGType) {
1366
1412
  GIObjectInfo* walk =
1367
1413
  reinterpret_cast<GIObjectInfo*>(gi_base_info_ref(reinterpret_cast<GIBaseInfo*>(objInfo)));
1368
1414
  GIFunctionInfo* found = nullptr;
@@ -1372,7 +1418,11 @@ static GIFunctionInfo* ResolveClassStructMethod(GIObjectInfo* objInfo, const cha
1372
1418
  if (gi_struct_info_is_gtype_struct(cs)) found = gi_struct_info_find_method(cs, name);
1373
1419
  gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(cs));
1374
1420
  }
1375
- if (found != nullptr) break;
1421
+ if (found != nullptr) {
1422
+ *declaringGType =
1423
+ gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(walk));
1424
+ break;
1425
+ }
1376
1426
  GIObjectInfo* parent = gi_object_info_get_parent(walk);
1377
1427
  gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(walk));
1378
1428
  walk = parent;
@@ -1396,23 +1446,39 @@ static GIFunctionInfo* ResolveClassStructMethod(GIObjectInfo* objInfo, const cha
1396
1446
  // call is served by the peek), while unrefing is the one call that could invalidate
1397
1447
  // the GParamSpecs list_properties just handed out, which the class owns. Since GLib
1398
1448
  // 2.84 g_type_class_unref is a documented no-op, so pairing buys nothing anyway.
1399
- static gpointer ClassStructInstance(GIObjectInfo* objInfo) {
1400
- GType gtype =
1401
- gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(objInfo));
1402
- if (gtype == G_TYPE_INVALID) return nullptr;
1449
+ //
1450
+ // nullptr for an UNCLASSED GType (boxed/struct, interface) as well as for an absent
1451
+ // one — `g_type_class_ref` has no class to hand back there and answers nullptr after
1452
+ // a CRITICAL. It is what makes the receiver retarget below refuse safely.
1453
+ static gpointer ClassStructInstanceForGType(GType gtype) {
1454
+ if (gtype == G_TYPE_INVALID || !G_TYPE_IS_CLASSED(gtype)) return nullptr;
1403
1455
  gpointer klass = g_type_class_peek(gtype);
1404
1456
  return klass != nullptr ? klass : g_type_class_ref(gtype);
1405
1457
  }
1406
1458
 
1407
- // callStaticMethod(namespace, typeName, methodName, args?) -> unknown
1459
+ // callStaticMethod(namespace, typeName, methodName, args?, receiverGType?) -> unknown
1408
1460
  // Invoke a type-level constructor/static function (e.g. Gio.File.new_for_path,
1409
1461
  // Gtk.Label.new) — a function found ON a type but taking no instance. The Node
1410
1462
  // twin of `Ns.Class.method(...)`.
1463
+ //
1464
+ // `receiverGType` is the GType of the object the call went THROUGH (`this`), and it
1465
+ // is honoured for CLASS-STRUCT methods only. gjs defines those on the constructor
1466
+ // and marshals `this` as the GTypeClass, so the very same function object answers
1467
+ // for every class it is applied to: `GObject.Object.list_properties.call(Gtk.ListItem)`
1468
+ // reads GtkListItem's 9 pspecs, and an inherited `Subclass.list_properties()` reads the
1469
+ // SUBCLASS's. node-gi bound the name to the type it was READ from, so both spellings
1470
+ // answered the declarer's pspecs — GObject's zero — with nothing thrown (#1438). A
1471
+ // plain static (Gio.File.new_for_path) ignores `this` in gjs too, so it ignores this
1472
+ // argument here; and a receiver that is not `g_type_is_a` the DECLARING type (see
1473
+ // ResolveClassStructMethod) is ignored rather than trusted, so no C function can be
1474
+ // handed the class pointer of an unrelated type.
1411
1475
  Napi::Value CallStaticMethod(const Napi::CallbackInfo& info) {
1412
1476
  Napi::Env env = info.Env();
1413
1477
  if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
1414
1478
  Napi::TypeError::New(
1415
- env, "callStaticMethod(namespace: string, typeName: string, methodName: string, args?: unknown[])")
1479
+ env,
1480
+ "callStaticMethod(namespace: string, typeName: string, methodName: string, args?: unknown[], "
1481
+ "receiverGType?: GType)")
1416
1482
  .ThrowAsJavaScriptException();
1417
1483
  return env.Null();
1418
1484
  }
@@ -1421,6 +1487,10 @@ Napi::Value CallStaticMethod(const Napi::CallbackInfo& info) {
1421
1487
  std::string method = info[2].As<Napi::String>().Utf8Value();
1422
1488
  Napi::Array args = (info.Length() >= 4 && info[3].IsArray()) ? info[3].As<Napi::Array>()
1423
1489
  : Napi::Array::New(env, 0);
1490
+ // 0 for every shape that is not a GType handle (absent, null, an ordinary object),
1491
+ // which is the "no receiver given" case — never an error, since `this` is whatever
1492
+ // the caller's call site happened to be.
1493
+ GType receiverGType = info.Length() >= 5 ? ReadGTypeHandle(info[4]) : G_TYPE_INVALID;
1424
1494
 
1425
1495
  GIRepository* repo = DupDefaultRepository();
1426
1496
  GIBaseInfo* typeInfo = gi_repository_find_by_name(repo, ns.c_str(), tn.c_str());
@@ -1439,13 +1509,19 @@ Napi::Value CallStaticMethod(const Napi::CallbackInfo& info) {
1439
1509
  GIObjectInfo* objInfo = reinterpret_cast<GIObjectInfo*>(typeInfo);
1440
1510
  func = gi_object_info_find_method(objInfo, method.c_str());
1441
1511
  if (func == nullptr) {
1442
- func = ResolveClassStructMethod(objInfo, method.c_str());
1512
+ GType declaringGType = G_TYPE_INVALID;
1513
+ func = ResolveClassStructMethod(objInfo, method.c_str(), &declaringGType);
1443
1514
  if (func != nullptr) {
1444
- classInstance = ClassStructInstance(objInfo);
1515
+ GType ownGType =
1516
+ gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(objInfo));
1517
+ const bool retarget = receiverGType != G_TYPE_INVALID && receiverGType != ownGType &&
1518
+ declaringGType != G_TYPE_INVALID &&
1519
+ g_type_is_a(receiverGType, declaringGType);
1520
+ classInstance = ClassStructInstanceForGType(retarget ? receiverGType : ownGType);
1445
1521
  if (classInstance == nullptr) {
1446
- // No GType behind the info, so there is no class to call it on. Drop back
1447
- // to the plain "no static method" error rather than invoking with a null
1448
- // instance, which would hand the C function a null GObjectClass*.
1522
+ // No class to call it on — no GType behind the info, or an unclassed one.
1523
+ // Drop back to the plain "no static method" error rather than invoking with
1524
+ // a null instance, which would hand the C function a null GObjectClass*.
1449
1525
  gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(func));
1450
1526
  func = nullptr;
1451
1527
  }
package/src/class.cc CHANGED
@@ -723,6 +723,7 @@ static Napi::Value InvokeVFuncPointer(Napi::Env env, GObject* obj, GICallableInf
723
723
  // an all-IN vfunc jsCursor == i, so the IN-only path is byte-identical to before.
724
724
  bool ok = true;
725
725
  size_t jsCursor = 0;
726
+ std::vector<LengthWrite> lengthWrites;
726
727
  for (unsigned int i = 0; i < nDeclared && ok; i++) {
727
728
  if (skip[i]) {
728
729
  // An OUT/INOUT array-length arg: wire its slot so the callee writes the count.
@@ -796,7 +797,7 @@ static Napi::Value InvokeVFuncPointer(Napi::Env env, GObject* obj, GICallableInf
796
797
  // transfer by ReadOutOrReturn below.
797
798
  Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
798
799
  jsCursor++;
799
- if (!IsSupportedContainerType(ti, &why)) {
800
+ if (!IsSupportedContainerType(ti, &why, ContainerUse::kReadBack)) {
800
801
  Napi::TypeError::New(env, where + ": INOUT " + why +
801
802
  " parameters are not yet supported")
802
803
  .ThrowAsJavaScriptException();
@@ -836,7 +837,8 @@ static Napi::Value InvokeVFuncPointer(Napi::Env env, GObject* obj, GICallableInf
836
837
  jsCursor++;
837
838
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
838
839
  if (JsToGIArgument(env, v, ti, &slots[i], &holds[i], tr, &ownedInStrings, nullptr,
839
- nullptr, nullptr, gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai))))
840
+ nullptr, nullptr, nullptr,
841
+ gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai))))
840
842
  giArgs[1 + i].v_pointer = &slots[i];
841
843
  else
842
844
  ok = false; // JsToGIArgument already threw
@@ -850,7 +852,7 @@ static Napi::Value InvokeVFuncPointer(Napi::Env env, GObject* obj, GICallableInf
850
852
  std::string why;
851
853
  Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
852
854
  jsCursor++;
853
- if (!IsSupportedContainerType(ti, &why)) {
855
+ if (!IsSupportedContainerType(ti, &why, ContainerUse::kIn)) {
854
856
  Napi::TypeError::New(env, where + ": IN " + why +
855
857
  " parameters are not yet supported")
856
858
  .ThrowAsJavaScriptException();
@@ -866,11 +868,21 @@ static Napi::Value InvokeVFuncPointer(Napi::Env env, GObject* obj, GICallableInf
866
868
  unsigned int L = 0;
867
869
  if (gi_type_info_get_array_length_index(ti, &L) && L < nDeclared &&
868
870
  dirs[L] == GI_DIRECTION_IN) {
869
- GIArgInfo* la = gi_callable_info_get_arg(ci, L);
870
- GITypeInfo* lt = gi_arg_info_get_type_info(la);
871
- WriteLengthValue(lt, &giArgs[1 + L], ccount);
872
- gi_base_info_unref(lt);
873
- gi_base_info_unref(la);
871
+ long other = 0;
872
+ if (!RecordLengthWrite(&lengthWrites, L, ccount, &other)) {
873
+ Napi::TypeError::New(
874
+ env, where + ": two arrays share length argument " + std::to_string(L) +
875
+ " but have different lengths (" + std::to_string(other) + " and " +
876
+ std::to_string(ccount) + ")")
877
+ .ThrowAsJavaScriptException();
878
+ ok = false;
879
+ } else {
880
+ GIArgInfo* la = gi_callable_info_get_arg(ci, L);
881
+ GITypeInfo* lt = gi_arg_info_get_type_info(la);
882
+ WriteLengthValue(lt, &giArgs[1 + L], ccount);
883
+ gi_base_info_unref(lt);
884
+ gi_base_info_unref(la);
885
+ }
874
886
  }
875
887
  }
876
888
  }
@@ -881,7 +893,8 @@ static Napi::Value InvokeVFuncPointer(Napi::Env env, GObject* obj, GICallableInf
881
893
  jsCursor++;
882
894
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
883
895
  ok = JsToGIArgument(env, v, ti, &giArgs[1 + i], &holds[i], tr, &ownedInStrings, nullptr,
884
- nullptr, nullptr, gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)));
896
+ nullptr, nullptr, nullptr,
897
+ gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)));
885
898
  }
886
899
 
887
900
  avalue.push_back(&giArgs[1 + i]);
package/src/common.h CHANGED
@@ -66,6 +66,11 @@ GIRepository* DupDefaultRepository();
66
66
  // Init, freed by the instance-data finalizer at env teardown.
67
67
  struct NodeGiEnvData {
68
68
  napi_ref errorBuilder = nullptr;
69
+ // The CLASS the builder above constructs — L1's `GLib.Error`. Registered with
70
+ // the builder (one call, so the two cannot drift apart) and read the other way
71
+ // round: a GError-typed IN arg uses it to recognise an L1 error and rebuild a
72
+ // real GError from its fields. Per-env for the same reason as errorBuilder.
73
+ napi_ref errorClass = nullptr;
69
74
  // L1-registered resolver for Gtk.Template `<signal handler="…">` callbacks: given
70
75
  // (instanceHandle, handlerName) it returns the instance's bound JS method (or
71
76
  // undefined). Stored per-env for the same reason as errorBuilder (a napi_ref is
@@ -279,22 +284,32 @@ struct CreatedClosures {
279
284
  std::vector<GClosure*> transferFull; // callee adopts; unref only when the callee never ran
280
285
  };
281
286
 
282
- // ---- JS bytes -> GBytes IN-args ----------------------------------------------
287
+ // ---- C objects this layer SYNTHESISES for one IN argument --------------------
283
288
  //
289
+ // Three IN arms build a throw-away C object out of a plain JS value, because the
290
+ // JS side has no handle to hand over: a Uint8Array for a `GLib.Bytes` arg, a
291
+ // number/string for a `GObject.Value` arg, an L1 `GLib.Error` for a `GError` arg.
292
+ // All three carry the SAME lifetime rule, which is why they share one type
293
+ // instead of a third near-identical struct: transfer-none → the callee borrowed
294
+ // (or copied) it, so we release it after the invoke; transfer-full → the callee
295
+ // adopted it, so it is released only when the invoke never ran.
296
+ template <typename T>
297
+ struct CreatedIn {
298
+ std::vector<T*> transferNone; // release after the invoke (callee borrowed/copied)
299
+ std::vector<T*> transferFull; // callee adopts; release only when the callee never ran
300
+ void Record(T* p, GITransfer transfer) {
301
+ (transfer == GI_TRANSFER_NOTHING ? transferNone : transferFull).push_back(p);
302
+ }
303
+ };
304
+
284
305
  // A JS Uint8Array (or any TypedArray/DataView/ArrayBuffer) passed for a
285
306
  // `GLib.Bytes`-typed IN parameter (e.g. GdkPixbuf.Pixbuf.new_from_bytes,
286
307
  // g_compute_checksum_for_bytes) is COPIED into a fresh GBytes — exactly what GJS
287
308
  // does (refs/gjs/gi/arg-cache.cpp GBytesIn::in → gjs_byte_array_get_bytes →
288
- // g_bytes_new). Lifetime follows gjs: a transfer-none arg drops the fresh ref
289
- // after the invoke (GBytesInTransferNone::release → g_boxed_free; a callee that
290
- // kept the bytes holds its own ref), a transfer-full arg leaves the ref with the
291
- // callee (GBytesIn::release skips via the ignore_release mark). GBytes created
292
- // during an invoke are recorded here so calls.cc can release them on the right
293
- // path (mirrors CreatedClosures).
294
- struct CreatedBytes {
295
- std::vector<GBytes*> transferNone; // unref after the invoke (callee borrowed/ref'd)
296
- std::vector<GBytes*> transferFull; // callee adopts; unref only when the callee never ran
297
- };
309
+ // g_bytes_new), and released per the rule above (gjs
310
+ // GBytesInTransferNone::release → g_boxed_free; GBytesIn::release skips for
311
+ // transfer-full via the ignore_release mark).
312
+ using CreatedBytes = CreatedIn<GBytes>;
298
313
 
299
314
  // A plain JS value handed to a `GObject.Value` IN-arg is BOXED into a fresh
300
315
  // GValue whose GType is guessed from the JS value — exactly what GJS does
@@ -305,14 +320,18 @@ struct CreatedBytes {
305
320
  // with "Unsupported interface IN argument", because a JS number is not a boxed
306
321
  // handle. An existing GObject.Value boxed handle still routes through the
307
322
  // boxed-handle path and is passed through untouched.
308
- //
309
- // Lifetime mirrors CreatedBytes: the box is freed after the invoke for
310
- // transfer-none (the callee copied whatever it needed — g_object_set_property
311
- // copies into the property), and left with the callee for transfer-full.
312
- struct CreatedValues {
313
- std::vector<GValue*> transferNone; // free after the invoke (callee borrowed)
314
- std::vector<GValue*> transferFull; // callee adopts; free only when it never ran
315
- };
323
+ using CreatedValues = CreatedIn<GValue>;
324
+
325
+ // An L1 `GLib.Error` (what a `catch` hands you, and what `new GLib.Error(…)`
326
+ // builds) has no `GError*` behind it — it is a JS class, not a boxed handle — so
327
+ // a `GError`-typed IN arg gets a fresh `g_error_new_literal` built from its
328
+ // domain/code/message. Without this, the hand-back direction the OUT direction
329
+ // exists for is only open to errors that came OUT of GI in the first place:
330
+ // `GLib.propagate_error(caught)` and `Gst.Message.new_error(src, caught, dbg)`
331
+ // both refused where gjs accepts, which is the shape an application actually
332
+ // writes. A marshalled GError (a boxed handle) still routes through
333
+ // TransferBoxedIn and is not copied here.
334
+ using CreatedErrors = CreatedIn<GError>;
316
335
 
317
336
  // Release a GValue this layer boxed: unset the held value (dropping the ref an
318
337
  // object/boxed/string GValue took) and free the struct. A plain `g_free` would
@@ -340,6 +359,9 @@ GClosure* NodeGiMakeGenericJsClosure(Napi::Env env, Napi::Value fn);
340
359
  // `values` (optional): enables the JS-value→GValue IN-arg boxing above; nullptr
341
360
  // (the default) keeps the previous behaviour (plain value → TypeError) on paths
342
361
  // that cannot release a created GValue.
362
+ // `errors` (optional): enables the L1-GLib.Error→GError IN-arg build above;
363
+ // nullptr (the default) keeps a marshalled boxed handle working and refuses the
364
+ // L1 class, on paths that cannot release a created GError.
343
365
  // `argName` (optional): the introspected argument name, used ONLY in error
344
366
  // messages so a refusal reads like gjs's ("Expected type string for argument
345
367
  // 'name' but got type number"); nullptr degrades to a message without the name.
@@ -350,6 +372,7 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
350
372
  CreatedClosures* closures = nullptr,
351
373
  CreatedBytes* bytes = nullptr,
352
374
  CreatedValues* values = nullptr,
375
+ CreatedErrors* errors = nullptr,
353
376
  const char* argName = nullptr);
354
377
 
355
378
  // ---- IN container building -----------------------------------------
@@ -362,15 +385,40 @@ struct InContainer {
362
385
  gpointer ptr;
363
386
  GITransfer transfer;
364
387
  long count; // element count (to free C-array strings)
388
+ // For a by-value GValue array: the caller's own GValue per element, or nullptr where
389
+ // the element came from a plain JS value and there is nothing to write back to. See
390
+ // the write-back note in FreeInContainer — some callees FILL the cells, and the
391
+ // typelib does not say which.
392
+ std::vector<gpointer> writeBack;
365
393
  };
366
394
 
367
- bool IsSupportedContainerType(GITypeInfo* type, std::string* why);
395
+ // How a container argument is used, which decides which ELEMENT kinds are
396
+ // marshallable. The two directions are not symmetric: the IN path builds a C buffer
397
+ // and can therefore lay out a BY-VALUE element (an enum's int, a record's own bytes),
398
+ // while a read-back — an OUT/INOUT parameter or a return — hands the buffer to
399
+ // ReadCElement, which still dereferences an interface element as a pointer. Passing
400
+ // kIn where the value is later read would admit a shape that marshals in and misreads
401
+ // out, so the caller states the direction rather than the predicate guessing it.
402
+ enum class ContainerUse { kIn, kReadBack };
403
+
404
+ bool IsSupportedContainerType(GITypeInfo* type, std::string* why, ContainerUse use);
368
405
  // Whether `type` is a supported OUT/INOUT marshalling type (fundamentals, strings,
369
406
  // object/interface/enum/flags, struct/boxed/union, and containers). Shared by the
370
407
  // function-invoke path (calls.cc) and the vfunc chain-up path (class.cc).
371
408
  bool IsSupportedOutType(GITypeInfo* type, std::string* why);
372
409
  size_t CElementSize(GITypeInfo* elem);
373
410
  void WriteLengthValue(GITypeInfo* lenType, GIArgument* slot, long n);
411
+
412
+ // One IN length-argument autofill, remembered so a SECOND array naming the same length
413
+ // argument can be checked against it rather than silently overwriting it.
414
+ struct LengthWrite {
415
+ unsigned int index;
416
+ long count;
417
+ };
418
+ // Record `count` for length argument `index`. Returns false when that argument was
419
+ // already filled with a DIFFERENT count, and reports the earlier one in *other.
420
+ bool RecordLengthWrite(std::vector<LengthWrite>* seen, unsigned int index, long count,
421
+ long* other);
374
422
  Napi::Value ReadOutOrReturn(Napi::Env env, GICallableInfo* callable, GITypeInfo* ti,
375
423
  GIArgument* arg, GITransfer transfer,
376
424
  std::vector<GIArgument>* slots);
@@ -582,6 +630,8 @@ Napi::Value BoxedMemberKind(const Napi::CallbackInfo& info);
582
630
  Napi::Value GetBoxedField(const Napi::CallbackInfo& info);
583
631
  Napi::Value SetBoxedField(const Napi::CallbackInfo& info);
584
632
  Napi::Value BoxedTypeName(const Napi::CallbackInfo& info);
633
+ // TEST-ONLY (see marshal.cc): the address a boxed handle wraps, as hex.
634
+ Napi::Value BoxedAddress(const Napi::CallbackInfo& info);
585
635
  // GParamSpec wrapping (object.cc): a tagged GObject-fundamental handle plus its
586
636
  // name/nick/blurb/flags/value_type/owner_type/default_value accessors.
587
637
  Napi::Value MakeParamSpecHandle(Napi::Env env, GParamSpec* pspec, GITransfer transfer);