@gjsify/node-gi 0.52.0 → 0.53.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/loop.cc CHANGED
@@ -379,8 +379,9 @@ Napi::Value IterateMainContext(const Napi::CallbackInfo& info) {
379
379
  struct PumpPoll {
380
380
  uv_poll_t handle;
381
381
  int fd;
382
- int events; // currently-subscribed uv event mask
383
- gboolean seen; // mark/sweep flag for SyncPumpPolls
382
+ int events; // currently-subscribed uv event mask
383
+ gboolean seen; // mark/sweep flag for SyncPumpPolls
384
+ gboolean started; // whether the watcher is currently armed (see PumpPollCb)
384
385
  };
385
386
 
386
387
  static gboolean g_pump_inited = FALSE;
@@ -398,7 +399,26 @@ static gboolean g_pump_poll_warned = FALSE;
398
399
  // Wake-only callbacks: readiness/expiry just ends libuv's poll sleep; the actual
399
400
  // GLib dispatch happens in the check phase of the same loop turn (PumpCheckCb).
400
401
  static void PumpTimerCb(uv_timer_t* /*t*/) {}
401
- static void PumpPollCb(uv_poll_t* /*p*/, int /*status*/, int /*events*/) {}
402
+
403
+ // A wake-up is an EDGE, so the watcher DISARMS itself on the first fire and
404
+ // PumpArmWakeups re-arms it on the next libuv turn. uv_poll is level-triggered
405
+ // and this callback reads nothing, so a GLib fd that stays ready — a dead D-Bus
406
+ // socket stuck at POLLHUP, or the context's own wakeup eventfd, which
407
+ // `block_source()` re-signals on every dispatch — leaves uv's backend fd
408
+ // PERPETUALLY readable. That fd is embedded in UvLoopSource, so GLib then reads
409
+ // "Node has work" on every prepare, and any foreign drain loop of the shape
410
+ // `while (g_main_context_iteration(ctx, FALSE));` never terminates: measured in
411
+ // `e_source_registry_new_sync`'s failure path (no D-Bus reachable), 6.9 M
412
+ // dispatches in 20 s at 100 % CPU with `uv_backend_timeout()` sitting at ~4.6 s,
413
+ // i.e. never once because Node actually had work. The pump's own drain would
414
+ // have cleared the GLib side, but it no-ops at `g_main_depth() > 0` — exactly
415
+ // where a foreign nested iteration runs. Regression
416
+ // `test/foreign-context-drain.test.mjs`.
417
+ static void PumpPollCb(uv_poll_t* p, int /*status*/, int /*events*/) {
418
+ PumpPoll* pp = reinterpret_cast<PumpPoll*>(p->data);
419
+ uv_poll_stop(p);
420
+ pp->started = FALSE;
421
+ }
402
422
 
403
423
  static void PumpPollCloseCb(uv_handle_t* h) {
404
424
  delete reinterpret_cast<PumpPoll*>(h->data);
@@ -440,9 +460,12 @@ static gboolean SyncPumpPolls(int nfds) {
440
460
  if (it != g_pump_polls->end()) {
441
461
  PumpPoll* pp = it->second;
442
462
  pp->seen = TRUE;
443
- if (pp->events != ev) {
463
+ // Re-arm a watcher that disarmed itself on its last fire, as well as one
464
+ // whose event mask changed.
465
+ if (pp->events != ev || !pp->started) {
444
466
  if (uv_poll_start(&pp->handle, ev, PumpPollCb) == 0) {
445
467
  pp->events = ev;
468
+ pp->started = TRUE;
446
469
  uv_unref(reinterpret_cast<uv_handle_t*>(&pp->handle));
447
470
  } else {
448
471
  all_ok = FALSE;
@@ -466,6 +489,7 @@ static gboolean SyncPumpPolls(int nfds) {
466
489
  all_ok = FALSE;
467
490
  continue;
468
491
  }
492
+ pp->started = TRUE;
469
493
  uv_unref(reinterpret_cast<uv_handle_t*>(&pp->handle));
470
494
  (*g_pump_polls)[fd] = pp;
471
495
  }
package/src/marshal.cc CHANGED
@@ -4,6 +4,7 @@
4
4
  #include "common.h"
5
5
 
6
6
  #include <cmath>
7
+ #include <cstring>
7
8
  #include <unordered_map>
8
9
  #include <utility>
9
10
 
@@ -789,7 +790,7 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
789
790
  // how an application POSTS the failure the bus accessors read back —
790
791
  // `GLib.propagate_error`, and the INOUT `err` of `GLib.prefix_error_literal`.
791
792
  // null/undefined → a NULL GError, the same leniency the object/boxed arms
792
- // keep (gjs refuses both for a non-nullable arg — status/open-todos.md).
793
+ // keep (gjs refuses both for a non-nullable arg — status/open-todos/README.md).
793
794
  if (v.IsNull() || v.IsUndefined()) {
794
795
  out->v_pointer = nullptr;
795
796
  return true;
@@ -1040,8 +1041,15 @@ Napi::Value GIArgumentToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
1040
1041
  // unchanged, which is right for a pointer-struct element and meaningless for a
1041
1042
  // four-byte enum or a twenty-four-byte record. So the by-value admission is scoped to
1042
1043
  // the one container kind that has somewhere to put the bytes.
1043
- static bool IsSupportedElementType(GITypeInfo* elem, std::string* why, bool byValueOk) {
1044
- switch (gi_type_info_get_tag(elem)) {
1044
+ //
1045
+ // `cArrayCell` is true for a C array in EITHER direction: its elements are sized
1046
+ // cells the IN path writes and ReadCElement reads. A GType element is admitted only
1047
+ // there — a list/hash node would carry it through the hash-pointer helpers, which
1048
+ // this file has never exercised for that tag.
1049
+ static bool IsSupportedElementType(GITypeInfo* elem, std::string* why, bool byValueOk,
1050
+ bool cArrayCell) {
1051
+ GITypeTag tag = gi_type_info_get_tag(elem);
1052
+ switch (tag) {
1045
1053
  case GI_TYPE_TAG_BOOLEAN:
1046
1054
  case GI_TYPE_TAG_INT8:
1047
1055
  case GI_TYPE_TAG_UINT8:
@@ -1096,8 +1104,21 @@ static bool IsSupportedElementType(GITypeInfo* elem, std::string* why, bool byVa
1096
1104
  if (iface != nullptr) gi_base_info_unref(iface);
1097
1105
  return ok;
1098
1106
  }
1107
+ // `GdaConnection.statement_execute_select_full`'s `col_types` is the call that
1108
+ // needed it (@gjsify/sqlite reads 64-bit integers exactly by typing columns). It
1109
+ // fell to the default below and was refused as a "nested-container element".
1110
+ case GI_TYPE_TAG_GTYPE:
1111
+ if (!cArrayCell && why != nullptr) *why = "GType element outside a C array";
1112
+ return cArrayCell;
1099
1113
  default:
1100
- if (why != nullptr) *why = "nested-container element";
1114
+ // Name the tag: this label used to be "nested-container element" for EVERY
1115
+ // unhandled tag, which sent the GType case above looking for a nested container.
1116
+ if (why != nullptr) {
1117
+ bool nested = tag == GI_TYPE_TAG_ARRAY || tag == GI_TYPE_TAG_GLIST ||
1118
+ tag == GI_TYPE_TAG_GSLIST || tag == GI_TYPE_TAG_GHASH;
1119
+ *why = nested ? "nested-container element"
1120
+ : std::string(gi_type_tag_to_string(tag)) + " element";
1121
+ }
1101
1122
  return false;
1102
1123
  }
1103
1124
  }
@@ -1121,10 +1142,10 @@ bool IsSupportedContainerType(GITypeInfo* type, std::string* why, ContainerUse u
1121
1142
  // GArray is deliberately excluded alongside the lists: its elements are sized,
1122
1143
  // but g_array_append_vals copies from a buffer this path would have to size the
1123
1144
  // same way, and no installed typelib has a by-value-element GArray as IN.
1124
- bool byValueOk = use == ContainerUse::kIn &&
1125
- gi_type_info_get_tag(type) == GI_TYPE_TAG_ARRAY &&
1126
- gi_type_info_get_array_type(type) == GI_ARRAY_TYPE_C;
1127
- bool ok = IsSupportedElementType(elem, why, byValueOk);
1145
+ bool cArray = gi_type_info_get_tag(type) == GI_TYPE_TAG_ARRAY &&
1146
+ gi_type_info_get_array_type(type) == GI_ARRAY_TYPE_C;
1147
+ bool byValueOk = use == ContainerUse::kIn && cArray;
1148
+ bool ok = IsSupportedElementType(elem, why, byValueOk, cArray);
1128
1149
  gi_base_info_unref(elem);
1129
1150
  return ok;
1130
1151
  }
@@ -1139,7 +1160,7 @@ bool IsSupportedContainerType(GITypeInfo* type, std::string* why, ContainerUse u
1139
1160
  ktag == GI_TYPE_TAG_INT8 || ktag == GI_TYPE_TAG_UINT8 ||
1140
1161
  ktag == GI_TYPE_TAG_INT16 || ktag == GI_TYPE_TAG_UINT16 ||
1141
1162
  ktag == GI_TYPE_TAG_INT32 || ktag == GI_TYPE_TAG_UINT32;
1142
- bool vok = vt != nullptr && IsSupportedElementType(vt, nullptr, false);
1163
+ bool vok = vt != nullptr && IsSupportedElementType(vt, nullptr, false, false);
1143
1164
  if (!kok && why != nullptr) *why = "unsupported GHashTable key";
1144
1165
  else if (!vok && why != nullptr) *why = "unsupported GHashTable value";
1145
1166
  if (kt != nullptr) gi_base_info_unref(kt);
@@ -1166,6 +1187,7 @@ size_t CElementSize(GITypeInfo* elem) {
1166
1187
  case GI_TYPE_TAG_UINT64: return 8;
1167
1188
  case GI_TYPE_TAG_FLOAT: return sizeof(gfloat);
1168
1189
  case GI_TYPE_TAG_DOUBLE: return sizeof(gdouble);
1190
+ case GI_TYPE_TAG_GTYPE: return sizeof(GType);
1169
1191
  case GI_TYPE_TAG_UTF8:
1170
1192
  case GI_TYPE_TAG_FILENAME:
1171
1193
  case GI_TYPE_TAG_INTERFACE: return sizeof(gpointer);
@@ -1329,6 +1351,7 @@ static Napi::Value ReadCElement(Napi::Env env, GITypeInfo* elem, const void* src
1329
1351
  case GI_TYPE_TAG_UINT64: a.v_uint64 = *static_cast<const guint64*>(src); break;
1330
1352
  case GI_TYPE_TAG_FLOAT: a.v_float = *static_cast<const gfloat*>(src); break;
1331
1353
  case GI_TYPE_TAG_DOUBLE: a.v_double = *static_cast<const gdouble*>(src); break;
1354
+ case GI_TYPE_TAG_GTYPE: a.v_size = *static_cast<const GType*>(src); break;
1332
1355
  case GI_TYPE_TAG_UTF8:
1333
1356
  case GI_TYPE_TAG_FILENAME: a.v_string = *static_cast<char* const*>(src); break;
1334
1357
  case GI_TYPE_TAG_INTERFACE: a.v_pointer = *static_cast<gpointer const*>(src); break;
@@ -1599,6 +1622,20 @@ static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v,
1599
1622
  case GI_TYPE_TAG_UINT64: a->v_uint64 = JsValueToUint64(v); return true;
1600
1623
  case GI_TYPE_TAG_FLOAT: a->v_float = static_cast<gfloat>(NodeGiToDouble(v)); return true;
1601
1624
  case GI_TYPE_TAG_DOUBLE: a->v_double = NodeGiToDouble(v); return true;
1625
+ case GI_TYPE_TAG_GTYPE: {
1626
+ // Stricter than a scalar GType argument, as gjs is (`test_array_gtype_in
1627
+ // ([undefined])` throws): a null element would become G_TYPE_INVALID, which in
1628
+ // a zero-terminated array silently truncates everything after it.
1629
+ if (v.IsNull() || v.IsUndefined()) {
1630
+ Napi::TypeError::New(env, "expected a GType handle (e.g. Class.$gtype) as an array element")
1631
+ .ThrowAsJavaScriptException();
1632
+ return false;
1633
+ }
1634
+ GType gt = 0;
1635
+ if (!UnwrapGTypeArg(env, v, &gt)) return false;
1636
+ a->v_size = static_cast<gsize>(gt);
1637
+ return true;
1638
+ }
1602
1639
  case GI_TYPE_TAG_UTF8:
1603
1640
  case GI_TYPE_TAG_FILENAME:
1604
1641
  a->v_string = (v.IsEmpty() || v.IsNull() || v.IsUndefined())
@@ -1679,6 +1716,330 @@ static size_t BoxedInfoSize(GIBaseInfo* info) {
1679
1716
  return 0;
1680
1717
  }
1681
1718
 
1719
+ static bool ByValueRecordToCell(Napi::Env env, GITypeInfo* elem, Napi::Value v, void* dst,
1720
+ size_t elemSize, gpointer* writeBackTo);
1721
+ static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfer transfer,
1722
+ gpointer* outPtr, long* outCount, std::vector<gpointer>* writeBack);
1723
+
1724
+ // ---- a call C makes INTO JS: OUT / INOUT parameters and the answer ---------
1725
+ //
1726
+ // C calls a JS `vfunc_*` override or a GI callback with a POINTER for each OUT/INOUT
1727
+ // parameter and reads the answer back through it. Both trampolines used to hand those
1728
+ // pointers to JS as if they were IN values and never wrote anything back, so every
1729
+ // override with an OUT parameter answered zeros: `Gtk.LayoutManager.vfunc_measure`
1730
+ // returned `[min, nat, minBaseline, natBaseline]` from JS and GTK read `0, 0, 0, 0`.
1731
+ // That is what left a React Native `<ScrollView horizontal>` rail zero-height under
1732
+ // node-gi while the same code measured correctly on gjs. One class serves both
1733
+ // trampolines so the two cannot answer in different shapes.
1734
+ //
1735
+ // The shape is gjs's (gi/function.cpp callback_closure_inner): a pure OUT is not a
1736
+ // JS argument, an INOUT passes its current value, a `void`-typed OUT is skipped
1737
+ // entirely; one output (the return, or a lone OUT) is the JS return value itself,
1738
+ // several are an array — the return first, then the OUT/INOUT in declaration order.
1739
+ //
1740
+ // OWNERSHIP follows the parameter's transfer, through the same JsToGIArgument the IN
1741
+ // path uses: on EVERYTHING the C caller adopts the value, so an object gets its own
1742
+ // ref and a boxed its own copy; on NOTHING it borrows the wrapper's. Only the slot's
1743
+ // own bytes are copied out of the union (CInElementSize), never all eight, because the
1744
+ // destination is the caller's `int`/`gboolean`/enum variable and not a GIArgument.
1745
+
1746
+ namespace {
1747
+
1748
+ // How many bytes one scalar-shaped OUT/INOUT slot's variable holds, or 0 when it is
1749
+ // not one: a by-value record not marked caller-allocates, a GType and the containers
1750
+ // have no element write here (a self-delimiting OUT array has its own, in WriteOut),
1751
+ // and are refused with the parameter's name rather than half-written.
1752
+ size_t OutSlotSize(GITypeInfo* ti) {
1753
+ if (IsByValueRecordElement(ti)) return 0;
1754
+ size_t size = CInElementSize(ti);
1755
+ return size <= sizeof(GIArgument) ? size : 0;
1756
+ }
1757
+
1758
+ // How many bytes to zero in an unanswered pure OUT: its scalar size, or one pointer
1759
+ // for a container, whose variable is the pointer the caller would free.
1760
+ size_t OutZeroSize(GITypeInfo* ti) {
1761
+ switch (gi_type_info_get_tag(ti)) {
1762
+ case GI_TYPE_TAG_ARRAY:
1763
+ case GI_TYPE_TAG_GLIST:
1764
+ case GI_TYPE_TAG_GSLIST:
1765
+ case GI_TYPE_TAG_GHASH:
1766
+ case GI_TYPE_TAG_ERROR: return sizeof(gpointer);
1767
+ case GI_TYPE_TAG_GTYPE: return sizeof(GType);
1768
+ default: return OutSlotSize(ti);
1769
+ }
1770
+ }
1771
+
1772
+ // GObject qdata quark for the GPtrArray of transfer-none string answers tied to an
1773
+ // object's lifetime (AssociateStringWithObject below). A function-local static is
1774
+ // safe here — g_quark_from_static_string is idempotent and already how this file's
1775
+ // other qdata keys are declared (see class.cc's NodeGiClassDataQuark).
1776
+ static GQuark NodeGiInstanceStringsQuark() {
1777
+ static GQuark q = g_quark_from_static_string("node-gi-instance-strings");
1778
+ return q;
1779
+ }
1780
+
1781
+ // Record `str` (already g_strdup'd) to be freed when `obj` is finalized. Mirrors
1782
+ // gjs's ObjectInstance::associate_string (refs/gjs/gi/object.cpp): a GPtrArray hung
1783
+ // off the object's own qdata, created on first use and freed automatically by
1784
+ // g_object_set_qdata_full's destroy notify.
1785
+ static void AssociateStringWithObject(GObject* obj, char* str) {
1786
+ auto* strings = static_cast<GPtrArray*>(g_object_get_qdata(obj, NodeGiInstanceStringsQuark()));
1787
+ if (strings == nullptr) {
1788
+ strings = g_ptr_array_new_with_free_func(g_free);
1789
+ g_object_set_qdata_full(obj, NodeGiInstanceStringsQuark(), strings,
1790
+ reinterpret_cast<GDestroyNotify>(g_ptr_array_unref));
1791
+ }
1792
+ g_ptr_array_add(strings, str);
1793
+ }
1794
+
1795
+ // A string C keeps. A transfer-full one is C's to free, so it gets its own copy. A
1796
+ // transfer-none one must outlive this frame with NOBODY freeing it — a JsToGIArgument
1797
+ // string would point into a std::string that dies here.
1798
+ //
1799
+ // `owner` (the vfunc's instance, when there is one) gets first refusal: tying the
1800
+ // string to ITS lifetime bounds the cost to that object's lifetime, exactly as gjs
1801
+ // does. Without an owner (a plain GI callback has none) g_intern_string is the only
1802
+ // option — valid forever, one copy per distinct value — and that IS gjs's own
1803
+ // fallback there too. Interning unconditionally (this file's previous approach) is
1804
+ // the wrong default for the common vfunc case: a JS override that answers a FRESH
1805
+ // transfer-none string every call (e.g. Gtk.Editable's vfunc_get_text on a
1806
+ // live-updating buffer) would intern one string PER CALL, forever, for the life of
1807
+ // the process — unbounded growth a real widget hits on every keystroke.
1808
+ char* StringForC(Napi::Value v, GITransfer transfer, GObject* owner) {
1809
+ if (!v.IsString()) return nullptr;
1810
+ std::string s = v.As<Napi::String>().Utf8Value();
1811
+ if (transfer != GI_TRANSFER_NOTHING) return g_strdup(s.c_str());
1812
+ if (owner != nullptr) {
1813
+ char* dup = g_strdup(s.c_str());
1814
+ AssociateStringWithObject(owner, dup);
1815
+ return dup;
1816
+ }
1817
+ return const_cast<char*>(g_intern_string(s.c_str()));
1818
+ }
1819
+
1820
+ bool JsToAnswerArgument(Napi::Env env, Napi::Value v, GITypeInfo* ti, GITransfer transfer,
1821
+ GIArgument* out, const char* argName, GObject* owner) {
1822
+ memset(out, 0, sizeof(*out));
1823
+ GITypeTag tag = gi_type_info_get_tag(ti);
1824
+ if (tag == GI_TYPE_TAG_UTF8 || tag == GI_TYPE_TAG_FILENAME) {
1825
+ out->v_string = StringForC(v, transfer, owner);
1826
+ return true;
1827
+ }
1828
+ std::string held;
1829
+ return JsToGIArgument(env, v, ti, out, &held, transfer, nullptr, nullptr, nullptr, nullptr,
1830
+ nullptr, argName);
1831
+ }
1832
+
1833
+ } // namespace
1834
+
1835
+ CToJsCall::CToJsCall(Napi::Env env, GICallableInfo* ci, std::string label, GObject* owner)
1836
+ : env_(env), ci_(ci), label_(std::move(label)), owner_(owner) {}
1837
+
1838
+ CToJsCall::~CToJsCall() {
1839
+ for (const Slot& s : outs_) {
1840
+ gi_base_info_unref(s.ti);
1841
+ gi_base_info_unref(s.ai);
1842
+ }
1843
+ }
1844
+
1845
+ void CToJsCall::ThrowUnsupported(const Slot& s) const {
1846
+ Napi::TypeError::New(env_, label_ + ": OUT/INOUT parameter '" +
1847
+ gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(s.ai)) +
1848
+ "' has a type node-gi cannot write back from JS")
1849
+ .ThrowAsJavaScriptException();
1850
+ }
1851
+
1852
+ bool CToJsCall::MarshalArgs(void** args, unsigned int offset, std::vector<napi_value>* jsArgs) {
1853
+ unsigned int n = gi_callable_info_get_n_args(ci_);
1854
+ jsArgs->reserve(n);
1855
+ // Every arg's C value, indexed like the callable's args, so a length-annotated
1856
+ // C-array arg can read its companion length (ReadOutOrReturn's `slots`). Without
1857
+ // it such an array is not zero-terminated and reads as EMPTY — measured: a JS
1858
+ // Gio.OutputStream's vfunc_write_fn(buffer) got [] and returned 0 where gjs
1859
+ // 1.88.1 hands it all 3 bytes, a silent short write (write_all would spin).
1860
+ // An ffi slot holds the value at its own width; OutSlotSize is that width for a
1861
+ // scalar and 0 for anything that can never be a length.
1862
+ // `isLength` marks those companions: gjs never hands one to JS (the array carries
1863
+ // its length), so the handler's later args keep their gjs positions —
1864
+ // vfunc_write_fn(buffer, cancellable), not (buffer, count, cancellable).
1865
+ std::vector<GIArgument> slots(n);
1866
+ std::vector<bool> isLength(n, false);
1867
+ for (unsigned int i = 0; i < n; i++) {
1868
+ GIArgInfo* ai = gi_callable_info_get_arg(ci_, i);
1869
+ GITypeInfo* ti = gi_arg_info_get_type_info(ai);
1870
+ GIDirection dir = gi_arg_info_get_direction(ai);
1871
+ unsigned int lenIdx = 0;
1872
+ if (gi_type_info_get_tag(ti) == GI_TYPE_TAG_ARRAY &&
1873
+ gi_type_info_get_array_length_index(ti, &lenIdx) && lenIdx < n)
1874
+ isLength[lenIdx] = true;
1875
+ size_t size = gi_type_info_get_tag(ti) == GI_TYPE_TAG_VOID ? 0 : OutSlotSize(ti);
1876
+ void* src = args[i + offset];
1877
+ if (dir == GI_DIRECTION_INOUT && src != nullptr && !gi_arg_info_is_caller_allocates(ai))
1878
+ src = static_cast<GIArgument*>(src)->v_pointer;
1879
+ else if (dir == GI_DIRECTION_OUT)
1880
+ src = nullptr;
1881
+ if (src != nullptr && size > 0) memcpy(&slots[i], src, size);
1882
+ gi_base_info_unref(ti);
1883
+ gi_base_info_unref(ai);
1884
+ }
1885
+ // After a failed conversion the loop keeps going, recording only the OUT slots, so
1886
+ // WriteAnswer can still zero every one of them — JS will not run to answer.
1887
+ bool failed = false;
1888
+ for (unsigned int i = 0; i < n; i++) {
1889
+ GIArgInfo* ai = gi_callable_info_get_arg(ci_, i);
1890
+ GITypeInfo* ti = gi_arg_info_get_type_info(ai);
1891
+ GIArgument* ffiSlot = static_cast<GIArgument*>(args[i + offset]);
1892
+ GIDirection dir = gi_arg_info_get_direction(ai);
1893
+ if (dir == GI_DIRECTION_IN || gi_type_info_get_tag(ti) == GI_TYPE_TAG_VOID) {
1894
+ if (dir == GI_DIRECTION_IN && !failed && !isLength[i]) {
1895
+ // ReadOutOrReturn, not GIArgumentToJs directly: a C-invoked callback's
1896
+ // IN argument can be a container too (Soup.ServerCallback's trailing
1897
+ // `query` is a GHashTable, GI_TYPE_TAG_GHASH) — GIArgumentToJs's switch
1898
+ // has no ARRAY/GLIST/GSLIST/GHASH arm and threw "Unsupported return
1899
+ // type tag 19" on EVERY invocation, aborting the callback (and, for a
1900
+ // scope=call libsoup request handler, the connection) before the JS
1901
+ // side ever ran. `slots` resolves a length-annotated C array (above).
1902
+ Napi::Value v = ReadOutOrReturn(env_, ci_, ti, ffiSlot, GI_TRANSFER_NOTHING, &slots);
1903
+ failed = env_.IsExceptionPending();
1904
+ if (!failed) jsArgs->push_back(v);
1905
+ }
1906
+ gi_base_info_unref(ti);
1907
+ gi_base_info_unref(ai);
1908
+ continue;
1909
+ }
1910
+ // The ffi slot of an OUT/INOUT holds the caller's pointer: to its variable, or for
1911
+ // a caller-allocates record to the record itself.
1912
+ outs_.push_back({ai, ti, ffiSlot->v_pointer});
1913
+ if (dir != GI_DIRECTION_INOUT || failed) continue;
1914
+ const Slot& s = outs_.back();
1915
+ GIArgument cur;
1916
+ memset(&cur, 0, sizeof(cur));
1917
+ if (s.dest == nullptr) {
1918
+ jsArgs->push_back(env_.Null());
1919
+ continue;
1920
+ }
1921
+ if (gi_arg_info_is_caller_allocates(ai)) {
1922
+ cur.v_pointer = s.dest;
1923
+ } else {
1924
+ size_t size = OutSlotSize(ti);
1925
+ if (size == 0) {
1926
+ ThrowUnsupported(s);
1927
+ failed = true;
1928
+ continue;
1929
+ }
1930
+ memcpy(&cur, s.dest, size);
1931
+ }
1932
+ // Same container gap as the IN-arg read above (an INOUT container arg is
1933
+ // rarer, but the fix is identical): ReadOutOrReturn, not GIArgumentToJs.
1934
+ Napi::Value v = ReadOutOrReturn(env_, ci_, ti, &cur, GI_TRANSFER_NOTHING, &slots);
1935
+ failed = env_.IsExceptionPending();
1936
+ if (!failed) jsArgs->push_back(v);
1937
+ }
1938
+ return !failed;
1939
+ }
1940
+
1941
+ bool CToJsCall::WriteOut(const Slot& s, Napi::Value v) {
1942
+ // A nullable OUT the caller passed NULL for: it did not ask, so there is nothing to
1943
+ // write and nothing to refuse.
1944
+ if (s.dest == nullptr) return true;
1945
+ const char* name = gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(s.ai));
1946
+ if (gi_arg_info_is_caller_allocates(s.ai)) {
1947
+ // The caller owns the storage and wants the record's BYTES in it. The by-value
1948
+ // array-cell writer already does exactly that and checks the handle is the SAME
1949
+ // record type first — copying the destination's size out of a smaller handle of
1950
+ // another type would read past its end. A GValue is initialised and deep-copied
1951
+ // rather than aliased, which is also gjs's one caller-allocates case here.
1952
+ GIBaseInfo* iface = gi_type_info_get_tag(s.ti) == GI_TYPE_TAG_INTERFACE
1953
+ ? gi_type_info_get_interface(s.ti)
1954
+ : nullptr;
1955
+ size_t size = BoxedInfoSize(iface);
1956
+ if (iface != nullptr) gi_base_info_unref(iface);
1957
+ if (size == 0) {
1958
+ ThrowUnsupported(s);
1959
+ return false;
1960
+ }
1961
+ return ByValueRecordToCell(env_, s.ti, v, s.dest, size, nullptr);
1962
+ }
1963
+ GITransfer transfer = gi_arg_info_get_ownership_transfer(s.ai);
1964
+ unsigned int lengthIndex = 0;
1965
+ if (gi_type_info_get_tag(s.ti) == GI_TYPE_TAG_ARRAY &&
1966
+ !gi_type_info_get_array_length_index(s.ti, &lengthIndex)) {
1967
+ // A self-delimiting array (zero-terminated, fixed-size, or a GArray/GPtrArray/
1968
+ // GByteArray) is one pointer the caller reads, built the way an IN array is. One
1969
+ // with a separate length parameter is refused below: that length is a second OUT
1970
+ // gjs would expect in the answer too, and writing it from the array is its own
1971
+ // decision, not a side effect of this one.
1972
+ gpointer arr = nullptr;
1973
+ long count = 0;
1974
+ if (!JsToCArray(env_, v, s.ti, transfer, &arr, &count, nullptr)) return false;
1975
+ *static_cast<gpointer*>(s.dest) = arr;
1976
+ return true;
1977
+ }
1978
+ size_t size = OutSlotSize(s.ti);
1979
+ if (size == 0) {
1980
+ ThrowUnsupported(s);
1981
+ return false;
1982
+ }
1983
+ GIArgument a;
1984
+ if (!JsToAnswerArgument(env_, v, s.ti, transfer, &a, name, owner_)) return false;
1985
+ memcpy(s.dest, &a, size);
1986
+ return true;
1987
+ }
1988
+
1989
+ void CToJsCall::WriteAnswer(napi_value ret, void* result) {
1990
+ GITypeInfo* retType = gi_callable_info_get_return_type(ci_);
1991
+ GITypeTag rtag = gi_type_info_get_tag(retType);
1992
+ bool hasRet = rtag != GI_TYPE_TAG_VOID;
1993
+ size_t nAnswers = outs_.size() + (hasRet ? 1 : 0);
1994
+ size_t written = 0;
1995
+ if (nAnswers > 0 && ret != nullptr && !env_.IsExceptionPending()) {
1996
+ Napi::Value rv(env_, ret);
1997
+ std::vector<Napi::Value> answers;
1998
+ if (nAnswers == 1) {
1999
+ answers.push_back(rv);
2000
+ } else if (rv.IsArray() && rv.As<Napi::Array>().Length() == nAnswers) {
2001
+ Napi::Array arr = rv.As<Napi::Array>();
2002
+ for (uint32_t k = 0; k < nAnswers && !env_.IsExceptionPending(); k++)
2003
+ answers.push_back(arr.Get(k));
2004
+ } else {
2005
+ Napi::TypeError::New(env_, label_ + " must return an array of " +
2006
+ std::to_string(nAnswers) +
2007
+ " values (its return value and OUT parameters)")
2008
+ .ThrowAsJavaScriptException();
2009
+ }
2010
+ if (answers.size() == nAnswers && !env_.IsExceptionPending()) {
2011
+ size_t k = 0;
2012
+ bool ok = true;
2013
+ if (hasRet) {
2014
+ GIArgument a;
2015
+ ok = JsToAnswerArgument(env_, answers[k++], retType,
2016
+ gi_callable_info_get_caller_owns(ci_), &a, "return value",
2017
+ owner_);
2018
+ // The ffi return slot is at least ffi_arg wide and was zeroed by the caller,
2019
+ // so the whole union can go in: libffi reads the low bytes it needs.
2020
+ if (ok && result != nullptr) *static_cast<GIArgument*>(result) = a;
2021
+ }
2022
+ while (ok && written < outs_.size()) {
2023
+ ok = WriteOut(outs_[written], answers[k++]);
2024
+ if (ok) written++;
2025
+ }
2026
+ }
2027
+ }
2028
+ // Whatever was not answered — JS threw, returned the wrong shape, or a value did
2029
+ // not convert — must not leave C reading an uninitialised variable: a pure OUT is
2030
+ // one the caller need not have initialised. Zero it (the return slot already is);
2031
+ // the JS exception stays pending for the trampoline to surface or propagate.
2032
+ for (size_t i = written; i < outs_.size(); i++) {
2033
+ const Slot& s = outs_[i];
2034
+ if (s.dest == nullptr || gi_arg_info_get_direction(s.ai) != GI_DIRECTION_OUT ||
2035
+ gi_arg_info_is_caller_allocates(s.ai))
2036
+ continue;
2037
+ size_t size = OutZeroSize(s.ti);
2038
+ if (size != 0) memset(s.dest, 0, size);
2039
+ }
2040
+ gi_base_info_unref(retType);
2041
+ }
2042
+
1682
2043
  // Write ONE by-value record element straight into its C-array cell.
1683
2044
  //
1684
2045
  // WHY THIS IS NOT A GIArgument, which is the whole reason the function exists. The
@@ -2381,6 +2742,7 @@ static bool ElementsAreReadable(GITypeInfo* type) {
2381
2742
  case GI_TYPE_TAG_UINT64:
2382
2743
  case GI_TYPE_TAG_FLOAT:
2383
2744
  case GI_TYPE_TAG_DOUBLE:
2745
+ case GI_TYPE_TAG_GTYPE:
2384
2746
  case GI_TYPE_TAG_UTF8:
2385
2747
  case GI_TYPE_TAG_FILENAME:
2386
2748
  readable = true;