@gjsify/node-gi 0.64.2 → 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/README.md +9 -0
- package/gi.js +440 -13
- package/host.nativescript.js +13 -0
- package/host.node.js +10 -0
- package/index.d.ts +12 -0
- package/index.js +55 -2
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/node_gi.node +0 -0
- package/prebuilds/darwin-x64/node_gi.node +0 -0
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/prebuilds/win32-arm64/node_gi.node +0 -0
- package/prebuilds/win32-x64/node_gi.node +0 -0
- package/src/signals.cc +45 -22
package/README.md
CHANGED
|
@@ -148,6 +148,15 @@ export halves, the legacy `imports.*` modules, cairo, and the measured
|
|
|
148
148
|
divergences from GJS — is
|
|
149
149
|
[docs/node-gi-gjs-surface.md](../../../docs/node-gi-gjs-surface.md).
|
|
150
150
|
|
|
151
|
+
One divergence is deliberate rather than measured, and it only exists under the
|
|
152
|
+
`nativescript` export condition: there `registerClass` accepts a parent class
|
|
153
|
+
that is not a GObject class, so an `@nativescript/core` widget can BE a real
|
|
154
|
+
GObject subclass (properties, signals, `bind_property`, `Gio.ListStore`
|
|
155
|
+
identity) while staying the widget its layout demands. Every other host throws
|
|
156
|
+
GJS's invalid-base-class error, unchanged — see
|
|
157
|
+
[§ `registerClass` over a FOREIGN parent class](../../../docs/node-gi-gjs-surface.md)
|
|
158
|
+
and [ADR 0105](../../../docs/adr/0105-real-gobject-backs-the-nativescript-subset-on-android.md).
|
|
159
|
+
|
|
151
160
|
## Runtimes (Node / Bun / Deno)
|
|
152
161
|
|
|
153
162
|
The engine is a **Node-API** addon, so the same binary loads and runs on **Node,
|
package/gi.js
CHANGED
|
@@ -11,7 +11,14 @@ import { ensureNative, onNativeLoaded } from './index.js';
|
|
|
11
11
|
// Timers, `process` and the windowing env record come from the host seam (host.node.js
|
|
12
12
|
// explains why they are the runtime's own, not the globals; host.nativescript.js is the
|
|
13
13
|
// NativeScript Android variant).
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
setImmediate,
|
|
16
|
+
setInterval,
|
|
17
|
+
clearInterval,
|
|
18
|
+
runtimeProcess,
|
|
19
|
+
windowingEnvWrites,
|
|
20
|
+
FOREIGN_PARENT_CLASSES,
|
|
21
|
+
} from '#host';
|
|
15
22
|
import { createGioDBus } from './overrides/gio-dbus.js';
|
|
16
23
|
import { createGioSettings } from './overrides/gio-settings.js';
|
|
17
24
|
|
|
@@ -434,7 +441,7 @@ function wrapSignalCallback(cb) {
|
|
|
434
441
|
// means `this` is the full userProto-carrying wrapper — the SAME cached proxy the user
|
|
435
442
|
// constructed. Emitter at param 0, as in wrapSignalCallback.
|
|
436
443
|
function resolveTemplateCallback(handle, handlerName) {
|
|
437
|
-
|
|
444
|
+
const dispatcher = (...args) => {
|
|
438
445
|
const proxy = wrapInstance(handle);
|
|
439
446
|
const userProto = proxy[USER_PROTO];
|
|
440
447
|
const desc = userProto !== undefined ? findProtoDescriptor(userProto, handlerName) : undefined;
|
|
@@ -445,6 +452,14 @@ function resolveTemplateCallback(handle, handlerName) {
|
|
|
445
452
|
}
|
|
446
453
|
return desc.value.apply(proxy, args.map(wrapReturn));
|
|
447
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;
|
|
448
463
|
}
|
|
449
464
|
onNativeLoaded(() => native.setTemplateCallbackResolver(resolveTemplateCallback));
|
|
450
465
|
|
|
@@ -464,7 +479,12 @@ function runCtorForCObject(handle, gtypeName) {
|
|
|
464
479
|
try {
|
|
465
480
|
// No props: GtkBuilder already applied the construct properties to the C object;
|
|
466
481
|
// the ctor's `params` arg is conventionally optional for a template child.
|
|
467
|
-
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);
|
|
468
488
|
} finally {
|
|
469
489
|
adoptHandle = prev;
|
|
470
490
|
}
|
|
@@ -1128,6 +1148,13 @@ function assignTemplateChildren(instance, handle, reg) {
|
|
|
1128
1148
|
// outside what either runtime honours.
|
|
1129
1149
|
const propertySettersByProto = new WeakMap();
|
|
1130
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
|
+
|
|
1131
1158
|
function findPropertySetter(userProto, propertyName) {
|
|
1132
1159
|
let perProto = propertySettersByProto.get(userProto);
|
|
1133
1160
|
if (perProto === undefined) propertySettersByProto.set(userProto, (perProto = new Map()));
|
|
@@ -1144,7 +1171,9 @@ function resolvePropertySetter(userProto, propertyName) {
|
|
|
1144
1171
|
const camel = propertyName.replace(/-([a-z])/g, (m) => m[1].toUpperCase());
|
|
1145
1172
|
for (const spelling of [propertyName, underscored, camel]) {
|
|
1146
1173
|
const desc = findProtoDescriptor(userProto, spelling);
|
|
1147
|
-
if (desc !== undefined && typeof desc.set === 'function'
|
|
1174
|
+
if (desc !== undefined && typeof desc.set === 'function' && !generatedForeignSetters.has(desc.set)) {
|
|
1175
|
+
return desc.set;
|
|
1176
|
+
}
|
|
1148
1177
|
}
|
|
1149
1178
|
return undefined;
|
|
1150
1179
|
}
|
|
@@ -1587,7 +1616,11 @@ function wrapInstance(handle, userProto) {
|
|
|
1587
1616
|
// the leaf's OWN methods resolve). Never DOWNGRADE: a more-derived cached proto is
|
|
1588
1617
|
// kept. The userProto lives on the target (read via the proxy here), so the
|
|
1589
1618
|
// upgrade is in place — identity (===) and any expando fields are preserved.
|
|
1590
|
-
|
|
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)) {
|
|
1591
1624
|
const current = cached[USER_PROTO];
|
|
1592
1625
|
if (current === undefined || isProtoSameOrDescendantOf(userProto, current)) {
|
|
1593
1626
|
linkInstanceProto(cached, userProto);
|
|
@@ -2612,7 +2645,11 @@ function findParentGType(klass) {
|
|
|
2612
2645
|
for (let c = Object.getPrototypeOf(klass); typeof c === 'function'; c = Object.getPrototypeOf(c)) {
|
|
2613
2646
|
const reg = registeredClasses.get(c);
|
|
2614
2647
|
if (reg !== undefined) {
|
|
2615
|
-
|
|
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 };
|
|
2616
2653
|
}
|
|
2617
2654
|
const gt = c.$gtypeName;
|
|
2618
2655
|
if (typeof gt === 'string') {
|
|
@@ -2620,9 +2657,19 @@ function findParentGType(klass) {
|
|
|
2620
2657
|
if (dot > 0) {
|
|
2621
2658
|
return { parentNamespace: gt.slice(0, dot), parentType: gt.slice(dot + 1) };
|
|
2622
2659
|
}
|
|
2623
|
-
// A dotless `$gtypeName`
|
|
2624
|
-
//
|
|
2625
|
-
//
|
|
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.
|
|
2626
2673
|
throw new TypeError("GObject.registerClass: parent '" + gt + "' has no resolvable GType");
|
|
2627
2674
|
}
|
|
2628
2675
|
}
|
|
@@ -2787,6 +2834,335 @@ function collectVfuncs(klass) {
|
|
|
2787
2834
|
* @param {Function} [maybeClass]
|
|
2788
2835
|
* @returns {Function} the same `klass` (now registered)
|
|
2789
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
|
+
|
|
2790
3166
|
// GJS's meta FIELD form: a class may declare its meta as static symbol fields
|
|
2791
3167
|
// (`Klass[GObject.GTypeName] = 'X'`) instead of passing a meta object, and GJS treats
|
|
2792
3168
|
// the symbols as the real source — `registerClass(meta, klass)` only copies meta onto
|
|
@@ -2826,13 +3202,19 @@ function registerClass(metaOrClass, maybeClass) {
|
|
|
2826
3202
|
meta = { ...fieldMeta, ...meta };
|
|
2827
3203
|
|
|
2828
3204
|
const parent = findParentGType(klass);
|
|
2829
|
-
|
|
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) {
|
|
2830
3211
|
throw new TypeError(
|
|
2831
3212
|
'GObject.registerClass: ' +
|
|
2832
3213
|
(klass.name || 'the class') +
|
|
2833
3214
|
' must extend a node-gi GObject class (e.g. GObject.Object)',
|
|
2834
3215
|
);
|
|
2835
3216
|
}
|
|
3217
|
+
const foreign = parent === undefined || parent.parentForeign === true;
|
|
2836
3218
|
|
|
2837
3219
|
const properties = [];
|
|
2838
3220
|
if (meta.Properties !== null && typeof meta.Properties === 'object') {
|
|
@@ -2874,10 +3256,16 @@ function registerClass(metaOrClass, maybeClass) {
|
|
|
2874
3256
|
// from its runtime GType handle (it has no introspection entry); an INTROSPECTED
|
|
2875
3257
|
// parent is resolved by namespace+type. Construction (the leaf's registered GType
|
|
2876
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.
|
|
2877
3263
|
const typeHandle =
|
|
2878
|
-
parent
|
|
2879
|
-
? native.
|
|
2880
|
-
:
|
|
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);
|
|
2881
3269
|
|
|
2882
3270
|
// G3: register the GType for the GIVEN class IN PLACE (no throwaway Subclass) and
|
|
2883
3271
|
// return that same class. `$gtypeName`/`$gtype` are defined as OWN properties:
|
|
@@ -2917,6 +3305,8 @@ function registerClass(metaOrClass, maybeClass) {
|
|
|
2917
3305
|
children: children.length > 0 ? children : undefined,
|
|
2918
3306
|
internalChildren: internalChildren.length > 0 ? internalChildren : undefined,
|
|
2919
3307
|
constructPropertyNames: constructPropertyNames.length > 0 ? constructPropertyNames : undefined,
|
|
3308
|
+
foreign: foreign ? true : undefined,
|
|
3309
|
+
foreignProto: foreign ? klass.prototype : undefined,
|
|
2920
3310
|
});
|
|
2921
3311
|
// Which properties the equal-value guard owns: the ones DECLARED here, union the
|
|
2922
3312
|
// registered parent's (a G2 subclass inherits its parent's custom properties and
|
|
@@ -2933,6 +3323,36 @@ function registerClass(metaOrClass, maybeClass) {
|
|
|
2933
3323
|
// Reverse index for runCtorForCObject: the engine identifies a C-created instance
|
|
2934
3324
|
// by its GType name (= gtypeName, what native.registerClass registered).
|
|
2935
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
|
+
}
|
|
2936
3356
|
// GJS parity: registration must be COMPLETE when registerClass returns. The
|
|
2937
3357
|
// engine installs the custom properties, signals and vfunc trampolines in
|
|
2938
3358
|
// class_init (src/class.cc), which GObject runs lazily on the first
|
|
@@ -3025,6 +3445,12 @@ function signalEmitByName(object, ...nameAndArgs) {
|
|
|
3025
3445
|
// makeClass). Merged enums are cached so identity is stable.
|
|
3026
3446
|
const OVERLAY_NAMES = new Set([
|
|
3027
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',
|
|
3028
3454
|
'GTypeName',
|
|
3029
3455
|
'properties',
|
|
3030
3456
|
'signals',
|
|
@@ -3094,6 +3520,7 @@ function decorateGObjectNamespace(baseNs) {
|
|
|
3094
3520
|
if (cache.has(prop)) return cache.get(prop);
|
|
3095
3521
|
let value;
|
|
3096
3522
|
if (prop === 'registerClass') value = registerClass;
|
|
3523
|
+
else if (prop === '$foreignParentClasses') value = FOREIGN_PARENT_CLASSES;
|
|
3097
3524
|
else if (prop === 'GTypeName') value = GTypeNameSymbol;
|
|
3098
3525
|
else if (prop === 'properties') value = propertiesSymbol;
|
|
3099
3526
|
else if (prop === 'signals') value = signalsSymbol;
|
package/host.nativescript.js
CHANGED
|
@@ -50,6 +50,19 @@ export const RUNTIME = 'nativescript';
|
|
|
50
50
|
*/
|
|
51
51
|
export const LAZY_NATIVE = true;
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Whether `GObject.registerClass` may register a class whose JS parent is NOT a node-gi
|
|
55
|
+
* GObject class — `class MyLabel extends Label {}`, where `Label` is a
|
|
56
|
+
* `@nativescript/core` widget. GJS rejects that outright and so does node-gi on every
|
|
57
|
+
* other host; this is the ONE deliberate divergence, and it is deliberate because on
|
|
58
|
+
* NativeScript there is no GJS to be compatible with: node-gi is the only GI runtime that
|
|
59
|
+
* ever executes here, so a program written against this cannot be a program that behaves
|
|
60
|
+
* differently under GJS. See the FOREIGN-PARENT section in gi.js, ADR 0105 § 2 and
|
|
61
|
+
* docs/node-gi-gjs-surface.md.
|
|
62
|
+
* @type {boolean}
|
|
63
|
+
*/
|
|
64
|
+
export const FOREIGN_PARENT_CLASSES = true;
|
|
65
|
+
|
|
53
66
|
// Timers from the global scope: NativeScript has no node:timers, and there is no
|
|
54
67
|
// GLib-backed replacement to avoid here. setImmediate is not a V8 builtin.
|
|
55
68
|
export const setImmediate =
|
package/host.node.js
CHANGED
|
@@ -61,6 +61,16 @@ export const RUNTIME =
|
|
|
61
61
|
*/
|
|
62
62
|
export const LAZY_NATIVE = false;
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Whether `GObject.registerClass` may register a class whose JS parent is NOT a node-gi
|
|
66
|
+
* GObject class. FALSE here, and that is the point: on Node/Bun/Deno/GJS a program can be
|
|
67
|
+
* run under real GJS, so registerClass must keep throwing GJS's invalid-base error
|
|
68
|
+
* (refs/gjs/modules/core/overrides/GObject.js) rather than quietly accept a class GJS
|
|
69
|
+
* would reject. Only host.nativescript.js enables it — see the note there.
|
|
70
|
+
* @type {boolean}
|
|
71
|
+
*/
|
|
72
|
+
export const FOREIGN_PARENT_CLASSES = false;
|
|
73
|
+
|
|
64
74
|
// GJS host mode: `GjsifyNapi.init()` installs the loader as
|
|
65
75
|
// `globalThis.__gjsifyNapiLoadAddon` and re-installs it if another consumer (the
|
|
66
76
|
// @gjsify/napi L1) already captured + deleted it. Needs the GjsifyNapi typelib
|
package/index.d.ts
CHANGED
|
@@ -542,6 +542,18 @@ export function emitSignal(handle: GObjectHandle, signalName: string, args?: unk
|
|
|
542
542
|
/** Disconnect a previously connected signal handler. */
|
|
543
543
|
export function disconnectSignal(handle: GObjectHandle, handlerId: number): void;
|
|
544
544
|
|
|
545
|
+
/**
|
|
546
|
+
* INTERNAL. Keep a callback alive for as long as `handle` is alive. Signal and
|
|
547
|
+
* template-callback GClosures hold their JS callback through a WEAK napi_ref, so
|
|
548
|
+
* a connected handler is not a GC root (which is what makes a handler capturing
|
|
549
|
+
* its own emitter collectable, as in GJS); this handle-keyed WeakMap is the
|
|
550
|
+
* ephemeron that keeps it callable meanwhile. `connectSignal` does it for you.
|
|
551
|
+
*/
|
|
552
|
+
export function retainClosure(handle: GObjectHandle, key: number | symbol, fn: Function): void;
|
|
553
|
+
|
|
554
|
+
/** INTERNAL. Drop a closure retained by {@link retainClosure}. */
|
|
555
|
+
export function releaseClosure(handle: GObjectHandle, key: number | symbol): void;
|
|
556
|
+
|
|
545
557
|
/**
|
|
546
558
|
* Register the L1 resolver mapping a Gtk.Template `<signal handler="…">` handler
|
|
547
559
|
* name to the instance's bound JS method (engine template-callback scope).
|
package/index.js
CHANGED
|
@@ -94,9 +94,16 @@ export function ensureNative() {
|
|
|
94
94
|
makePumpPendingCount = addon.makePumpPendingCount;
|
|
95
95
|
pumpKick = addon.pumpKick;
|
|
96
96
|
setMicrotaskDrain = addon.setMicrotaskDrain;
|
|
97
|
-
connectSignal = addon.connectSignal;
|
|
98
97
|
emitSignal = addon.emitSignal;
|
|
99
|
-
|
|
98
|
+
connectSignal = (handle, signalName, callback, after) => {
|
|
99
|
+
const id = addon.connectSignal(handle, signalName, callback, after);
|
|
100
|
+
retainClosure(handle, id, callback);
|
|
101
|
+
return id;
|
|
102
|
+
};
|
|
103
|
+
disconnectSignal = (handle, handlerId) => {
|
|
104
|
+
releaseClosure(handle, handlerId);
|
|
105
|
+
return addon.disconnectSignal(handle, handlerId);
|
|
106
|
+
};
|
|
100
107
|
setTemplateCallbackResolver = addon.setTemplateCallbackResolver;
|
|
101
108
|
setConstructCallback = addon.setConstructCallback;
|
|
102
109
|
setPropertySetCallback = addon.setPropertySetCallback;
|
|
@@ -205,6 +212,52 @@ export let mainContextHasPending;
|
|
|
205
212
|
export let makePumpPendingCount;
|
|
206
213
|
export let pumpKick;
|
|
207
214
|
export let setMicrotaskDrain;
|
|
215
|
+
// ---- signal-closure keep-alive (GJS's associate_closure, built in JS) ----
|
|
216
|
+
//
|
|
217
|
+
// A signal / template-callback GClosure holds its JS callback through a WEAK
|
|
218
|
+
// napi_ref (src/signals.cc), so a C-held closure is NOT a GC root. That is what
|
|
219
|
+
// makes `obj.connect('x', () => obj.y)` collectable instead of immortal, and it
|
|
220
|
+
// is GJS's design: its signal closures keep the callable unrooted and TRACE it
|
|
221
|
+
// from the owning wrapper (ObjectInstance::m_closures).
|
|
222
|
+
//
|
|
223
|
+
// node-gi has no tracing hook into V8, so the same edge is expressed as the one
|
|
224
|
+
// ephemeron a non-extensible External can take part in: a WeakMap keyed by the
|
|
225
|
+
// canonical handle. Handle reachable (JS holds the wrapper, or C owns the object
|
|
226
|
+
// and the toggle ref roots the handle) → callback reachable → a connected handler
|
|
227
|
+
// still fires. Handle reachable ONLY through the callback it retains → key and
|
|
228
|
+
// value die together, which is exactly the cycle that used to leak.
|
|
229
|
+
//
|
|
230
|
+
// Pruned on disconnect. A handler disconnected by a route that bypasses
|
|
231
|
+
// `disconnectSignal` (the introspected GObject.signal_handler_disconnect) leaves
|
|
232
|
+
// its entry behind until the handle itself goes — harmless, the closure is
|
|
233
|
+
// already gone, and the same divergence the gi.js handler-id registry documents.
|
|
234
|
+
const retainedClosures = new WeakMap();
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Keep `fn` alive for as long as `handle` is alive. INTERNAL — the JS half of the
|
|
238
|
+
* weak closure refs in src/signals.cc.
|
|
239
|
+
* @param {object} handle canonical GObject handle (the WeakMap key / lifetime)
|
|
240
|
+
* @param {number | symbol} key handler id, or a unique key for a non-signal closure
|
|
241
|
+
* @param {Function} fn the callback the native closure holds weakly
|
|
242
|
+
*/
|
|
243
|
+
export function retainClosure(handle, key, fn) {
|
|
244
|
+
let byKey = retainedClosures.get(handle);
|
|
245
|
+
if (byKey === undefined) {
|
|
246
|
+
byKey = new Map();
|
|
247
|
+
retainedClosures.set(handle, byKey);
|
|
248
|
+
}
|
|
249
|
+
byKey.set(key, fn);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Drop a retained closure (on disconnect). INTERNAL — see retainClosure.
|
|
254
|
+
* @param {object} handle canonical GObject handle
|
|
255
|
+
* @param {number | symbol} key the key it was retained under
|
|
256
|
+
*/
|
|
257
|
+
export function releaseClosure(handle, key) {
|
|
258
|
+
retainedClosures.get(handle)?.delete(key);
|
|
259
|
+
}
|
|
260
|
+
|
|
208
261
|
export let connectSignal;
|
|
209
262
|
export let emitSignal;
|
|
210
263
|
export let disconnectSignal;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/node-gi",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.4",
|
|
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
|
|
Binary file
|
package/src/signals.cc
CHANGED
|
@@ -7,22 +7,36 @@ namespace nodegi {
|
|
|
7
7
|
|
|
8
8
|
// ---- signals (milestone 1) ----
|
|
9
9
|
//
|
|
10
|
-
// A GClosure that wraps a JS callback. The
|
|
11
|
-
// napi_ref
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// into the signal return GValue.
|
|
10
|
+
// A GClosure that wraps a JS callback. The closure's finalize notifier drops the
|
|
11
|
+
// napi_ref. The generic marshal converts the signal's GValue params to JS — the
|
|
12
|
+
// EMITTER instance (param 0) first, then the signal's own params (1..n), matching
|
|
13
|
+
// GJS — and the JS return into the signal return GValue.
|
|
15
14
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// the
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
15
|
+
// REF STRENGTH IS THE WHOLE CONTRACT, and it differs per closure KIND, exactly as
|
|
16
|
+
// in GJS (refs/gjs/gi/closure.h: `create_marshaled` roots, `create_for_signal`
|
|
17
|
+
// does not):
|
|
18
|
+
//
|
|
19
|
+
// * GENERIC closure (a JS function marshalled as a GClosure IN-arg, invoked by
|
|
20
|
+
// arbitrary C) — STRONG. Nothing on the JS side owns it; the ref IS the owner.
|
|
21
|
+
// * SIGNAL closure (`.connect()`) and TEMPLATE-CALLBACK closure — WEAK. A
|
|
22
|
+
// handler that captures its own emitter otherwise forms a
|
|
23
|
+
// GObject → GClosure → napi_ref → callback → wrapper → toggle ref → GObject
|
|
24
|
+
// cycle that spans C, and no GC can break it: `obj.connect('x', () => obj.y)`
|
|
25
|
+
// made `obj` immortal. GJS has the identical cycle on paper and collects it,
|
|
26
|
+
// because its signal closures keep the callable UNROOTED and trace it from
|
|
27
|
+
// the owning wrapper (ObjectInstance::m_closures / trace_impl), which puts
|
|
28
|
+
// the whole cycle inside one heap the GC can reason about.
|
|
29
|
+
//
|
|
30
|
+
// node-gi has no tracing hook, so the equivalent edge is built in JS: index.js
|
|
31
|
+
// retains the callback in a WeakMap keyed by the canonical handle — the only
|
|
32
|
+
// ephemeron V8 offers over a non-extensible External, and the same shape
|
|
33
|
+
// `associate_closure` has. The handle is reachable exactly when GJS would trace
|
|
34
|
+
// the closure (JS holds the wrapper, or C owns the object and toggle.cc roots
|
|
35
|
+
// it), so a connected handler still fires for as long as anything can emit to it,
|
|
36
|
+
// and the self-referential cycle collects. A collected callback leaves the weak
|
|
37
|
+
// ref empty and the marshal returns early (it already tolerates a null value).
|
|
38
|
+
// Measured against gjs 1.88.1 with a FinalizationRegistry witness; verified by the
|
|
39
|
+
// signal-cycle cases in test/gc-identity.test.mjs.
|
|
26
40
|
|
|
27
41
|
struct JsClosureData {
|
|
28
42
|
napi_env env;
|
|
@@ -191,8 +205,11 @@ static void JsGenericClosureMarshal(GClosure* closure, GValue* return_value, gui
|
|
|
191
205
|
|
|
192
206
|
// JS function → a floating marshaled GClosure (the engine's twin of GJS's
|
|
193
207
|
// Gjs::Closure::create_marshaled for "boxed" closure args — refs/gjs/gi/
|
|
194
|
-
// arg-cache.cpp GClosureInTransferNone::in). The closure holds a
|
|
195
|
-
// the function
|
|
208
|
+
// arg-cache.cpp GClosureInTransferNone::in). The closure holds a STRONG ref to
|
|
209
|
+
// the function — deliberately, and unlike the signal/template closures above:
|
|
210
|
+
// `create_marshaled` defaults `root = true` (refs/gjs/gi/closure.h), because no
|
|
211
|
+
// JS-side owner exists for a closure handed to arbitrary C. JsClosureFinalize
|
|
212
|
+
// drops it when the closure is invalidated /
|
|
196
213
|
// finalized (the callee released its last ref, or the invoke-site released the
|
|
197
214
|
// only ref because the callee never kept one). Ref/sink + release policy lives at
|
|
198
215
|
// the marshalling site (marshal.cc / calls.cc CreatedClosures).
|
|
@@ -231,9 +248,11 @@ Napi::Value ConnectSignal(const Napi::CallbackInfo& info) {
|
|
|
231
248
|
return env.Null();
|
|
232
249
|
}
|
|
233
250
|
|
|
251
|
+
// WEAK (refcount 0) — the keep-alive is the JS-side handle→callback edge
|
|
252
|
+
// index.js installs; see the ref-strength contract at the top of this file.
|
|
234
253
|
JsClosureData* jc = g_new0(JsClosureData, 1);
|
|
235
254
|
jc->env = env;
|
|
236
|
-
napi_create_reference(env, info[2],
|
|
255
|
+
napi_create_reference(env, info[2], 0, &jc->callback);
|
|
237
256
|
|
|
238
257
|
GClosure* closure = g_closure_new_simple(sizeof(GClosure), jc);
|
|
239
258
|
g_closure_set_marshal(closure, JsClosureMarshal);
|
|
@@ -468,13 +487,17 @@ static GClosure* NodeGiScopeCreateClosure(void* selfScope, void* builder,
|
|
|
468
487
|
}
|
|
469
488
|
|
|
470
489
|
// Wrap the resolved (instance-bound, arg-wrapping) JS function in a JsClosure —
|
|
471
|
-
// the same closure type a `.connect()` produces
|
|
472
|
-
// the
|
|
473
|
-
//
|
|
490
|
+
// the same closure type a `.connect()` produces, and WEAK for the same reason:
|
|
491
|
+
// the resolver's dispatcher captures the widget handle, so a strong ref made
|
|
492
|
+
// every templated widget immortal. gi.js's resolveTemplateCallback retains the
|
|
493
|
+
// dispatcher on the widget handle before returning it, which is what GJS's Gtk
|
|
494
|
+
// override does with `Gi.associateClosure(lifetimeObject, …)`
|
|
495
|
+
// (refs/gjs/modules/core/overrides/Gtk.js). g_object_watch_closure additionally
|
|
496
|
+
// ties the closure to the widget and invalidates it on finalize, exactly like
|
|
474
497
|
// GtkBuilderCScope's own create_closure.
|
|
475
498
|
JsClosureData* jc = g_new0(JsClosureData, 1);
|
|
476
499
|
jc->env = rawEnv;
|
|
477
|
-
napi_create_reference(rawEnv, resolved,
|
|
500
|
+
napi_create_reference(rawEnv, resolved, 0, &jc->callback);
|
|
478
501
|
GClosure* closure = g_closure_new_simple(sizeof(GClosure), jc);
|
|
479
502
|
g_closure_set_marshal(closure, JsClosureMarshal);
|
|
480
503
|
g_closure_add_finalize_notifier(closure, jc, JsClosureFinalize);
|