@gjsify/node-gi 0.40.0 → 0.41.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/README.md CHANGED
@@ -609,8 +609,7 @@ shared per-instance JS object yet), so use GObject **properties** for any state
609
609
  that must be visible both inside a vfunc and on the instance (those live in C and
610
610
  are consistent; the unified instance identity arrives with the toggle-ref work);
611
611
  a JS↔GObject reference cycle on a custom instance leaks (the same cycle-leak
612
- caveat the signal/vfunc layer carries); and multi-level registered subclass
613
- chains (registering a subclass of a registered subclass) are not yet supported.
612
+ caveat the signal/vfunc layer carries).
614
613
 
615
614
  #### `GObject` conveniences (signals, `GObject.Value`, `Object.new`)
616
615
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "GObject-Introspection runtime for Node.js — load gi:// namespaces (GLib/GObject/Gio/…) with GJS-compatible semantics. Vendored + rewritten from node-gtk under MIT.",
5
5
  "type": "module",
6
6
  "license": "MIT",
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/class.cc CHANGED
@@ -669,8 +669,6 @@ static void NodeGiClassInit(gpointer g_class, gpointer class_data) {
669
669
  // return-tuple convention `[returnValue?, ...outArgs]` — one value bare, several as
670
670
  // an Array, matching exactly what a JS override of that vfunc receives as its call
671
671
  // args. Read-back reuses ReadOutOrReturn (array-length slots, containers, boxed).
672
- // INOUT containers stay deferred (the same rare, ownership-tricky case the function
673
- // path defers) with a clear, catchable throw BEFORE the ffi_call.
674
672
  //
675
673
  // MULTI-LEVEL chain-up (registered chains, G2): chains to the DEEPEST registered
676
674
  // override's captured parent (the C-side default below the whole registered chain),
package/src/marshal.cc CHANGED
@@ -1736,14 +1736,134 @@ static bool ReadFieldArg(GIFieldInfo* field, gpointer base, GITypeInfo* ftype,
1736
1736
  return false;
1737
1737
  }
1738
1738
 
1739
- // Marshal a field GIArgument to JS. ARRAY tags dispatch to GIArrayToJs (length
1740
- // derived from zero-terminated / fixed-size — the common GStrv field case);
1741
- // everything else through GIArgumentToJs. TRANSFER NOTHING throughout — a field
1742
- // read is a borrow (the container keeps ownership), so the JS value is a copy and
1743
- // no container/element memory is freed.
1744
- static Napi::Value FieldArgToJs(Napi::Env env, GITypeInfo* ftype, GIArgument* arg) {
1739
+ // Field #`index` of a struct/union, or nullptr. The by-NAME lookup is
1740
+ // `FindBoxedField`; this is the by-POSITION one an `array length=` annotation needs.
1741
+ static GIFieldInfo* BoxedFieldAt(GIBaseInfo* owner, unsigned index) {
1742
+ if (GI_IS_STRUCT_INFO(owner)) {
1743
+ GIStructInfo* s = reinterpret_cast<GIStructInfo*>(owner);
1744
+ return index < gi_struct_info_get_n_fields(s) ? gi_struct_info_get_field(s, index) : nullptr;
1745
+ }
1746
+ if (GI_IS_UNION_INFO(owner)) {
1747
+ GIUnionInfo* u = reinterpret_cast<GIUnionInfo*>(owner);
1748
+ return index < gi_union_info_get_n_fields(u) ? gi_union_info_get_field(u, index) : nullptr;
1749
+ }
1750
+ return nullptr;
1751
+ }
1752
+
1753
+ // Can `ReadCElement` actually read this array's elements?
1754
+ //
1755
+ // Resolving a length is only ever an improvement where the reader can then walk that
1756
+ // many elements. Two shapes it cannot: an element tag it answers with `Undefined()`
1757
+ // (its `default:`), and an INLINE (by-value) interface element — for those it
1758
+ // dereferences `src` as a pointer while `CElementSize` reports `sizeof(gpointer)`
1759
+ // rather than the record's real size, so a resolved length walks garbage. Measured:
1760
+ // `new Pango.GlyphString(); gs.set_size(3); gs.glyphs[0].glyph` SIGSEGVs with a
1761
+ // length and returns an empty array without one.
1762
+ //
1763
+ // `calls.cc` draws exactly this line at its own CALLER_ALLOCATES site, with the same
1764
+ // reason recorded there — a by-value element array needs `gi_struct_info_get_size`
1765
+ // per element plus a field-access read-back. That is ONE deferred piece of work, and
1766
+ // this predicate keeps the field path on the same side of it rather than opening a
1767
+ // second, crashing entrance to it.
1768
+ static bool ElementsAreReadable(GITypeInfo* type) {
1769
+ GITypeInfo* elem = gi_type_info_get_param_type(type, 0);
1770
+ if (elem == nullptr) return false;
1771
+ bool readable;
1772
+ switch (gi_type_info_get_tag(elem)) {
1773
+ case GI_TYPE_TAG_BOOLEAN:
1774
+ case GI_TYPE_TAG_INT8:
1775
+ case GI_TYPE_TAG_UINT8:
1776
+ case GI_TYPE_TAG_INT16:
1777
+ case GI_TYPE_TAG_UINT16:
1778
+ case GI_TYPE_TAG_INT32:
1779
+ case GI_TYPE_TAG_UINT32:
1780
+ case GI_TYPE_TAG_INT64:
1781
+ case GI_TYPE_TAG_UINT64:
1782
+ case GI_TYPE_TAG_FLOAT:
1783
+ case GI_TYPE_TAG_DOUBLE:
1784
+ case GI_TYPE_TAG_UTF8:
1785
+ case GI_TYPE_TAG_FILENAME:
1786
+ readable = true;
1787
+ break;
1788
+ // A pointer array of interfaces is a `gpointer` per slot, which is what
1789
+ // `ReadCElement` reads. By value it is the record itself, which is not.
1790
+ case GI_TYPE_TAG_INTERFACE:
1791
+ readable = gi_type_info_is_pointer(elem);
1792
+ break;
1793
+ default:
1794
+ readable = false;
1795
+ break;
1796
+ }
1797
+ gi_base_info_unref(elem);
1798
+ return readable;
1799
+ }
1800
+
1801
+ // The element count for an array FIELD whose GIR carries `array length=<n>`,
1802
+ // meaning "sibling field <n> of this same struct holds it".
1803
+ //
1804
+ // This is the one GIArrayToJs call site that used to pass a hard `-1`, and the cost
1805
+ // was silence: `GstMapInfo.data` is `<array length="3" c:type="guint8*">` with field 3
1806
+ // being `size`, so a mapped 32-byte buffer marshalled as `size=32, data.length=0` — no
1807
+ // error, no warning, an empty array indistinguishable from a genuinely empty one. That
1808
+ // made audio inaudible on node for an entire investigation because every layer above
1809
+ // reported success. gjs reads 32 for the identical struct.
1810
+ //
1811
+ // The ledger entry that recorded this said the dependency was one "GI cannot express
1812
+ // for a struct-field READ". It can, and does: the annotation is in the GIR and survives
1813
+ // into the typelib, which is why the CALL-argument path (`calls.cc`) has resolved it for
1814
+ // as long as it has existed. Only the field reader ignored it.
1815
+ //
1816
+ // Returns false when there is no annotation, when the index is out of range, or when
1817
+ // the sibling is not integer-typed — all of which leave the caller on its previous
1818
+ // zero-terminated/fixed-size derivation rather than inventing a length.
1819
+ static bool FieldArrayLength(GITypeInfo* ftype, GIBaseInfo* owner, gpointer base, long* out) {
1820
+ unsigned int index = 0;
1821
+ if (!gi_type_info_get_array_length_index(ftype, &index)) return false;
1822
+ if (!ElementsAreReadable(ftype)) return false;
1823
+ GIFieldInfo* lengthField = BoxedFieldAt(owner, index);
1824
+ if (lengthField == nullptr) return false;
1825
+
1826
+ bool resolved = false;
1827
+ GITypeInfo* ltype = gi_field_info_get_type_info(lengthField);
1828
+ if (ltype != nullptr) {
1829
+ GIArgument larg;
1830
+ if (ReadFieldArg(lengthField, base, ltype, &larg)) {
1831
+ switch (gi_type_info_get_tag(ltype)) {
1832
+ case GI_TYPE_TAG_INT8: *out = larg.v_int8; resolved = true; break;
1833
+ case GI_TYPE_TAG_UINT8: *out = larg.v_uint8; resolved = true; break;
1834
+ case GI_TYPE_TAG_INT16: *out = larg.v_int16; resolved = true; break;
1835
+ case GI_TYPE_TAG_UINT16: *out = larg.v_uint16; resolved = true; break;
1836
+ case GI_TYPE_TAG_INT32: *out = larg.v_int32; resolved = true; break;
1837
+ case GI_TYPE_TAG_UINT32: *out = static_cast<long>(larg.v_uint32); resolved = true; break;
1838
+ case GI_TYPE_TAG_INT64: *out = static_cast<long>(larg.v_int64); resolved = true; break;
1839
+ case GI_TYPE_TAG_UINT64: *out = static_cast<long>(larg.v_uint64); resolved = true; break;
1840
+ default: break; // non-integer sibling: not a length, leave it alone
1841
+ }
1842
+ }
1843
+ gi_base_info_unref(ltype);
1844
+ }
1845
+ gi_base_info_unref(lengthField);
1846
+ // A signed length field holding a negative value is a broken typelib, not a count.
1847
+ // Declining sends it down the zero-terminated/fixed-size derivation the caller would
1848
+ // have used anyway — which is also what a negative reaching `GIArrayToJs` does, so this
1849
+ // changes no observable behaviour today. It is here to keep the CONTRACT of this
1850
+ // function honest: it returns a resolved element count or nothing.
1851
+ if (resolved && *out < 0) resolved = false;
1852
+ return resolved;
1853
+ }
1854
+
1855
+ // Marshal a field GIArgument to JS. ARRAY tags dispatch to GIArrayToJs with the
1856
+ // length resolved from an `array length=` sibling field where the GIR declares one,
1857
+ // and otherwise derived from zero-terminated / fixed-size (the common GStrv field
1858
+ // case). Everything else goes through GIArgumentToJs. TRANSFER NOTHING throughout —
1859
+ // a field read is a borrow (the container keeps ownership), so the JS value is a copy
1860
+ // and no container/element memory is freed.
1861
+ static Napi::Value FieldArgToJs(Napi::Env env, GITypeInfo* ftype, GIArgument* arg,
1862
+ GIBaseInfo* owner, gpointer base) {
1745
1863
  if (gi_type_info_get_tag(ftype) == GI_TYPE_TAG_ARRAY) {
1746
- return GIArrayToJs(env, ftype, arg, GI_TRANSFER_NOTHING, /*length=*/-1);
1864
+ long length = -1;
1865
+ if (!FieldArrayLength(ftype, owner, base, &length)) length = -1;
1866
+ return GIArrayToJs(env, ftype, arg, GI_TRANSFER_NOTHING, length);
1747
1867
  }
1748
1868
  return GIArgumentToJs(env, ftype, arg, GI_TRANSFER_NOTHING);
1749
1869
  }
@@ -1854,7 +1974,7 @@ Napi::Value GetBoxedField(const Napi::CallbackInfo& info) {
1854
1974
  }
1855
1975
  GIArgument arg;
1856
1976
  if (ReadFieldArg(field, bh->ptr, ftype, &arg)) {
1857
- result = FieldArgToJs(env, ftype, &arg);
1977
+ result = FieldArgToJs(env, ftype, &arg, ti, bh->ptr);
1858
1978
  } else {
1859
1979
  Napi::Error::New(env, std::string("Reading field ") + typeName + "." + name + " is not supported")
1860
1980
  .ThrowAsJavaScriptException();