@gjsify/node-gi 0.51.1 → 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/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;