@gjsify/node-gi 0.59.0 → 0.59.1
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.js +15 -15
- package/host.nativescript.js +186 -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/index.js
CHANGED
|
@@ -5,203 +5,19 @@
|
|
|
5
5
|
// a native package's JS entry is a loader, not a tsc artifact, and the repo
|
|
6
6
|
// ignores `lib/`. The addon is Node-API (ABI-stable), so the same binary loads on
|
|
7
7
|
// Node, Bun and Deno; runtime-specific behaviour is gated off RUNTIME below.
|
|
8
|
-
import {
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
10
|
-
import { hostTarget, nativeCandidates, packageRoot } from './native-paths.js';
|
|
11
|
-
import { describeAddonLoadFailure } from './load-diagnostics.js';
|
|
12
|
-
import {
|
|
13
|
-
activateBundledGtkRuntime,
|
|
14
|
-
activateBundledOpenGL,
|
|
15
|
-
activateGiLibraryPath,
|
|
16
|
-
maybePrependGtkRuntimeDllPath,
|
|
17
|
-
maybeReexecForGtkRuntime,
|
|
18
|
-
maybeWireGtkWindowingEnv,
|
|
19
|
-
} from './gtk-runtime.js';
|
|
20
|
-
import { activateNativePrebuilds } from './native-prebuilds.js';
|
|
21
|
-
|
|
22
|
-
// macOS bundled GTK: dyld reads DYLD_FALLBACK_LIBRARY_PATH only at launch, and GI
|
|
23
|
-
// needs it to g_module_open typelib backers (get_type / Pango / Gdk / Graphene) by
|
|
24
|
-
// leaf soname — so the re-exec must happen BEFORE the addon's GTK closure is
|
|
25
|
-
// dlopen'd. Never returns when it re-execs.
|
|
26
|
-
maybeReexecForGtkRuntime();
|
|
27
|
-
|
|
28
|
-
// Windows bundled GTK: Windows re-reads the DLL search path at every LoadLibrary
|
|
29
|
-
// (unlike dyld's launch-time capture), so mutating process.env.PATH in-process
|
|
30
|
-
// before the require() below covers both the static DLL imports and the runtime
|
|
31
|
-
// g_module_open of typelib backers — no re-exec needed.
|
|
32
|
-
maybePrependGtkRuntimeDllPath();
|
|
33
|
-
|
|
34
|
-
// Windows/macOS full-windowing GTK: point GSETTINGS_SCHEMA_DIR / GDK_PIXBUF_MODULE_FILE /
|
|
35
|
-
// XDG_DATA_DIRS / FONTCONFIG_* at the data a real GTK window needs, and PANGOCAIRO_BACKEND
|
|
36
|
-
// at the one backend that reads any of the font half. Strict no-op for the display-free
|
|
37
|
-
// bundle, which therefore loads byte-unchanged. On win32 these writes reach the Win32
|
|
38
|
-
// environment block ONLY — gi.js replays them into the C runtime's copy on the first
|
|
39
|
-
// requireGi(), which is the environment pango's and fontconfig's getenv() read.
|
|
40
|
-
maybeWireGtkWindowingEnv();
|
|
41
|
-
|
|
42
|
-
const require = createRequire(import.meta.url);
|
|
43
|
-
const here = packageRoot;
|
|
8
|
+
import { RUNTIME, loadNativeHost } from '#host';
|
|
44
9
|
|
|
45
10
|
/**
|
|
46
|
-
* Which JS runtime we are on.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* uv_backend_fd — so Bun/Deno use gi.js's portable GLib-iteration pump, and GJS needs
|
|
50
|
-
* neither (its host loop already IS GLib's default main context). Detection order is
|
|
51
|
-
* load-bearing: GJS first via `imports` + `print` (no other runtime defines both), then
|
|
52
|
-
* Bun/Deno, which both expose a `process` shim.
|
|
53
|
-
* @type {'bun' | 'deno' | 'gjs' | 'node'}
|
|
11
|
+
* Which JS runtime we are on. Detection — and why each runtime gets the main-loop
|
|
12
|
+
* treatment it gets — lives at the detection site, behind `#host`.
|
|
13
|
+
* @type {'bun' | 'deno' | 'gjs' | 'node' | 'nativescript'}
|
|
54
14
|
*/
|
|
55
|
-
export
|
|
56
|
-
typeof globalThis.imports !== 'undefined' && typeof globalThis.print === 'function'
|
|
57
|
-
? 'gjs'
|
|
58
|
-
: typeof globalThis.Bun !== 'undefined'
|
|
59
|
-
? 'bun'
|
|
60
|
-
: typeof globalThis.Deno !== 'undefined'
|
|
61
|
-
? 'deno'
|
|
62
|
-
: 'node';
|
|
15
|
+
export { RUNTIME };
|
|
63
16
|
|
|
64
17
|
/** Whether we are on Node.js (the only runtime with the libuv main-loop bridge). */
|
|
65
18
|
export const isNodeRuntime = RUNTIME === 'node';
|
|
66
19
|
|
|
67
|
-
|
|
68
|
-
// `globalThis.__gjsifyNapiLoadAddon` and re-installs it if another consumer (the
|
|
69
|
-
// @gjsify/napi L1) already captured + deleted it. Needs the GjsifyNapi typelib
|
|
70
|
-
// resolvable via GI_TYPELIB_PATH/LD_LIBRARY_PATH.
|
|
71
|
-
function gjsLoadAddon(path) {
|
|
72
|
-
let load = globalThis.__gjsifyNapiLoadAddon;
|
|
73
|
-
if (typeof load !== 'function') {
|
|
74
|
-
const GjsifyNapi = globalThis.imports?.gi?.GjsifyNapi;
|
|
75
|
-
if (!GjsifyNapi || !GjsifyNapi.init()) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
'@gjsify/node-gi: GJS host mode needs the @gjsify/napi shim — the GjsifyNapi ' +
|
|
78
|
-
'typelib is not loadable (point GI_TYPELIB_PATH and LD_LIBRARY_PATH at its prebuild).',
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
load = globalThis.__gjsifyNapiLoadAddon;
|
|
82
|
-
if (typeof load !== 'function') {
|
|
83
|
-
throw new Error('@gjsify/node-gi: GjsifyNapi.init() did not install the addon loader.');
|
|
84
|
-
}
|
|
85
|
-
// Capture-then-delete, the same no-side-channel discipline as the L1.
|
|
86
|
-
delete globalThis.__gjsifyNapiLoadAddon;
|
|
87
|
-
}
|
|
88
|
-
return load(path);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function loadNative() {
|
|
92
|
-
for (const candidate of nativeCandidates()) {
|
|
93
|
-
if (existsSync(candidate)) {
|
|
94
|
-
try {
|
|
95
|
-
return RUNTIME === 'gjs' ? gjsLoadAddon(candidate) : require(candidate);
|
|
96
|
-
} catch (err) {
|
|
97
|
-
// The addon FILE is here and still would not load — on win32/darwin
|
|
98
|
-
// that means a missing GTK dependency closure, but the raw error
|
|
99
|
-
// names only the .node, in the OS's language, about a file that
|
|
100
|
-
// plainly exists (#1063: `Das angegebene Modul wurde nicht
|
|
101
|
-
// gefunden.` on a present node_gi.node; #994: a missing load-time
|
|
102
|
-
// library surfacing as "Unsupported type void, deriving from
|
|
103
|
-
// fundamental void"). The cause is knowable here, so say it.
|
|
104
|
-
throw new Error(describeAddonLoadFailure(candidate, err), { cause: err });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
throw new Error(
|
|
109
|
-
`@gjsify/node-gi: native addon not found for ${RUNTIME} on ${hostTarget()}. ` +
|
|
110
|
-
`Expected a prebuild at prebuilds/${hostTarget()}/node_gi.node or a local build. ` +
|
|
111
|
-
'Run `node-gyp rebuild` in ' +
|
|
112
|
-
here +
|
|
113
|
-
' (requires a C++ toolchain and the girepository-2.0 / glib-2.0 development headers), ' +
|
|
114
|
-
'or install a package build that ships a prebuild for your platform. ' +
|
|
115
|
-
'Under a bundled GJS app (import.meta anchors at the bundle), set NODE_GI_NATIVE to the addon path.',
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const native = loadNative();
|
|
120
|
-
|
|
121
|
-
// Activate a relocated GTK/GI runtime bundle (@gjsify/gtk-runtime-<os>-<arch> or a
|
|
122
|
-
// staged prebuilds/<os>-<arch>/gtk/) BEFORE any namespace is required: it prepends the
|
|
123
|
-
// bundle's typelib dir to the GIRepository search path env-free, so gi:// namespaces
|
|
124
|
-
// load with no Homebrew/system GTK. Darwin + win32 only; without a bundle the addon
|
|
125
|
-
// uses the host GTK as before.
|
|
126
|
-
try {
|
|
127
|
-
activateBundledGtkRuntime(native);
|
|
128
|
-
} catch {
|
|
129
|
-
// Never fatal: a missing/partial bundle just leaves the host GTK in charge.
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// win32: decide whether the bundle's Mesa opengl32.dll or the host's OpenGL serves GTK, and
|
|
133
|
-
// preload the bundled one when it wins. Here because gtk-4-1.dll imports OPENGL32 statically,
|
|
134
|
-
// so it must happen before any namespace loads GTK. Reports rather than throws.
|
|
135
|
-
activateBundledOpenGL(native);
|
|
136
|
-
|
|
137
|
-
// Every OTHER staged prebuild typelib (@gjsify/webkit-native and its kin): same
|
|
138
|
-
// env-free treatment, so a namespace whose typelib is installed in node_modules
|
|
139
|
-
// resolves under a plain `node app.mjs` and not only under `gjsify run`. Deliberately
|
|
140
|
-
// AFTER the GTK activation — that one owns the bundle-vs-host policy (ADR 0023) and
|
|
141
|
-
// this one skips gtk-runtime-* for exactly that reason.
|
|
142
|
-
try {
|
|
143
|
-
activateNativePrebuilds(native);
|
|
144
|
-
} catch {
|
|
145
|
-
// Same contract as the calls around it: a failed discovery leaves the search
|
|
146
|
-
// path untouched, and the caller gets GI's own "typelib not found".
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// And where the LIBRARIES those typelibs name actually live. Separate from the
|
|
150
|
-
// call above because it is not about a bundle: it applies to whichever GTK the
|
|
151
|
-
// policy chose, and it is the only loader repair bun and deno get on macOS —
|
|
152
|
-
// the re-exec above is Node-shaped and skips them.
|
|
153
|
-
try {
|
|
154
|
-
activateGiLibraryPath(native);
|
|
155
|
-
} catch {
|
|
156
|
-
// Same contract as above: never fatal.
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Cross-runtime microtask checkpoint (Bun/Deno only). Node's napi_make_callback runs
|
|
160
|
-
// the nextTick + microtask checkpoint when its callback scope closes, so promise
|
|
161
|
-
// continuations queued by a loop-dispatched GLib→JS callback drain at the callback
|
|
162
|
-
// boundary even while a blocking GLib.MainLoop.run()/Gio.Application.run() owns the
|
|
163
|
-
// thread; GJS gets the same from SpiderMonkey draining the promise-job queue when the
|
|
164
|
-
// last JS frame exits. Bun and Deno checkpoint NOWHERE there (Deno's
|
|
165
|
-
// napi_make_callback is a plain Function::Call), and with their event loop paused for
|
|
166
|
-
// the blocking run's lifetime the queue never drained: an async DBus method handler
|
|
167
|
-
// never sent its reply and the client timed out, while sync handlers worked. So
|
|
168
|
-
// register the runtime's own drain primitive; the loop-dispatched trampolines
|
|
169
|
-
// (src/signals.cc / calls.cc / class.cc) invoke it after each OUTERMOST callback
|
|
170
|
-
// returns (NodeGiMaybeDrainMicrotasks).
|
|
171
|
-
if (!isNodeRuntime && RUNTIME !== 'gjs') {
|
|
172
|
-
try {
|
|
173
|
-
let drain = null;
|
|
174
|
-
if (RUNTIME === 'bun') {
|
|
175
|
-
// JSC: drains the VM's microtask queue; callable mid-stack by design
|
|
176
|
-
// (Bun's own processTicksAndRejections calls it from JS).
|
|
177
|
-
({ drainMicrotasks: drain } = require('bun:jsc'));
|
|
178
|
-
} else if (typeof globalThis.Deno?.internal === 'symbol') {
|
|
179
|
-
// V8: the checkpoint needs BOTH queues. core.runMicrotasks is only
|
|
180
|
-
// Isolate::PerformMicrotaskCheckpoint, while Deno's node-compat
|
|
181
|
-
// process.nextTick queue is a SEPARATE deno_core queue that only the
|
|
182
|
-
// runtime's own event loop drains — and that loop is paused for the whole
|
|
183
|
-
// lifetime of a blocking run(). node:stream delivers 'end' via nextTick
|
|
184
|
-
// (endReadableNT), so a microtask-only drain got body chunks but never the
|
|
185
|
-
// end: every asset load hung at readyState 3 forever on Deno while the SAME
|
|
186
|
-
// bundle settled on Bun, whose nextTick rides JSC's microtask queue.
|
|
187
|
-
// core.runNextTicks covers both queues in one call, like node's
|
|
188
|
-
// task_queues.js. Regression: test/blocking-run-checkpoint.test.mjs.
|
|
189
|
-
const core = globalThis.Deno[globalThis.Deno.internal]?.core;
|
|
190
|
-
if (typeof core?.runNextTicks === 'function') {
|
|
191
|
-
drain = () => core.runNextTicks();
|
|
192
|
-
} else if (typeof core?.runMicrotasks === 'function') {
|
|
193
|
-
// Older deno_core without runNextTicks: promise continuations
|
|
194
|
-
// settle, nextTick consumers like node:stream 'end' still starve.
|
|
195
|
-
drain = () => core.runMicrotasks();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (typeof drain === 'function') native.setMicrotaskDrain(drain);
|
|
199
|
-
} catch {
|
|
200
|
-
// Unregistered ⇒ promise continuations drain only when the runtime's own loop
|
|
201
|
-
// runs (startMainContextPump), so async DBus replies inside a BLOCKING run stay
|
|
202
|
-
// unavailable. Never fatal — the sync surface is unaffected.
|
|
203
|
-
}
|
|
204
|
-
}
|
|
20
|
+
const native = loadNativeHost();
|
|
205
21
|
|
|
206
22
|
// Straight re-exports of the native engine surface. Each contract — arguments,
|
|
207
23
|
// ownership, the OUT/INOUT return-tuple convention, the keep-alive rules of the
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @nativescript/webpack plugin hook (it discovers this exact file name in every dependency
|
|
3
|
+
// and `require()`s it). Adds the `nativescript` export condition so package.json `imports`
|
|
4
|
+
// (`#host`) resolves to host.nativescript.js; webpack's `target('node')` would otherwise
|
|
5
|
+
// pick the Node host.
|
|
6
|
+
//
|
|
7
|
+
// This package is `"type": "module"`, so the file is ESM: the `module.exports` export name
|
|
8
|
+
// is what `require()` of an ES module hands back as the CommonJS export (Node >= 22.12).
|
|
9
|
+
function hook(webpack) {
|
|
10
|
+
webpack.chainWebpack((config) => {
|
|
11
|
+
// `...` keeps webpack's own defaults (import/require/node/default, per request kind).
|
|
12
|
+
config.resolve.set('conditionNames', ['nativescript', '...']);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { hook as 'module.exports' };
|
package/overrides/byte-array.js
CHANGED
|
@@ -42,6 +42,23 @@ const ENCODE_LABELS = new Map([
|
|
|
42
42
|
['utf16le', 'utf16le'],
|
|
43
43
|
]);
|
|
44
44
|
|
|
45
|
+
// Buffer.from(string, 'latin1' | 'ascii' | 'utf16le') for hosts without Buffer: latin1 and
|
|
46
|
+
// ascii keep the low byte of each UTF-16 code unit, utf16le writes each unit little-endian.
|
|
47
|
+
function encodeWithoutBuffer(string, bufferEncoding) {
|
|
48
|
+
if (bufferEncoding === 'utf16le') {
|
|
49
|
+
const out = new Uint8Array(string.length * 2);
|
|
50
|
+
for (let i = 0; i < string.length; i++) {
|
|
51
|
+
const c = string.charCodeAt(i);
|
|
52
|
+
out[2 * i] = c & 0xff;
|
|
53
|
+
out[2 * i + 1] = c >> 8;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
const out = new Uint8Array(string.length);
|
|
58
|
+
for (let i = 0; i < string.length; i++) out[i] = string.charCodeAt(i) & 0xff;
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
function isUtf8Label(encoding) {
|
|
46
63
|
const label = String(encoding).toLowerCase();
|
|
47
64
|
return label === 'utf-8' || label === 'utf8';
|
|
@@ -95,8 +112,13 @@ export function createByteArray(requireGi) {
|
|
|
95
112
|
} else {
|
|
96
113
|
const bufferEncoding = ENCODE_LABELS.get(String(encoding).toLowerCase());
|
|
97
114
|
if (!bufferEncoding) throw new Error(`Unsupported encoding for fromString(): ${encoding}`);
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
// NativeScript has no Buffer; encodeWithoutBuffer yields the same bytes.
|
|
116
|
+
if (typeof Buffer === 'function') {
|
|
117
|
+
const buf = Buffer.from(string, bufferEncoding);
|
|
118
|
+
encoded = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
119
|
+
} else {
|
|
120
|
+
encoded = encodeWithoutBuffer(string, bufferEncoding);
|
|
121
|
+
}
|
|
100
122
|
}
|
|
101
123
|
const data = zeroTerminated(encoded);
|
|
102
124
|
// Copy into a fresh plain Uint8Array (gjs returns a standalone buffer, and
|
package/package.json
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/node-gi",
|
|
3
|
-
"version": "0.59.
|
|
3
|
+
"version": "0.59.1",
|
|
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",
|
|
7
7
|
"gypfile": true,
|
|
8
8
|
"main": "./index.js",
|
|
9
9
|
"types": "./index.d.ts",
|
|
10
|
+
"imports": {
|
|
11
|
+
"#host": {
|
|
12
|
+
"nativescript": "./host.nativescript.js",
|
|
13
|
+
"default": "./host.node.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
10
16
|
"exports": {
|
|
11
17
|
".": {
|
|
12
18
|
"types": "./index.d.ts",
|
|
13
19
|
"default": "./index.js"
|
|
14
20
|
},
|
|
21
|
+
"./package.json": "./package.json",
|
|
15
22
|
"./gi": {
|
|
16
23
|
"types": "./gi.d.ts",
|
|
17
24
|
"default": "./gi.js"
|
|
@@ -39,6 +46,9 @@
|
|
|
39
46
|
},
|
|
40
47
|
"files": [
|
|
41
48
|
"index.js",
|
|
49
|
+
"host.node.js",
|
|
50
|
+
"host.nativescript.js",
|
|
51
|
+
"nativescript.webpack.js",
|
|
42
52
|
"index.d.ts",
|
|
43
53
|
"gi.js",
|
|
44
54
|
"gi.d.ts",
|
|
@@ -86,6 +96,7 @@
|
|
|
86
96
|
"install": "node scripts/install.mjs",
|
|
87
97
|
"build": "node-gyp rebuild",
|
|
88
98
|
"build:native": "node-gyp rebuild",
|
|
99
|
+
"build:android": "node scripts/build-android.mjs",
|
|
89
100
|
"build:prebuild": "node-gyp rebuild && node scripts/stage-prebuild.mjs",
|
|
90
101
|
"rebuild": "node-gyp rebuild",
|
|
91
102
|
"clean": "node-gyp clean",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/addon.cc
CHANGED
|
@@ -64,6 +64,13 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
64
64
|
// Without it the process stays in the C locale, where GNU gettext refuses to
|
|
65
65
|
// translate; the block in private.cc records what that cost.
|
|
66
66
|
NodeGiInitProcessLocale();
|
|
67
|
+
// Also before any GLib call: on Android, point GLib's messages at logcat. Without
|
|
68
|
+
// it every g_warning explaining a failed GI/GTK call is written to a stderr that
|
|
69
|
+
// goes to /dev/null (android-log.cc).
|
|
70
|
+
InitAndroidLog(env, exports);
|
|
71
|
+
// Only the export; the bring-up itself runs when the host asks for it, since it
|
|
72
|
+
// dlopens the whole GTK stack (android-gdk.cc).
|
|
73
|
+
InitAndroidGdk(env, exports);
|
|
67
74
|
// The owner env + its JS/main thread are captured lazily at the first wrap
|
|
68
75
|
// (EnsureDrainAsync) — NOT here, since Init runs once PER env and a per-env
|
|
69
76
|
// overwrite would mis-identify the main thread under worker_threads. The cleanup
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Android only: bring GDK's Android backend up without GTK's RuntimeApplication.
|
|
3
|
+
//
|
|
4
|
+
// GDK's Android backend is initialized by gdk_android_initialize(env, classloader,
|
|
5
|
+
// context) — the only exported entry point into it; everything else it needs
|
|
6
|
+
// (gdk_android_set_latest_activity in particular) is hidden. GTK ships one caller:
|
|
7
|
+
// JNI_OnLoad in libgtk-4.so, which runs when something calls
|
|
8
|
+
// System.loadLibrary("gtk-4") and which passes NULL for the context, plus
|
|
9
|
+
// RuntimeApplication.startRuntime(), which passes the android.app.Application — and
|
|
10
|
+
// also spawns GTK's own thread and dlopens an application library with a main().
|
|
11
|
+
//
|
|
12
|
+
// A JS host cannot use either. It is not the Application (NativeScript is), so
|
|
13
|
+
// startRuntime is out; and the NULL context from JNI_OnLoad is not enough: gtk_init
|
|
14
|
+
// calls Context.getSystemService() on it and the process dies with
|
|
15
|
+
// "JNI DETECTED ERROR IN APPLICATION: can't call ... getSystemService ... on null
|
|
16
|
+
// object". GIRepository's own dlopen of libgtk-4.so does not run JNI_OnLoad at all.
|
|
17
|
+
//
|
|
18
|
+
// So node-gi calls gdk_android_initialize itself, with the context the host names:
|
|
19
|
+
// the same (Application, app classloader) pair startRuntime uses, and nothing else of
|
|
20
|
+
// it. GTK's comment on that call — "This is *really* questionable, as thiz isn't
|
|
21
|
+
// actually an activity. I've updated the code to handle this case" — is the upstream
|
|
22
|
+
// licence for an Application here: it carries gtk_init over until a real
|
|
23
|
+
// ToplevelActivity reports itself through GdkContext._set_latest_activity().
|
|
24
|
+
//
|
|
25
|
+
// Opt-in, never automatic: dlopening libgtk-4.so pulls in the whole GTK stack, and a
|
|
26
|
+
// host that only wants GLib must not pay for it. See __NODE_GI_ANDROID_GDK in
|
|
27
|
+
// host.nativescript.js.
|
|
28
|
+
|
|
29
|
+
#include "common.h"
|
|
30
|
+
|
|
31
|
+
#ifdef __ANDROID__
|
|
32
|
+
|
|
33
|
+
#include <dlfcn.h>
|
|
34
|
+
#include <jni.h>
|
|
35
|
+
|
|
36
|
+
#include <mutex>
|
|
37
|
+
#include <string>
|
|
38
|
+
|
|
39
|
+
// Both set by JNI_OnLoad at the bottom of this file; file-static so both ends see them.
|
|
40
|
+
// The bootstrap class name is the only thing that travels the other way: JNI_OnLoad needs
|
|
41
|
+
// it to find the app's classloader, and only the host knows what the app called it.
|
|
42
|
+
static JavaVM* g_vm = nullptr;
|
|
43
|
+
static jobject g_app_loader = nullptr;
|
|
44
|
+
static std::string g_bootstrap_class;
|
|
45
|
+
|
|
46
|
+
namespace nodegi {
|
|
47
|
+
|
|
48
|
+
namespace {
|
|
49
|
+
|
|
50
|
+
// Describe-and-clear: the exception text is the whole diagnosis, and leaving it pending
|
|
51
|
+
// turns the next JNI call into an abort.
|
|
52
|
+
bool Failed(JNIEnv* env) {
|
|
53
|
+
if (env->ExceptionCheck() == JNI_FALSE) return false;
|
|
54
|
+
env->ExceptionDescribe();
|
|
55
|
+
env->ExceptionClear();
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// App classes are invisible to FindClass from here: with no Java frame below the Node-API
|
|
60
|
+
// call, ART resolves against the system classloader, which does not know com.tns.* — the
|
|
61
|
+
// same reason a one-argument Class.forName() fails on this thread. Hence the loader
|
|
62
|
+
// JNI_OnLoad cached, with the thread's context loader and plain FindClass behind it for a
|
|
63
|
+
// host whose classes the system loader does have.
|
|
64
|
+
jclass LoadAppClass(JNIEnv* env, const std::string& jni_name) {
|
|
65
|
+
std::string dotted = jni_name;
|
|
66
|
+
for (char& c : dotted)
|
|
67
|
+
if (c == '/') c = '.';
|
|
68
|
+
|
|
69
|
+
jobject loader = g_app_loader;
|
|
70
|
+
if (loader == nullptr) {
|
|
71
|
+
jclass thread_class = env->FindClass("java/lang/Thread");
|
|
72
|
+
jmethodID current =
|
|
73
|
+
env->GetStaticMethodID(thread_class, "currentThread", "()Ljava/lang/Thread;");
|
|
74
|
+
jobject thread = env->CallStaticObjectMethod(thread_class, current);
|
|
75
|
+
if (Failed(env) || thread == nullptr) return nullptr;
|
|
76
|
+
jmethodID get_loader = env->GetMethodID(thread_class, "getContextClassLoader",
|
|
77
|
+
"()Ljava/lang/ClassLoader;");
|
|
78
|
+
loader = env->CallObjectMethod(thread, get_loader);
|
|
79
|
+
if (Failed(env) || loader == nullptr)
|
|
80
|
+
return static_cast<jclass>(env->FindClass(jni_name.c_str()));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
jclass loader_class = env->FindClass("java/lang/ClassLoader");
|
|
84
|
+
jmethodID load_class = env->GetMethodID(loader_class, "loadClass",
|
|
85
|
+
"(Ljava/lang/String;)Ljava/lang/Class;");
|
|
86
|
+
jstring name = env->NewStringUTF(dotted.c_str());
|
|
87
|
+
auto found = static_cast<jclass>(env->CallObjectMethod(loader, load_class, name));
|
|
88
|
+
if (Failed(env) || found == nullptr)
|
|
89
|
+
return static_cast<jclass>(env->FindClass(jni_name.c_str()));
|
|
90
|
+
return found;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// androidHasJavaVm() -> boolean
|
|
94
|
+
//
|
|
95
|
+
// Whether JNI_OnLoad has run, i.e. whether the addon has a JavaVM and the app's
|
|
96
|
+
// classloader. Both arrive together and neither can be obtained any other way, so this
|
|
97
|
+
// one answer covers both — the host asks before and after the bootstrap load.
|
|
98
|
+
Napi::Value AndroidHasJavaVm(const Napi::CallbackInfo& info) {
|
|
99
|
+
return Napi::Boolean::New(info.Env(), g_vm != nullptr);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// androidRegisterJniBootstrap(jniClassName) -> undefined
|
|
103
|
+
//
|
|
104
|
+
// Names the class the host is about to initialize to trigger JNI_OnLoad, in JNI form
|
|
105
|
+
// ('dev/example/JniBootstrap'). JNI_OnLoad looks it up to reach the app's classloader:
|
|
106
|
+
// during a load that ART performs, FindClass resolves against the classloader of the
|
|
107
|
+
// class that called System.loadLibrary, which is the one moment app classes are visible
|
|
108
|
+
// to this library. Must therefore be called BEFORE that load.
|
|
109
|
+
Napi::Value AndroidRegisterJniBootstrap(const Napi::CallbackInfo& info) {
|
|
110
|
+
Napi::Env env = info.Env();
|
|
111
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
112
|
+
Napi::TypeError::New(env, "androidRegisterJniBootstrap(jniClassName): a string is required")
|
|
113
|
+
.ThrowAsJavaScriptException();
|
|
114
|
+
return env.Undefined();
|
|
115
|
+
}
|
|
116
|
+
g_bootstrap_class = info[0].As<Napi::String>().Utf8Value();
|
|
117
|
+
return env.Undefined();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
bool g_initialized = false;
|
|
121
|
+
// Both JS threads of a NativeScript app share this library — the UI thread and a Worker,
|
|
122
|
+
// and in the stage-5 spike both import node-gi — so the one-time guard needs a lock of its
|
|
123
|
+
// own: gdk_android_initialize has none, and a second run leaks another set of global refs
|
|
124
|
+
// and re-registers every native.
|
|
125
|
+
std::mutex g_init_mutex;
|
|
126
|
+
|
|
127
|
+
// androidInitGdk(contextClass, contextMethod, contextSignature) -> boolean
|
|
128
|
+
//
|
|
129
|
+
// contextClass/contextMethod name a static method returning the android.content.Context
|
|
130
|
+
// to hand GDK, in JNI form — e.g. ('com/tns/NativeScriptApplication', 'getInstance',
|
|
131
|
+
// '()Landroid/app/Application;'). The host names it because only the host knows how to
|
|
132
|
+
// reach its Application without @hide APIs (android.app.ActivityThread is one).
|
|
133
|
+
// Idempotent, per process rather than per thread — see g_init_mutex.
|
|
134
|
+
Napi::Value AndroidInitGdk(const Napi::CallbackInfo& info) {
|
|
135
|
+
Napi::Env napi_env = info.Env();
|
|
136
|
+
std::lock_guard<std::mutex> guard(g_init_mutex);
|
|
137
|
+
if (g_initialized) return Napi::Boolean::New(napi_env, true);
|
|
138
|
+
|
|
139
|
+
if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
|
|
140
|
+
Napi::TypeError::New(napi_env,
|
|
141
|
+
"androidInitGdk(contextClass, contextMethod, contextSignature): "
|
|
142
|
+
"three strings in JNI form are required")
|
|
143
|
+
.ThrowAsJavaScriptException();
|
|
144
|
+
return napi_env.Undefined();
|
|
145
|
+
}
|
|
146
|
+
std::string class_name = info[0].As<Napi::String>().Utf8Value();
|
|
147
|
+
std::string method_name = info[1].As<Napi::String>().Utf8Value();
|
|
148
|
+
std::string signature = info[2].As<Napi::String>().Utf8Value();
|
|
149
|
+
|
|
150
|
+
if (g_vm == nullptr) {
|
|
151
|
+
Napi::Error::New(napi_env,
|
|
152
|
+
"androidInitGdk: no JavaVM — this addon's JNI_OnLoad never ran. "
|
|
153
|
+
"Initialize an app class that calls System.loadLibrary(\"node_gi\") "
|
|
154
|
+
"first; a dlopen of the library does not run it.")
|
|
155
|
+
.ThrowAsJavaScriptException();
|
|
156
|
+
return napi_env.Undefined();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// A thread this call attaches has to be detached again: ART aborts a thread that exits
|
|
160
|
+
// while still attached ("Native thread exiting without having called
|
|
161
|
+
// DetachCurrentThread"). Safe because gdk_android_initialize keeps the JavaVM and global
|
|
162
|
+
// refs, never a JNIEnv, and GDK reattaches per call through its own thread-env guard.
|
|
163
|
+
JNIEnv* env = nullptr;
|
|
164
|
+
bool attached = false;
|
|
165
|
+
if (g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) == JNI_EDETACHED) {
|
|
166
|
+
if (g_vm->AttachCurrentThread(&env, nullptr) == JNI_OK)
|
|
167
|
+
attached = true;
|
|
168
|
+
else
|
|
169
|
+
env = nullptr;
|
|
170
|
+
}
|
|
171
|
+
if (env == nullptr) {
|
|
172
|
+
Napi::Error::New(napi_env, "androidInitGdk: could not attach this thread to the JavaVM")
|
|
173
|
+
.ThrowAsJavaScriptException();
|
|
174
|
+
return napi_env.Undefined();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// gdk_android_initialize keeps global refs to both arguments, so a local frame is
|
|
178
|
+
// enough and nothing here outlives it.
|
|
179
|
+
env->PushLocalFrame(16);
|
|
180
|
+
std::string error;
|
|
181
|
+
do {
|
|
182
|
+
jclass provider = LoadAppClass(env, class_name);
|
|
183
|
+
if (Failed(env) || provider == nullptr) {
|
|
184
|
+
error = "androidInitGdk: no class " + class_name + " on this thread's classloader";
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
jmethodID getter = env->GetStaticMethodID(provider, method_name.c_str(), signature.c_str());
|
|
188
|
+
if (Failed(env) || getter == nullptr) {
|
|
189
|
+
error = "androidInitGdk: " + class_name + " has no static " + method_name + signature;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
jobject context = env->CallStaticObjectMethod(provider, getter);
|
|
193
|
+
if (Failed(env) || context == nullptr) {
|
|
194
|
+
error = "androidInitGdk: " + class_name + "." + method_name + "() returned no Context";
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The classloader GDK resolves org.gtk.android.* with has to be the one that holds
|
|
199
|
+
// them: the loader of the app's own classes, which is the context's.
|
|
200
|
+
jclass class_class = env->FindClass("java/lang/Class");
|
|
201
|
+
jmethodID get_loader = env->GetMethodID(class_class, "getClassLoader",
|
|
202
|
+
"()Ljava/lang/ClassLoader;");
|
|
203
|
+
jobject loader = env->CallObjectMethod(env->GetObjectClass(context), get_loader);
|
|
204
|
+
if (Failed(env) || loader == nullptr) {
|
|
205
|
+
error = "androidInitGdk: the Context's class has no classloader";
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// RTLD_GLOBAL: the backend resolves GDK symbols out of this handle later, and
|
|
210
|
+
// GIRepository may have loaded libgtk-4.so locally already.
|
|
211
|
+
void* gtk = dlopen("libgtk-4.so", RTLD_NOW | RTLD_GLOBAL);
|
|
212
|
+
if (gtk == nullptr) {
|
|
213
|
+
error = std::string("androidInitGdk: dlopen(libgtk-4.so) failed: ") + dlerror();
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
using InitFn = int (*)(JNIEnv*, jobject, jobject);
|
|
217
|
+
auto initialize = reinterpret_cast<InitFn>(dlsym(gtk, "gdk_android_initialize"));
|
|
218
|
+
if (initialize == nullptr) {
|
|
219
|
+
error = "androidInitGdk: libgtk-4.so exports no gdk_android_initialize "
|
|
220
|
+
"(GTK built without the Android backend?)";
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
if (initialize(env, loader, context) == 0) {
|
|
224
|
+
error = "androidInitGdk: gdk_android_initialize() refused — see the GTK log";
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
g_initialized = true;
|
|
228
|
+
} while (false);
|
|
229
|
+
env->PopLocalFrame(nullptr);
|
|
230
|
+
if (attached) g_vm->DetachCurrentThread();
|
|
231
|
+
|
|
232
|
+
if (!g_initialized) {
|
|
233
|
+
Napi::Error::New(napi_env, error).ThrowAsJavaScriptException();
|
|
234
|
+
return napi_env.Undefined();
|
|
235
|
+
}
|
|
236
|
+
return Napi::Boolean::New(napi_env, true);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
} // namespace
|
|
240
|
+
|
|
241
|
+
void InitAndroidGdk(Napi::Env env, Napi::Object exports) {
|
|
242
|
+
exports.Set("androidHasJavaVm", Napi::Function::New(env, AndroidHasJavaVm));
|
|
243
|
+
exports.Set("androidRegisterJniBootstrap",
|
|
244
|
+
Napi::Function::New(env, AndroidRegisterJniBootstrap));
|
|
245
|
+
exports.Set("androidInitGdk", Napi::Function::New(env, AndroidInitGdk));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
} // namespace nodegi
|
|
249
|
+
|
|
250
|
+
// Runs when ART loads this library on behalf of a class — not on the dlopen that the JS
|
|
251
|
+
// runtime does. Two things are available here and nowhere else: the JavaVM, and a
|
|
252
|
+
// FindClass that resolves against the app's classloader rather than the system one.
|
|
253
|
+
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
|
|
254
|
+
g_vm = vm;
|
|
255
|
+
if (g_bootstrap_class.empty()) return JNI_VERSION_1_6;
|
|
256
|
+
|
|
257
|
+
JNIEnv* env = nullptr;
|
|
258
|
+
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK || env == nullptr)
|
|
259
|
+
return JNI_VERSION_1_6;
|
|
260
|
+
|
|
261
|
+
env->PushLocalFrame(4);
|
|
262
|
+
jclass bootstrap = env->FindClass(g_bootstrap_class.c_str());
|
|
263
|
+
if (!nodegi::Failed(env) && bootstrap != nullptr) {
|
|
264
|
+
jmethodID get_loader = env->GetMethodID(env->GetObjectClass(bootstrap), "getClassLoader",
|
|
265
|
+
"()Ljava/lang/ClassLoader;");
|
|
266
|
+
jobject loader = env->CallObjectMethod(bootstrap, get_loader);
|
|
267
|
+
if (!nodegi::Failed(env) && loader != nullptr) g_app_loader = env->NewGlobalRef(loader);
|
|
268
|
+
}
|
|
269
|
+
env->PopLocalFrame(nullptr);
|
|
270
|
+
return JNI_VERSION_1_6;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#else
|
|
274
|
+
|
|
275
|
+
namespace nodegi {
|
|
276
|
+
|
|
277
|
+
// Off Android there is no JavaVM and no Android GDK backend to initialize; the exports
|
|
278
|
+
// stay so a host can ask without branching on the platform first.
|
|
279
|
+
static Napi::Value AndroidFalse(const Napi::CallbackInfo& info) {
|
|
280
|
+
return Napi::Boolean::New(info.Env(), false);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
void InitAndroidGdk(Napi::Env env, Napi::Object exports) {
|
|
284
|
+
exports.Set("androidHasJavaVm", Napi::Function::New(env, AndroidFalse));
|
|
285
|
+
exports.Set("androidRegisterJniBootstrap", Napi::Function::New(env, AndroidFalse));
|
|
286
|
+
exports.Set("androidInitGdk", Napi::Function::New(env, AndroidFalse));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
} // namespace nodegi
|
|
290
|
+
|
|
291
|
+
#endif
|