@gjsify/node-gi 0.38.1 → 0.40.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
@@ -479,6 +479,47 @@ A *passive* fd source with no pending op (e.g. only a listening
479
479
  `Gio.SocketService`) does not keep the process alive on its own, and a
480
480
  purely-sync program still exits immediately.
481
481
 
482
+ #### Class prototypes and the lookup order
483
+
484
+ `Ns.Class.prototype` is a real prototype: reading a name on it MATERIALIZES the
485
+ introspected method behind that name (the JS twin of the `resolve` hook GJS
486
+ installs on every GObject prototype), and every wrapper resolves its members
487
+ through the prototype of its runtime GType — a `registerClass` subclass's own
488
+ prototype, otherwise the introspected class's — before any native fallback. So
489
+ instrumenting a method works the way it does on GJS:
490
+
491
+ ```js
492
+ const proto = Gio.ZlibDecompressor.prototype;
493
+ const original = proto.convert; // the introspected method
494
+ proto.convert = function (...args) { // instrument it
495
+ calls++;
496
+ return original.apply(this, args);
497
+ };
498
+ const d = new Gio.ZlibDecompressor({ format: Gio.ZlibCompressorFormat.GZIP });
499
+ d.convert === proto.convert; // true — and the wrapper IS called
500
+ proto.convert = original; // put it back
501
+ ```
502
+
503
+ Until 0.39 an instance ignored its prototype: the assignment stuck, `d.convert`
504
+ still reached the native method, and a spy-based test reported itself installed
505
+ while measuring nothing. Byte-compared against GJS by the `prototype-chain`
506
+ conformance program.
507
+
508
+ Lookup order on a wrapper: own JS field → the class prototype chain →
509
+ `runAsync` / the GObject.js shims → GObject property → inherited `Object.prototype`
510
+ member → a `Gio._promisify` registration → introspected method → `undefined`.
511
+
512
+ Deliberate divergences from GJS, all measured:
513
+
514
+ - class prototypes are NOT chained to their base classes, so `instanceof` goes
515
+ through `g_type_is_a` (see below) and an interface method materializes once per
516
+ implementing class instead of being one shared function;
517
+ - GObject **properties** are not surfaced as prototype accessors — they resolve on
518
+ the instance only;
519
+ - a private concrete GType (`GLocalFile`) resolves to its nearest INTROSPECTABLE
520
+ ancestor rather than to the interface GJS would use, which is why
521
+ `Gio._promisify` also keeps a per-class registry.
522
+
482
523
  #### `GLib.Variant` (build + unpack, GJS semantics)
483
524
 
484
525
  `new GLib.Variant(signature, value)` recursively builds a GVariant from a type
package/gi.js CHANGED
@@ -149,16 +149,16 @@ function buildGError(domainName, domainQuark, code, message) {
149
149
  native.setErrorBuilder(buildGError);
150
150
 
151
151
  // Stamps a makeClass prototype with its { namespace, typeName } so Gio._promisify can
152
- // record WHICH introspected class a registration belongs to — introspected instances
153
- // have no live prototype chain to resolve by, so it is matched against the instance's
154
- // GType via native.isInstanceOf.
152
+ // record WHICH introspected class a registration belongs to — matched against the
153
+ // instance's GType via native.isInstanceOf when the promisified prototype is not the
154
+ // one the instance resolves through.
155
155
  const CLASS_INFO = Symbol('nodeGiClassInfo');
156
156
 
157
157
  // Promisified async methods: GI method name (snake_case) → registrations
158
- // `{ namespace?, typeName?, wrapper }`. Introspected-class instances resolve methods
159
- // dynamically (no prototype chain), so the instance proxy consults this registry;
160
- // when two classes promisify the SAME method name it picks the registration whose
161
- // class the instance is-a.
158
+ // `{ namespace?, typeName?, wrapper }`. An instance whose concrete GType resolves to a
159
+ // DIFFERENT prototype than the promisified one (a private GLocalFile against
160
+ // Gio.File.prototype) reaches the wrapper only through this registry; when two classes
161
+ // promisify the SAME method name it picks the registration whose class the instance is-a.
162
162
  const promisifiedMethods = new Map();
163
163
 
164
164
  function wrapReturn(value) {
@@ -1113,11 +1113,42 @@ function instanceHasMethod(handle, name) {
1113
1113
  return present;
1114
1114
  }
1115
1115
 
1116
- // Wrap a live GObject handle as a GJS-shaped instance. When `userProto` is given
1117
- // (a registerClass subclass's prototype) the wrapper resolves the user class's
1118
- // own prototype members FIRST — so `inst.myMethod()` runs the JS method with the
1119
- // wrapper as `this` — then falls back to GObject property get/set and GI method
1120
- // routing. `.connect()/.emit()/.disconnect()` work in both modes.
1116
+ // The JS prototype a wrapper resolves its members through, by runtime GType name.
1117
+ // Stable for the process — a GType neither gains ancestors nor changes class.
1118
+ const protoByGTypeName = new Map();
1119
+
1120
+ // EVERY wrapper gets a prototype, as in GJS where an instance's [[Prototype]] IS
1121
+ // its class's prototype: without one, a member assigned to `Ns.Class.prototype` was
1122
+ // accepted and observable there yet invisible to the instance, which kept reaching
1123
+ // the native method underneath (#1175) — a spy that reports itself installed and
1124
+ // then measures nothing. A registerClass subclass's own prototype when the GType is
1125
+ // one we registered, else the introspected class's.
1126
+ function protoForInstance(handle) {
1127
+ const gtypeName = native.getTypeName(handle);
1128
+ if (gtypeName === null) return undefined;
1129
+ const cached = protoByGTypeName.get(gtypeName);
1130
+ if (cached !== undefined) return cached === null ? undefined : cached;
1131
+ const registered = classesByGType.get(gtypeName);
1132
+ let proto = registered !== undefined ? registered.prototype : null;
1133
+ if (proto === null) {
1134
+ // Only LOADED namespaces are searched, and a namespace object is reached
1135
+ // through the same per-namespace cache `requireGi` fills, so the prototype
1136
+ // resolved here is the very object a program can patch.
1137
+ const info = native.classInfoForTypeName(gtypeName);
1138
+ const cls = info === null ? undefined : namespaceObject(info.namespace)[info.name];
1139
+ if (cls !== undefined && cls !== null) proto = cls.prototype ?? null;
1140
+ }
1141
+ protoByGTypeName.set(gtypeName, proto);
1142
+ return proto === null ? undefined : proto;
1143
+ }
1144
+
1145
+ // Wrap a live GObject handle as a GJS-shaped instance. The wrapper resolves its
1146
+ // class prototype chain FIRST (a registerClass subclass's own methods, anything
1147
+ // the program put on the introspected prototype) — so `inst.myMethod()` runs the
1148
+ // JS method with the wrapper as `this` — then falls back to GObject property
1149
+ // get/set and GI method routing. `userProto` overrides the prototype derived from
1150
+ // the handle's GType, for the construction paths that already know the leaf class.
1151
+ // `.connect()/.emit()/.disconnect()` work in both modes.
1121
1152
  function wrapInstance(handle, userProto) {
1122
1153
  const cached = instanceCache.get(handle);
1123
1154
  if (cached !== undefined) {
@@ -1139,7 +1170,8 @@ function wrapInstance(handle, userProto) {
1139
1170
  return cached;
1140
1171
  }
1141
1172
  const target = { [HANDLE]: handle };
1142
- if (userProto !== undefined) target[USER_PROTO] = userProto;
1173
+ const proto = userProto ?? protoForInstance(handle);
1174
+ if (proto !== undefined) target[USER_PROTO] = proto;
1143
1175
  const proxy = new Proxy(target, {
1144
1176
  get(t, prop) {
1145
1177
  if (prop === HANDLE) return handle;
@@ -1181,12 +1213,22 @@ function wrapInstance(handle, userProto) {
1181
1213
  default:
1182
1214
  break;
1183
1215
  }
1216
+ // A plain JS field previously written on THIS wrapper. With the toggle-ref
1217
+ // bridge the wrapper is CANONICAL (one proxy per GObject, cached by the
1218
+ // canonical native handle), so a plain field IS shared across the
1219
+ // vfunc<->instance boundary and survives a round-trip + GC while C owns the
1220
+ // object. Read FIRST, before the class prototype — ordinary JS lookup order.
1221
+ // (GObject PROPERTIES remain the right choice for state C must also see; the
1222
+ // `set` trap routes those to GObject, so no expando competes with one.)
1223
+ if (Object.prototype.hasOwnProperty.call(t, prop)) return t[prop];
1184
1224
  const up = t[USER_PROTO];
1185
1225
  if (up !== undefined) {
1186
1226
  const desc = findProtoDescriptor(up, prop);
1187
1227
  if (desc !== undefined) {
1188
- if (typeof desc.value === 'function') return (...args) => desc.value.apply(proxy, args);
1189
1228
  if (typeof desc.get === 'function') return desc.get.call(proxy);
1229
+ // The prototype's OWN function, never a re-bound thunk: `this` comes
1230
+ // from the call site anyway, and re-binding breaks the identity a
1231
+ // spy is recognised by — `inst.m === Cls.prototype.m` (#1175).
1190
1232
  return desc.value;
1191
1233
  }
1192
1234
  }
@@ -1210,25 +1252,16 @@ function wrapInstance(handle, userProto) {
1210
1252
  if (native.hasProperty(handle, propName)) {
1211
1253
  return wrapReturn(native.getProperty(handle, propName));
1212
1254
  }
1213
- // Surface a plain JS field previously written on THIS wrapper. With the
1214
- // toggle-ref bridge the wrapper is now CANONICAL (one proxy per GObject,
1215
- // cached by the canonical native handle), so a plain field IS shared across
1216
- // the vfunc<->instance boundary and survives a round-trip + GC while C owns
1217
- // the object — a vfunc's `this` resolves to the same cached proxy as
1218
- // construct, and `store.get_item(x)` returns the same proxy a setter wrote
1219
- // to. Own-property only (set via the `set` trap), so an introspected GI
1220
- // method of the same name is never shadowed unless the user explicitly
1221
- // assigned an expando. (GObject PROPERTIES remain the right choice for state
1222
- // that must also be visible to C / other language bindings.)
1223
- if (Object.prototype.hasOwnProperty.call(t, prop)) return t[prop];
1224
1255
  // LAST resort before treating an unknown name as a GI method: an INHERITED
1225
1256
  // member (Object.prototype.hasOwnProperty / isPrototypeOf / … — RESERVED
1226
1257
  // already covers toString/valueOf/etc.) must resolve to the real function,
1227
1258
  // not a GI callMethod thunk that would throw on `inst.hasOwnProperty('x')`.
1228
1259
  if (prop in t) return t[prop];
1229
- // A Gio._promisify'd async method (registerClass subclasses pick it up via
1230
- // their userProto above; introspected instances have no prototype chain, so
1231
- // they resolve it here from the registry, per-class). Bound to this instance.
1260
+ // A Gio._promisify'd async method. The prototype lookup above already finds
1261
+ // it whenever the promisified prototype is on the instance's chain; the
1262
+ // registry covers what a chain cannot reach — a private concrete type
1263
+ // (GLocalFile) resolves to its nearest INTROSPECTABLE ancestor, so an
1264
+ // instance of it never sees `Gio.File.prototype`. Bound to this instance.
1232
1265
  const promisified = resolvePromisified(handle, camelToSnake(prop));
1233
1266
  if (promisified !== undefined) return (...args) => promisified.apply(proxy, args);
1234
1267
  // GJS parity: an UNKNOWN member is `undefined`, never a throw-on-call
@@ -1335,6 +1368,91 @@ function defineLazyGType(ctor, namespace, typeName) {
1335
1368
  });
1336
1369
  }
1337
1370
 
1371
+ // The GObject behind a prototype method's `this`. A prototype function is
1372
+ // reachable without an instance (`Cls.prototype.m()`, a detached reference), so
1373
+ // the engine must never be handed `undefined` for the instance argument.
1374
+ function prototypeMethodHandle(self, where) {
1375
+ const handle = self === null || self === undefined ? undefined : self[HANDLE];
1376
+ if (handle === undefined) throw new TypeError(`${where}: \`this\` is not a node-gi instance`);
1377
+ return handle;
1378
+ }
1379
+
1380
+ // The invocable form of an introspected instance method, as it sits on a class
1381
+ // prototype: `this` supplies the GObject, so ONE function serves every instance of
1382
+ // the class and `inst.m === Cls.prototype.m` holds (gjs parity).
1383
+ function makeGiMethod(where, method) {
1384
+ const fn = function (...args) {
1385
+ return wrapReturn(native.callMethod(prototypeMethodHandle(this, where), method, unwrapArgs(args)));
1386
+ };
1387
+ Object.defineProperty(fn, 'name', { value: method, configurable: true });
1388
+ return fn;
1389
+ }
1390
+
1391
+ // A class prototype that MATERIALIZES its introspected methods on first look-up —
1392
+ // the JS twin of the SpiderMonkey `resolve` hook gjs installs on every GObject
1393
+ // prototype (refs/gjs gi/object.cpp). Without it a node-gi prototype stayed
1394
+ // permanently empty, so `Cls.prototype.method` read `undefined` and wrapping a
1395
+ // method had nothing to wrap (#1175). Lazy, not eager: a class like Gtk.Widget
1396
+ // carries hundreds of inherited methods and a program touches a handful.
1397
+ function makeClassPrototype(namespace, typeName) {
1398
+ const target = {};
1399
+ // Put the vfunc chain-up Proxy beneath the prototype so a registerClass
1400
+ // subclass's `super.vfunc_<name>(...)` resolves (see vfuncChainProto).
1401
+ Object.setPrototypeOf(target, vfuncChainProto);
1402
+ // Stamp the prototype with its class identity so Gio._promisify(Cls.prototype, …)
1403
+ // can record which class a registration belongs to (non-enumerable).
1404
+ Object.defineProperty(target, CLASS_INFO, {
1405
+ value: { namespace, typeName },
1406
+ enumerable: false,
1407
+ configurable: true,
1408
+ });
1409
+ const where = `${namespace}.${typeName}.prototype`;
1410
+ const resolved = new Map(); // accessor name -> is an introspected method of this class
1411
+ // Writable + configurable: a program REPLACES a method to instrument it and puts
1412
+ // the original back afterwards, which is the whole point of the prototype.
1413
+ // Enumerable, like the methods gjs's resolve hook defines.
1414
+ const define = (prop, value) => {
1415
+ Object.defineProperty(target, prop, { value, writable: true, enumerable: true, configurable: true });
1416
+ return true;
1417
+ };
1418
+ // Defines the member on the target and reports whether the name resolved, so the
1419
+ // traps answer from the target itself and stay within the Proxy invariants (a
1420
+ // reported own property must really exist on an extensible target).
1421
+ const materialize = (prop) => {
1422
+ if (typeof prop !== 'string' || RESERVED.has(prop)) return false;
1423
+ if (Object.prototype.hasOwnProperty.call(target, prop)) return true;
1424
+ // gjs's GObject.Object.prototype members are JS shims over namespace-level
1425
+ // functions (see objectPrototypeShim), and `bind_property_full` even HAS an
1426
+ // introspected GIR shadow — bind_with_closures, whose GValue write-back a
1427
+ // marshaled GClosure cannot satisfy — so the shim must win here too.
1428
+ const shim = objectPrototypeShim(prop);
1429
+ if (shim !== undefined) {
1430
+ return define(prop, function (...args) {
1431
+ return shim(prototypeMethodHandle(this, `${where}.${prop}`), args);
1432
+ });
1433
+ }
1434
+ let present = resolved.get(prop);
1435
+ if (present === undefined) {
1436
+ present = native.hasClassMethod(namespace, typeName, prop);
1437
+ resolved.set(prop, present);
1438
+ }
1439
+ return present && define(prop, makeGiMethod(where, prop));
1440
+ };
1441
+ return new Proxy(target, {
1442
+ get(t, prop, receiver) {
1443
+ materialize(prop);
1444
+ return Reflect.get(t, prop, receiver);
1445
+ },
1446
+ getOwnPropertyDescriptor(t, prop) {
1447
+ materialize(prop);
1448
+ return Reflect.getOwnPropertyDescriptor(t, prop);
1449
+ },
1450
+ has(t, prop) {
1451
+ return materialize(prop) || Reflect.has(t, prop);
1452
+ },
1453
+ });
1454
+ }
1455
+
1338
1456
  function makeClass(namespace, typeName) {
1339
1457
  const ctor = function ctor(props) {
1340
1458
  // G3/G2 construction routing. `new.target` is the leaf class the `new` was
@@ -1382,20 +1500,12 @@ function makeClass(namespace, typeName) {
1382
1500
  Object.defineProperty(ctor, 'name', { value: typeName, configurable: true });
1383
1501
  ctor.$gtypeName = `${namespace}.${typeName}`;
1384
1502
  defineLazyGType(ctor, namespace, typeName);
1385
- // Put the vfunc chain-up Proxy beneath this base class's prototype so a
1386
- // registerClass subclass's `super.vfunc_<name>(...)` resolves (see above).
1387
- Object.setPrototypeOf(ctor.prototype, vfuncChainProto);
1388
- // Stamp the prototype with its class identity so Gio._promisify(Cls.prototype, …)
1389
- // can record which class a registration belongs to (non-enumerable).
1390
- Object.defineProperty(ctor.prototype, CLASS_INFO, {
1391
- value: { namespace, typeName },
1392
- enumerable: false,
1393
- configurable: true,
1394
- });
1503
+ ctor.prototype = makeClassPrototype(namespace, typeName);
1395
1504
  // `inst instanceof Ns.Class` — GJS parity for the WHOLE GObject hierarchy, not
1396
- // just the leaf class. An instance is a Proxy over a bare `{[HANDLE]}` (no live JS
1397
- // prototype chain linking Adw.ApplicationWindow → Gtk.ApplicationWindow → …), so
1398
- // the default instanceof (a prototype-chain walk) reported `false` for every base
1505
+ // just the leaf class. An instance is a Proxy carrying its class prototype as a
1506
+ // SYMBOL rather than as [[Prototype]], and class prototypes are not linked to their
1507
+ // bases (no Adw.ApplicationWindow → Gtk.ApplicationWindow → … chain), so the
1508
+ // default instanceof (a prototype-chain walk) reported `false` for every base
1399
1509
  // class. Resolve it by the GObject type system instead: `native.isInstanceOf`
1400
1510
  // (g_type_is_a) recognises subclasses AND implemented interfaces, exactly like GJS.
1401
1511
  // Guarded by `isGObjectHandle` first — a bare/boxed/variant handle (or any non-node-gi
@@ -1456,6 +1566,14 @@ function makeClass(namespace, typeName) {
1456
1566
  return (...args) => wrapReturn(native.callStaticMethod(namespace, typeName, giName, unwrapArgs(args)));
1457
1567
  },
1458
1568
  });
1569
+ // Replacing ctor.prototype dropped the automatic back-link; restore it pointing at
1570
+ // the class the user actually holds (the Proxy), not the raw ctor behind it.
1571
+ Object.defineProperty(ctor.prototype, 'constructor', {
1572
+ value: proxy,
1573
+ writable: true,
1574
+ enumerable: false,
1575
+ configurable: true,
1576
+ });
1459
1577
  return proxy;
1460
1578
  }
1461
1579
 
@@ -2316,6 +2434,30 @@ function createNamespace(namespace) {
2316
2434
 
2317
2435
  const namespaceCache = new Map();
2318
2436
 
2437
+ // The ONE namespace object per GI namespace, as in gjs where `imports.gi.Ns` is a
2438
+ // singleton. Keyed by NAMESPACE alone: a repository cannot hold two versions at
2439
+ // once, and keying by `Ns@version` handed `requireGi('Gio','2.0')` and
2440
+ // `requireGi('Gio')` two independent objects — `Gio.File !== Gio.File` between the
2441
+ // spellings, a prototype patched through one invisible through the other. Does NOT
2442
+ // load the typelib (requireGi does that first).
2443
+ function namespaceObject(namespace) {
2444
+ let ns = namespaceCache.get(namespace);
2445
+ if (ns === undefined) {
2446
+ ns = createNamespace(namespace);
2447
+ // The GObject namespace also carries the GJS runtime statics (registerClass +
2448
+ // ParamSpec/ParamFlags/SignalFlags) layered over its introspected members.
2449
+ if (namespace === 'GObject') ns = decorateGObjectNamespace(ns);
2450
+ // The GLib namespace carries the GJS-shaped GLib.Variant ergonomics
2451
+ // (new GLib.Variant(sig, value) + deepUnpack/unpack/recursiveUnpack) and the
2452
+ // GLib.Error class.
2453
+ else if (namespace === 'GLib') ns = decorateGLibNamespace(ns);
2454
+ // The Gio namespace carries Gio._promisify (async → Promise).
2455
+ else if (namespace === 'Gio') ns = decorateGioNamespace(ns);
2456
+ namespaceCache.set(namespace, ns);
2457
+ }
2458
+ return ns;
2459
+ }
2460
+
2319
2461
  // Whether the libuv↔GLib bridge has been attached this process. The native
2320
2462
  // startMainLoop is itself idempotent; this just avoids the extra call.
2321
2463
  let loopAttached = false;
@@ -2373,22 +2515,7 @@ export function requireGi(namespace, version) {
2373
2515
  }
2374
2516
  loopAttached = true;
2375
2517
  }
2376
- const key = version ? `${namespace}@${version}` : namespace;
2377
- let ns = namespaceCache.get(key);
2378
- if (ns === undefined) {
2379
- ns = createNamespace(namespace);
2380
- // The GObject namespace also carries the GJS runtime statics (registerClass +
2381
- // ParamSpec/ParamFlags/SignalFlags) layered over its introspected members.
2382
- if (namespace === 'GObject') ns = decorateGObjectNamespace(ns);
2383
- // The GLib namespace carries the GJS-shaped GLib.Variant ergonomics
2384
- // (new GLib.Variant(sig, value) + deepUnpack/unpack/recursiveUnpack) and the
2385
- // GLib.Error class.
2386
- else if (namespace === 'GLib') ns = decorateGLibNamespace(ns);
2387
- // The Gio namespace carries Gio._promisify (async → Promise).
2388
- else if (namespace === 'Gio') ns = decorateGioNamespace(ns);
2389
- namespaceCache.set(key, ns);
2390
- }
2391
- return ns;
2518
+ return namespaceObject(namespace);
2392
2519
  }
2393
2520
 
2394
2521
  /**
package/index.d.ts CHANGED
@@ -99,6 +99,15 @@ export function callMethod(handle: GObjectHandle, methodName: string, args?: unk
99
99
  */
100
100
  export function hasMethod(handle: GObjectHandle, methodName: string): boolean;
101
101
 
102
+ /**
103
+ * The CLASS-level twin of {@link hasMethod}: `true` iff `Ns.Type` declares an
104
+ * invocable instance method by that name (literal introspected name or
105
+ * snake_case alias), across own, implemented-interface and inherited methods.
106
+ * Answers by TYPE rather than by instance handle, which is what lets a class
107
+ * prototype resolve its methods before any instance of it exists.
108
+ */
109
+ export function hasClassMethod(namespace: string, typeName: string, methodName: string): boolean;
110
+
102
111
  /**
103
112
  * Invoke a type-level constructor/static function (e.g. `Gio.File.new_for_path`,
104
113
  * `Gtk.Label.new`) — a function found on a type but taking no instance. The Node
@@ -254,6 +263,15 @@ export function hasProperty(handle: GObjectHandle, name: string): boolean;
254
263
  /** The runtime GType name of a GObject handle (e.g. "GSimpleAction"). */
255
264
  export function getTypeName(handle: GObjectHandle): string;
256
265
 
266
+ /**
267
+ * The introspected type that OWNS a runtime GType name — the reverse of
268
+ * {@link getGType}, walking up to the nearest ancestor carrying object info (a
269
+ * private concrete type such as `GLocalFile` has none of its own). Searches only
270
+ * LOADED namespaces, so it never pulls in a typelib as a side effect. `null` when
271
+ * the GType is unknown or no ancestor is introspectable.
272
+ */
273
+ export function classInfoForTypeName(gtypeName: string): { namespace: string; name: string } | null;
274
+
257
275
  /**
258
276
  * The runtime GType of an introspected registered type, as an opaque node-gi
259
277
  * GType handle ({@link TypeHandle} — a tag-distinct External carrying the GType,
@@ -525,6 +543,7 @@ declare const native: {
525
543
  callFunction: typeof callFunction;
526
544
  callMethod: typeof callMethod;
527
545
  hasMethod: typeof hasMethod;
546
+ hasClassMethod: typeof hasClassMethod;
528
547
  callStaticMethod: typeof callStaticMethod;
529
548
  constructStruct: typeof constructStruct;
530
549
  newObject: typeof newObject;
@@ -535,6 +554,7 @@ declare const native: {
535
554
  setProperty: typeof setProperty;
536
555
  hasProperty: typeof hasProperty;
537
556
  getTypeName: typeof getTypeName;
557
+ classInfoForTypeName: typeof classInfoForTypeName;
538
558
  getGType: typeof getGType;
539
559
  isInstanceOf: typeof isInstanceOf;
540
560
  isGObjectHandle: typeof isGObjectHandle;
package/index.js CHANGED
@@ -197,6 +197,7 @@ export const prependLibraryPath = native.prependLibraryPath;
197
197
  export const callFunction = native.callFunction;
198
198
  export const callMethod = native.callMethod;
199
199
  export const hasMethod = native.hasMethod;
200
+ export const hasClassMethod = native.hasClassMethod;
200
201
  export const callStaticMethod = native.callStaticMethod;
201
202
  export const constructStruct = native.constructStruct;
202
203
  export const newObject = native.newObject;
@@ -216,6 +217,7 @@ export const getProperty = native.getProperty;
216
217
  export const setProperty = native.setProperty;
217
218
  export const hasProperty = native.hasProperty;
218
219
  export const getTypeName = native.getTypeName;
220
+ export const classInfoForTypeName = native.classInfoForTypeName;
219
221
  export const getGType = native.getGType;
220
222
  export const isInstanceOf = native.isInstanceOf;
221
223
  export const isGObjectHandle = native.isGObjectHandle;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.38.1",
3
+ "version": "0.40.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/addon.cc CHANGED
@@ -78,6 +78,7 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
78
78
  exports.Set("callFunction", Napi::Function::New(env, CallFunction));
79
79
  exports.Set("callMethod", Napi::Function::New(env, CallMethod));
80
80
  exports.Set("hasMethod", Napi::Function::New(env, HasMethod));
81
+ exports.Set("hasClassMethod", Napi::Function::New(env, HasClassMethod));
81
82
  exports.Set("callStaticMethod", Napi::Function::New(env, CallStaticMethod));
82
83
  exports.Set("constructStruct", Napi::Function::New(env, ConstructStruct));
83
84
  exports.Set("newObject", Napi::Function::New(env, NewObject));
@@ -90,6 +91,7 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
90
91
  exports.Set("setProperty", Napi::Function::New(env, SetProperty));
91
92
  exports.Set("hasProperty", Napi::Function::New(env, HasProperty));
92
93
  exports.Set("getTypeName", Napi::Function::New(env, GetTypeName));
94
+ exports.Set("classInfoForTypeName", Napi::Function::New(env, ClassInfoForTypeName));
93
95
  exports.Set("getGType", Napi::Function::New(env, GetGType));
94
96
  exports.Set("isInstanceOf", Napi::Function::New(env, IsInstanceOf));
95
97
  exports.Set("isGObjectHandle", Napi::Function::New(env, IsGObjectHandle));
package/src/calls.cc CHANGED
@@ -673,6 +673,23 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
673
673
  // don't. The out-container is freed (or borrowed) by ReadOutOrReturn per the
674
674
  // SAME (OUT) transfer. Verified vs gjs 1.88 (GIMarshallingTests.array_inout,
675
675
  // garray/glist/gslist/gptrarray/ghashtable_utf8_none_inout, bytearray_full_inout).
676
+ //
677
+ // THAT ABI IS TAKEN FROM THE ANNOTATION AND CANNOT BE VERIFIED HERE. `c:type`
678
+ // — the only place `gchar*` and `gchar**` differ — lives in the GIR XML and is
679
+ // NOT compiled into the typelib. `GLib.base64_decode_inplace` is annotated
680
+ // `direction="inout"` over a `c:type="gchar*"`, one star; compared field by
681
+ // field against a genuine `gchar***` INOUT (`GLib.OptionContext.parse`'s
682
+ // `argv`) its metadata is identical — direction INOUT, transfer EVERYTHING,
683
+ // tag ARRAY, is_pointer true, caller-allocates false. A mis-annotated callee
684
+ // therefore receives `&slots[i]` and writes its output OVER the GIArgument
685
+ // union, and the read-back below marshals and frees a clobbered pointer.
686
+ // Measured: five runs of that one call, five different return values; gjs
687
+ // corrupts identically, so there is no oracle to compare against either.
688
+ //
689
+ // Nothing to detect on this side — the discriminator is data the runtime does
690
+ // not have — and a per-function denylist ossifies. Upstream annotation bug:
691
+ // `status/upstream-patch-candidates.md`. `test/arrays.test.mjs` records why
692
+ // that call was removed from the suite and must not come back.
676
693
  Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
677
694
  jsCursor++;
678
695
  if (!IsSupportedContainerType(ti, &why)) {
@@ -1066,9 +1083,10 @@ static std::string CamelToSnake(const std::string& name) {
1066
1083
 
1067
1084
  // The instance's concrete GType may lack introspection info (e.g. a private
1068
1085
  // GLocalFile); walk up to the nearest ancestor GType that has an object info.
1069
- // Returns a new ref or nullptr. Shared by CallMethod (invoke) and HasMethod
1070
- // (feature detection) so both resolve identically.
1071
- static GIObjectInfo* FindNearestObjectInfo(GIRepository* repo, GType gtype) {
1086
+ // Returns a new ref or nullptr. Shared by CallMethod (invoke), HasMethod
1087
+ // (feature detection) and object.cc's ClassInfoForTypeName (the L1 prototype
1088
+ // lookup) so all three resolve the same way.
1089
+ GIObjectInfo* FindNearestObjectInfo(GIRepository* repo, GType gtype) {
1072
1090
  for (GType t = gtype; t != 0; t = g_type_parent(t)) {
1073
1091
  GIBaseInfo* bi = gi_repository_find_by_gtype(repo, t);
1074
1092
  if (bi != nullptr) {
@@ -1225,6 +1243,54 @@ Napi::Value HasMethod(const Napi::CallbackInfo& info) {
1225
1243
  return Napi::Boolean::New(env, invocable);
1226
1244
  }
1227
1245
 
1246
+ // hasClassMethod(namespace, typeName, methodName) -> boolean
1247
+ // The CLASS-level twin of hasMethod: TRUE iff `Ns.Type` declares an invocable
1248
+ // instance method by that name (literal introspected name first, snake_case
1249
+ // alias second — the resolution callMethod itself performs), across own,
1250
+ // implemented-interface and inherited methods. Keyed by TYPE rather than by an
1251
+ // instance handle because it backs the lazy method resolution on the class
1252
+ // PROTOTYPE (#1175), which must answer before any instance exists.
1253
+ Napi::Value HasClassMethod(const Napi::CallbackInfo& info) {
1254
+ Napi::Env env = info.Env();
1255
+ if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
1256
+ Napi::TypeError::New(env,
1257
+ "hasClassMethod(namespace: string, typeName: string, methodName: string)")
1258
+ .ThrowAsJavaScriptException();
1259
+ return env.Null();
1260
+ }
1261
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
1262
+ std::string tn = info[1].As<Napi::String>().Utf8Value();
1263
+ std::string method = info[2].As<Napi::String>().Utf8Value();
1264
+
1265
+ GIRepository* repo = DupDefaultRepository();
1266
+ GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), tn.c_str());
1267
+ GIFunctionInfo* func = nullptr;
1268
+ if (base != nullptr) {
1269
+ if (GI_IS_OBJECT_INFO(base)) {
1270
+ GType gtype =
1271
+ gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(base));
1272
+ // An object info always carries a registered GType; the guard is for the
1273
+ // interface-list step, which would warn on G_TYPE_INVALID.
1274
+ if (gtype != G_TYPE_INVALID) {
1275
+ func = ResolveInstanceMethod(repo, gtype, reinterpret_cast<GIObjectInfo*>(base), method);
1276
+ }
1277
+ } else if (GI_IS_INTERFACE_INFO(base)) {
1278
+ GIInterfaceInfo* iface = reinterpret_cast<GIInterfaceInfo*>(base);
1279
+ func = gi_interface_info_find_method(iface, method.c_str());
1280
+ if (func == nullptr) {
1281
+ const std::string snake = CamelToSnake(method);
1282
+ if (snake != method) func = gi_interface_info_find_method(iface, snake.c_str());
1283
+ }
1284
+ }
1285
+ gi_base_info_unref(base);
1286
+ }
1287
+ g_object_unref(repo);
1288
+ if (func == nullptr) return Napi::Boolean::New(env, false);
1289
+ const bool invocable = gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func));
1290
+ gi_base_info_unref(func);
1291
+ return Napi::Boolean::New(env, invocable);
1292
+ }
1293
+
1228
1294
  // callStaticMethod(namespace, typeName, methodName, args?) -> unknown
1229
1295
  // Invoke a type-level constructor/static function (e.g. Gio.File.new_for_path,
1230
1296
  // Gtk.Label.new) — a function found ON a type but taking no instance. The Node
package/src/common.h CHANGED
@@ -556,6 +556,11 @@ Napi::Value PrependLibraryPath(const Napi::CallbackInfo& info);
556
556
  Napi::Value CallFunction(const Napi::CallbackInfo& info);
557
557
  Napi::Value CallMethod(const Napi::CallbackInfo& info);
558
558
  Napi::Value HasMethod(const Napi::CallbackInfo& info);
559
+ Napi::Value HasClassMethod(const Napi::CallbackInfo& info);
560
+ // The nearest ancestor GType carrying object introspection info (a concrete type
561
+ // may be private — GLocalFile); a new ref, or nullptr. Shared with object.cc so
562
+ // method dispatch and prototype lookup resolve to the same class.
563
+ GIObjectInfo* FindNearestObjectInfo(GIRepository* repo, GType gtype);
559
564
  Napi::Value CallStaticMethod(const Napi::CallbackInfo& info);
560
565
  // The `new <Struct>()` [[Construct]] path: route to the struct's 'new'
561
566
  // constructor when it has one, else zero-allocate (GJS gi/boxed.cpp parity).
@@ -588,6 +593,7 @@ Napi::Value GetProperty(const Napi::CallbackInfo& info);
588
593
  Napi::Value SetProperty(const Napi::CallbackInfo& info);
589
594
  Napi::Value HasProperty(const Napi::CallbackInfo& info);
590
595
  Napi::Value GetTypeName(const Napi::CallbackInfo& info);
596
+ Napi::Value ClassInfoForTypeName(const Napi::CallbackInfo& info);
591
597
  Napi::Value GetGType(const Napi::CallbackInfo& info);
592
598
  Napi::Value IsInstanceOf(const Napi::CallbackInfo& info);
593
599
  Napi::Value IsGObjectHandle(const Napi::CallbackInfo& info);
package/src/loop.cc CHANGED
@@ -509,14 +509,30 @@ static void PumpDrainContext() {
509
509
  if (scope != nullptr) napi_close_handle_scope(g_loop_env, scope);
510
510
  }
511
511
 
512
- // The prepare+query HINT pass: learn GLib's earliest-timer deadline + poll fds
513
- // and mirror them into the uv timer / uv_poll watchers, so libuv's poll sleep
514
- // ends exactly when GLib next has work. The context is left "prepared" without a
515
- // matching check/dispatch — safe: the next g_main_context_iteration() (ours or a
516
- // blocking loop's) simply re-prepares. Runs only at g_main_depth() == 0; the
512
+ // The prepare+query+check HINT pass: learn GLib's earliest-timer deadline + poll
513
+ // fds and mirror them into the uv timer / uv_poll watchers, so libuv's poll sleep
514
+ // ends exactly when GLib next has work. Runs only at g_main_depth() == 0; the
517
515
  // g_in_uv_pump flag is held across it so UvLoopSource stays parked (masking uv's
518
516
  // backend fd out of the queried set — it must not be uv_poll'd, and uv's own
519
517
  // backend timeout must not feed back into the timer hint).
518
+ //
519
+ // The CHECK is not cosmetic and must never be dropped again: a GSource may hold a
520
+ // LOCK between its prepare() and its check(), and leaving the context prepared
521
+ // deadlocks the process on the next call into that library. GDK's Wayland event
522
+ // source is exactly that shape — its prepare() takes libwayland's designated-reader
523
+ // slot (`wl_display_prepare_read_queue`) and only its check() releases it
524
+ // (`wl_display_read_events` / `wl_display_cancel_read`). Measured (gjsify #1145,
525
+ // gtk 4.22 / Fedora 44 / Mesa Vulkan): with the hint pass ending at query, the very
526
+ // next `gtk_window_present()` blocked forever inside `wl_display_roundtrip_queue`
527
+ // under `gsk_renderer_new_for_surface_full`, so the app never finished showing its
528
+ // window while its GDBus worker thread kept answering Peer.Ping and Introspect —
529
+ // a live process at ~0.5 % CPU that read as "devtools hangs". Only Wayland shows
530
+ // it (GdkX11's source holds nothing across the pair), only node (bun/deno pump
531
+ // through complete `g_main_context_iteration`s), and only once GTK was initialised
532
+ // at g_main_depth() == 0 — `Gio.Application.register()` before `runAsync()` does
533
+ // exactly that, which is why an app that only ever calls `run()` never saw it.
534
+ // Passing revents = 0 is the right reading: this pass did NOT poll, so no fd is
535
+ // reported ready and every source that took something in prepare() gives it back.
520
536
  static void PumpArmWakeups() {
521
537
  if (!g_pump_inited || g_main_depth() > 0 || g_in_uv_pump) return;
522
538
  GMainContext* ctx = g_main_context_default();
@@ -531,6 +547,14 @@ static void PumpArmWakeups() {
531
547
  nfds = g_main_context_query(ctx, prio, &timeout, g_pump_fds->data(),
532
548
  static_cast<gint>(g_pump_fds->size()));
533
549
  }
550
+ // g_main_context_query fills fd + events and leaves revents untouched, so the
551
+ // previous pass's values would otherwise be replayed as fresh readiness.
552
+ for (int i = 0; i < nfds; i++) (*g_pump_fds)[i].revents = 0;
553
+ // No dispatch pairs with this check: g_main_context_prepare() clears
554
+ // pending_dispatches on entry (glib gmain.c), and PumpDrainContext() — a full
555
+ // iteration — runs on the same libuv turn, so anything this marks ready is
556
+ // dispatched there rather than being stranded.
557
+ g_main_context_check(ctx, prio, g_pump_fds->data(), nfds);
534
558
  g_in_uv_pump = FALSE;
535
559
 
536
560
  if (!SyncPumpPolls(nfds)) {
package/src/object.cc CHANGED
@@ -745,6 +745,39 @@ Napi::Value GetTypeName(const Napi::CallbackInfo& info) {
745
745
  return Napi::String::New(env, G_OBJECT_TYPE_NAME(obj));
746
746
  }
747
747
 
748
+ // classInfoForTypeName(gtypeName) -> { namespace, name } | null
749
+ // The introspected type that OWNS a runtime GType name — the reverse of
750
+ // getGType, over the same nearest-ancestor walk method resolution uses (a
751
+ // private concrete type such as GLocalFile carries no info of its own). L1 needs
752
+ // it to hand a wrapper the JS prototype of its class, so that a member the
753
+ // program put on `Ns.Class.prototype` is what the instance resolves (#1175).
754
+ // Searches only LOADED namespaces (gi_repository_find_by_gtype), so it never
755
+ // pulls in a typelib as a side effect of wrapping an object.
756
+ Napi::Value ClassInfoForTypeName(const Napi::CallbackInfo& info) {
757
+ Napi::Env env = info.Env();
758
+ if (info.Length() < 1 || !info[0].IsString()) {
759
+ Napi::TypeError::New(env, "classInfoForTypeName(gtypeName: string)")
760
+ .ThrowAsJavaScriptException();
761
+ return env.Null();
762
+ }
763
+ std::string typeName = info[0].As<Napi::String>().Utf8Value();
764
+ GType gtype = g_type_from_name(typeName.c_str());
765
+ if (gtype == G_TYPE_INVALID) return env.Null();
766
+ GIRepository* repo = DupDefaultRepository();
767
+ GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
768
+ Napi::Value result = env.Null();
769
+ if (objInfo != nullptr) {
770
+ GIBaseInfo* base = reinterpret_cast<GIBaseInfo*>(objInfo);
771
+ Napi::Object out = Napi::Object::New(env);
772
+ out.Set("namespace", Napi::String::New(env, gi_base_info_get_namespace(base)));
773
+ out.Set("name", Napi::String::New(env, gi_base_info_get_name(base)));
774
+ gi_base_info_unref(base);
775
+ result = out;
776
+ }
777
+ g_object_unref(repo);
778
+ return result;
779
+ }
780
+
748
781
  // getGType(namespace, name) -> GType handle | null
749
782
  // The runtime GType of an introspected registered type (object/interface/struct/
750
783
  // union/enum/flags), as a node-gi GType handle. The L1 layer surfaces it as a