@gjsify/node-gi 0.59.0 → 0.60.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/binding.gyp +2 -0
- package/gettext.js +2 -2
- package/gi.d.ts +10 -0
- package/gi.js +129 -16
- package/host.nativescript.js +242 -0
- package/host.node.js +224 -0
- package/index.js +6 -190
- package/nativescript.webpack.js +16 -0
- package/overrides/byte-array.js +24 -2
- package/package.json +12 -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/addon.cc +7 -0
- package/src/android-gdk.cc +291 -0
- package/src/android-log.cc +187 -0
- package/src/common.h +23 -0
- package/src/loop.cc +478 -141
- package/src/marshal.cc +34 -0
- package/src/signals.cc +14 -1
package/binding.gyp
CHANGED
package/gettext.js
CHANGED
|
@@ -54,7 +54,7 @@ let announcedStaleAddon = false;
|
|
|
54
54
|
function noLocaleBinders(fn) {
|
|
55
55
|
if (!announcedStaleAddon) {
|
|
56
56
|
announcedStaleAddon = true;
|
|
57
|
-
process
|
|
57
|
+
globalThis.process?.emitWarning?.(
|
|
58
58
|
`@gjsify/node-gi: the loaded native addon predates the gettext binders, so ` +
|
|
59
59
|
`${fn}() cannot take effect and messages stay untranslated. Rebuild the addon ` +
|
|
60
60
|
`(npm run rebuild) or update @gjsify/node-gi.`,
|
|
@@ -190,7 +190,7 @@ export function bindtextdomainCodeset(_domainName, _codeset) {
|
|
|
190
190
|
*/
|
|
191
191
|
export const LocaleCategory = haveLocaleBinders
|
|
192
192
|
? native.localeCategories()
|
|
193
|
-
: (FALLBACK_CATEGORIES[process
|
|
193
|
+
: (FALLBACK_CATEGORIES[globalThis.process?.platform] ?? FALLBACK_CATEGORIES.linux);
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
196
|
* The GJS `Gettext` module as a default export — the object shape
|
package/gi.d.ts
CHANGED
|
@@ -138,6 +138,16 @@ export interface GObjectNamespace extends GiNamespace {
|
|
|
138
138
|
*/
|
|
139
139
|
registerClass(klass: Function): Function;
|
|
140
140
|
registerClass(meta: RegisterClassMeta, klass: Function): Function;
|
|
141
|
+
/**
|
|
142
|
+
* Static-field key for {@link RegisterClassMeta.GTypeName} — gjs's meta FIELD form,
|
|
143
|
+
* `Klass[GObject.GTypeName] = 'X'` instead of a meta object. A meta object passed to
|
|
144
|
+
* `registerClass` still wins over the field.
|
|
145
|
+
*/
|
|
146
|
+
GTypeName: symbol;
|
|
147
|
+
/** Static-field key for {@link RegisterClassMeta.Properties} (the field form of the meta). */
|
|
148
|
+
properties: symbol;
|
|
149
|
+
/** Static-field key for {@link RegisterClassMeta.Signals} (the field form of the meta). */
|
|
150
|
+
signals: symbol;
|
|
141
151
|
ParamSpec: ParamSpecFactories;
|
|
142
152
|
/**
|
|
143
153
|
* The FULL introspected `GParamFlags` bitfield (READABLE/WRITABLE/READWRITE/
|
package/gi.js
CHANGED
|
@@ -7,20 +7,11 @@
|
|
|
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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
// dispatch GLib sources would itself be a GLib source nobody dispatches.
|
|
15
|
-
import { setImmediate, setInterval, clearInterval } from 'node:timers';
|
|
16
|
-
// Same reasoning for `process`: `globalThis.process` may be the `@gjsify/process`
|
|
17
|
-
// polyfill, whose EventEmitter never emits the runtime's 'beforeExit', so the
|
|
18
|
-
// loop-liveness hooks below would silently never fire.
|
|
19
|
-
import runtimeProcess from 'node:process';
|
|
10
|
+
// Timers, `process` and the windowing env record come from the host seam (host.node.js
|
|
11
|
+
// explains why they are the runtime's own, not the globals; host.nativescript.js is the
|
|
12
|
+
// NativeScript Android variant).
|
|
13
|
+
import { setImmediate, setInterval, clearInterval, runtimeProcess, windowingEnvWrites } from '#host';
|
|
20
14
|
import { createGioDBus } from './overrides/gio-dbus.js';
|
|
21
|
-
// The windowing loader's record of what it wrote into process.env. Read here and not there
|
|
22
|
-
// because replaying it needs a GLib (mirrorWindowingEnvIntoCrt below).
|
|
23
|
-
import { windowingEnvWrites } from './gtk-runtime.js';
|
|
24
15
|
|
|
25
16
|
// The raw native GObject handle on a wrapped instance, so it can be unwrapped again
|
|
26
17
|
// when passed back into the engine as a GI argument.
|
|
@@ -818,6 +809,12 @@ function handlerIdsForFunc(handle, fn) {
|
|
|
818
809
|
// routing keys on the leaf so construction is correct at any depth.
|
|
819
810
|
const registeredClasses = new Map();
|
|
820
811
|
|
|
812
|
+
// A registered class's prototype → the kebab names of the CUSTOM GObject properties
|
|
813
|
+
// it declared, plus every registered ancestor's. Keyed by the prototype because that
|
|
814
|
+
// is what an instance carries (USER_PROTO), and the set trap is the only consumer:
|
|
815
|
+
// see the equal-value guard in wrapInstance.
|
|
816
|
+
const customPropertiesByProto = new WeakMap();
|
|
817
|
+
|
|
821
818
|
// GTypeName → registered JS class, the reverse of registeredClasses. The engine's
|
|
822
819
|
// NodeGiConstructor hands runCtorForCObject (below) the C-created instance's GType
|
|
823
820
|
// NAME (g_type_name), which this resolves back to the class to Reflect.construct.
|
|
@@ -1082,6 +1079,11 @@ function syncPumpKeepAlive() {
|
|
|
1082
1079
|
pumpHoldsRuntime = hold;
|
|
1083
1080
|
}
|
|
1084
1081
|
|
|
1082
|
+
/** Hosts whose own loop already dispatches GLib, so the portable timer pump must not arm. */
|
|
1083
|
+
function hostPumpsItself() {
|
|
1084
|
+
return native.isNodeRuntime || native.RUNTIME === 'gjs' || native.RUNTIME === 'nativescript';
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1085
1087
|
/**
|
|
1086
1088
|
* Start co-pumping the default GLib main context from a runtime timer (Bun/Deno).
|
|
1087
1089
|
* No-op on Node, where the libuv↔GLib bridge already co-pumps. Reference-counted;
|
|
@@ -1097,7 +1099,7 @@ export function startMainContextPump() {
|
|
|
1097
1099
|
// Node: the libuv↔GLib bridge already co-pumps. GJS: the host loop IS
|
|
1098
1100
|
// GLib's default main context — a JS-timer pump would only iterate the
|
|
1099
1101
|
// context recursively from within its own dispatch.
|
|
1100
|
-
if (
|
|
1102
|
+
if (hostPumpsItself()) return () => {};
|
|
1101
1103
|
pumpRefCount++;
|
|
1102
1104
|
if (pumpTimer === null) {
|
|
1103
1105
|
if (pendingView === null) pendingView = native.makePumpPendingCount();
|
|
@@ -1122,7 +1124,7 @@ export function startMainContextPump() {
|
|
|
1122
1124
|
|
|
1123
1125
|
/** Decrement the main-context pump reference count; clears the timer at zero. */
|
|
1124
1126
|
export function stopMainContextPump() {
|
|
1125
|
-
if (
|
|
1127
|
+
if (hostPumpsItself() || pumpRefCount === 0) return;
|
|
1126
1128
|
pumpRefCount--;
|
|
1127
1129
|
if (pumpRefCount === 0 && pumpTimer !== null) {
|
|
1128
1130
|
clearInterval(pumpTimer);
|
|
@@ -1313,6 +1315,33 @@ function wrapInstance(handle, userProto) {
|
|
|
1313
1315
|
const target = { [HANDLE]: handle };
|
|
1314
1316
|
const proto = userProto ?? protoForInstance(handle);
|
|
1315
1317
|
if (proto !== undefined) linkInstanceProto(target, proto);
|
|
1318
|
+
// Which CUSTOM properties of this instance have a value yet — the thing GJS's
|
|
1319
|
+
// generated accessors track with the PRESENCE of their private field, and what its
|
|
1320
|
+
// setter's equal-value guard actually compares against (`value !== this[privateName]`,
|
|
1321
|
+
// refs/gjs/modules/core/_common.js). Not the ParamSpec default: on gjs 1.88.1 a fresh
|
|
1322
|
+
// object's `o.p = <default>` DOES notify (the field holds `undefined`), while the same
|
|
1323
|
+
// assignment after a READ does not — the getter installs the default into the field
|
|
1324
|
+
// first (`if (!(privateName in this)) this[privateName] = defaultValue`) — and nor
|
|
1325
|
+
// does it after `new K({p: <default>})`, since the construct value went through the
|
|
1326
|
+
// setter. Three behaviours, one state: has the value been materialised.
|
|
1327
|
+
//
|
|
1328
|
+
// Seeded lazily from what the ENGINE has stored (`storedPropertyNames` — construct
|
|
1329
|
+
// values included), then extended by every read and write through this wrapper. A set
|
|
1330
|
+
// from C on a property no JS code has touched is the one gap, and it falls back to
|
|
1331
|
+
// notifying — what node-gi did before the guard existed. Same for an addon too old to
|
|
1332
|
+
// export storedPropertyNames (the cross-version pairing of the shipped-closure legs,
|
|
1333
|
+
// see runJsPropertySetter): the guard then only narrows, never widens.
|
|
1334
|
+
let materialised;
|
|
1335
|
+
const materialise = (name) => {
|
|
1336
|
+
if (materialised === undefined) {
|
|
1337
|
+
materialised = new Set(
|
|
1338
|
+
typeof native.storedPropertyNames === 'function' ? native.storedPropertyNames(handle) : [],
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
const had = materialised.has(name);
|
|
1342
|
+
materialised.add(name);
|
|
1343
|
+
return had;
|
|
1344
|
+
};
|
|
1316
1345
|
const proxy = new Proxy(target, {
|
|
1317
1346
|
get(t, prop) {
|
|
1318
1347
|
if (prop === HANDLE) return handle;
|
|
@@ -1404,6 +1433,14 @@ function wrapInstance(handle, userProto) {
|
|
|
1404
1433
|
if (shim !== undefined) return (...args) => shim(handle, args);
|
|
1405
1434
|
const propName = toKebab(prop);
|
|
1406
1435
|
if (native.hasProperty(handle, propName)) {
|
|
1436
|
+
// A READ materialises a custom property's value, exactly as GJS's generated
|
|
1437
|
+
// getter writes the default into its private field on first look-up — which
|
|
1438
|
+
// is what makes a LATER assignment of that same default silent. See the
|
|
1439
|
+
// equal-value guard in the set trap.
|
|
1440
|
+
const up = t[USER_PROTO];
|
|
1441
|
+
if (up !== undefined && customPropertiesByProto.get(up)?.has(propName) === true) {
|
|
1442
|
+
materialise(propName);
|
|
1443
|
+
}
|
|
1407
1444
|
return wrapReturn(native.getProperty(handle, propName));
|
|
1408
1445
|
}
|
|
1409
1446
|
// LAST resort before treating an unknown name as a GI method: an INHERITED
|
|
@@ -1448,6 +1485,29 @@ function wrapInstance(handle, userProto) {
|
|
|
1448
1485
|
}
|
|
1449
1486
|
const propName = toKebab(prop);
|
|
1450
1487
|
if (native.hasProperty(handle, propName)) {
|
|
1488
|
+
// Equal-value guard, GJS's `_generateAccessors` setter verbatim:
|
|
1489
|
+
// `if (value !== this[privateName]) { …; this.notify(name) }`
|
|
1490
|
+
// (refs/gjs/modules/core/_common.js). GJS puts it in the accessor it
|
|
1491
|
+
// GENERATES for a declared property, so it covers exactly the custom
|
|
1492
|
+
// properties with no accessor of the class's own — the branch above
|
|
1493
|
+
// already took every class-declared accessor, and `set_property`
|
|
1494
|
+
// bypasses the accessor and so keeps notifying unconditionally on both
|
|
1495
|
+
// runtimes (measured on gjs 1.88.1: assignment [1,1,2], set_property
|
|
1496
|
+
// [1,2,3]). node-gi installs no generated accessors (the engine store
|
|
1497
|
+
// IS the backing store, see findPropertySetter), so without this the
|
|
1498
|
+
// store's unconditional g_object_notify_by_pspec made every assignment
|
|
1499
|
+
// notify — [1,2,3] where gjs gives [1,1,2], and the three spellings of
|
|
1500
|
+
// a dashed name notified once EACH instead of sharing one.
|
|
1501
|
+
//
|
|
1502
|
+
// Only once the value is MATERIALISED (see `materialise`): an unset
|
|
1503
|
+
// property's private field is `undefined` on gjs, never the ParamSpec
|
|
1504
|
+
// default, so the first assignment notifies even when it equals the
|
|
1505
|
+
// default. The dash spelling is the key, since toKebab canonicalises
|
|
1506
|
+
// all three spellings onto it.
|
|
1507
|
+
const custom = up !== undefined && customPropertiesByProto.get(up)?.has(propName) === true;
|
|
1508
|
+
if (custom && materialise(propName)) {
|
|
1509
|
+
if (wrapReturn(native.getProperty(handle, propName)) === value) return true;
|
|
1510
|
+
}
|
|
1451
1511
|
native.setProperty(handle, propName, unwrapArg(value));
|
|
1452
1512
|
return true;
|
|
1453
1513
|
}
|
|
@@ -1699,10 +1759,17 @@ function makeClass(namespace, typeName) {
|
|
|
1699
1759
|
handle = native.constructType(reg.typeHandle, props ? unwrapProps(props) : {});
|
|
1700
1760
|
}
|
|
1701
1761
|
const instance = wrapInstance(handle, nt.prototype);
|
|
1702
|
-
assignTemplateChildren(instance, handle, reg);
|
|
1703
1762
|
// Route construct-time property values through the class's JS setters now
|
|
1704
1763
|
// that USER_PROTO is attached — before the user ctor body runs (GJS order).
|
|
1764
|
+
//
|
|
1765
|
+
// BEFORE the template children, because that is the order GJS produces: a
|
|
1766
|
+
// construct property's setter runs inside g_object_new, where the template
|
|
1767
|
+
// is not yet built, so it sees `this._child === undefined`; the children
|
|
1768
|
+
// appear only once construction returns. Binding them first made a setter
|
|
1769
|
+
// observe a child GJS cannot have handed it yet, so a class that guards on
|
|
1770
|
+
// `if (this._child)` took the wrong branch on node-gi and only on node-gi.
|
|
1705
1771
|
flushPropertiesToJsSetters(instance, handle, reg);
|
|
1772
|
+
assignTemplateChildren(instance, handle, reg);
|
|
1706
1773
|
return instance;
|
|
1707
1774
|
}
|
|
1708
1775
|
// `nt` is a subclass of this introspected GObject class but was NEVER passed to
|
|
@@ -2353,6 +2420,19 @@ function collectVfuncs(klass) {
|
|
|
2353
2420
|
* @param {Function} [maybeClass]
|
|
2354
2421
|
* @returns {Function} the same `klass` (now registered)
|
|
2355
2422
|
*/
|
|
2423
|
+
// GJS's meta FIELD form: a class may declare its meta as static symbol fields
|
|
2424
|
+
// (`Klass[GObject.GTypeName] = 'X'`) instead of passing a meta object, and GJS treats
|
|
2425
|
+
// the symbols as the real source — `registerClass(meta, klass)` only copies meta onto
|
|
2426
|
+
// them and reads them back (refs/gjs/modules/core/_common.js). The descriptions match
|
|
2427
|
+
// GJS's so a stringified symbol reads the same in both runtimes.
|
|
2428
|
+
//
|
|
2429
|
+
// GObject.interfaces / GObject.interface requires are deliberately absent: registerClass
|
|
2430
|
+
// implements no interfaces in EITHER form yet, and a symbol that is accepted and then
|
|
2431
|
+
// ignored promises more than the object form delivers.
|
|
2432
|
+
const GTypeNameSymbol = Symbol('GType name');
|
|
2433
|
+
const propertiesSymbol = Symbol('GObject properties');
|
|
2434
|
+
const signalsSymbol = Symbol('GObject signals');
|
|
2435
|
+
|
|
2356
2436
|
function registerClass(metaOrClass, maybeClass) {
|
|
2357
2437
|
let meta;
|
|
2358
2438
|
let klass;
|
|
@@ -2367,6 +2447,17 @@ function registerClass(metaOrClass, maybeClass) {
|
|
|
2367
2447
|
throw new TypeError('GObject.registerClass: expected a class to register');
|
|
2368
2448
|
}
|
|
2369
2449
|
|
|
2450
|
+
// Fold in whatever the class declared as symbol fields, with the meta OBJECT winning
|
|
2451
|
+
// — the same precedence GJS gets from copying meta onto the symbols before reading
|
|
2452
|
+
// them. Until the symbols existed on the namespace, `{[G.GTypeName]: 'X'}` wrote a
|
|
2453
|
+
// literal "undefined" key and the whole declaration vanished without a word: the
|
|
2454
|
+
// GTypeName fell back to the class name and the signals were never registered.
|
|
2455
|
+
const fieldMeta = {};
|
|
2456
|
+
if (klass[GTypeNameSymbol] !== undefined) fieldMeta.GTypeName = klass[GTypeNameSymbol];
|
|
2457
|
+
if (klass[propertiesSymbol] !== undefined) fieldMeta.Properties = klass[propertiesSymbol];
|
|
2458
|
+
if (klass[signalsSymbol] !== undefined) fieldMeta.Signals = klass[signalsSymbol];
|
|
2459
|
+
meta = { ...fieldMeta, ...meta };
|
|
2460
|
+
|
|
2370
2461
|
const parent = findParentGType(klass);
|
|
2371
2462
|
if (parent === undefined) {
|
|
2372
2463
|
throw new TypeError(
|
|
@@ -2460,6 +2551,18 @@ function registerClass(metaOrClass, maybeClass) {
|
|
|
2460
2551
|
internalChildren: internalChildren.length > 0 ? internalChildren : undefined,
|
|
2461
2552
|
constructPropertyNames: constructPropertyNames.length > 0 ? constructPropertyNames : undefined,
|
|
2462
2553
|
});
|
|
2554
|
+
// Which properties the equal-value guard owns: the ones DECLARED here, union the
|
|
2555
|
+
// registered parent's (a G2 subclass inherits its parent's custom properties and
|
|
2556
|
+
// the guard must follow them down). Introspected properties are deliberately
|
|
2557
|
+
// absent — their C setter decides whether an equal value notifies, and guarding
|
|
2558
|
+
// them here would answer for it.
|
|
2559
|
+
const inheritedCustom = registeredClasses.get(Object.getPrototypeOf(klass))?.customProperties;
|
|
2560
|
+
const customProperties = new Set(inheritedCustom);
|
|
2561
|
+
for (const p of properties) {
|
|
2562
|
+
if (typeof p.name === 'string') customProperties.add(p.name);
|
|
2563
|
+
}
|
|
2564
|
+
registeredClasses.get(klass).customProperties = customProperties;
|
|
2565
|
+
customPropertiesByProto.set(klass.prototype, customProperties);
|
|
2463
2566
|
// Reverse index for runCtorForCObject: the engine identifies a C-created instance
|
|
2464
2567
|
// by its GType name (= gtypeName, what native.registerClass registered).
|
|
2465
2568
|
classesByGType.set(gtypeName, klass);
|
|
@@ -2555,6 +2658,9 @@ function signalEmitByName(object, ...nameAndArgs) {
|
|
|
2555
2658
|
// makeClass). Merged enums are cached so identity is stable.
|
|
2556
2659
|
const OVERLAY_NAMES = new Set([
|
|
2557
2660
|
'registerClass',
|
|
2661
|
+
'GTypeName',
|
|
2662
|
+
'properties',
|
|
2663
|
+
'signals',
|
|
2558
2664
|
'ParamSpec',
|
|
2559
2665
|
'ParamFlags',
|
|
2560
2666
|
'SignalFlags',
|
|
@@ -2621,6 +2727,9 @@ function decorateGObjectNamespace(baseNs) {
|
|
|
2621
2727
|
if (cache.has(prop)) return cache.get(prop);
|
|
2622
2728
|
let value;
|
|
2623
2729
|
if (prop === 'registerClass') value = registerClass;
|
|
2730
|
+
else if (prop === 'GTypeName') value = GTypeNameSymbol;
|
|
2731
|
+
else if (prop === 'properties') value = propertiesSymbol;
|
|
2732
|
+
else if (prop === 'signals') value = signalsSymbol;
|
|
2624
2733
|
else if (prop === 'ParamSpec') value = ParamSpec;
|
|
2625
2734
|
else if (prop === 'ParamFlags') value = mergeFlags(baseNs.ParamFlags, ParamFlags);
|
|
2626
2735
|
else if (prop === 'SignalFlags') value = mergeFlags(baseNs.SignalFlags, SignalFlags);
|
|
@@ -2794,6 +2903,10 @@ export function requireGi(namespace, version) {
|
|
|
2794
2903
|
// ready and re-arm — if GLib still has scheduled work, the armed (ref'd)
|
|
2795
2904
|
// uv timer revives the loop; otherwise the process exits normally.
|
|
2796
2905
|
runtimeProcess.on('beforeExit', () => native.pumpKick());
|
|
2906
|
+
} else if (native.RUNTIME === 'nativescript') {
|
|
2907
|
+
// The native ALooper pump owns GLib dispatch on Android: no beforeExit kick
|
|
2908
|
+
// (the Looper never runs empty) and no portable timer pump.
|
|
2909
|
+
native.startMainLoop();
|
|
2797
2910
|
} else if (native.RUNTIME !== 'gjs') {
|
|
2798
2911
|
// Bun/Deno: no libuv to hook, so the portable timer pump IS the auto-pump.
|
|
2799
2912
|
// Armed HERE, not left to the caller, so that `bun bundle.mjs` /
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @gjsify/node-gi — the NativeScript (Android) host, selected by the `nativescript`
|
|
3
|
+
// condition on `#host` (see nativescript.webpack.js, which adds the condition to
|
|
4
|
+
// @nativescript/webpack's resolver). NativeScript's V8 is a Node-API host WITHOUT Node's
|
|
5
|
+
// module system: no node:fs/module/path/process/timers, no prebuild tree, no re-exec.
|
|
6
|
+
// This file is the whole difference — gi.js and index.js import the same names from
|
|
7
|
+
// `#host` on every runtime.
|
|
8
|
+
//
|
|
9
|
+
// Configuration (globals, set before the first import — this package must not hard-import
|
|
10
|
+
// @nativescript/core):
|
|
11
|
+
// __NODE_GI_APP_DIR app-private writable dir (default: the files dir, derived from
|
|
12
|
+
// com.tns.NativeScriptApplication). HOME / XDG_{DATA,CONFIG,CACHE}_HOME and
|
|
13
|
+
// XDG_RUNTIME_DIR are pointed below it, via android.system.Os.setenv
|
|
14
|
+
// (a libc setenv) before the addon — and so GLib — is loaded: GLib caches
|
|
15
|
+
// these dirs on first use, a later GLib.setenv would be too late.
|
|
16
|
+
// __NODE_GI_TYPELIB_PATH directory holding the *.typelib files (the app bundles them, e.g.
|
|
17
|
+
// `knownFolders.currentApp().path + '/girepository-1.0'`); prepended to
|
|
18
|
+
// the GIRepository search path. Default: `<app dir>/app/girepository-1.0`.
|
|
19
|
+
// __NODE_GI_SYSTEM_DIR root of the bundled GNOME system prefix, holding `share/` and `etc/`
|
|
20
|
+
// as GTK's own Android runtime lays them out (XDG_DATA_DIRS=<root>/share,
|
|
21
|
+
// XDG_CONFIG_DIRS=<root>/etc, plus FONTCONFIG_PATH=<root>/etc/fonts since
|
|
22
|
+
// fontconfig's compiled-in /etc/fonts is Android's own and holds no
|
|
23
|
+
// fonts.conf). Unset ⇒ nothing is written and GLib keeps its /usr/…
|
|
24
|
+
// defaults, which exist on no Android device: a host that bundles its
|
|
25
|
+
// own GSettings schemas, icon themes or GTK resources must set it.
|
|
26
|
+
// GTK does the same from startRuntime() via g_set_user_dirs(), which is
|
|
27
|
+
// not introspectable — see refs/gtk/gdk/android/gdkandroidruntime.c.
|
|
28
|
+
// __NODE_GI_ANDROID_GDK truthy ⇒ bring GDK's Android backend up while loading the addon:
|
|
29
|
+
// gdk_android_initialize(env, app classloader, Application), the pair
|
|
30
|
+
// GTK's RuntimeApplication.startRuntime() passes. Needed before the
|
|
31
|
+
// first Gtk call and NOT done by anything else — GIRepository dlopens
|
|
32
|
+
// libgtk-4.so, which (unlike System.loadLibrary) never runs its
|
|
33
|
+
// JNI_OnLoad, and that JNI_OnLoad passes a NULL context anyway, on
|
|
34
|
+
// which gtk_init's Context.getSystemService() aborts the process.
|
|
35
|
+
// Unset ⇒ libgtk-4.so is not loaded here at all, which is what a
|
|
36
|
+
// GLib-only host wants (src/android-gdk.cc).
|
|
37
|
+
// __NODE_GI_ANDROID_JNI_BOOTSTRAP
|
|
38
|
+
// required by the above: the fully-qualified name of a class in
|
|
39
|
+
// the APK whose static initializer calls
|
|
40
|
+
// System.loadLibrary("node_gi"). Nothing else runs this addon's
|
|
41
|
+
// JNI_OnLoad, and without that there is no JavaVM to reach GDK
|
|
42
|
+
// with — see ensureJavaVm().
|
|
43
|
+
|
|
44
|
+
/** @type {'nativescript'} */
|
|
45
|
+
export const RUNTIME = 'nativescript';
|
|
46
|
+
|
|
47
|
+
// Timers from the global scope: NativeScript has no node:timers, and there is no
|
|
48
|
+
// GLib-backed replacement to avoid here. setImmediate is not a V8 builtin.
|
|
49
|
+
export const setImmediate =
|
|
50
|
+
typeof globalThis.setImmediate === 'function'
|
|
51
|
+
? globalThis.setImmediate.bind(globalThis)
|
|
52
|
+
: (fn, ...args) => globalThis.setTimeout(fn, 0, ...args);
|
|
53
|
+
export const setInterval = globalThis.setInterval.bind(globalThis);
|
|
54
|
+
export const clearInterval = globalThis.clearInterval.bind(globalThis);
|
|
55
|
+
|
|
56
|
+
// The only `process` surface gi.js reads is on('beforeExit'), and NativeScript has no such
|
|
57
|
+
// event: its loop is the Android main Looper, which never "runs empty". The stand-in
|
|
58
|
+
// swallows registrations (nothing needs re-arming — the native ALooper pump owns GLib
|
|
59
|
+
// dispatch). exit() is the honest Android answer: end this process.
|
|
60
|
+
const env = {};
|
|
61
|
+
export const runtimeProcess = {
|
|
62
|
+
env,
|
|
63
|
+
platform: 'android',
|
|
64
|
+
argv: [],
|
|
65
|
+
on() {
|
|
66
|
+
return runtimeProcess;
|
|
67
|
+
},
|
|
68
|
+
once() {
|
|
69
|
+
return runtimeProcess;
|
|
70
|
+
},
|
|
71
|
+
exit(code = 0) {
|
|
72
|
+
const proc = globalThis.android?.os?.Process;
|
|
73
|
+
if (proc) proc.killProcess(proc.myPid());
|
|
74
|
+
throw new Error(`@gjsify/node-gi: process.exit(${code}) is unavailable outside Android`);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// No bundled-GTK windowing loader on Android: nothing was written, nothing to replay.
|
|
79
|
+
export function windowingEnvWrites() {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// NativeScript's runtime class hands out the Application without @nativescript/core
|
|
84
|
+
// (android.app.ActivityThread is a hidden API and absent from the metadata).
|
|
85
|
+
function deriveAppDir() {
|
|
86
|
+
try {
|
|
87
|
+
return globalThis.com?.tns?.NativeScriptApplication?.getInstance()?.getFilesDir()?.getPath() ?? null;
|
|
88
|
+
} catch {
|
|
89
|
+
// NativeScript surfaces a Java exception as a JS one, and getFilesDir() raises
|
|
90
|
+
// before the Application is fully constructed. No dir is not a failure here: it
|
|
91
|
+
// means the host sets __NODE_GI_APP_DIR itself, or keeps GLib's defaults.
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setNativeEnv(name, value) {
|
|
97
|
+
env[name] = value;
|
|
98
|
+
globalThis.android?.system?.Os?.setenv(name, value, true);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function appDirectory() {
|
|
102
|
+
const set = globalThis.__NODE_GI_APP_DIR;
|
|
103
|
+
if (typeof set === 'string' && set !== '') return set;
|
|
104
|
+
return deriveAppDir();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The addon needs a JavaVM to hand GDK a JNIEnv, and a native library gets one only from
|
|
108
|
+
// its own JNI_OnLoad — which ART runs when it loads the library on behalf of a class.
|
|
109
|
+
// NativeScript's `system_lib://` is a plain dlopen: the library is mapped and its
|
|
110
|
+
// Node-API exports work, but ART never saw it, so JNI_OnLoad did not run.
|
|
111
|
+
//
|
|
112
|
+
// Loading it a second time from Java does run it, and costs nothing — same soname in the
|
|
113
|
+
// same linker namespace is the same handle, not a second copy. The catch is where the
|
|
114
|
+
// load is issued from, because ART resolves the library against the *calling class's*
|
|
115
|
+
// classloader:
|
|
116
|
+
//
|
|
117
|
+
// java.lang.System.loadLibrary('node_gi') straight from JS fails with "library
|
|
118
|
+
// libnode_gi.so not found": the call arrives through JNI with no Java frame below it,
|
|
119
|
+
// so the caller is the system classloader, whose search path is /system/lib* only.
|
|
120
|
+
// System.load() with the absolute path from ApplicationInfo.nativeLibraryDir does not
|
|
121
|
+
// throw but leaves the VM unset — measured on API 36.
|
|
122
|
+
//
|
|
123
|
+
// So the load has to come from a class in the APK: __NODE_GI_ANDROID_JNI_BOOTSTRAP names
|
|
124
|
+
// one whose static initializer calls System.loadLibrary("node_gi"), and initializing it
|
|
125
|
+
// through Class.forName runs that block under the app's own classloader. Registering the
|
|
126
|
+
// name with the addon first is what lets its JNI_OnLoad cache that classloader, the only
|
|
127
|
+
// moment app classes are visible to it.
|
|
128
|
+
function ensureJavaVm(native) {
|
|
129
|
+
if (typeof native.androidHasJavaVm !== 'function' || native.androidHasJavaVm()) return;
|
|
130
|
+
const bootstrap = globalThis.__NODE_GI_ANDROID_JNI_BOOTSTRAP;
|
|
131
|
+
if (typeof bootstrap !== 'string' || bootstrap === '') {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'@gjsify/node-gi: __NODE_GI_ANDROID_GDK needs __NODE_GI_ANDROID_JNI_BOOTSTRAP — ' +
|
|
134
|
+
'the name of an app class whose static initializer calls ' +
|
|
135
|
+
'System.loadLibrary("node_gi"). Only such a class can run this addon\'s ' +
|
|
136
|
+
'JNI_OnLoad, and without it there is no JavaVM to initialize GDK with.',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
native.androidRegisterJniBootstrap(bootstrap.replace(/\./g, '/'));
|
|
140
|
+
const loader = globalThis.com?.tns?.NativeScriptApplication?.getInstance()?.getClassLoader();
|
|
141
|
+
globalThis.java?.lang?.Class?.forName(bootstrap, true, loader);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function configureEnvironment() {
|
|
145
|
+
const systemDir = globalThis.__NODE_GI_SYSTEM_DIR;
|
|
146
|
+
if (typeof systemDir === 'string' && systemDir !== '') {
|
|
147
|
+
setNativeEnv('XDG_DATA_DIRS', `${systemDir}/share`);
|
|
148
|
+
setNativeEnv('XDG_CONFIG_DIRS', `${systemDir}/etc`);
|
|
149
|
+
setNativeEnv('FONTCONFIG_PATH', `${systemDir}/etc/fonts`);
|
|
150
|
+
}
|
|
151
|
+
const appDir = appDirectory();
|
|
152
|
+
if (appDir === null) return;
|
|
153
|
+
setNativeEnv('HOME', appDir);
|
|
154
|
+
setNativeEnv('XDG_DATA_HOME', `${appDir}/.local/share`);
|
|
155
|
+
setNativeEnv('XDG_CONFIG_HOME', `${appDir}/.config`);
|
|
156
|
+
setNativeEnv('XDG_CACHE_HOME', `${appDir}/.cache`);
|
|
157
|
+
setNativeEnv('XDG_RUNTIME_DIR', `${appDir}/.run`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The names under which NativeScript's own module loader can be reached from the GLOBAL scope,
|
|
162
|
+
* in the order a bundle may offer them. `system_lib://` is the runtime's scheme, so whichever
|
|
163
|
+
* of these answers, it is the same loader underneath.
|
|
164
|
+
*
|
|
165
|
+
* * `require` — the loader the runtime installs globally. A CommonJS bundle also has a
|
|
166
|
+
* module-scoped `require`, but that one is the BUNDLER's; reading the global by name is
|
|
167
|
+
* what keeps the two apart, and it is why this list is read off a scope object rather
|
|
168
|
+
* than written as bare identifiers.
|
|
169
|
+
* * `__nativeRequire` — the same loader under the name the runtime uses internally
|
|
170
|
+
* (measured in @nativescript/android 9.1's libNativeScript.so, beside `ns:module`'s
|
|
171
|
+
* `createRequire`). It is the fallback for a runtime that stops exposing `require`
|
|
172
|
+
* globally, which an ESM-only bundle would otherwise have no way to reach.
|
|
173
|
+
*/
|
|
174
|
+
const RUNTIME_REQUIRE_GLOBALS = ['require', '__nativeRequire'];
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Pick the function that can load `system_lib://libnode_gi.so`.
|
|
178
|
+
*
|
|
179
|
+
* WHY THIS IS A CHAIN AND NOT webpack's ONE NAME. `__non_webpack_require__` only exists
|
|
180
|
+
* because webpack REWRITES that free identifier to the runtime's require; under
|
|
181
|
+
* @nativescript/vite (Rolldown, ESM output) nothing rewrites it, so the old single check made
|
|
182
|
+
* every non-webpack NativeScript bundle fail at the first `requireGi` with a message telling
|
|
183
|
+
* the app to switch bundlers — which is not a fix available to a vite app.
|
|
184
|
+
*
|
|
185
|
+
* ORDER MATTERS. webpack's rewrite wins first: in a webpack bundle the module-scoped `require`
|
|
186
|
+
* is webpack's own and would resolve `system_lib://…` against the bundle graph instead of the
|
|
187
|
+
* runtime. Only after that do the runtime's global names get a turn.
|
|
188
|
+
*
|
|
189
|
+
* Pure on purpose — `scope` is an argument so the chain is testable without a device
|
|
190
|
+
* (`test/host-nativescript.test.mjs`).
|
|
191
|
+
*
|
|
192
|
+
* @param {Record<string, unknown>} scope the global object to read the runtime names off
|
|
193
|
+
* @param {unknown} webpackRequire webpack's rewritten `__non_webpack_require__`, if any
|
|
194
|
+
* @returns {{ load: ((id: string) => any) | null, via: string | null }}
|
|
195
|
+
*/
|
|
196
|
+
export function resolveNativeRequire(scope, webpackRequire) {
|
|
197
|
+
if (typeof webpackRequire === 'function') {
|
|
198
|
+
return { load: /** @type {(id: string) => any} */ (webpackRequire), via: '__non_webpack_require__' };
|
|
199
|
+
}
|
|
200
|
+
for (const name of RUNTIME_REQUIRE_GLOBALS) {
|
|
201
|
+
const candidate = scope?.[name];
|
|
202
|
+
if (typeof candidate === 'function') {
|
|
203
|
+
return { load: /** @type {(id: string) => any} */ (candidate), via: name };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { load: null, via: null };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Load the addon from the APK's native libs and point GI at the app's typelibs.
|
|
211
|
+
* @returns {Record<string, any>} the native engine
|
|
212
|
+
*/
|
|
213
|
+
export function loadNativeHost() {
|
|
214
|
+
configureEnvironment();
|
|
215
|
+
// The bare identifier is what webpack rewrites; everything else is read off the global
|
|
216
|
+
// scope inside resolveNativeRequire.
|
|
217
|
+
const { load } = resolveNativeRequire(
|
|
218
|
+
globalThis,
|
|
219
|
+
typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : undefined,
|
|
220
|
+
);
|
|
221
|
+
if (load === null) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
'@gjsify/node-gi: the NativeScript host found no module loader to pass ' +
|
|
224
|
+
"'system_lib://libnode_gi.so' to. It looked for __non_webpack_require__ (webpack " +
|
|
225
|
+
`rewrites it) and then for ${RUNTIME_REQUIRE_GLOBALS.map((n) => `globalThis.${n}`).join(' and ')}` +
|
|
226
|
+
" — the names NativeScript's own loader answers to. None of them is a function, which " +
|
|
227
|
+
'means this is not a NativeScript runtime, or its loader moved.',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const native = load('system_lib://libnode_gi.so');
|
|
231
|
+
const appDir = appDirectory();
|
|
232
|
+
// `app/` is where NativeScript extracts the bundled assets under the files dir.
|
|
233
|
+
const typelibs = globalThis.__NODE_GI_TYPELIB_PATH ?? (appDir === null ? null : `${appDir}/app/girepository-1.0`);
|
|
234
|
+
if (typeof typelibs === 'string' && typelibs !== '') native.prependSearchPath(typelibs);
|
|
235
|
+
// The addon names the Context provider rather than guessing: on NativeScript that is
|
|
236
|
+
// the runtime's own Application holder, the same one deriveAppDir() reads.
|
|
237
|
+
if (globalThis.__NODE_GI_ANDROID_GDK && typeof native.androidInitGdk === 'function') {
|
|
238
|
+
ensureJavaVm(native);
|
|
239
|
+
native.androidInitGdk('com/tns/NativeScriptApplication', 'getInstance', '()Landroid/app/Application;');
|
|
240
|
+
}
|
|
241
|
+
return native;
|
|
242
|
+
}
|