@gjsify/node-gi 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gi.js +96 -1
- package/globals.js +16 -0
- package/gtk-runtime.js +12 -4
- package/index.js +10 -0
- package/overrides/package.js +167 -0
- package/package.json +1 -1
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/src/addon.cc +1 -0
- package/src/class.cc +133 -0
- package/src/common.h +21 -0
- package/src/object.cc +8 -0
- package/src/repo.cc +1 -0
package/gi.js
CHANGED
|
@@ -255,6 +255,31 @@ function resolveTemplateCallback(handle, handlerName) {
|
|
|
255
255
|
}
|
|
256
256
|
native.setTemplateCallbackResolver(resolveTemplateCallback);
|
|
257
257
|
|
|
258
|
+
// Run a registered class's JS constructor for a GObject the ENGINE instantiated
|
|
259
|
+
// from C — a GtkBuilder composite-template InternalChild is the common case. node-gi's
|
|
260
|
+
// JS-`new` path runs the ctor via the makeClass super-substitution; a C-created
|
|
261
|
+
// instance has no such driver, so NodeGiConstructor (the overridden `constructor`
|
|
262
|
+
// vfunc) calls this with the instance's canonical handle + its GType name. We set
|
|
263
|
+
// `adoptHandle` and Reflect.construct the class: its `super()` reaches the makeClass
|
|
264
|
+
// base ctor, which adopts the handle (no second GObject) and returns the canonical
|
|
265
|
+
// wrapper, so the ctor body runs against it (`this._x = …` become expandos on the
|
|
266
|
+
// one wrapper GtkBuilder-fetched children later resolve to). GJS-faithful
|
|
267
|
+
// (gjs_object_constructor's native-construction branch runs the JS constructor).
|
|
268
|
+
function runCtorForCObject(handle, gtypeName) {
|
|
269
|
+
const cls = classesByGType.get(gtypeName);
|
|
270
|
+
if (cls === undefined) return; // not a node-gi-registered class (shouldn't happen)
|
|
271
|
+
const prev = adoptHandle;
|
|
272
|
+
adoptHandle = handle;
|
|
273
|
+
try {
|
|
274
|
+
// No props: GtkBuilder already applied the construct properties to the C object;
|
|
275
|
+
// the ctor's `params` arg is conventionally optional for a template child.
|
|
276
|
+
Reflect.construct(cls, []);
|
|
277
|
+
} finally {
|
|
278
|
+
adoptHandle = prev;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
native.setConstructCallback(runCtorForCObject);
|
|
282
|
+
|
|
258
283
|
// Wrap a non-GObject GObject-fundamental (e.g. a GskRenderNode from
|
|
259
284
|
// Gtk.Snapshot.to_node, a GdkEvent) as an OPAQUE, round-trippable handle. The
|
|
260
285
|
// native tagged External already carries the raw pointer (its Data) + drops the
|
|
@@ -835,6 +860,18 @@ function handlerIdsForFunc(handle, fn) {
|
|
|
835
860
|
// routing keys on the leaf so construction is correct at any depth.
|
|
836
861
|
const registeredClasses = new Map();
|
|
837
862
|
|
|
863
|
+
// GTypeName → registered JS class, the reverse of registeredClasses. The engine's
|
|
864
|
+
// NodeGiConstructor hands runCtorForCObject (below) the C-created instance's GType
|
|
865
|
+
// NAME (g_type_name), which this resolves back to the class to Reflect.construct.
|
|
866
|
+
const classesByGType = new Map();
|
|
867
|
+
|
|
868
|
+
// When set (by runCtorForCObject), the makeClass base ctor ADOPTS this pre-existing
|
|
869
|
+
// canonical handle instead of constructing a second GObject — so a C/GtkBuilder-
|
|
870
|
+
// created instance's user ctor body runs on the wrapper the engine already built.
|
|
871
|
+
// A module-scoped latch is safe: it is set immediately before a synchronous
|
|
872
|
+
// Reflect.construct and consumed by the base ctor before any nested construction.
|
|
873
|
+
let adoptHandle;
|
|
874
|
+
|
|
838
875
|
// Surface a templated type's bound children on a freshly-constructed instance
|
|
839
876
|
// (GJS convention: public `this.<name>`, internal `this._<name>`, '-' → '_'). The
|
|
840
877
|
// engine already ran init_template during constructType, so getTemplateChild
|
|
@@ -853,6 +890,34 @@ function assignTemplateChildren(instance, handle, reg) {
|
|
|
853
890
|
}
|
|
854
891
|
}
|
|
855
892
|
|
|
893
|
+
// Push construct-time GObject-property values through the class's own JS SETTERS.
|
|
894
|
+
//
|
|
895
|
+
// A class can declare a GObject property (`Properties: { displayWidth: ParamSpec }`,
|
|
896
|
+
// often CONSTRUCT with a default) AND a matching JS accessor
|
|
897
|
+
// (`get/set displayWidth` over a `_displayWidth` backing field). On GJS the property
|
|
898
|
+
// set vfunc delegates to the JS setter (`JS_SetProperty`), so the CONSTRUCT default
|
|
899
|
+
// reaches `_displayWidth` at construction. node-gi builds the wrapper AFTER
|
|
900
|
+
// g_object_new, so at construct time there is no USER_PROTO to route through — the
|
|
901
|
+
// value lands only in the engine's per-instance store, and the class's getter
|
|
902
|
+
// (reading `_displayWidth`) sees `undefined` (the Learn6502 Display "DrawingArea is
|
|
903
|
+
// required" wall). Once USER_PROTO is attached (here, right after construction), read
|
|
904
|
+
// each such property's construct value back out (from the store, or the ParamSpec
|
|
905
|
+
// default) and run it through the JS setter — the node-gi analogue of GJS applying a
|
|
906
|
+
// CONSTRUCT property via the JS setter, and BEFORE the user ctor body runs (same
|
|
907
|
+
// order as GJS). Only properties whose name resolves to a prototype SETTER are
|
|
908
|
+
// touched, so a plain GObject property (no JS accessor) keeps the store as its single
|
|
909
|
+
// backing store — unchanged.
|
|
910
|
+
function flushConstructProperties(instance, handle, reg) {
|
|
911
|
+
if (reg.constructPropertyNames === undefined) return;
|
|
912
|
+
const up = instance[USER_PROTO];
|
|
913
|
+
if (up === undefined) return;
|
|
914
|
+
for (const name of reg.constructPropertyNames) {
|
|
915
|
+
const desc = findProtoDescriptor(up, name);
|
|
916
|
+
if (desc === undefined || typeof desc.set !== 'function') continue;
|
|
917
|
+
desc.set.call(instance, wrapReturn(native.getProperty(handle, name)));
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
856
921
|
// ---- Gio.Application.runAsync (L1, GJS-shaped) ----
|
|
857
922
|
//
|
|
858
923
|
// The Node twin of GJS's `Gio.Application.prototype.runAsync`
|
|
@@ -1295,9 +1360,22 @@ function makeClass(namespace, typeName) {
|
|
|
1295
1360
|
if (nt !== undefined && nt !== proxy) {
|
|
1296
1361
|
const reg = registeredClasses.get(nt);
|
|
1297
1362
|
if (reg !== undefined) {
|
|
1298
|
-
|
|
1363
|
+
let handle;
|
|
1364
|
+
if (adoptHandle !== undefined) {
|
|
1365
|
+
// C-created adoption (runCtorForCObject): the engine already built the
|
|
1366
|
+
// GObject; use its canonical handle instead of constructing another.
|
|
1367
|
+
// Consume the latch atomically so a nested `new` in the ctor body takes
|
|
1368
|
+
// the normal constructType path.
|
|
1369
|
+
handle = adoptHandle;
|
|
1370
|
+
adoptHandle = undefined;
|
|
1371
|
+
} else {
|
|
1372
|
+
handle = native.constructType(reg.typeHandle, props ? unwrapProps(props) : {});
|
|
1373
|
+
}
|
|
1299
1374
|
const instance = wrapInstance(handle, nt.prototype);
|
|
1300
1375
|
assignTemplateChildren(instance, handle, reg);
|
|
1376
|
+
// Route construct-time property values through the class's JS setters now
|
|
1377
|
+
// that USER_PROTO is attached — before the user ctor body runs (GJS order).
|
|
1378
|
+
flushConstructProperties(instance, handle, reg);
|
|
1301
1379
|
return instance;
|
|
1302
1380
|
}
|
|
1303
1381
|
// `nt` is a subclass of this introspected GObject class but was NEVER passed to
|
|
@@ -1958,11 +2036,28 @@ function registerClass(metaOrClass, maybeClass) {
|
|
|
1958
2036
|
// Record the registration so the introspected base ctor (makeClass) routes
|
|
1959
2037
|
// `new X(args)` (where new.target === klass) to constructType(typeHandle, …) +
|
|
1960
2038
|
// the canonical wrapper. Template children move here from the old Subclass.
|
|
2039
|
+
// The names of CONSTRUCT / CONSTRUCT_ONLY properties — the ONLY ones GObject
|
|
2040
|
+
// applies at construction. The flush (flushConstructProperties) must be limited to
|
|
2041
|
+
// these: a plain READWRITE property is NOT set during construction, so running its
|
|
2042
|
+
// JS setter early (GObject never would) fires the setter's side effects against
|
|
2043
|
+
// still-uninitialised instance state (the SourceView `selectable` → `_signalHandlers`
|
|
2044
|
+
// forEach crash). Matches GJS, which only runs setters for props GObject applies at
|
|
2045
|
+
// construct. Flags are the ABI-stable G_PARAM_CONSTRUCT (1<<2) | CONSTRUCT_ONLY (1<<3).
|
|
2046
|
+
const PARAM_CONSTRUCT_MASK = 0x4 | 0x8;
|
|
2047
|
+
const constructPropertyNames = properties
|
|
2048
|
+
.filter((p) => typeof p.flags === 'number' && (p.flags & PARAM_CONSTRUCT_MASK) !== 0)
|
|
2049
|
+
.map((p) => p.name)
|
|
2050
|
+
.filter((n) => typeof n === 'string');
|
|
1961
2051
|
registeredClasses.set(klass, {
|
|
1962
2052
|
typeHandle,
|
|
1963
2053
|
children: children.length > 0 ? children : undefined,
|
|
1964
2054
|
internalChildren: internalChildren.length > 0 ? internalChildren : undefined,
|
|
2055
|
+
// CONSTRUCT-property names, for the construct-property flush (below).
|
|
2056
|
+
constructPropertyNames: constructPropertyNames.length > 0 ? constructPropertyNames : undefined,
|
|
1965
2057
|
});
|
|
2058
|
+
// Reverse index for runCtorForCObject: the engine identifies a C-created instance
|
|
2059
|
+
// by its GType name (= gtypeName, what native.registerClass registered).
|
|
2060
|
+
classesByGType.set(gtypeName, klass);
|
|
1966
2061
|
return klass;
|
|
1967
2062
|
}
|
|
1968
2063
|
|
package/globals.js
CHANGED
|
@@ -23,6 +23,7 @@ import cairo from './cairo.js';
|
|
|
23
23
|
// GLib.MainLoop convenience layer) — the GJS script modules many older sources use.
|
|
24
24
|
import * as signalsModule from './overrides/_signals.js';
|
|
25
25
|
import { createMainloop } from './overrides/mainloop.js';
|
|
26
|
+
import { createPackage } from './overrides/package.js';
|
|
26
27
|
|
|
27
28
|
// GJS stringifies each argument with String() and joins with a space (no
|
|
28
29
|
// util.inspect object formatting) — match that for fidelity.
|
|
@@ -109,6 +110,10 @@ function makeImports() {
|
|
|
109
110
|
gettext,
|
|
110
111
|
cairo,
|
|
111
112
|
signals: { addSignalMethods, _connect, _connectAfter, _disconnect, _emit, _signalHandlerIsConnected, _disconnectAll },
|
|
113
|
+
// GJS's `imports.searchPath` — `imports.package.init` unshifts the app's
|
|
114
|
+
// moduledir onto it. Inert on node-gi (ESM + gi://), but present + safe so
|
|
115
|
+
// source that reads/mutates it doesn't throw.
|
|
116
|
+
searchPath: [],
|
|
112
117
|
versions: Object.create(null),
|
|
113
118
|
};
|
|
114
119
|
let mainloop;
|
|
@@ -120,6 +125,17 @@ function makeImports() {
|
|
|
120
125
|
enumerable: true,
|
|
121
126
|
configurable: true,
|
|
122
127
|
});
|
|
128
|
+
// `imports.package` — the GJS application bootstrap (pkg.init/initGettext/
|
|
129
|
+
// initFormat). Lazy like `mainloop`: a bundle that never calls it pulls no GLib.
|
|
130
|
+
let pkg;
|
|
131
|
+
Object.defineProperty(imports, 'package', {
|
|
132
|
+
get() {
|
|
133
|
+
if (pkg === undefined) pkg = createPackage({ requireGi, gettext, imports });
|
|
134
|
+
return pkg;
|
|
135
|
+
},
|
|
136
|
+
enumerable: true,
|
|
137
|
+
configurable: true,
|
|
138
|
+
});
|
|
123
139
|
return imports;
|
|
124
140
|
}
|
|
125
141
|
|
package/gtk-runtime.js
CHANGED
|
@@ -158,7 +158,12 @@ export function maybePrependGtkRuntimeDllPath() {
|
|
|
158
158
|
* @returns {void}
|
|
159
159
|
*/
|
|
160
160
|
export function maybeWireGtkWindowingEnv() {
|
|
161
|
-
|
|
161
|
+
// win32 AND darwin ship a batteries-included --windowing bundle whose runtime
|
|
162
|
+
// DATA (gschemas / icons / loaders / gtksource / fonts) is located by these env
|
|
163
|
+
// vars — GLib/GTK read them at init, not at launch (unlike dyld's DYLD_FALLBACK),
|
|
164
|
+
// so setting them in-process works on both. The only platform difference is the
|
|
165
|
+
// XDG_DATA_DIRS separator (';' on win32, ':' on unix/darwin).
|
|
166
|
+
if (process.platform !== 'win32' && process.platform !== 'darwin') return;
|
|
162
167
|
const bundle = resolveGtkRuntimeBundle();
|
|
163
168
|
if (!bundle) return; // strict no-op when no bundle is present
|
|
164
169
|
|
|
@@ -166,6 +171,7 @@ export function maybeWireGtkWindowingEnv() {
|
|
|
166
171
|
// gschemas.compiled = the windowing-data marker; absent → display-free bundle.
|
|
167
172
|
if (!existsSync(join(schemaDir, 'gschemas.compiled'))) return;
|
|
168
173
|
|
|
174
|
+
const pathSep = process.platform === 'win32' ? ';' : ':';
|
|
169
175
|
const setIfUnset = (name, value) => {
|
|
170
176
|
if (!process.env[name]) process.env[name] = value;
|
|
171
177
|
};
|
|
@@ -177,11 +183,13 @@ export function maybeWireGtkWindowingEnv() {
|
|
|
177
183
|
setIfUnset('GDK_PIXBUF_MODULE_FILE', loaderCache);
|
|
178
184
|
}
|
|
179
185
|
|
|
186
|
+
// XDG_DATA_DIRS → <bundle>/share so the icon themes (share/icons) AND GtkSource's
|
|
187
|
+
// language-specs/styles (share/gtksourceview-5) resolve.
|
|
180
188
|
const shareDir = join(bundle.dir, 'share');
|
|
181
|
-
if (existsSync(
|
|
189
|
+
if (existsSync(shareDir)) {
|
|
182
190
|
const cur = process.env.XDG_DATA_DIRS ?? '';
|
|
183
|
-
if (!cur.split(
|
|
184
|
-
process.env.XDG_DATA_DIRS = cur ? `${shareDir}
|
|
191
|
+
if (!cur.split(pathSep).filter(Boolean).includes(shareDir)) {
|
|
192
|
+
process.env.XDG_DATA_DIRS = cur ? `${shareDir}${pathSep}${cur}` : shareDir;
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
195
|
|
package/index.js
CHANGED
|
@@ -663,6 +663,16 @@ export const disconnectSignal = native.disconnectSignal;
|
|
|
663
663
|
*/
|
|
664
664
|
export const setTemplateCallbackResolver = native.setTemplateCallbackResolver;
|
|
665
665
|
|
|
666
|
+
/**
|
|
667
|
+
* Register the L1 callback the engine's overridden `constructor` vfunc invokes to
|
|
668
|
+
* run a registered class's JS constructor for a GObject it instantiated from C (a
|
|
669
|
+
* GtkBuilder composite-template InternalChild). Given (instanceHandle, gtypeName)
|
|
670
|
+
* it Reflect.constructs the class in adopt mode (see gi.js runCtorForCObject).
|
|
671
|
+
* @param {(handle: unknown, gtypeName: string) => void} cb
|
|
672
|
+
* @returns {void}
|
|
673
|
+
*/
|
|
674
|
+
export const setConstructCallback = native.setConstructCallback;
|
|
675
|
+
|
|
666
676
|
/**
|
|
667
677
|
* Install (or clear, with `null`) the structured-log writer function — the
|
|
668
678
|
* engine's twin of GjsPrivate.log_set_writer_func backing GLib.log_set_writer_func.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
|
|
2
|
+
// SPDX-FileCopyrightText: 2013 Giovanni Campagna <scampa.giovanni@gmail.com>
|
|
3
|
+
//
|
|
4
|
+
// Adapted from GJS (refs/gjs/modules/script/package.js). Copyright (c) 2013
|
|
5
|
+
// Giovanni Campagna. MIT OR LGPL-2.0-or-later.
|
|
6
|
+
// Modifications: the legacy `imports.package` — the GJS application bootstrap
|
|
7
|
+
// (`pkg.init` / `initGettext` / `initFormat`) — ported to @gjsify/node-gi. A GNOME
|
|
8
|
+
// app's entry point does `imports.package.init({ name, version, prefix, libdir })`
|
|
9
|
+
// then `pkg.initGettext()` / `pkg.initFormat()`. The GJS original's heavy
|
|
10
|
+
// source-tree detection + GResource registration + module-search machinery is NOT
|
|
11
|
+
// load-bearing for a node-gi consumer (the app carries its own dir constants and
|
|
12
|
+
// registers its own resources — see easy6502 app-gnome resources.ts), so this is
|
|
13
|
+
// the minimal, honest init: set `globalThis.pkg`, wire `_`/`C_`/`N_` and
|
|
14
|
+
// `String.prototype.format`, and keep `imports.searchPath` / `GLib.set_prgname`
|
|
15
|
+
// calls safe. Mirrors overrides/mainloop.js's `createX(deps)` shape.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the `imports.package` object bound to the L1 backend.
|
|
19
|
+
* @param {{ requireGi: Function, gettext: any, imports: Record<string, any> }} deps
|
|
20
|
+
*/
|
|
21
|
+
export function createPackage({ requireGi, gettext, imports }) {
|
|
22
|
+
const pkg = {
|
|
23
|
+
// Public fields GJS source may read. Populated by init().
|
|
24
|
+
name: undefined,
|
|
25
|
+
version: undefined,
|
|
26
|
+
prefix: undefined,
|
|
27
|
+
libdir: undefined,
|
|
28
|
+
datadir: undefined,
|
|
29
|
+
pkgdatadir: undefined,
|
|
30
|
+
pkglibdir: undefined,
|
|
31
|
+
moduledir: undefined,
|
|
32
|
+
localedir: undefined,
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {{ name?: string, version?: string, prefix?: string, libdir?: string, datadir?: string }} params
|
|
36
|
+
*/
|
|
37
|
+
init(params = {}) {
|
|
38
|
+
// The accessor GJS apps use right after: `pkg.initGettext()`.
|
|
39
|
+
globalThis.pkg = pkg;
|
|
40
|
+
|
|
41
|
+
pkg.name = params.name;
|
|
42
|
+
pkg.version = params.version;
|
|
43
|
+
pkg.prefix = params.prefix;
|
|
44
|
+
pkg.libdir = params.libdir;
|
|
45
|
+
pkg.datadir = params.datadir ?? (params.prefix ? `${params.prefix}/share` : undefined);
|
|
46
|
+
|
|
47
|
+
// Installed-layout dirs — NOT load-bearing on node-gi (apps carry their own
|
|
48
|
+
// constants + register their own resources), computed for source-compat only.
|
|
49
|
+
if (pkg.name) {
|
|
50
|
+
if (pkg.datadir) {
|
|
51
|
+
pkg.pkgdatadir = `${pkg.datadir}/${pkg.name}`;
|
|
52
|
+
pkg.moduledir = pkg.pkgdatadir;
|
|
53
|
+
pkg.localedir = `${pkg.datadir}/locale`;
|
|
54
|
+
}
|
|
55
|
+
if (pkg.libdir) pkg.pkglibdir = `${pkg.libdir}/${pkg.name}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Cheap + safe: keeps ARGV[0]/app-id semantics (window-manager grouping etc.).
|
|
59
|
+
try {
|
|
60
|
+
const GLib = requireGi('GLib', '2.0');
|
|
61
|
+
if (pkg.name && typeof GLib.set_prgname === 'function') GLib.set_prgname(pkg.name);
|
|
62
|
+
} catch {
|
|
63
|
+
/* GLib not loadable in this context — skip */
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// GJS unshifts moduledir onto imports.searchPath (script-import machinery).
|
|
67
|
+
// node-gi apps use ESM + gi://, so the search path is inert here — but keep
|
|
68
|
+
// the array present + the unshift safe so source that reads it doesn't throw.
|
|
69
|
+
if (!Array.isArray(imports.searchPath)) imports.searchPath = [];
|
|
70
|
+
if (pkg.moduledir) imports.searchPath.unshift(pkg.moduledir);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
initGettext() {
|
|
74
|
+
if (pkg.name) {
|
|
75
|
+
try {
|
|
76
|
+
gettext.bindtextdomain?.(pkg.name, pkg.localedir ?? null);
|
|
77
|
+
gettext.textdomain?.(pkg.name);
|
|
78
|
+
} catch {
|
|
79
|
+
/* passthrough gettext — no catalog, ignore */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// The globals GJS apps + compiled Blueprint use pervasively.
|
|
83
|
+
globalThis._ = (msgid) => gettext.gettext(msgid);
|
|
84
|
+
globalThis.C_ = (context, msgid) => gettext.pgettext(context, msgid);
|
|
85
|
+
globalThis.N_ = (msgid) => msgid;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
initFormat() {
|
|
89
|
+
// GJS sets `String.prototype.format = imports.format.format` (backed by the
|
|
90
|
+
// native `_format` vprintf). node-gi has no `imports.format`, so supply an
|
|
91
|
+
// equivalent JS vprintf here (see makeVprintf).
|
|
92
|
+
const vprintf = makeVprintf();
|
|
93
|
+
Object.defineProperty(String.prototype, 'format', {
|
|
94
|
+
configurable: true,
|
|
95
|
+
writable: true,
|
|
96
|
+
enumerable: false,
|
|
97
|
+
value: function format(...args) {
|
|
98
|
+
return vprintf(this, args);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Script-import machinery is not ported — node-gi apps use ESM + gi://. These
|
|
104
|
+
// keep the surface present for source that references them (Learn6502 does not).
|
|
105
|
+
require() {
|
|
106
|
+
/* no-op: apps import via ESM / gi:// on node-gi */
|
|
107
|
+
},
|
|
108
|
+
requireSymbol() {
|
|
109
|
+
return true;
|
|
110
|
+
},
|
|
111
|
+
checkSymbol() {
|
|
112
|
+
return true;
|
|
113
|
+
},
|
|
114
|
+
start(params = {}) {
|
|
115
|
+
pkg.init(params);
|
|
116
|
+
if (typeof params.main === 'function') params.main(globalThis.ARGV ? ['', ...globalThis.ARGV] : []);
|
|
117
|
+
},
|
|
118
|
+
run() {
|
|
119
|
+
/* the app drives its own main() on node-gi */
|
|
120
|
+
},
|
|
121
|
+
initSubmodule() {
|
|
122
|
+
/* n/a on node-gi */
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return pkg;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// A small vprintf covering the GJS `String.prototype.format` spec
|
|
130
|
+
// (refs/gjs/modules/script/format.js): `%s %d %x %f`, `%.Nf` precision, `%Ns`/`%Nd`
|
|
131
|
+
// width, `0`-padding for numerics, and `%%` → literal `%`. Positional `%N$s` is
|
|
132
|
+
// supported (GJS honours it). Learn6502 only uses `%s`; the rest is fidelity.
|
|
133
|
+
function makeVprintf() {
|
|
134
|
+
return function vprintf(fmt, args) {
|
|
135
|
+
let auto = 0;
|
|
136
|
+
return String(fmt).replace(
|
|
137
|
+
/%(?:(\d+)\$)?(0)?(\d+)?(?:\.(\d+))?([%sdxf])/g,
|
|
138
|
+
(match, pos, zero, width, prec, spec) => {
|
|
139
|
+
if (spec === '%') return '%';
|
|
140
|
+
const arg = pos !== undefined ? args[Number(pos) - 1] : args[auto++];
|
|
141
|
+
let str;
|
|
142
|
+
switch (spec) {
|
|
143
|
+
case 's':
|
|
144
|
+
str = String(arg);
|
|
145
|
+
break;
|
|
146
|
+
case 'd':
|
|
147
|
+
str = String(Math.trunc(Number(arg)));
|
|
148
|
+
break;
|
|
149
|
+
case 'x':
|
|
150
|
+
str = Math.trunc(Number(arg)).toString(16);
|
|
151
|
+
break;
|
|
152
|
+
case 'f':
|
|
153
|
+
str = prec !== undefined ? Number(arg).toFixed(Number(prec)) : String(Number(arg));
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
return match;
|
|
157
|
+
}
|
|
158
|
+
if (width !== undefined) {
|
|
159
|
+
const w = Number(width);
|
|
160
|
+
const pad = zero && spec !== 's' ? '0' : ' ';
|
|
161
|
+
while (str.length < w) str = pad + str;
|
|
162
|
+
}
|
|
163
|
+
return str;
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/node-gi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "GObject-Introspection runtime for Node.js — load gi:// namespaces (GLib/GObject/Gio/…) with GJS-compatible semantics. Vendored + rewritten from node-gtk under MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
Binary file
|
|
Binary file
|
package/src/addon.cc
CHANGED
|
@@ -120,6 +120,7 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
120
120
|
exports.Set("disconnectSignal", Napi::Function::New(env, DisconnectSignal));
|
|
121
121
|
exports.Set("setTemplateCallbackResolver",
|
|
122
122
|
Napi::Function::New(env, SetTemplateCallbackResolver));
|
|
123
|
+
exports.Set("setConstructCallback", Napi::Function::New(env, SetConstructCallback));
|
|
123
124
|
// GjsPrivate-mirroring helpers (private.cc): the structured-log writer func +
|
|
124
125
|
// the bind_property_full / BindingGroup.bind_full transform trampolines.
|
|
125
126
|
exports.Set("logSetWriterFunc", Napi::Function::New(env, LogSetWriterFunc));
|
package/src/class.cc
CHANGED
|
@@ -383,11 +383,121 @@ static void NodeGiSetProperty(GObject* obj, guint prop_id, const GValue* value,
|
|
|
383
383
|
if (cd != nullptr && cd->parentSet != nullptr) cd->parentSet(obj, prop_id, value, pspec);
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
// ---- run the JS constructor for C/GtkBuilder-instantiated instances ----
|
|
387
|
+
//
|
|
388
|
+
// node-gi runs a registered class's JS constructor ONLY on a JS-side `new Sub()`
|
|
389
|
+
// (the makeClass new.target path in gi.js routes it to constructType, and ES
|
|
390
|
+
// super-substitution runs the ctor body). A GObject built from C — a GtkBuilder
|
|
391
|
+
// composite-template InternalChild is the common case — never reaches that path,
|
|
392
|
+
// so its ctor body (e.g. `this._x = …`) never runs. GJS solves this by overriding
|
|
393
|
+
// the GObjectClass `constructor` vfunc so BOTH paths funnel through it
|
|
394
|
+
// (refs/gjs/gi/gobject.cpp gjs_object_constructor). We mirror that: NodeGiConstructor
|
|
395
|
+
// chains to the first non-node-gi C constructor to create the instance, then — only
|
|
396
|
+
// for the C-driven path — runs the JS ctor on the canonical wrapper via the L1
|
|
397
|
+
// construct callback. The JS-`new` path is unchanged (the latch NodeGiJsConstructing
|
|
398
|
+
// is raised by ConstructGObject, so we return the instance and let makeClass run it).
|
|
399
|
+
|
|
400
|
+
// Thread-local so a worker_threads env has its own latch (GtkBuilder construction
|
|
401
|
+
// runs on the same thread as the ConstructGObject that raised it).
|
|
402
|
+
static thread_local bool g_nodegiJsConstructing = false;
|
|
403
|
+
bool NodeGiJsConstructing() { return g_nodegiJsConstructing; }
|
|
404
|
+
void NodeGiSetJsConstructing(bool v) { g_nodegiJsConstructing = v; }
|
|
405
|
+
|
|
406
|
+
static void RunJsConstructorForCObject(napi_env env, GObject* obj) {
|
|
407
|
+
// Teardown/terminate: never enter JS during GC/context teardown (GJS-faithful;
|
|
408
|
+
// same gate as the vfunc trampoline). The C object is still fully constructed.
|
|
409
|
+
if (!NodeGiJsAvailable(env)) return;
|
|
410
|
+
Napi::Env napiEnv(env);
|
|
411
|
+
Napi::HandleScope scope(napiEnv);
|
|
412
|
+
// JS dispatched from a pump-driven iteration must not inherit the in-iteration
|
|
413
|
+
// flag (mirrors the vfunc trampoline).
|
|
414
|
+
NodeGiPumpJsDispatchScope pumpWindow;
|
|
415
|
+
|
|
416
|
+
// Build this instance's OWN composite template first (a no-op unless it carries
|
|
417
|
+
// one), so the JS ctor's assignTemplateChildren sees bound children — the JS-`new`
|
|
418
|
+
// path does this in ConstructGObject before makeClass reads them.
|
|
419
|
+
MaybeInitTemplate(napiEnv, obj);
|
|
420
|
+
|
|
421
|
+
NodeGiEnvData* d = EnvData(env);
|
|
422
|
+
napi_value cb = nullptr;
|
|
423
|
+
if (d == nullptr || d->constructCallback == nullptr ||
|
|
424
|
+
napi_get_reference_value(env, d->constructCallback, &cb) != napi_ok || cb == nullptr) {
|
|
425
|
+
return; // L1 hasn't registered the callback (nothing to run)
|
|
426
|
+
}
|
|
427
|
+
napi_value handleVal = WrapGObject(napiEnv, obj, GI_TRANSFER_NOTHING);
|
|
428
|
+
napi_value nameVal = nullptr;
|
|
429
|
+
napi_create_string_utf8(env, g_type_name(G_OBJECT_TYPE(obj)), NAPI_AUTO_LENGTH, &nameVal);
|
|
430
|
+
napi_value args[2] = {handleVal, nameVal};
|
|
431
|
+
napi_value undef = nullptr;
|
|
432
|
+
napi_get_undefined(env, &undef);
|
|
433
|
+
napi_value ret = nullptr;
|
|
434
|
+
// A plain call (not napi_make_callback): the ctor is synchronous and we must not
|
|
435
|
+
// drain the microtask queue mid-g_object_new. Matches GJS's template-callback
|
|
436
|
+
// resolver + gjs_object_constructor's JS::Construct (no checkpoint).
|
|
437
|
+
napi_status st = napi_call_function(env, undef, cb, 2, args, &ret);
|
|
438
|
+
if (st != napi_ok) {
|
|
439
|
+
// The ctor threw. Never leave a pending JS exception across the return into C
|
|
440
|
+
// (GObject/GtkBuilder can't handle it) — clear it and fold the message into a
|
|
441
|
+
// g_warning (GJS logs the uncaught exception here too).
|
|
442
|
+
napi_value ex = nullptr;
|
|
443
|
+
if (napi_get_and_clear_last_exception(env, &ex) == napi_ok && ex != nullptr) {
|
|
444
|
+
napi_value msg = nullptr;
|
|
445
|
+
std::string detail;
|
|
446
|
+
if (napi_get_named_property(env, ex, "message", &msg) == napi_ok) {
|
|
447
|
+
size_t len = 0;
|
|
448
|
+
if (napi_get_value_string_utf8(env, msg, nullptr, 0, &len) == napi_ok) {
|
|
449
|
+
detail.resize(len);
|
|
450
|
+
napi_get_value_string_utf8(env, msg, detail.data(), len + 1, &len);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
g_warning("node-gi: JS constructor for %s threw: %s", g_type_name(G_OBJECT_TYPE(obj)),
|
|
454
|
+
detail.empty() ? "(no message)" : detail.c_str());
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// The overridden GObjectClass `constructor` vfunc (installed on every registered
|
|
460
|
+
// type in NodeGiClassInit). GJS parity: gi/gobject.cpp gjs_object_constructor.
|
|
461
|
+
static GObject* NodeGiConstructor(GType type, guint n_construct_properties,
|
|
462
|
+
GObjectConstructParam* construct_properties) {
|
|
463
|
+
// Chain to the first non-node-gi (C) constructor UP the hierarchy, skipping our
|
|
464
|
+
// own — that actually allocates + constructs the instance (running the real C
|
|
465
|
+
// construction chain). GJS does the identical skip-walk (gobject.cpp:155).
|
|
466
|
+
GType parent = g_type_parent(type);
|
|
467
|
+
while (parent != 0) {
|
|
468
|
+
gpointer pk = g_type_class_peek(parent);
|
|
469
|
+
if (pk == nullptr || G_OBJECT_CLASS(pk)->constructor != NodeGiConstructor) break;
|
|
470
|
+
parent = g_type_parent(parent);
|
|
471
|
+
}
|
|
472
|
+
gpointer pk = parent != 0 ? g_type_class_peek(parent) : nullptr;
|
|
473
|
+
GObject* obj = (pk != nullptr && G_OBJECT_CLASS(pk)->constructor != nullptr)
|
|
474
|
+
? G_OBJECT_CLASS(pk)->constructor(type, n_construct_properties,
|
|
475
|
+
construct_properties)
|
|
476
|
+
: nullptr;
|
|
477
|
+
if (obj == nullptr) return nullptr;
|
|
478
|
+
|
|
479
|
+
// JS-`new` path: the makeClass super-substitution runs the user ctor after
|
|
480
|
+
// g_object_new returns — do NOT run it here (would double-run).
|
|
481
|
+
if (NodeGiJsConstructing()) return obj;
|
|
482
|
+
|
|
483
|
+
// C/GtkBuilder-driven: the user ctor has no other driver. Run it on the leaf
|
|
484
|
+
// registered class's env (correct across worker envs).
|
|
485
|
+
NodeGiClassData* cd = FindClassData(G_OBJECT_TYPE(obj));
|
|
486
|
+
if (cd != nullptr && cd->env != nullptr) RunJsConstructorForCObject(cd->env, obj);
|
|
487
|
+
return obj;
|
|
488
|
+
}
|
|
489
|
+
|
|
386
490
|
static void NodeGiClassInit(gpointer g_class, gpointer class_data) {
|
|
387
491
|
NodeGiClassData* cd = static_cast<NodeGiClassData*>(class_data);
|
|
388
492
|
GObjectClass* oc = G_OBJECT_CLASS(g_class);
|
|
389
493
|
g_type_set_qdata(G_TYPE_FROM_CLASS(g_class), NodeGiClassDataQuark(), cd);
|
|
390
494
|
|
|
495
|
+
// Route construction through NodeGiConstructor so a C/GtkBuilder-instantiated
|
|
496
|
+
// instance (a composite-template InternalChild) gets its JS constructor run —
|
|
497
|
+
// GJS parity (gi/gobject.cpp). The JS-`new` path is unaffected: NodeGiConstructor
|
|
498
|
+
// returns early while the NodeGiJsConstructing() latch is raised.
|
|
499
|
+
oc->constructor = NodeGiConstructor;
|
|
500
|
+
|
|
391
501
|
if (!cd->properties.empty()) {
|
|
392
502
|
cd->parentGet = oc->get_property; // capture before override (chain target)
|
|
393
503
|
cd->parentSet = oc->set_property;
|
|
@@ -941,6 +1051,9 @@ static Napi::Value RegisterClassImpl(Napi::Env env, const std::string& name, GTy
|
|
|
941
1051
|
|
|
942
1052
|
// Parse the optional { properties, signals } and build the class metadata.
|
|
943
1053
|
NodeGiClassData* cd = new NodeGiClassData();
|
|
1054
|
+
// Stamp the registering env so NodeGiConstructor (no user_data slot) can call
|
|
1055
|
+
// into JS from this class's cd for a C/GtkBuilder-instantiated instance.
|
|
1056
|
+
cd->env = env;
|
|
944
1057
|
cd->parentGet = nullptr;
|
|
945
1058
|
cd->parentSet = nullptr;
|
|
946
1059
|
if (optsValue.IsObject()) {
|
|
@@ -1167,4 +1280,24 @@ Napi::Value ConstructType(const Napi::CallbackInfo& info) {
|
|
|
1167
1280
|
return ConstructGObject(env, gtype, props, std::string(g_type_name(gtype)));
|
|
1168
1281
|
}
|
|
1169
1282
|
|
|
1283
|
+
// setConstructCallback(cb) -> void. L1 registers the callback NodeGiConstructor
|
|
1284
|
+
// invokes to run a registered class's JS constructor for a C/GtkBuilder-created
|
|
1285
|
+
// instance: (instanceHandle, gtypeName) → Reflect.construct(class) in adopt mode
|
|
1286
|
+
// (see gi.js runCtorForCObject). Mirrors setTemplateCallbackResolver.
|
|
1287
|
+
Napi::Value SetConstructCallback(const Napi::CallbackInfo& info) {
|
|
1288
|
+
Napi::Env env = info.Env();
|
|
1289
|
+
if (info.Length() < 1 || !info[0].IsFunction()) {
|
|
1290
|
+
Napi::TypeError::New(env, "setConstructCallback(cb: function)").ThrowAsJavaScriptException();
|
|
1291
|
+
return env.Undefined();
|
|
1292
|
+
}
|
|
1293
|
+
NodeGiEnvData* d = EnvData(env);
|
|
1294
|
+
if (d == nullptr) return env.Undefined();
|
|
1295
|
+
if (d->constructCallback != nullptr) {
|
|
1296
|
+
napi_delete_reference(env, d->constructCallback);
|
|
1297
|
+
d->constructCallback = nullptr;
|
|
1298
|
+
}
|
|
1299
|
+
napi_create_reference(env, info[0], 1, &d->constructCallback);
|
|
1300
|
+
return env.Undefined();
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1170
1303
|
} // namespace nodegi
|
package/src/common.h
CHANGED
|
@@ -82,6 +82,13 @@ struct NodeGiEnvData {
|
|
|
82
82
|
// (napi_make_callback already runs the checkpoint there). Per-env for the same
|
|
83
83
|
// reason as errorBuilder (a napi_ref is env-specific).
|
|
84
84
|
napi_ref microtaskDrain = nullptr;
|
|
85
|
+
// L1 callback that runs a registered class's JS constructor body on a
|
|
86
|
+
// pre-existing canonical wrapper — invoked by NodeGiConstructor for a GObject
|
|
87
|
+
// instantiated from C (a GtkBuilder composite-template InternalChild), whose JS
|
|
88
|
+
// ctor the node-gi JS-`new` path never reaches. (handle, gtypeName) →
|
|
89
|
+
// Reflect.construct(class) in adopt mode (see gi.js runCtorForCObject). Per-env
|
|
90
|
+
// for the same reason as errorBuilder (a napi_ref is env-specific).
|
|
91
|
+
napi_ref constructCallback = nullptr;
|
|
85
92
|
};
|
|
86
93
|
|
|
87
94
|
void NodeGiEnvDataFinalize(napi_env env, void* data, void* hint);
|
|
@@ -457,6 +464,11 @@ struct NodeGiClassData {
|
|
|
457
464
|
// for the class lifetime (process-permanent, like cd itself); its JS env qdata is
|
|
458
465
|
// refreshed per construction so create_closure resolves handlers in the live env.
|
|
459
466
|
GObject* templateScope = nullptr;
|
|
467
|
+
// The napi_env this class was registered on. NodeGiConstructor (the overridden
|
|
468
|
+
// GObjectClass `constructor` vfunc) has no user_data slot, so it reads the env to
|
|
469
|
+
// call into JS from cd — correct even across worker envs (each registers its own
|
|
470
|
+
// class, so the leaf's cd carries the right env).
|
|
471
|
+
napi_env env = nullptr;
|
|
460
472
|
|
|
461
473
|
// The cold `delete cd` failure paths (a bad GParamSpec, or g_type_register_static
|
|
462
474
|
// returning 0) must not leak the owned inline-template GBytes. A destructor frees
|
|
@@ -488,6 +500,14 @@ void MaybeInitTemplate(Napi::Env env, GObject* obj);
|
|
|
488
500
|
// its env, both above that point.
|
|
489
501
|
void NodeGiInstallTemplateScopeOnClass(NodeGiClassData* cd, gpointer g_class);
|
|
490
502
|
|
|
503
|
+
// A thread-local latch the node-gi JS-`new` path (ConstructGObject) raises around
|
|
504
|
+
// g_object_new_with_properties. NodeGiConstructor (the overridden GObjectClass
|
|
505
|
+
// `constructor` vfunc) reads it to tell a JS-driven construction — where the
|
|
506
|
+
// makeClass super-substitution runs the user ctor — apart from a C/GtkBuilder-driven
|
|
507
|
+
// one, where NodeGiConstructor must run the user ctor itself (see class.cc / gi.js).
|
|
508
|
+
bool NodeGiJsConstructing();
|
|
509
|
+
void NodeGiSetJsConstructing(bool v);
|
|
510
|
+
|
|
491
511
|
// ---- N-API entry points (registered in addon.cc's Init) ----
|
|
492
512
|
|
|
493
513
|
// repo.cc
|
|
@@ -546,6 +566,7 @@ Napi::Value RegisterClass(const Napi::CallbackInfo& info);
|
|
|
546
566
|
Napi::Value RegisterClassFromGType(const Napi::CallbackInfo& info);
|
|
547
567
|
Napi::Value ConstructType(const Napi::CallbackInfo& info);
|
|
548
568
|
Napi::Value CallParentVfunc(const Napi::CallbackInfo& info);
|
|
569
|
+
Napi::Value SetConstructCallback(const Napi::CallbackInfo& info);
|
|
549
570
|
|
|
550
571
|
// template.cc
|
|
551
572
|
Napi::Value GetTemplateChild(const Napi::CallbackInfo& info);
|
package/src/object.cc
CHANGED
|
@@ -595,7 +595,15 @@ Napi::Value ConstructGObject(Napi::Env env, GType gtype, Napi::Object props,
|
|
|
595
595
|
|
|
596
596
|
GObject* obj = nullptr;
|
|
597
597
|
if (ok) {
|
|
598
|
+
// Raise the JS-`new` latch across construction: this is the node-gi JS-driven
|
|
599
|
+
// path, where the makeClass super-substitution runs the user ctor AFTER this
|
|
600
|
+
// returns. NodeGiConstructor reads the latch and does NOT re-run the ctor (a
|
|
601
|
+
// C/GtkBuilder-driven g_object_new leaves it clear, so NodeGiConstructor runs
|
|
602
|
+
// the ctor there). Save/restore keeps a reentrant construct correct.
|
|
603
|
+
bool prevConstructing = NodeGiJsConstructing();
|
|
604
|
+
NodeGiSetJsConstructing(true);
|
|
598
605
|
obj = g_object_new_with_properties(gtype, n, cnames.data(), values.data());
|
|
606
|
+
NodeGiSetJsConstructing(prevConstructing);
|
|
599
607
|
}
|
|
600
608
|
for (guint j = 0; j < initialised; j++) g_value_unset(&values[j]);
|
|
601
609
|
g_type_class_unref(klass);
|
package/src/repo.cc
CHANGED
|
@@ -17,6 +17,7 @@ void NodeGiEnvDataFinalize(napi_env env, void* data, void* /*hint*/) {
|
|
|
17
17
|
napi_delete_reference(env, d->templateCallbackResolver);
|
|
18
18
|
if (d->cairoWrappers != nullptr) napi_delete_reference(env, d->cairoWrappers);
|
|
19
19
|
if (d->microtaskDrain != nullptr) napi_delete_reference(env, d->microtaskDrain);
|
|
20
|
+
if (d->constructCallback != nullptr) napi_delete_reference(env, d->constructCallback);
|
|
20
21
|
delete d;
|
|
21
22
|
}
|
|
22
23
|
|