@gjsify/node-gi 0.52.0 → 0.54.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/README.md +2 -1
- package/binding.gyp +1 -0
- package/gi.js +34 -1
- package/gtk-runtime.d.ts +33 -0
- package/gtk-runtime.js +180 -6
- package/index.d.ts +14 -0
- package/index.js +12 -0
- package/package.json +13 -13
- 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-x64/node_gi.node +0 -0
- package/src/addon.cc +1 -0
- package/src/calls.cc +10 -34
- package/src/class.cc +14 -37
- package/src/common.h +38 -0
- package/src/loop.cc +28 -4
- package/src/marshal.cc +371 -9
- package/src/object.cc +6 -0
- package/src/opengl-win32.cc +234 -0
- package/src/toggle.cc +1 -1
- package/system-gi.js +1 -1
package/README.md
CHANGED
|
@@ -124,8 +124,9 @@ console.log(action.get_name()); // 'greet' — introspected method
|
|
|
124
124
|
action.enabled = false; // property set → set_property
|
|
125
125
|
|
|
126
126
|
const cancellable = new Gio.Cancellable();
|
|
127
|
-
cancellable.
|
|
127
|
+
cancellable.connect_after('cancelled', () => console.log('cancelled'));
|
|
128
128
|
cancellable.cancel(); // fires the signal
|
|
129
|
+
// (plain `connect` is Cancellable's OWN g_cancellable_connect(cb), as on gjs)
|
|
129
130
|
|
|
130
131
|
console.log(Gio.BusType.SESSION); // 2 — enums, flags and constants
|
|
131
132
|
const file = Gio.File.new_for_path('/usr/bin/gjs');
|
package/binding.gyp
CHANGED
package/gi.js
CHANGED
|
@@ -36,6 +36,19 @@ const USER_PROTO = Symbol('nodeGiUserProto');
|
|
|
36
36
|
// make every wrapper look thenable).
|
|
37
37
|
const RESERVED = new Set(['then', 'toString', 'valueOf', 'constructor', 'inspect']);
|
|
38
38
|
|
|
39
|
+
// Names the instance wrapper's `get` trap otherwise shortcuts straight to the
|
|
40
|
+
// GObject signal API (see wrapInstance below). A concrete class can introspect
|
|
41
|
+
// its OWN method under one of these — `Soup.Server.disconnect(): void` is
|
|
42
|
+
// `soup_server_disconnect`, zero-arg, nothing to do with a signal handler id —
|
|
43
|
+
// and on gjs that own prototype property already shadows the inherited
|
|
44
|
+
// override through ordinary JS lookup. This Proxy has no such chain, so
|
|
45
|
+
// `wrapInstance` checks for an own introspected member under the name FIRST
|
|
46
|
+
// and only falls through to the shortcut when the class declares none.
|
|
47
|
+
// Regression: `Http2SecureServer.close()` called `this._soupServer.disconnect()`
|
|
48
|
+
// (real libsoup API, zero args) and got `disconnectSignal(handle, handlerId:
|
|
49
|
+
// number)` — the shortcut's `disconnect(id)` — under node-gi on Node.
|
|
50
|
+
const SIGNAL_API_SHORTCUTS = new Set(['connect', 'connect_after', 'emit', 'disconnect']);
|
|
51
|
+
|
|
39
52
|
// GJS accepts both snake_case and camelCase for methods/properties: map a JS accessor
|
|
40
53
|
// to the GI method name (snake_case) and to a GObject property name (kebab-case); a
|
|
41
54
|
// name already in the target case passes through.
|
|
@@ -1304,6 +1317,19 @@ function wrapInstance(handle, userProto) {
|
|
|
1304
1317
|
get(t, prop) {
|
|
1305
1318
|
if (prop === HANDLE) return handle;
|
|
1306
1319
|
if (typeof prop !== 'string' || RESERVED.has(prop)) return t[prop];
|
|
1320
|
+
if (SIGNAL_API_SHORTCUTS.has(prop)) {
|
|
1321
|
+
const up = t[USER_PROTO];
|
|
1322
|
+
const own = up !== undefined ? findProtoDescriptor(up, prop) : undefined;
|
|
1323
|
+
if (own !== undefined) return typeof own.get === 'function' ? own.get.call(proxy) : own.value;
|
|
1324
|
+
// An INTERFACE method under the name never reaches a class prototype
|
|
1325
|
+
// when the concrete type is private: GSocks5Proxy wraps as its nearest
|
|
1326
|
+
// introspectable ancestor (GObject.Object), yet on gjs 1.88.1
|
|
1327
|
+
// `socks5.connect` IS g_proxy_connect (Gio.Proxy, 3 IN args) — measured.
|
|
1328
|
+
// The same native walk the GI-method fallback below uses sees it.
|
|
1329
|
+
if (instanceHasMethod(handle, prop)) {
|
|
1330
|
+
return (...args) => wrapReturn(native.callMethod(handle, prop, unwrapArgs(args)));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1307
1333
|
switch (prop) {
|
|
1308
1334
|
case '$typeName':
|
|
1309
1335
|
// The instance's concrete RUNTIME GType name —
|
|
@@ -2298,8 +2324,15 @@ function collectVfuncs(klass) {
|
|
|
2298
2324
|
if (desc === undefined || typeof desc.value !== 'function') continue;
|
|
2299
2325
|
seen.add(key);
|
|
2300
2326
|
const userFn = desc.value;
|
|
2327
|
+
// The ARGUMENTS cross the same boundary `this` does and need the same
|
|
2328
|
+
// treatment: the engine hands a GObject arg over as a raw handle, so a
|
|
2329
|
+
// `Gtk.LayoutManager.vfunc_measure(widget, …)` override received an object
|
|
2330
|
+
// with no prototype and `widget.get_ancestor` was `undefined`. The return
|
|
2331
|
+
// goes back through `unwrapArg` because the engine reads a GObject return
|
|
2332
|
+
// as a handle, never as the wrapper user code naturally hands back.
|
|
2333
|
+
// Same contract as `wrapCallbackFn`, which callbacks have always had.
|
|
2301
2334
|
vfuncs[key.slice('vfunc_'.length)] = function (...args) {
|
|
2302
|
-
return userFn.apply(wrapInstance(this, klass.prototype), args);
|
|
2335
|
+
return unwrapArg(userFn.apply(wrapInstance(this, klass.prototype), args.map(wrapReturn)));
|
|
2303
2336
|
};
|
|
2304
2337
|
}
|
|
2305
2338
|
}
|
package/gtk-runtime.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export function maybeReexecForGtkRuntime(): void;
|
|
|
13
13
|
export function maybePrependGtkRuntimeDllPath(): void;
|
|
14
14
|
/** Windows: wire the env for a full-windowing bundle's runtime data — GSettings schemas, gdk-pixbuf loaders, icon themes, fontconfig (no-op off win32 / for a display-free bundle). */
|
|
15
15
|
export function maybeWireGtkWindowingEnv(): void;
|
|
16
|
+
/** darwin: point libgda at the bundle's SQLite provider via `GDA_TOP_BUILD_DIR` (only when unset; no-op for a bundle without `lib/libgda-6.0/providers`). */
|
|
17
|
+
export function wireGdaProviders(bundle: { dir: string }): void;
|
|
16
18
|
/** Activate the bundled GTK runtime for the native engine, if one is present. */
|
|
17
19
|
export function activateBundledGtkRuntime(native: {
|
|
18
20
|
prependSearchPath: (p: string) => void;
|
|
@@ -26,3 +28,34 @@ export function appGiLibraryDirs(opts?: { platform?: NodeJS.Platform | string; e
|
|
|
26
28
|
export function activateGiLibraryPath(native: { prependLibraryPath?: (p: string) => void }): string[];
|
|
27
29
|
/** TEST-ONLY: allow the activation to run again. */
|
|
28
30
|
export function resetGiLibraryPathForTests(): void;
|
|
31
|
+
|
|
32
|
+
/** What `native.probeHostOpenGL()` reports on win32 — each field "" when absent. */
|
|
33
|
+
export interface HostOpenGL {
|
|
34
|
+
wddmIcd: string;
|
|
35
|
+
registryIcd: string;
|
|
36
|
+
loadedFrom: string;
|
|
37
|
+
}
|
|
38
|
+
/** The OpenGL a win32 process uses: the optional GL package's Mesa opengl32.dll or the host's. */
|
|
39
|
+
export interface OpenGLDecision {
|
|
40
|
+
source: 'bundle' | 'system';
|
|
41
|
+
reason: string;
|
|
42
|
+
/** Set when the host has no ICD and the optional GL package is not installed. */
|
|
43
|
+
missing?: true;
|
|
44
|
+
}
|
|
45
|
+
/** `@gjsify/gl-runtime-<tag>` — the optional package carrying Mesa for driverless hosts. */
|
|
46
|
+
export function glRuntimePackageName(tag?: string): string;
|
|
47
|
+
/** The optional GL package's opengl32.dll (monorepo sibling or installed package), or null. */
|
|
48
|
+
export function resolveGlRuntime(): string | null;
|
|
49
|
+
/** Decide which OpenGL serves GTK on win32 (pure). `override` is `GJSIFY_OPENGL`. */
|
|
50
|
+
export function decideOpenGLSource(opts: {
|
|
51
|
+
bundled: string | null;
|
|
52
|
+
host: HostOpenGL;
|
|
53
|
+
override?: string;
|
|
54
|
+
}): OpenGLDecision;
|
|
55
|
+
/** win32: preload the optional GL package's opengl32.dll when the host has no OpenGL ICD (no-op elsewhere). */
|
|
56
|
+
export function activateBundledOpenGL(native: {
|
|
57
|
+
probeHostOpenGL?: () => HostOpenGL;
|
|
58
|
+
preloadOpenGL?: (path: string) => string;
|
|
59
|
+
}): (OpenGLDecision & { loadedFrom: string | null }) | null;
|
|
60
|
+
/** The decision `activateBundledOpenGL` made for this process, or null. */
|
|
61
|
+
export function openGLActivation(): (OpenGLDecision & { loadedFrom: string | null }) | null;
|
package/gtk-runtime.js
CHANGED
|
@@ -89,6 +89,22 @@ export function resolveGtkRuntimeBundle() {
|
|
|
89
89
|
return null;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The bundle the GTK-in-use is served by, but only when it can open a WINDOW — the
|
|
94
|
+
* `share/glib-2.0/schemas/gschemas.compiled` marker the --windowing build produces.
|
|
95
|
+
* Null for no bundle, a system GTK, or the DEFAULT display-free bundle.
|
|
96
|
+
*
|
|
97
|
+
* ONE definition of that marker for both callers that need it: the windowing env
|
|
98
|
+
* wiring, which is a strict no-op without the data it locates, and the OpenGL advice
|
|
99
|
+
* below, which must not tell a headless process to add a GL package. Spelled twice the
|
|
100
|
+
* question "can this process show a window?" had two answers to drift apart.
|
|
101
|
+
* @returns {{ dir: string, libDir: string, typelibDir: string } | null}
|
|
102
|
+
*/
|
|
103
|
+
function windowingBundle() {
|
|
104
|
+
const bundle = gtkSource() === 'bundle' ? resolveGtkRuntimeBundle() : null;
|
|
105
|
+
return bundle && existsSync(join(bundle.dir, 'share', 'glib-2.0', 'schemas', 'gschemas.compiled')) ? bundle : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
// ---- WHICH GTK WINS ---------------------------------------------------------
|
|
93
109
|
//
|
|
94
110
|
// Two GTKs can be reachable at once: a batteries-included bundle
|
|
@@ -308,7 +324,7 @@ export function maybeReexecForGtkRuntime() {
|
|
|
308
324
|
// gjs process, one hop earlier: `buildNativeEnv()` in `@gjsify/cli` puts
|
|
309
325
|
// `systemGiLibraryDirs()` on the child's `DYLD_FALLBACK_LIBRARY_PATH` (its
|
|
310
326
|
// `system-gi.ts` is the pinned mirror of this module's `system-gi.js`). A gjs
|
|
311
|
-
// launched by hand keeps the gap — tracked in `status/open-todos.md`.
|
|
327
|
+
// launched by hand keeps the gap — tracked in `status/open-todos/README.md`.
|
|
312
328
|
// Same probe index.js uses for RUNTIME — as with the Bun/Deno pair above, the
|
|
313
329
|
// check has to live here too because this runs before that const is defined.
|
|
314
330
|
if (typeof globalThis.imports !== 'undefined' && typeof globalThis.print === 'function') return;
|
|
@@ -415,13 +431,10 @@ export function maybeWireGtkWindowingEnv() {
|
|
|
415
431
|
if (process.platform !== 'win32' && process.platform !== 'darwin') return;
|
|
416
432
|
// Same gate as the DLL/dylib path: the runtime DATA belongs to the bundle, so
|
|
417
433
|
// it is wired only when the bundle is the source the policy picked.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (!bundle) return; // strict no-op when no bundle is present
|
|
434
|
+
const bundle = windowingBundle();
|
|
435
|
+
if (!bundle) return; // strict no-op without a windowing bundle
|
|
421
436
|
|
|
422
437
|
const schemaDir = join(bundle.dir, 'share', 'glib-2.0', 'schemas');
|
|
423
|
-
// gschemas.compiled = the windowing-data marker; absent → display-free bundle.
|
|
424
|
-
if (!existsSync(join(schemaDir, 'gschemas.compiled'))) return;
|
|
425
438
|
|
|
426
439
|
const pathSep = process.platform === 'win32' ? ';' : ':';
|
|
427
440
|
// Recorded, not just written. On win32 `process.env.X = v` reaches ONE of the two
|
|
@@ -612,6 +625,26 @@ export function maybeWireGtkWindowingEnv() {
|
|
|
612
625
|
if (existsSync(gioModules)) setIfUnset('GIO_MODULE_DIR', gioModules);
|
|
613
626
|
}
|
|
614
627
|
|
|
628
|
+
/**
|
|
629
|
+
* Point libgda at the bundle's database providers (darwin; `@gjsify/sqlite` / `node:sqlite`).
|
|
630
|
+
*
|
|
631
|
+
* libgda loads a provider as a GModule from a directory COMPILED IN as the build prefix —
|
|
632
|
+
* `<keg>/lib/libgda-6.0/providers` — and, with that keg absent, `Gda.Connection.new_from_string
|
|
633
|
+
* ('SQLite', …)` throws `No provider 'SQLite' installed`. With the keg PRESENT it is worse: brew's
|
|
634
|
+
* provider loads against brew's libgda and glib beside the bundle's own. The one override libgda
|
|
635
|
+
* has is `GDA_TOP_BUILD_DIR`, read as `<dir>/providers` at the first provider scan, which is long
|
|
636
|
+
* after this runs. In the JS and not in a launcher for the reason `GJSIFY_GI_LIBRARY_PATH` is:
|
|
637
|
+
* a signed app loses `DYLD_*`, never `process.env`.
|
|
638
|
+
* Set only when unset, so an operator pointing libgda elsewhere wins; a no-op for a bundle
|
|
639
|
+
* that carries no provider dir.
|
|
640
|
+
* @param {{ dir: string }} bundle
|
|
641
|
+
*/
|
|
642
|
+
export function wireGdaProviders(bundle) {
|
|
643
|
+
const gdaDir = join(bundle.dir, 'lib', 'libgda-6.0');
|
|
644
|
+
if (!existsSync(join(gdaDir, 'providers'))) return;
|
|
645
|
+
if (!process.env.GDA_TOP_BUILD_DIR) process.env.GDA_TOP_BUILD_DIR = gdaDir;
|
|
646
|
+
}
|
|
647
|
+
|
|
615
648
|
/** The plugin scanner's leaf name — the bundles ship the same tree on both OSes. */
|
|
616
649
|
function gstScannerLeaf() {
|
|
617
650
|
return process.platform === 'win32' ? 'gst-plugin-scanner.exe' : 'gst-plugin-scanner';
|
|
@@ -777,8 +810,149 @@ export function activateBundledGtkRuntime(native) {
|
|
|
777
810
|
// variable REPLACES rather than extends.
|
|
778
811
|
process.env.DYLD_FALLBACK_LIBRARY_PATH = composeDyldFallback([bundle.libDir]);
|
|
779
812
|
}
|
|
813
|
+
wireGdaProviders(bundle);
|
|
780
814
|
}
|
|
781
815
|
|
|
782
816
|
activated = bundle;
|
|
783
817
|
return bundle;
|
|
784
818
|
}
|
|
819
|
+
|
|
820
|
+
// --- OpenGL on win32 (#1097) -------------------------------------------------
|
|
821
|
+
//
|
|
822
|
+
// GTK 4 needs desktop GL >= 3.2 core over WGL on win32 — the gvsbuild GTK is built without
|
|
823
|
+
// EGL, so ANGLE is unreachable, and without Vulkan. A host with no OpenGL ICD (a GPU-less VM,
|
|
824
|
+
// an RDP session, a CI runner) offers only Microsoft's GDI generic OpenGL 1.1, which GDK
|
|
825
|
+
// rejects: every Gtk.GLArea then reads "No GL implementation is available" and GSK falls
|
|
826
|
+
// back to cairo. The OPTIONAL package `@gjsify/gl-runtime-win32-x64` carries Mesa's
|
|
827
|
+
// opengl32.dll for exactly those hosts; it is ~22 MB, so it is a consumer's opt-in and never
|
|
828
|
+
// part of the bundle or a dependency of node-gi (ADR 0023). This decides per process whether
|
|
829
|
+
// it or the host's OpenGL serves GTK. Why it has to be a native preload rather than a PATH
|
|
830
|
+
// entry: src/opengl-win32.cc.
|
|
831
|
+
//
|
|
832
|
+
// `GJSIFY_OPENGL=bundle|system` overrides — `system` keeps a host's OpenGL even when the
|
|
833
|
+
// probe finds no ICD, `bundle` takes Mesa over a vendor driver.
|
|
834
|
+
|
|
835
|
+
/** The optional GL package's npm name for a platform-arch tag. */
|
|
836
|
+
export function glRuntimePackageName(tag = `${process.platform}-${process.arch}`) {
|
|
837
|
+
return `@gjsify/gl-runtime-${tag}`;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Where the optional GL package's opengl32.dll is, or null. Resolved BY NAME, like the GTK
|
|
842
|
+
* bundle: the monorepo sibling first (what CI builds in place), then the installed package.
|
|
843
|
+
* @returns {string | null}
|
|
844
|
+
*/
|
|
845
|
+
export function resolveGlRuntime() {
|
|
846
|
+
const tag = `${process.platform}-${process.arch}`;
|
|
847
|
+
const candidates = [join(here, '..', `gl-runtime-${tag}`, 'bin')];
|
|
848
|
+
try {
|
|
849
|
+
candidates.push(join(dirname(require.resolve(glRuntimePackageName(tag))), 'bin'));
|
|
850
|
+
} catch {
|
|
851
|
+
// Not installed — the common case; decideOpenGLSource says what that costs.
|
|
852
|
+
}
|
|
853
|
+
for (const dir of candidates) {
|
|
854
|
+
const dll = join(dir, 'opengl32.dll');
|
|
855
|
+
if (existsSync(dll)) return dll;
|
|
856
|
+
}
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** @typedef {{ wddmIcd: string, registryIcd: string, loadedFrom: string }} HostOpenGL */
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Which OpenGL this process should use. PURE, so every branch runs on any host.
|
|
864
|
+
*
|
|
865
|
+
* A host ICD wins by default: Mesa over a real GPU would still work (its d3d12 driver runs on
|
|
866
|
+
* the hardware) but replaces the vendor's driver for this process, which is a regression
|
|
867
|
+
* nobody asked for. An opengl32 that is ALREADY loaded is a fact, not a preference — no
|
|
868
|
+
* override can move it, so it is reported rather than fought. `missing` marks the one case a
|
|
869
|
+
* consumer can fix: no ICD, no package.
|
|
870
|
+
*
|
|
871
|
+
* @param {object} opts
|
|
872
|
+
* @param {string | null} opts.bundled absolute path of the GL package's opengl32.dll, or null
|
|
873
|
+
* @param {HostOpenGL} opts.host `native.probeHostOpenGL()`
|
|
874
|
+
* @param {string} [opts.override] raw `GJSIFY_OPENGL`
|
|
875
|
+
* @returns {{ source: 'bundle' | 'system', reason: string, missing?: true }}
|
|
876
|
+
*/
|
|
877
|
+
export function decideOpenGLSource({ bundled, host, override }) {
|
|
878
|
+
if (host.loadedFrom) return { source: 'system', reason: `opengl32 was already loaded from ${host.loadedFrom}` };
|
|
879
|
+
if (override === 'system') return { source: 'system', reason: 'GJSIFY_OPENGL=system' };
|
|
880
|
+
if (bundled && override === 'bundle') return { source: 'bundle', reason: 'GJSIFY_OPENGL=bundle' };
|
|
881
|
+
if (host.wddmIcd)
|
|
882
|
+
return { source: 'system', reason: `the display driver provides an OpenGL ICD (${host.wddmIcd})` };
|
|
883
|
+
if (host.registryIcd) return { source: 'system', reason: `an OpenGL ICD is registered (${host.registryIcd})` };
|
|
884
|
+
if (!bundled) {
|
|
885
|
+
return {
|
|
886
|
+
source: 'system',
|
|
887
|
+
missing: true,
|
|
888
|
+
reason:
|
|
889
|
+
'this host has no OpenGL driver (only the GDI generic OpenGL 1.1, which GTK rejects), so ' +
|
|
890
|
+
'Gtk.GLArea fails and GSK renders with cairo. Add ' +
|
|
891
|
+
`${glRuntimePackageName('win32-x64')} as a dependency to ship Mesa for such hosts ` +
|
|
892
|
+
'(VMs, RDP sessions, CI), or set GJSIFY_OPENGL=system to accept this',
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
return { source: 'bundle', reason: 'the host has no OpenGL ICD, only the GDI generic OpenGL 1.1' };
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
let openGL = null;
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Apply `decideOpenGLSource` to this process: preload the GL package's opengl32.dll when it
|
|
902
|
+
* wins. MUST run before anything loads gtk-4-1.dll, which imports OPENGL32 statically —
|
|
903
|
+
* index.js calls it right after the addon loads. Strict no-op off win32. Idempotent.
|
|
904
|
+
* @param {{ probeHostOpenGL?: () => HostOpenGL, preloadOpenGL?: (p: string) => string }} native
|
|
905
|
+
* @param {object} [deps] the host facts this reads, so a spec can drive the win32 advice path
|
|
906
|
+
* on a host that has neither win32 nor a bundle: `platform` (default `process.platform`),
|
|
907
|
+
* `bundled` (the GL package's opengl32.dll, default `resolveGlRuntime()`), `windowing`
|
|
908
|
+
* (default `hasWindowingRuntime()`). Every default is the real reading; no production caller
|
|
909
|
+
* passes this — it exists so the warning and the preload fallback are executed by a test at all.
|
|
910
|
+
* @returns {{ source: 'bundle' | 'system', reason: string, loadedFrom: string | null } | null}
|
|
911
|
+
*/
|
|
912
|
+
export function activateBundledOpenGL(native, deps) {
|
|
913
|
+
if (openGL !== null) return openGL || null;
|
|
914
|
+
openGL = false;
|
|
915
|
+
const platform = deps?.platform ?? process.platform;
|
|
916
|
+
if (platform !== 'win32' || typeof native?.probeHostOpenGL !== 'function') return null;
|
|
917
|
+
const candidate = deps?.bundled !== undefined ? deps.bundled : resolveGlRuntime();
|
|
918
|
+
const host = native.probeHostOpenGL();
|
|
919
|
+
const decision = decideOpenGLSource({ bundled: candidate, host, override: process.env.GJSIFY_OPENGL });
|
|
920
|
+
if (decision.source === 'system') {
|
|
921
|
+
// Said once, at the point the process could still be told what to add — the
|
|
922
|
+
// alternative is a GLArea painting "No GL implementation is available" with nothing
|
|
923
|
+
// naming the package. Only where a WINDOWING runtime is present: a headless process
|
|
924
|
+
// never asks for GL, and this would be noise there.
|
|
925
|
+
const canOpenWindows = deps?.windowing ?? hasWindowingRuntime();
|
|
926
|
+
if (decision.missing && canOpenWindows) {
|
|
927
|
+
process.emitWarning(decision.reason, { code: 'GJSIFY_OPENGL_MISSING' });
|
|
928
|
+
}
|
|
929
|
+
openGL = { ...decision, loadedFrom: host.loadedFrom || null };
|
|
930
|
+
return openGL;
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
openGL = { ...decision, loadedFrom: native.preloadOpenGL(candidate) };
|
|
934
|
+
} catch (error) {
|
|
935
|
+
// preloadOpenGL throws with the loader's own reason (a missing libgallium_wgl.dll, a
|
|
936
|
+
// blocked file). The process still runs — GTK falls back to cairo — so this is a
|
|
937
|
+
// warning with the cause, where silence would read as "the host has no GL".
|
|
938
|
+
const reason = `preloading the bundled OpenGL failed: ${error.message}`;
|
|
939
|
+
process.emitWarning(reason, { code: 'GJSIFY_OPENGL_PRELOAD' });
|
|
940
|
+
openGL = { source: 'system', reason, loadedFrom: null };
|
|
941
|
+
}
|
|
942
|
+
return openGL;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** TEST-ONLY: drop the memoized decision so a spec can run the activation again. */
|
|
946
|
+
export function resetOpenGLForTests() {
|
|
947
|
+
openGL = null;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** Whether the GTK in use can open windows — the bundle's windowing marker. */
|
|
951
|
+
function hasWindowingRuntime() {
|
|
952
|
+
return !!windowingBundle();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** What `activateBundledOpenGL` decided for this process, or null when it did not apply. */
|
|
956
|
+
export function openGLActivation() {
|
|
957
|
+
return openGL || null;
|
|
958
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -73,6 +73,17 @@ export function prependSearchPath(path: string): void;
|
|
|
73
73
|
/** Prepend a directory GI searches for the SHARED LIBRARY a typelib names. */
|
|
74
74
|
export function prependLibraryPath(path: string): void;
|
|
75
75
|
|
|
76
|
+
/** win32 only: the host's OpenGL ICDs (display driver / registry) and any opengl32 already loaded; each "" when absent. */
|
|
77
|
+
export const probeHostOpenGL: (() => { wddmIcd: string; registryIcd: string; loadedFrom: string }) | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* win32 only: load an opengl32.dll by ABSOLUTE path and return the path the loaded module
|
|
80
|
+
* reports. THROWS when the loader refuses it (a missing sibling, a blocked file) — the reason
|
|
81
|
+
* is in the message, and the caller treats it as "this host has no usable GL", not a crash.
|
|
82
|
+
*/
|
|
83
|
+
export const preloadOpenGL: ((path: string) => string) | undefined;
|
|
84
|
+
/** win32 only: GL_VENDOR/GL_RENDERER/GL_VERSION of the context current on this thread, or null without one. */
|
|
85
|
+
export const currentGLStrings: (() => { vendor: string; renderer: string; version: string } | null) | undefined;
|
|
86
|
+
|
|
76
87
|
/**
|
|
77
88
|
* Invoke a namespace-level GObject-Introspection function (not an instance
|
|
78
89
|
* method) with primitive/string/object args. OUT and INOUT parameters are
|
|
@@ -591,6 +602,9 @@ declare const native: {
|
|
|
591
602
|
setErrorBuilder: typeof setErrorBuilder;
|
|
592
603
|
prependSearchPath: typeof prependSearchPath;
|
|
593
604
|
prependLibraryPath: typeof prependLibraryPath;
|
|
605
|
+
probeHostOpenGL: typeof probeHostOpenGL;
|
|
606
|
+
preloadOpenGL: typeof preloadOpenGL;
|
|
607
|
+
currentGLStrings: typeof currentGLStrings;
|
|
594
608
|
callFunction: typeof callFunction;
|
|
595
609
|
callMethod: typeof callMethod;
|
|
596
610
|
hasMethod: typeof hasMethod;
|
package/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { hostTarget, nativeCandidates, packageRoot } from './native-paths.js';
|
|
|
11
11
|
import { describeAddonLoadFailure } from './load-diagnostics.js';
|
|
12
12
|
import {
|
|
13
13
|
activateBundledGtkRuntime,
|
|
14
|
+
activateBundledOpenGL,
|
|
14
15
|
activateGiLibraryPath,
|
|
15
16
|
maybePrependGtkRuntimeDllPath,
|
|
16
17
|
maybeReexecForGtkRuntime,
|
|
@@ -128,6 +129,11 @@ try {
|
|
|
128
129
|
// Never fatal: a missing/partial bundle just leaves the host GTK in charge.
|
|
129
130
|
}
|
|
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
|
+
|
|
131
137
|
// Every OTHER staged prebuild typelib (@gjsify/webkit-native and its kin): same
|
|
132
138
|
// env-free treatment, so a namespace whose typelib is installed in node_modules
|
|
133
139
|
// resolves under a plain `node app.mjs` and not only under `gjsify run`. Deliberately
|
|
@@ -305,4 +311,10 @@ export const textdomain = native.textdomain;
|
|
|
305
311
|
export const bindtextdomain = native.bindtextdomain;
|
|
306
312
|
export const localeCategories = native.localeCategories;
|
|
307
313
|
|
|
314
|
+
// win32 only (undefined elsewhere): which OpenGL the process got — see gtk-runtime.js
|
|
315
|
+
// activateBundledOpenGL. currentGLStrings asks the implementation behind the context
|
|
316
|
+
// current on this thread, so a caller can tell Mesa from a vendor driver.
|
|
317
|
+
export const probeHostOpenGL = native.probeHostOpenGL;
|
|
318
|
+
export const currentGLStrings = native.currentGLStrings;
|
|
319
|
+
|
|
308
320
|
export default native;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/node-gi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.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",
|
|
@@ -88,23 +88,23 @@
|
|
|
88
88
|
"build:prebuild": "node-gyp rebuild && node scripts/stage-prebuild.mjs",
|
|
89
89
|
"rebuild": "node-gyp rebuild",
|
|
90
90
|
"clean": "node-gyp clean",
|
|
91
|
-
"test": "
|
|
92
|
-
"test:node": "
|
|
93
|
-
"test:gc": "
|
|
94
|
-
"test:dbus": "dbus-run-session --config-file=test/session.conf -- env
|
|
91
|
+
"test": "node scripts/pinned-env.mjs --test",
|
|
92
|
+
"test:node": "node scripts/pinned-env.mjs --test",
|
|
93
|
+
"test:gc": "node scripts/pinned-env.mjs --test --expose-gc",
|
|
94
|
+
"test:dbus": "dbus-run-session --config-file=test/session.conf -- node scripts/pinned-env.mjs --test --expose-gc test/dbus.test.mjs test/dbus-async.test.mjs",
|
|
95
95
|
"test:bun": "node scripts/cross-runtime.mjs bun",
|
|
96
96
|
"test:conformance": "node scripts/conformance.mjs",
|
|
97
97
|
"test:conformance:gjs-napi": "node scripts/conformance.mjs --runtimes=gjs-napi",
|
|
98
98
|
"test:gimarshalling": "node scripts/gimarshalling.mjs",
|
|
99
99
|
"test:deno": "node scripts/cross-runtime.mjs deno",
|
|
100
|
-
"test:cairo": "
|
|
101
|
-
"test:cairo-canvas2d": "
|
|
102
|
-
"test:gtk": "
|
|
103
|
-
"test:gtk-cairo": "
|
|
104
|
-
"test:adw": "
|
|
105
|
-
"test:gtk-template": "
|
|
106
|
-
"test:gtk-template-callbacks": "
|
|
107
|
-
"test:gtk-template-signals": "
|
|
100
|
+
"test:cairo": "node scripts/pinned-env.mjs --test test/cairo.test.mjs",
|
|
101
|
+
"test:cairo-canvas2d": "node scripts/pinned-env.mjs --test test/cairo-canvas2d.test.mjs",
|
|
102
|
+
"test:gtk": "node scripts/pinned-env.mjs --test test/gtk-smoke.test.mjs",
|
|
103
|
+
"test:gtk-cairo": "node scripts/pinned-env.mjs --test test/cairo-drawfunc.test.mjs",
|
|
104
|
+
"test:adw": "node scripts/pinned-env.mjs --test test/adw-smoke.test.mjs",
|
|
105
|
+
"test:gtk-template": "node scripts/pinned-env.mjs --test test/gtk-template.test.mjs",
|
|
106
|
+
"test:gtk-template-callbacks": "node scripts/pinned-env.mjs --test test/gtk-template-callbacks.test.mjs",
|
|
107
|
+
"test:gtk-template-signals": "node scripts/pinned-env.mjs --test test/gtk-template-signals.test.mjs"
|
|
108
108
|
},
|
|
109
109
|
"dependencies": {
|
|
110
110
|
"node-addon-api": "^8.0.0"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/addon.cc
CHANGED
|
@@ -155,6 +155,7 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
155
155
|
exports.Set("localeCategories", Napi::Function::New(env, LocaleCategories));
|
|
156
156
|
// The native cairo binding + foreign-struct registration (the `__cairo` export).
|
|
157
157
|
InitCairo(env, exports);
|
|
158
|
+
InitOpenGLWin32(env, exports);
|
|
158
159
|
return exports;
|
|
159
160
|
}
|
|
160
161
|
|
package/src/calls.cc
CHANGED
|
@@ -223,58 +223,34 @@ static void NodeGiCallbackTrampoline(ffi_cif* /*cif*/, void* result, void** args
|
|
|
223
223
|
if (cb->scope != GI_SCOPE_TYPE_CALL) depthReset = std::make_unique<SyncEmitDepthReset>();
|
|
224
224
|
|
|
225
225
|
GICallableInfo* ci = cb->info;
|
|
226
|
-
|
|
226
|
+
// Arguments and the answer in gjs's OUT/INOUT shape, shared with the vfunc
|
|
227
|
+
// trampoline (CToJsCall, marshal.cc): a pure OUT is not a JS argument and is
|
|
228
|
+
// written back from the return value. ffi hands each argument's storage as
|
|
229
|
+
// args[i] (a user_data/void IN arg marshals to undefined, which JS ignores).
|
|
230
|
+
CToJsCall call(napiEnv, ci, std::string("callback '") +
|
|
231
|
+
gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ci)) + "'");
|
|
227
232
|
std::vector<napi_value> jsArgs;
|
|
228
|
-
|
|
229
|
-
bool ok = true;
|
|
230
|
-
for (unsigned int i = 0; i < n; i++) {
|
|
231
|
-
GIArgInfo* ai = gi_callable_info_get_arg(ci, i);
|
|
232
|
-
GITypeInfo* ti = gi_arg_info_get_type_info(ai);
|
|
233
|
-
// ffi hands each argument's storage as args[i]; reinterpret as a GIArgument
|
|
234
|
-
// union (a user_data/void arg marshals to undefined, which JS ignores).
|
|
235
|
-
Napi::Value v = GIArgumentToJs(napiEnv, ti, static_cast<GIArgument*>(args[i]), GI_TRANSFER_NOTHING);
|
|
236
|
-
gi_base_info_unref(ti);
|
|
237
|
-
gi_base_info_unref(ai);
|
|
238
|
-
if (napiEnv.IsExceptionPending()) {
|
|
239
|
-
ok = false;
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
jsArgs.push_back(v);
|
|
243
|
-
}
|
|
233
|
+
bool ok = call.MarshalArgs(args, 0, &jsArgs);
|
|
244
234
|
|
|
245
235
|
// Zero the result slot first (it is >= ffi_arg wide; narrow returns leave the
|
|
246
236
|
// upper bytes indeterminate otherwise).
|
|
247
237
|
if (result != nullptr) static_cast<GIArgument*>(result)->v_uint64 = 0;
|
|
248
238
|
|
|
249
|
-
|
|
239
|
+
napi_value ret = nullptr;
|
|
250
240
|
if (ok) {
|
|
251
241
|
napi_value fn = nullptr;
|
|
252
242
|
if (napi_get_reference_value(env, cb->jsFn, &fn) == napi_ok && fn != nullptr) {
|
|
253
243
|
napi_value global = nullptr;
|
|
254
244
|
napi_get_global(env, &global);
|
|
255
|
-
napi_value ret = nullptr;
|
|
256
245
|
// napi_make_callback drains nextTick/microtasks around the call (Node; on
|
|
257
246
|
// Bun/Deno the checkpoint is run by NodeGiMaybeDrainMicrotasks below).
|
|
258
247
|
g_loopDispatchDepth++;
|
|
259
248
|
napi_status st = napi_make_callback(env, nullptr, global, fn, jsArgs.size(), jsArgs.data(), &ret);
|
|
260
249
|
g_loopDispatchDepth--;
|
|
261
|
-
if (st
|
|
262
|
-
GITypeTag rtag = gi_type_info_get_tag(retType);
|
|
263
|
-
if (rtag == GI_TYPE_TAG_UTF8 || rtag == GI_TYPE_TAG_FILENAME) {
|
|
264
|
-
// Hand the caller an owned copy — a JsToGIArgument string would point
|
|
265
|
-
// into a std::string that dies with this frame.
|
|
266
|
-
Napi::Value rv(env, ret);
|
|
267
|
-
static_cast<GIArgument*>(result)->v_string =
|
|
268
|
-
rv.IsString() ? g_strdup(rv.As<Napi::String>().Utf8Value().c_str()) : nullptr;
|
|
269
|
-
} else if (rtag != GI_TYPE_TAG_VOID) {
|
|
270
|
-
std::string held;
|
|
271
|
-
JsToGIArgument(napiEnv, Napi::Value(env, ret), retType, static_cast<GIArgument*>(result),
|
|
272
|
-
&held);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
250
|
+
if (st != napi_ok) ret = nullptr;
|
|
275
251
|
}
|
|
276
252
|
}
|
|
277
|
-
|
|
253
|
+
call.WriteAnswer(ret, result);
|
|
278
254
|
// A CALL-scope callback (e.g. a list `foreach`) runs synchronously inside a
|
|
279
255
|
// JS-initiated GI invoke, so a pending exception must propagate to that JS
|
|
280
256
|
// caller — leave it to surface at the invoke's N-API boundary. A NOTIFIED/ASYNC
|
package/src/class.cc
CHANGED
|
@@ -223,37 +223,27 @@ static void NodeGiVFuncTrampoline(ffi_cif* /*cif*/, void* result, void** args,
|
|
|
223
223
|
|
|
224
224
|
GICallableInfo* ci = reinterpret_cast<GICallableInfo*>(vf->info);
|
|
225
225
|
// args[0] is the instance; declared args follow at args[1..].
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
GObject* instance = static_cast<GObject*>(static_cast<GIArgument*>(args[0])->v_pointer);
|
|
227
|
+
Napi::Value recv = WrapGObject(napiEnv, instance, GI_TRANSFER_NOTHING);
|
|
228
|
+
|
|
229
|
+
// Arguments and the answer in gjs's OUT/INOUT shape, shared with the GI-callback
|
|
230
|
+
// trampoline (CToJsCall, marshal.cc). args[0] is the instance, so the declared
|
|
231
|
+
// arguments start at offset 1. `instance` is also the owner a transfer-none string
|
|
232
|
+
// answer ties its lifetime to (StringForC, marshal.cc) instead of interning it
|
|
233
|
+
// forever — a real vfunc can answer a fresh string every call (e.g.
|
|
234
|
+
// Gtk.Editable's vfunc_get_text on a live-updating buffer).
|
|
235
|
+
CToJsCall call(napiEnv, ci, "vfunc '" + vf->name + "'", instance);
|
|
231
236
|
std::vector<napi_value> jsArgs;
|
|
232
|
-
|
|
233
|
-
bool ok = true;
|
|
234
|
-
for (unsigned int i = 0; i < n; i++) {
|
|
235
|
-
GIArgInfo* ai = gi_callable_info_get_arg(ci, i);
|
|
236
|
-
GITypeInfo* ti = gi_arg_info_get_type_info(ai);
|
|
237
|
-
Napi::Value v =
|
|
238
|
-
GIArgumentToJs(napiEnv, ti, static_cast<GIArgument*>(args[i + 1]), GI_TRANSFER_NOTHING);
|
|
239
|
-
gi_base_info_unref(ti);
|
|
240
|
-
gi_base_info_unref(ai);
|
|
241
|
-
if (napiEnv.IsExceptionPending()) {
|
|
242
|
-
ok = false;
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
jsArgs.push_back(v);
|
|
246
|
-
}
|
|
237
|
+
bool ok = call.MarshalArgs(args, 1, &jsArgs);
|
|
247
238
|
|
|
248
239
|
// Zero the result slot first (it is >= ffi_arg wide; narrow returns leave the
|
|
249
240
|
// upper bytes indeterminate otherwise).
|
|
250
241
|
if (result != nullptr) static_cast<GIArgument*>(result)->v_uint64 = 0;
|
|
251
242
|
|
|
252
|
-
|
|
243
|
+
napi_value ret = nullptr;
|
|
253
244
|
if (ok) {
|
|
254
245
|
napi_value fn = nullptr;
|
|
255
246
|
if (napi_get_reference_value(env, vf->fn, &fn) == napi_ok && fn != nullptr) {
|
|
256
|
-
napi_value ret = nullptr;
|
|
257
247
|
// napi_make_callback drains nextTick/microtasks around the call (Node; on
|
|
258
248
|
// Bun/Deno the checkpoint is run by NodeGiMaybeDrainMicrotasks below); the
|
|
259
249
|
// wrapped instance is the receiver (`this`).
|
|
@@ -261,23 +251,10 @@ static void NodeGiVFuncTrampoline(ffi_cif* /*cif*/, void* result, void** args,
|
|
|
261
251
|
napi_status st =
|
|
262
252
|
napi_make_callback(env, nullptr, recv, fn, jsArgs.size(), jsArgs.data(), &ret);
|
|
263
253
|
g_loopDispatchDepth--;
|
|
264
|
-
if (st
|
|
265
|
-
GITypeTag rtag = gi_type_info_get_tag(retType);
|
|
266
|
-
if (rtag == GI_TYPE_TAG_UTF8 || rtag == GI_TYPE_TAG_FILENAME) {
|
|
267
|
-
// Hand the caller an owned copy — a JsToGIArgument string would point
|
|
268
|
-
// into a std::string that dies with this frame.
|
|
269
|
-
Napi::Value rv(env, ret);
|
|
270
|
-
static_cast<GIArgument*>(result)->v_string =
|
|
271
|
-
rv.IsString() ? g_strdup(rv.As<Napi::String>().Utf8Value().c_str()) : nullptr;
|
|
272
|
-
} else if (rtag != GI_TYPE_TAG_VOID) {
|
|
273
|
-
std::string held;
|
|
274
|
-
JsToGIArgument(napiEnv, Napi::Value(env, ret), retType, static_cast<GIArgument*>(result),
|
|
275
|
-
&held);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
254
|
+
if (st != napi_ok) ret = nullptr;
|
|
278
255
|
}
|
|
279
256
|
}
|
|
280
|
-
|
|
257
|
+
call.WriteAnswer(ret, result);
|
|
281
258
|
// A pending JS exception surfaces at the next N-API boundary (e.g. when the
|
|
282
259
|
// constructType / method call that triggered this vfunc returns).
|
|
283
260
|
//
|