@gjsify/node-gi 0.64.1 → 0.64.4

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/gi.js CHANGED
@@ -7,10 +7,18 @@
7
7
  // Reference: gjs/modules/esm/gi.js for the require shape; node-gtk (romgrk, MIT) for the
8
8
  // binding lineage. Hand-authored JS — the package ships no build step.
9
9
  import * as native from './index.js';
10
+ import { ensureNative, onNativeLoaded } from './index.js';
10
11
  // Timers, `process` and the windowing env record come from the host seam (host.node.js
11
12
  // explains why they are the runtime's own, not the globals; host.nativescript.js is the
12
13
  // NativeScript Android variant).
13
- import { setImmediate, setInterval, clearInterval, runtimeProcess, windowingEnvWrites } from '#host';
14
+ import {
15
+ setImmediate,
16
+ setInterval,
17
+ clearInterval,
18
+ runtimeProcess,
19
+ windowingEnvWrites,
20
+ FOREIGN_PARENT_CLASSES,
21
+ } from '#host';
14
22
  import { createGioDBus } from './overrides/gio-dbus.js';
15
23
  import { createGioSettings } from './overrides/gio-settings.js';
16
24
 
@@ -360,17 +368,19 @@ function makeErrorDomainClass(namespace, typeName, members, quark) {
360
368
  // Called by the engine on a failed GI invoke, with ownership of the GError* already
361
369
  // transferred to the boxed handle. Named, so the position properties report the
362
370
  // application frame rather than this one.
363
- native.setErrorBuilder(function buildThrownGError(handle, quark, code, message) {
364
- // Same cross-version pairing: an addon older than this JS calls the builder with
365
- // the GError's FIELDS (domainName, quark, code, message) rather than a handle, and
366
- // there is no second `errorClass` argument to give it. Rebuilding the GError from
367
- // the fields keeps the ONE shape on that pairing too — it costs only the original
368
- // GError's address, which nothing can read.
369
- if (typeof handle === 'string') {
370
- return wrapGError(newErrorLiteral(quark, code, message), true, buildThrownGError);
371
- }
372
- return wrapGError(handle, true, buildThrownGError);
373
- });
371
+ onNativeLoaded(() =>
372
+ native.setErrorBuilder(function buildThrownGError(handle, quark, code, message) {
373
+ // Same cross-version pairing: an addon older than this JS calls the builder with
374
+ // the GError's FIELDS (domainName, quark, code, message) rather than a handle, and
375
+ // there is no second `errorClass` argument to give it. Rebuilding the GError from
376
+ // the fields keeps the ONE shape on that pairing too — it costs only the original
377
+ // GError's address, which nothing can read.
378
+ if (typeof handle === 'string') {
379
+ return wrapGError(newErrorLiteral(quark, code, message), true, buildThrownGError);
380
+ }
381
+ return wrapGError(handle, true, buildThrownGError);
382
+ }),
383
+ );
374
384
 
375
385
  // Stamps a makeClass prototype with its { namespace, typeName } so Gio._promisify can
376
386
  // record WHICH introspected class a registration belongs to — matched against the
@@ -431,7 +441,7 @@ function wrapSignalCallback(cb) {
431
441
  // means `this` is the full userProto-carrying wrapper — the SAME cached proxy the user
432
442
  // constructed. Emitter at param 0, as in wrapSignalCallback.
433
443
  function resolveTemplateCallback(handle, handlerName) {
434
- return (...args) => {
444
+ const dispatcher = (...args) => {
435
445
  const proxy = wrapInstance(handle);
436
446
  const userProto = proxy[USER_PROTO];
437
447
  const desc = userProto !== undefined ? findProtoDescriptor(userProto, handlerName) : undefined;
@@ -442,8 +452,16 @@ function resolveTemplateCallback(handle, handlerName) {
442
452
  }
443
453
  return desc.value.apply(proxy, args.map(wrapReturn));
444
454
  };
455
+ // The native closure holds this dispatcher WEAKLY (src/signals.cc), and the
456
+ // dispatcher captures `handle` — so without a handle-keyed retain it would be
457
+ // garbage the moment create_closure returns, and the widget would be immortal.
458
+ // GJS's Gtk override reaches the same lifetime with
459
+ // `Gi.associateClosure(lifetimeObject, _createClosure(…))`. A fresh key per
460
+ // closure: one handler name can back several `<signal>` elements.
461
+ native.retainClosure(handle, Symbol(handlerName), dispatcher);
462
+ return dispatcher;
445
463
  }
446
- native.setTemplateCallbackResolver(resolveTemplateCallback);
464
+ onNativeLoaded(() => native.setTemplateCallbackResolver(resolveTemplateCallback));
447
465
 
448
466
  // Run a registered class's JS constructor for a GObject the ENGINE instantiated from C
449
467
  // — typically a GtkBuilder composite-template InternalChild, which has no JS-`new` to
@@ -461,12 +479,17 @@ function runCtorForCObject(handle, gtypeName) {
461
479
  try {
462
480
  // No props: GtkBuilder already applied the construct properties to the C object;
463
481
  // the ctor's `params` arg is conventionally optional for a template child.
464
- Reflect.construct(cls, []);
482
+ const instance = Reflect.construct(cls, []);
483
+ // A FOREIGN-PARENT class has no makeClass base ctor to consume the latch, and its
484
+ // attach is lazy — so if the ctor body touched nothing, `adoptHandle` would still
485
+ // be set here and a later attach would build a SECOND GObject, orphaning the one
486
+ // C already created. Force the attach while the latch is still live.
487
+ if (adoptHandle !== undefined && isForeignInstance(instance)) foreignHandle(instance);
465
488
  } finally {
466
489
  adoptHandle = prev;
467
490
  }
468
491
  }
469
- native.setConstructCallback(runCtorForCObject);
492
+ onNativeLoaded(() => native.setConstructCallback(runCtorForCObject));
470
493
 
471
494
  // A non-GObject GObject-fundamental (GskRenderNode, GdkEvent) as an OPAQUE,
472
495
  // round-trippable handle: the native tagged External already carries the pointer and
@@ -927,13 +950,50 @@ function decorateGioNamespace(baseNs) {
927
950
  });
928
951
  }
929
952
 
953
+ // The internals of the PROXY links on an introspected prototype chain: the Proxy
954
+ // itself -> its target plus the materializer its getOwnPropertyDescriptor trap runs
955
+ // (`null` for a Proxy that traps no descriptor read). Registered by
956
+ // makeClassPrototype and vfuncChainProto; read only by findProtoDescriptor.
957
+ //
958
+ // Why: a descriptor read THROUGH a Proxy costs ~110 ns of V8 invariant machinery on
959
+ // top of the handler, and an introspected chain is mostly Proxies. Measured on the
960
+ // bench's depth-3 subject, a full MISS walk took 328 ns — 154 ns on the
961
+ // makeClassPrototype link, 65 ns on vfuncChainProto, 12 ns on the one plain link.
962
+ // With the target in hand the same answer costs 27 ns, because the trap body is
963
+ // exactly `materialize(prop); return Reflect.getOwnPropertyDescriptor(target, prop)`
964
+ // and running it directly skips only the machinery, never a step of the computation.
965
+ //
966
+ // This is NOT a cache of the ANSWER: nothing is remembered between calls. The target
967
+ // is the object every mutation of the prototype lands on — none of the handlers trap
968
+ // `set`, `defineProperty`, `deleteProperty` or `setPrototypeOf`, so
969
+ // `Cls.prototype.m = f`, `Object.defineProperty(…)`, `delete` and a spliced chain
970
+ // write through to it — and the descriptor still comes from a fresh read of it. That
971
+ // has to hold, because GJS pins nothing: on gjs 1.88.1 every one of those mutations
972
+ // is observable on an already-wrapped instance (measured), so an answer cache would
973
+ // need an invalidation signal JS does not offer. test/proto-descriptor-liveness.test.mjs
974
+ // is the proof obligation.
975
+ const protoInternals = new WeakMap();
976
+
930
977
  // Walk a JS prototype chain (excluding Object.prototype) for an own property
931
978
  // descriptor of `prop` — used to surface a registerClass subclass's own methods
932
- // /getters/setters on the GObject wrapper.
979
+ // /getters/setters on the GObject wrapper. One full walk runs per property GET, per
980
+ // SET and per `emit`, so the Proxy bypass above is on the hottest path there is.
933
981
  function findProtoDescriptor(proto, prop) {
934
- for (let p = proto; p !== null && p !== Object.prototype; p = Object.getPrototypeOf(p)) {
935
- const d = Object.getOwnPropertyDescriptor(p, prop);
982
+ for (let p = proto; p !== null && p !== Object.prototype;) {
983
+ const internals = protoInternals.get(p);
984
+ if (internals === undefined) {
985
+ const d = Object.getOwnPropertyDescriptor(p, prop);
986
+ if (d !== undefined) return d;
987
+ p = Object.getPrototypeOf(p);
988
+ continue;
989
+ }
990
+ const { target, materialize } = internals;
991
+ if (materialize !== null) materialize(prop);
992
+ const d = Object.getOwnPropertyDescriptor(target, prop);
936
993
  if (d !== undefined) return d;
994
+ // The target's prototype, not the Proxy's: neither handler traps
995
+ // getPrototypeOf, so these are the same link — one fewer trap to pay.
996
+ p = Object.getPrototypeOf(target);
937
997
  }
938
998
  return undefined;
939
999
  }
@@ -1088,6 +1148,13 @@ function assignTemplateChildren(instance, handle, reg) {
1088
1148
  // outside what either runtime honours.
1089
1149
  const propertySettersByProto = new WeakMap();
1090
1150
 
1151
+ // The property setters THIS file generated for a foreign-parent class (route D, see the
1152
+ // FOREIGN-PARENT section). They must be invisible to resolvePropertySetter: a generated
1153
+ // setter's whole body is `native.setProperty`, and the engine answers every custom-property
1154
+ // store by calling runJsPropertySetter — which would resolve the generated setter and store
1155
+ // again, forever. Only a setter the CLASS declared is a JS setter worth replaying.
1156
+ const generatedForeignSetters = new WeakSet();
1157
+
1091
1158
  function findPropertySetter(userProto, propertyName) {
1092
1159
  let perProto = propertySettersByProto.get(userProto);
1093
1160
  if (perProto === undefined) propertySettersByProto.set(userProto, (perProto = new Map()));
@@ -1104,7 +1171,9 @@ function resolvePropertySetter(userProto, propertyName) {
1104
1171
  const camel = propertyName.replace(/-([a-z])/g, (m) => m[1].toUpperCase());
1105
1172
  for (const spelling of [propertyName, underscored, camel]) {
1106
1173
  const desc = findProtoDescriptor(userProto, spelling);
1107
- if (desc !== undefined && typeof desc.set === 'function') return desc.set;
1174
+ if (desc !== undefined && typeof desc.set === 'function' && !generatedForeignSetters.has(desc.set)) {
1175
+ return desc.set;
1176
+ }
1108
1177
  }
1109
1178
  return undefined;
1110
1179
  }
@@ -1131,9 +1200,9 @@ function runJsPropertySetter(handle, propertyName) {
1131
1200
  // function` until the next release republishes the binary. Every native call added
1132
1201
  // here degrades instead; without the callback, a post-construction set simply does
1133
1202
  // not reach the JS setter, which is what 0.50.0 did.
1134
- if (typeof native.setPropertySetCallback === 'function') {
1135
- native.setPropertySetCallback(runJsPropertySetter);
1136
- }
1203
+ onNativeLoaded(() => {
1204
+ if (typeof native.setPropertySetCallback === 'function') native.setPropertySetCallback(runJsPropertySetter);
1205
+ });
1137
1206
 
1138
1207
  // Which properties the construct-time replay must run.
1139
1208
  //
@@ -1342,6 +1411,7 @@ export function startMainContextPump() {
1342
1411
  // GLib's default main context — a JS-timer pump would only iterate the
1343
1412
  // context recursively from within its own dispatch.
1344
1413
  if (hostPumpsItself()) return () => {};
1414
+ ensureNative();
1345
1415
  pumpRefCount++;
1346
1416
  if (pumpTimer === null) {
1347
1417
  if (pendingView === null) pendingView = native.makePumpPendingCount();
@@ -1546,7 +1616,11 @@ function wrapInstance(handle, userProto) {
1546
1616
  // the leaf's OWN methods resolve). Never DOWNGRADE: a more-derived cached proto is
1547
1617
  // kept. The userProto lives on the target (read via the proxy here), so the
1548
1618
  // upgrade is in place — identity (===) and any expando fields are preserved.
1549
- if (userProto !== undefined && userProto !== cached[USER_PROTO]) {
1619
+ // NEVER re-link a foreign-parent instance (route D): linkInstanceProto does
1620
+ // `Object.setPrototypeOf`, and that object's [[Prototype]] IS its NativeScript
1621
+ // widget chain — replacing it would turn a real Label into something `_addView`
1622
+ // rejects. Its user prototype is already the registered one, fixed at attach.
1623
+ if (userProto !== undefined && userProto !== cached[USER_PROTO] && !isForeignInstance(cached)) {
1550
1624
  const current = cached[USER_PROTO];
1551
1625
  if (current === undefined || isProtoSameOrDescendantOf(userProto, current)) {
1552
1626
  linkInstanceProto(cached, userProto);
@@ -1812,7 +1886,8 @@ function wrapInstance(handle, userProto) {
1812
1886
  // the introspected parent's `vfunc_x` is a thunk into the actual C vtable entry.
1813
1887
  // Non-`vfunc_` lookups fall through to Object.prototype (so the base prototype
1814
1888
  // keeps its normal object behaviour).
1815
- const vfuncChainProto = new Proxy(Object.create(Object.prototype), {
1889
+ const vfuncChainTarget = Object.create(Object.prototype);
1890
+ const vfuncChainProto = new Proxy(vfuncChainTarget, {
1816
1891
  get(target, prop, receiver) {
1817
1892
  if (typeof prop === 'string' && prop.startsWith('vfunc_')) {
1818
1893
  const vfuncName = prop.slice('vfunc_'.length);
@@ -1827,6 +1902,10 @@ const vfuncChainProto = new Proxy(Object.create(Object.prototype), {
1827
1902
  return Reflect.get(target, prop, receiver);
1828
1903
  },
1829
1904
  });
1905
+ // No descriptor trap here: a `vfunc_*` access is synthesized by `get` alone and was
1906
+ // never reported as a descriptor, so the walk's answer for this link always came from
1907
+ // the target. `null` materializer = "read the target, that is the whole trap".
1908
+ protoInternals.set(vfuncChainProto, { target: vfuncChainTarget, materialize: null });
1830
1909
 
1831
1910
  // Surface a lazy `$gtype` getter on an introspected class/struct ctor: the real
1832
1911
  // GObject GType (a node-gi GType handle), resolved on first read via the engine
@@ -1994,7 +2073,7 @@ function makeClassPrototype(namespace, typeName) {
1994
2073
  }
1995
2074
  return arity >= 0 && define(prop, makeGiMethod(where, prop, arity));
1996
2075
  };
1997
- return new Proxy(target, {
2076
+ const proto = new Proxy(target, {
1998
2077
  get(t, prop, receiver) {
1999
2078
  materialize(prop);
2000
2079
  return Reflect.get(t, prop, receiver);
@@ -2007,6 +2086,10 @@ function makeClassPrototype(namespace, typeName) {
2007
2086
  return materialize(prop) || Reflect.has(t, prop);
2008
2087
  },
2009
2088
  });
2089
+ // Let findProtoDescriptor run the descriptor trap's body directly on the target —
2090
+ // same two steps, without V8's per-trap invariant checks (see protoInternals).
2091
+ protoInternals.set(proto, { target, materialize });
2092
+ return proto;
2010
2093
  }
2011
2094
 
2012
2095
  function makeClass(namespace, typeName) {
@@ -2526,6 +2609,8 @@ function paramSpecGTyped(type) {
2526
2609
  const ParamSpec = Object.freeze({
2527
2610
  string: paramSpecPlain('string'),
2528
2611
  boolean: paramSpecPlain('boolean'),
2612
+ char: paramSpecRanged('char'),
2613
+ uchar: paramSpecRanged('uchar'),
2529
2614
  int: paramSpecRanged('int'),
2530
2615
  uint: paramSpecRanged('uint'),
2531
2616
  int64: paramSpecRanged('int64'),
@@ -2560,7 +2645,11 @@ function findParentGType(klass) {
2560
2645
  for (let c = Object.getPrototypeOf(klass); typeof c === 'function'; c = Object.getPrototypeOf(c)) {
2561
2646
  const reg = registeredClasses.get(c);
2562
2647
  if (reg !== undefined) {
2563
- return { parentGTypeHandle: reg.typeHandle };
2648
+ // `parentForeign` carries the FOREIGN-PARENT marker down the chain: a subclass
2649
+ // of `registerClass(…, class extends Label {})` has a perfectly ordinary
2650
+ // registered parent by this test, yet ITS instances are still NativeScript
2651
+ // objects that need the attach path rather than the wrapper Proxy.
2652
+ return { parentGTypeHandle: reg.typeHandle, parentForeign: reg.foreign === true };
2564
2653
  }
2565
2654
  const gt = c.$gtypeName;
2566
2655
  if (typeof gt === 'string') {
@@ -2568,9 +2657,19 @@ function findParentGType(klass) {
2568
2657
  if (dot > 0) {
2569
2658
  return { parentNamespace: gt.slice(0, dot), parentType: gt.slice(dot + 1) };
2570
2659
  }
2571
- // A dotless `$gtypeName` with no registeredClasses entry should never occur
2572
- // (only registered types are dotless, and they are in the registry); be
2573
- // explicit rather than mis-resolving the namespace split.
2660
+ // A dotless `$gtypeName` and no registry entry, with the name INHERITED: `c` is
2661
+ // an ordinary unregistered subclass of a registered ancestor further up, reading
2662
+ // that ancestor's static through the class-side prototype chain. Keep walking —
2663
+ // `c` contributes no GType, and the registered ancestor above it is the parent.
2664
+ // gjs answers the same way (measured, 1.88.1): `registerClass(C extends B)` where
2665
+ // `class B extends RegisteredA {}` gives `type_parent(C) === A`. Without this the
2666
+ // walk threw on its own inherited value, which is every port class that puts a
2667
+ // plain subclass between two registrations — `AdwSwitchRow` between the two
2668
+ // route-D mixins (ADR 0105 § 2) is the case that found it.
2669
+ if (!Object.prototype.hasOwnProperty.call(c, '$gtypeName')) continue;
2670
+ // An OWN dotless `$gtypeName` with no registry entry should never occur (only
2671
+ // registered types are dotless, and they are in the registry); be explicit
2672
+ // rather than mis-resolving the namespace split.
2574
2673
  throw new TypeError("GObject.registerClass: parent '" + gt + "' has no resolvable GType");
2575
2674
  }
2576
2675
  }
@@ -2735,6 +2834,335 @@ function collectVfuncs(klass) {
2735
2834
  * @param {Function} [maybeClass]
2736
2835
  * @returns {Function} the same `klass` (now registered)
2737
2836
  */
2837
+ // ---- FOREIGN-PARENT classes: a registered GObject attached to a host widget ----
2838
+ //
2839
+ // THE ONE DELIBERATE DIVERGENCE FROM GJS, and it only exists where GJS cannot run.
2840
+ // Gated on `FOREIGN_PARENT_CLASSES` (#host): true on NativeScript, false everywhere
2841
+ // else, where registerClass keeps throwing GJS's invalid-base error verbatim. See
2842
+ // host.nativescript.js for why that gate is sound, ADR 0105 § 2 for the decision and
2843
+ // docs/node-gi-gjs-surface.md for the surface this adds.
2844
+ //
2845
+ // What it buys: `registerClass({GTypeName, Properties, Signals}, class MyLabel extends
2846
+ // Label {})` where `Label` is a `@nativescript/core` widget. The JS object stays a REAL
2847
+ // Label — its [[Prototype]] is the untouched NativeScript chain — so `stack.addChild(w)`,
2848
+ // `ContentView.content = w` and `_addChildFromBuilder` pass their hard `instanceof
2849
+ // ViewBase` gates with no `.view` unwrap anywhere in consumer code. Beside it sits a real
2850
+ // GObject of a registered GType whose GType PARENT is GObject.Object (the GType parent is
2851
+ // independent of the JS prototype chain), so the object also answers `instanceof
2852
+ // GObject.Object`, carries custom properties with `notify::`, and emits registered signals.
2853
+ //
2854
+ // ATTACH, never wrap. Everywhere else node-gi hands back a Proxy whose get/set traps
2855
+ // resolve the GObject surface dynamically; here there is no Proxy to put traps on, so:
2856
+ // • `[HANDLE]`/`[USER_PROTO]` become non-enumerable OWN props of the widget object
2857
+ // (`Symbol.hasInstance` resolves `instanceof` through `instance[HANDLE]`, which is
2858
+ // what makes the widget a GObject.Object);
2859
+ // • everything the traps would have answered — connect/emit/notify, the property
2860
+ // accessors, the GObject.Object methods — is installed as OWN members of
2861
+ // `klass.prototype`, once, at registerClass time;
2862
+ // • `Object.setPrototypeOf` is never called on such an object (see linkInstanceProto).
2863
+ //
2864
+ // LAZY. The GObject is constructed on the first access that needs it (`foreignHandle`),
2865
+ // because nothing in a NativeScript widget's construction runs node-gi code: there is no
2866
+ // base ctor to route `new` through the way makeClass does. Every entry point goes through
2867
+ // `foreignHandle`, including `unwrapArg` reading `[HANDLE]` through the prototype getter,
2868
+ // so by the time anything can OBSERVE the GObject it exists — `new MyLabel() instanceof
2869
+ // GObject.Object` attaches and answers true. Construct properties are the one thing this
2870
+ // cannot carry (the widget ctor owns its own arguments); set them after construction.
2871
+ //
2872
+ // NOT supported, deliberately: `vfunc_*` overrides. collectVfuncs finds none on a widget
2873
+ // chain, and a vfunc trampoline would need to re-link the instance prototype — the one
2874
+ // thing this path must never do.
2875
+ const FOREIGN_MATERIALISED = Symbol('nodeGiForeignMaterialised');
2876
+
2877
+ // A registered foreign-parent class's PROTOTYPE → its registration. The instance carries
2878
+ // no class pointer we can trust (its `constructor` is the widget's), so the attach walks
2879
+ // the real prototype chain and takes the nearest entry — which is the LEAF's, since a
2880
+ // subclass's own prototype comes first.
2881
+ const foreignRegByProto = new WeakMap();
2882
+
2883
+ // Gated first, so on a host where foreign parents cannot exist this is one boolean read
2884
+ // rather than a prototype-chain walk — it sits on wrapInstance's upgrade path.
2885
+ function isForeignInstance(obj) {
2886
+ return FOREIGN_PARENT_CLASSES && foreignRegFor(obj) !== undefined;
2887
+ }
2888
+
2889
+ function foreignRegFor(obj) {
2890
+ for (let p = Object.getPrototypeOf(obj); p !== null; p = Object.getPrototypeOf(p)) {
2891
+ const reg = foreignRegByProto.get(p);
2892
+ if (reg !== undefined) return reg;
2893
+ }
2894
+ return undefined;
2895
+ }
2896
+
2897
+ // Construct this object's GObject and bind the two together. Not a wrap: the object is
2898
+ // handed back unchanged apart from three non-enumerable own symbols.
2899
+ function attachForeignGObject(obj, reg) {
2900
+ let handle;
2901
+ if (adoptHandle !== undefined) {
2902
+ // A C-created instance (runCtorForCObject) — the engine already built the GObject.
2903
+ // Consume the latch atomically, exactly as the makeClass base ctor does.
2904
+ handle = adoptHandle;
2905
+ adoptHandle = undefined;
2906
+ } else {
2907
+ handle = native.constructType(reg.typeHandle, {});
2908
+ }
2909
+ // HANDLE first: it shadows the prototype getter, so anything the flush below runs can
2910
+ // re-enter `foreignHandle` without constructing a second GObject.
2911
+ Object.defineProperty(obj, HANDLE, { value: handle, configurable: true });
2912
+ Object.defineProperty(obj, USER_PROTO, { value: reg.foreignProto, configurable: true });
2913
+ instanceCache.set(handle, obj);
2914
+ flushPropertiesToJsSetters(obj, handle, reg);
2915
+ return handle;
2916
+ }
2917
+
2918
+ // This object's GObject handle, attaching on first demand. Every member installed below
2919
+ // goes through here, as does `unwrapArg` (via the prototype [HANDLE] getter).
2920
+ function foreignHandle(obj) {
2921
+ if (Object.hasOwn(obj, HANDLE)) return obj[HANDLE];
2922
+ const reg = foreignRegFor(obj);
2923
+ if (reg === undefined) {
2924
+ throw new TypeError('node-gi: this object is not an instance of a GObject.registerClass()-registered class');
2925
+ }
2926
+ return attachForeignGObject(obj, reg);
2927
+ }
2928
+
2929
+ // Which custom properties of this instance hold a materialised value — the state GJS's
2930
+ // generated accessors keep in their private field, and what the equal-value guard
2931
+ // compares against. Same rule as the wrapper's `materialise` (see wrapInstance): seeded
2932
+ // from what the engine has stored, then extended by every read and write.
2933
+ function foreignMaterialise(obj, name) {
2934
+ let seen = obj[FOREIGN_MATERIALISED];
2935
+ if (seen === undefined) {
2936
+ seen = new Set(typeof native.storedPropertyNames === 'function' ? native.storedPropertyNames(obj[HANDLE]) : []);
2937
+ Object.defineProperty(obj, FOREIGN_MATERIALISED, { value: seen, configurable: true });
2938
+ }
2939
+ const had = seen.has(name);
2940
+ seen.add(name);
2941
+ return had;
2942
+ }
2943
+
2944
+ // The GObject.Object instance methods installed as callMethod thunks. A MODEST list on
2945
+ // purpose — only introspectable, GJS-reachable members with no gpointer/GValue-array
2946
+ // argument (`g_object_get_data`, `getv` and friends are deliberately absent rather than
2947
+ // present and throwing). The wrapper Proxy answers these from introspection dynamically;
2948
+ // this path has to name them.
2949
+ const FOREIGN_OBJECT_METHODS = [
2950
+ 'get_property',
2951
+ 'set_property',
2952
+ 'bind_property',
2953
+ 'notify_by_pspec',
2954
+ 'freeze_notify',
2955
+ 'thaw_notify',
2956
+ 'run_dispose',
2957
+ 'ref',
2958
+ 'unref',
2959
+ 'ref_sink',
2960
+ 'force_floating',
2961
+ 'is_floating',
2962
+ ];
2963
+
2964
+ function snakeToCamel(name) {
2965
+ return name.replace(/_([a-z])/g, (m) => m[1].toUpperCase());
2966
+ }
2967
+
2968
+ /** The distinct spellings a member is installed under: one entry for an underscore-free name. */
2969
+ function memberSpellings(name) {
2970
+ const camel = snakeToCamel(name);
2971
+ return camel === name ? [name] : [name, camel];
2972
+ }
2973
+
2974
+ // Install a member under `name` unless the host chain already answers under it — a
2975
+ // widget's own method ALWAYS wins, because consumer code and NativeScript's own
2976
+ // internals call it. Returns false (and the caller records the skip) when it loses.
2977
+ function defineForeignMember(proto, name, descriptor, skipped) {
2978
+ if (findProtoDescriptor(proto, name) !== undefined) {
2979
+ skipped.push(name);
2980
+ return false;
2981
+ }
2982
+ Object.defineProperty(proto, name, { configurable: true, enumerable: false, ...descriptor });
2983
+ return true;
2984
+ }
2985
+
2986
+ // Everything the instance wrapper's traps would have resolved, as own members of a
2987
+ // foreign-parent class's prototype. Returns the names the host chain already owned, so a
2988
+ // program (and the route-D spec) can see exactly what it did NOT get.
2989
+ function installForeignGObjectSurface(proto, properties) {
2990
+ const skipped = [];
2991
+
2992
+ // `[HANDLE]` as a lazily-attaching GETTER is the whole load-bearing trick: it makes
2993
+ // `instanceof GObject.Object`, `unwrapArg` and every member below attach on demand,
2994
+ // with no construction hook on the widget side. An attach replaces it with an own
2995
+ // data property, so this getter runs at most once per instance.
2996
+ Object.defineProperty(proto, HANDLE, {
2997
+ configurable: true,
2998
+ get() {
2999
+ return foreignHandle(this);
3000
+ },
3001
+ });
3002
+
3003
+ defineForeignMember(
3004
+ proto,
3005
+ '$typeName',
3006
+ {
3007
+ get() {
3008
+ return native.getTypeName(foreignHandle(this));
3009
+ },
3010
+ },
3011
+ skipped,
3012
+ );
3013
+
3014
+ // Signal API. The same four thunks the wrapper builds, including the (fn → ids)
3015
+ // bookkeeping signal_handlers_*_by_func resolves through.
3016
+ defineForeignMember(
3017
+ proto,
3018
+ 'connect',
3019
+ {
3020
+ value: function connect(signal, cb) {
3021
+ const handle = foreignHandle(this);
3022
+ const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), false);
3023
+ recordSignalHandler(handle, cb, id);
3024
+ return id;
3025
+ },
3026
+ },
3027
+ skipped,
3028
+ );
3029
+ const connectAfter = function connect_after(signal, cb) {
3030
+ const handle = foreignHandle(this);
3031
+ const id = native.connectSignal(handle, signal, wrapSignalCallback(cb), true);
3032
+ recordSignalHandler(handle, cb, id);
3033
+ return id;
3034
+ };
3035
+ defineForeignMember(proto, 'connect_after', { value: connectAfter }, skipped);
3036
+ defineForeignMember(proto, 'connectAfter', { value: connectAfter }, skipped);
3037
+ defineForeignMember(
3038
+ proto,
3039
+ 'disconnect',
3040
+ {
3041
+ value: function disconnect(id) {
3042
+ const handle = foreignHandle(this);
3043
+ forgetSignalHandlerId(handle, id);
3044
+ return native.disconnectSignal(handle, id);
3045
+ },
3046
+ },
3047
+ skipped,
3048
+ );
3049
+ defineForeignMember(
3050
+ proto,
3051
+ 'emit',
3052
+ {
3053
+ value: function emit(signal, ...args) {
3054
+ return wrapReturn(native.emitSignal(foreignHandle(this), signal, unwrapArgs(args)));
3055
+ },
3056
+ },
3057
+ skipped,
3058
+ );
3059
+
3060
+ // `notify` is the ONE name that cannot be skipped and cannot be taken: NativeScript's
3061
+ // Observable.prototype.notify({eventName, object}) is called by the widget machinery
3062
+ // itself (and by consumer code), while GObject's notify(propertyName) is what a
3063
+ // custom property's setter needs. They are told apart by ARGUMENT TYPE — a string is
3064
+ // a GObject property name, anything else is a NativeScript event payload — and the
3065
+ // inherited implementation is captured HERE, before this definition shadows it, so
3066
+ // both keep working on one object. This is the only collision in the
3067
+ // @nativescript/core 9.1 Label chain (measured: Observable.prototype.notify is the
3068
+ // sole hit for the whole installed surface).
3069
+ const inheritedNotify = findProtoDescriptor(proto, 'notify');
3070
+ const hostNotify = inheritedNotify !== undefined ? inheritedNotify.value : undefined;
3071
+ Object.defineProperty(proto, 'notify', {
3072
+ configurable: true,
3073
+ enumerable: false,
3074
+ value: function notify(arg, ...rest) {
3075
+ if (typeof arg === 'string') {
3076
+ return wrapReturn(native.callMethod(foreignHandle(this), 'notify', [arg]));
3077
+ }
3078
+ if (typeof hostNotify === 'function') return hostNotify.call(this, arg, ...rest);
3079
+ throw new TypeError('notify(): expected a GObject property name');
3080
+ },
3081
+ });
3082
+
3083
+ for (const name of FOREIGN_OBJECT_METHODS) {
3084
+ const thunk = function giMethod(...args) {
3085
+ return wrapReturn(native.callMethod(foreignHandle(this), name, unwrapArgs(args)));
3086
+ };
3087
+ // `memberSpellings(name)` and not `[name, snakeToCamel(name)]`: a name with no
3088
+ // underscore camel-cases to ITSELF (`ref`, `unref`), so the second call would
3089
+ // find the descriptor the first one just installed and record a skip against
3090
+ // the host for a member the host never had. `$foreignSkipped` is read as "what
3091
+ // did the widget already own", so a self-skip there is a false statement.
3092
+ for (const spelling of memberSpellings(name)) {
3093
+ defineForeignMember(proto, spelling, { value: thunk }, skipped);
3094
+ }
3095
+ }
3096
+
3097
+ // gjs's GObject.Object.prototype shims over namespace-level g_signal_* /
3098
+ // bind_property_full — not introspected instance methods, so named explicitly here
3099
+ // just as the wrapper's get trap routes them explicitly.
3100
+ for (const name of [
3101
+ 'block_signal_handler',
3102
+ 'unblock_signal_handler',
3103
+ 'stop_emission_by_name',
3104
+ 'bind_property_full',
3105
+ ]) {
3106
+ for (const spelling of memberSpellings(name)) {
3107
+ const shim = objectPrototypeShim(spelling);
3108
+ defineForeignMember(
3109
+ proto,
3110
+ spelling,
3111
+ {
3112
+ value: function shimmed(...args) {
3113
+ return shim(foreignHandle(this), args);
3114
+ },
3115
+ },
3116
+ skipped,
3117
+ );
3118
+ }
3119
+ }
3120
+
3121
+ installForeignPropertyAccessors(proto, properties, skipped);
3122
+ return skipped;
3123
+ }
3124
+
3125
+ // GJS-style generated accessors for the DECLARED custom properties, under all three
3126
+ // spellings GJS makes equivalent (dash, underscore, camel — `_checkAccessors` mirrors the
3127
+ // declared one onto the other two). Elsewhere node-gi installs none and resolves the
3128
+ // spellings at the point of use instead (findPropertySetter), because the wrapper Proxy
3129
+ // can; this path has no Proxy, so the accessors have to exist. A ROUTE-D-ONLY divergence
3130
+ // from the rest of node-gi, and the reason the equal-value guard is replicated here.
3131
+ //
3132
+ // A spelling the host chain already answers is left ALONE: NativeScript defines `text`,
3133
+ // `width`, … on its own prototypes with real setters driving the native view, and
3134
+ // shadowing one would break the widget. The engine's store still routes to it —
3135
+ // runJsPropertySetter resolves the class-or-chain setter — so declaring a GObject
3136
+ // property named after a widget property composes instead of colliding.
3137
+ function installForeignPropertyAccessors(proto, properties, skipped) {
3138
+ for (const spec of properties) {
3139
+ const name = spec.name;
3140
+ if (typeof name !== 'string' || name.length === 0) continue;
3141
+ const spellings = new Set([
3142
+ name,
3143
+ name.replace(/-/g, '_'),
3144
+ name.replace(/-([a-z])/g, (m) => m[1].toUpperCase()),
3145
+ ]);
3146
+ const get = function () {
3147
+ const handle = foreignHandle(this);
3148
+ foreignMaterialise(this, name);
3149
+ return wrapReturn(native.getProperty(handle, name));
3150
+ };
3151
+ const set = function (value) {
3152
+ const handle = foreignHandle(this);
3153
+ // Equal-value guard, GJS's _generateAccessors setter verbatim — see the same
3154
+ // guard in wrapInstance's set trap for why it is only armed once the value
3155
+ // has been materialised.
3156
+ if (foreignMaterialise(this, name) && wrapReturn(native.getProperty(handle, name)) === value) return;
3157
+ native.setProperty(handle, name, unwrapArg(value));
3158
+ };
3159
+ generatedForeignSetters.add(set);
3160
+ for (const spelling of spellings) {
3161
+ defineForeignMember(proto, spelling, { get, set }, skipped);
3162
+ }
3163
+ }
3164
+ }
3165
+
2738
3166
  // GJS's meta FIELD form: a class may declare its meta as static symbol fields
2739
3167
  // (`Klass[GObject.GTypeName] = 'X'`) instead of passing a meta object, and GJS treats
2740
3168
  // the symbols as the real source — `registerClass(meta, klass)` only copies meta onto
@@ -2774,13 +3202,19 @@ function registerClass(metaOrClass, maybeClass) {
2774
3202
  meta = { ...fieldMeta, ...meta };
2775
3203
 
2776
3204
  const parent = findParentGType(klass);
2777
- if (parent === undefined) {
3205
+ // A parent that is not a node-gi GObject class at all. GJS throws here
3206
+ // ("registerClass() used with invalid base class", refs/gjs GObject.js) and so does
3207
+ // every host but NativeScript — where the FOREIGN-PARENT path instead registers the
3208
+ // GType under GObject.Object and ATTACHES it to the host widget object. See the
3209
+ // FOREIGN-PARENT section above.
3210
+ if (parent === undefined && !FOREIGN_PARENT_CLASSES) {
2778
3211
  throw new TypeError(
2779
3212
  'GObject.registerClass: ' +
2780
3213
  (klass.name || 'the class') +
2781
3214
  ' must extend a node-gi GObject class (e.g. GObject.Object)',
2782
3215
  );
2783
3216
  }
3217
+ const foreign = parent === undefined || parent.parentForeign === true;
2784
3218
 
2785
3219
  const properties = [];
2786
3220
  if (meta.Properties !== null && typeof meta.Properties === 'object') {
@@ -2822,10 +3256,16 @@ function registerClass(metaOrClass, maybeClass) {
2822
3256
  // from its runtime GType handle (it has no introspection entry); an INTROSPECTED
2823
3257
  // parent is resolved by namespace+type. Construction (the leaf's registered GType
2824
3258
  // via the makeClass new.target routing) is already multi-level-correct either way.
3259
+ // A FOREIGN parent contributes no GType at all (it is a host widget class), so the
3260
+ // registered type is rooted at GObject.Object — the GType parent is independent of
3261
+ // the JS prototype chain, which is exactly what makes route D possible. A foreign
3262
+ // SUBCLASS still has a registered parent and takes the ordinary by-GType branch.
2825
3263
  const typeHandle =
2826
- parent.parentGTypeHandle !== undefined
2827
- ? native.registerClassFromGType(gtypeName, parent.parentGTypeHandle, options)
2828
- : native.registerClass(gtypeName, parent.parentNamespace, parent.parentType, options);
3264
+ parent === undefined
3265
+ ? native.registerClass(gtypeName, 'GObject', 'Object', options)
3266
+ : parent.parentGTypeHandle !== undefined
3267
+ ? native.registerClassFromGType(gtypeName, parent.parentGTypeHandle, options)
3268
+ : native.registerClass(gtypeName, parent.parentNamespace, parent.parentType, options);
2829
3269
 
2830
3270
  // G3: register the GType for the GIVEN class IN PLACE (no throwaway Subclass) and
2831
3271
  // return that same class. `$gtypeName`/`$gtype` are defined as OWN properties:
@@ -2865,6 +3305,8 @@ function registerClass(metaOrClass, maybeClass) {
2865
3305
  children: children.length > 0 ? children : undefined,
2866
3306
  internalChildren: internalChildren.length > 0 ? internalChildren : undefined,
2867
3307
  constructPropertyNames: constructPropertyNames.length > 0 ? constructPropertyNames : undefined,
3308
+ foreign: foreign ? true : undefined,
3309
+ foreignProto: foreign ? klass.prototype : undefined,
2868
3310
  });
2869
3311
  // Which properties the equal-value guard owns: the ones DECLARED here, union the
2870
3312
  // registered parent's (a G2 subclass inherits its parent's custom properties and
@@ -2881,6 +3323,36 @@ function registerClass(metaOrClass, maybeClass) {
2881
3323
  // Reverse index for runCtorForCObject: the engine identifies a C-created instance
2882
3324
  // by its GType name (= gtypeName, what native.registerClass registered).
2883
3325
  classesByGType.set(gtypeName, klass);
3326
+ // FOREIGN parent: install the GObject surface as own members of this prototype and
3327
+ // index the prototype, so an instance can find ITS leaf registration by walking the
3328
+ // real (host-widget) prototype chain. Both before type_class_ref, like every record
3329
+ // above it. `foreignSkipped` names the members the host chain already owned — the
3330
+ // honest answer to "what did this class NOT get", which the route-D spec reports.
3331
+ if (foreign) {
3332
+ const reg = registeredClasses.get(klass);
3333
+ reg.foreignSkipped = installForeignGObjectSurface(klass.prototype, properties);
3334
+ foreignRegByProto.set(klass.prototype, reg);
3335
+ }
3336
+ // `$foreignSkipped`, readable from outside, because "which GObject spelling did the
3337
+ // host widget already own?" is the one question this route cannot answer in the
3338
+ // abstract: it depends on the parent class, so it has to be reported per
3339
+ // registration rather than documented once.
3340
+ //
3341
+ // On EVERY registration of this host, not only the foreign ones, and an empty array
3342
+ // where nothing was skipped. A class's static side is a Proxy that answers any
3343
+ // unknown name with a static-method thunk, so `X.$foreignSkipped` is already
3344
+ // truthy — a FUNCTION — wherever the property is absent, and `if
3345
+ // (X.$foreignSkipped.length)` would then throw rather than read "nothing". Defining
3346
+ // it is what makes the read mean what it says. Gated on the host flag all the same,
3347
+ // so the default host's registerClass keeps exactly GJS's static surface.
3348
+ if (FOREIGN_PARENT_CLASSES) {
3349
+ Object.defineProperty(klass, '$foreignSkipped', {
3350
+ value: Object.freeze([...(registeredClasses.get(klass).foreignSkipped ?? [])]),
3351
+ configurable: true,
3352
+ enumerable: false,
3353
+ writable: false,
3354
+ });
3355
+ }
2884
3356
  // GJS parity: registration must be COMPLETE when registerClass returns. The
2885
3357
  // engine installs the custom properties, signals and vfunc trampolines in
2886
3358
  // class_init (src/class.cc), which GObject runs lazily on the first
@@ -2973,6 +3445,12 @@ function signalEmitByName(object, ...nameAndArgs) {
2973
3445
  // makeClass). Merged enums are cached so identity is stable.
2974
3446
  const OVERLAY_NAMES = new Set([
2975
3447
  'registerClass',
3448
+ // Whether THIS host registers a class over a non-GObject parent (the FOREIGN-PARENT
3449
+ // section above) or throws on one. A caller that wants route D has no other way to
3450
+ // ask: the only alternative is to register and catch, which registers a GType as a
3451
+ // side effect of a question and turns "this host cannot" into the same outcome as
3452
+ // "this registration was wrong". Read by @gjsify/adwaita-gobject-native's engine.
3453
+ '$foreignParentClasses',
2976
3454
  'GTypeName',
2977
3455
  'properties',
2978
3456
  'signals',
@@ -3042,6 +3520,7 @@ function decorateGObjectNamespace(baseNs) {
3042
3520
  if (cache.has(prop)) return cache.get(prop);
3043
3521
  let value;
3044
3522
  if (prop === 'registerClass') value = registerClass;
3523
+ else if (prop === '$foreignParentClasses') value = FOREIGN_PARENT_CLASSES;
3045
3524
  else if (prop === 'GTypeName') value = GTypeNameSymbol;
3046
3525
  else if (prop === 'properties') value = propertiesSymbol;
3047
3526
  else if (prop === 'signals') value = signalsSymbol;
@@ -3190,6 +3669,7 @@ function mirrorWindowingEnvIntoCrt() {
3190
3669
  * @returns {Record<string, unknown>}
3191
3670
  */
3192
3671
  export function requireGi(namespace, version) {
3672
+ ensureNative();
3193
3673
  native.requireNamespace(namespace, version);
3194
3674
  // The one moment the win32 CRT-environment mirror can run: the addon is loaded, so there
3195
3675
  // is a GLib to call, and nothing has built a font map or loaded a fontconfig