@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 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.connect('cancelled', () => console.log('cancelled'));
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
@@ -10,6 +10,7 @@
10
10
  "src/loop.cc",
11
11
  "src/marshal.cc",
12
12
  "src/object.cc",
13
+ "src/opengl-win32.cc",
13
14
  "src/private.cc",
14
15
  "src/repo.cc",
15
16
  "src/signals.cc",
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
- if (gtkSource() !== 'bundle') return;
419
- const bundle = resolveGtkRuntimeBundle();
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.52.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": "LC_ALL=C NODE_GI_NATIVE=build node --test",
92
- "test:node": "LC_ALL=C NODE_GI_NATIVE=build node --test",
93
- "test:gc": "LC_ALL=C NODE_GI_NATIVE=build node --test --expose-gc",
94
- "test:dbus": "dbus-run-session --config-file=test/session.conf -- env LC_ALL=C NODE_GI_NATIVE=build node --test --expose-gc test/dbus.test.mjs test/dbus-async.test.mjs",
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": "LC_ALL=C NODE_GI_NATIVE=build node --test test/cairo.test.mjs",
101
- "test:cairo-canvas2d": "LC_ALL=C NODE_GI_NATIVE=build node --test test/cairo-canvas2d.test.mjs",
102
- "test:gtk": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-smoke.test.mjs",
103
- "test:gtk-cairo": "LC_ALL=C NODE_GI_NATIVE=build node --test test/cairo-drawfunc.test.mjs",
104
- "test:adw": "LC_ALL=C NODE_GI_NATIVE=build node --test test/adw-smoke.test.mjs",
105
- "test:gtk-template": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-template.test.mjs",
106
- "test:gtk-template-callbacks": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-template-callbacks.test.mjs",
107
- "test:gtk-template-signals": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-template-signals.test.mjs"
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
- unsigned int n = gi_callable_info_get_n_args(ci);
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
- jsArgs.reserve(n);
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
- GITypeInfo* retType = gi_callable_info_get_return_type(ci);
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 == napi_ok && result != nullptr) {
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
- gi_base_info_unref(retType);
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
- Napi::Value recv = WrapGObject(
227
- napiEnv, static_cast<GObject*>(static_cast<GIArgument*>(args[0])->v_pointer),
228
- GI_TRANSFER_NOTHING);
229
-
230
- unsigned int n = gi_callable_info_get_n_args(ci);
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
- jsArgs.reserve(n);
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
- GITypeInfo* retType = gi_callable_info_get_return_type(ci);
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 == napi_ok && result != nullptr) {
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
- gi_base_info_unref(retType);
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
  //