@gjsify/node-gi 0.52.0 → 0.53.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
@@ -26,3 +26,34 @@ export function appGiLibraryDirs(opts?: { platform?: NodeJS.Platform | string; e
26
26
  export function activateGiLibraryPath(native: { prependLibraryPath?: (p: string) => void }): string[];
27
27
  /** TEST-ONLY: allow the activation to run again. */
28
28
  export function resetGiLibraryPathForTests(): void;
29
+
30
+ /** What `native.probeHostOpenGL()` reports on win32 — each field "" when absent. */
31
+ export interface HostOpenGL {
32
+ wddmIcd: string;
33
+ registryIcd: string;
34
+ loadedFrom: string;
35
+ }
36
+ /** The OpenGL a win32 process uses: the optional GL package's Mesa opengl32.dll or the host's. */
37
+ export interface OpenGLDecision {
38
+ source: 'bundle' | 'system';
39
+ reason: string;
40
+ /** Set when the host has no ICD and the optional GL package is not installed. */
41
+ missing?: true;
42
+ }
43
+ /** `@gjsify/gl-runtime-<tag>` — the optional package carrying Mesa for driverless hosts. */
44
+ export function glRuntimePackageName(tag?: string): string;
45
+ /** The optional GL package's opengl32.dll (monorepo sibling or installed package), or null. */
46
+ export function resolveGlRuntime(): string | null;
47
+ /** Decide which OpenGL serves GTK on win32 (pure). `override` is `GJSIFY_OPENGL`. */
48
+ export function decideOpenGLSource(opts: {
49
+ bundled: string | null;
50
+ host: HostOpenGL;
51
+ override?: string;
52
+ }): OpenGLDecision;
53
+ /** win32: preload the optional GL package's opengl32.dll when the host has no OpenGL ICD (no-op elsewhere). */
54
+ export function activateBundledOpenGL(native: {
55
+ probeHostOpenGL?: () => HostOpenGL;
56
+ preloadOpenGL?: (path: string) => string;
57
+ }): (OpenGLDecision & { loadedFrom: string | null }) | null;
58
+ /** The decision `activateBundledOpenGL` made for this process, or null. */
59
+ 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
@@ -782,3 +795,143 @@ export function activateBundledGtkRuntime(native) {
782
795
  activated = bundle;
783
796
  return bundle;
784
797
  }
798
+
799
+ // --- OpenGL on win32 (#1097) -------------------------------------------------
800
+ //
801
+ // GTK 4 needs desktop GL >= 3.2 core over WGL on win32 — the gvsbuild GTK is built without
802
+ // EGL, so ANGLE is unreachable, and without Vulkan. A host with no OpenGL ICD (a GPU-less VM,
803
+ // an RDP session, a CI runner) offers only Microsoft's GDI generic OpenGL 1.1, which GDK
804
+ // rejects: every Gtk.GLArea then reads "No GL implementation is available" and GSK falls
805
+ // back to cairo. The OPTIONAL package `@gjsify/gl-runtime-win32-x64` carries Mesa's
806
+ // opengl32.dll for exactly those hosts; it is ~22 MB, so it is a consumer's opt-in and never
807
+ // part of the bundle or a dependency of node-gi (ADR 0023). This decides per process whether
808
+ // it or the host's OpenGL serves GTK. Why it has to be a native preload rather than a PATH
809
+ // entry: src/opengl-win32.cc.
810
+ //
811
+ // `GJSIFY_OPENGL=bundle|system` overrides — `system` keeps a host's OpenGL even when the
812
+ // probe finds no ICD, `bundle` takes Mesa over a vendor driver.
813
+
814
+ /** The optional GL package's npm name for a platform-arch tag. */
815
+ export function glRuntimePackageName(tag = `${process.platform}-${process.arch}`) {
816
+ return `@gjsify/gl-runtime-${tag}`;
817
+ }
818
+
819
+ /**
820
+ * Where the optional GL package's opengl32.dll is, or null. Resolved BY NAME, like the GTK
821
+ * bundle: the monorepo sibling first (what CI builds in place), then the installed package.
822
+ * @returns {string | null}
823
+ */
824
+ export function resolveGlRuntime() {
825
+ const tag = `${process.platform}-${process.arch}`;
826
+ const candidates = [join(here, '..', `gl-runtime-${tag}`, 'bin')];
827
+ try {
828
+ candidates.push(join(dirname(require.resolve(glRuntimePackageName(tag))), 'bin'));
829
+ } catch {
830
+ // Not installed — the common case; decideOpenGLSource says what that costs.
831
+ }
832
+ for (const dir of candidates) {
833
+ const dll = join(dir, 'opengl32.dll');
834
+ if (existsSync(dll)) return dll;
835
+ }
836
+ return null;
837
+ }
838
+
839
+ /** @typedef {{ wddmIcd: string, registryIcd: string, loadedFrom: string }} HostOpenGL */
840
+
841
+ /**
842
+ * Which OpenGL this process should use. PURE, so every branch runs on any host.
843
+ *
844
+ * A host ICD wins by default: Mesa over a real GPU would still work (its d3d12 driver runs on
845
+ * the hardware) but replaces the vendor's driver for this process, which is a regression
846
+ * nobody asked for. An opengl32 that is ALREADY loaded is a fact, not a preference — no
847
+ * override can move it, so it is reported rather than fought. `missing` marks the one case a
848
+ * consumer can fix: no ICD, no package.
849
+ *
850
+ * @param {object} opts
851
+ * @param {string | null} opts.bundled absolute path of the GL package's opengl32.dll, or null
852
+ * @param {HostOpenGL} opts.host `native.probeHostOpenGL()`
853
+ * @param {string} [opts.override] raw `GJSIFY_OPENGL`
854
+ * @returns {{ source: 'bundle' | 'system', reason: string, missing?: true }}
855
+ */
856
+ export function decideOpenGLSource({ bundled, host, override }) {
857
+ if (host.loadedFrom) return { source: 'system', reason: `opengl32 was already loaded from ${host.loadedFrom}` };
858
+ if (override === 'system') return { source: 'system', reason: 'GJSIFY_OPENGL=system' };
859
+ if (bundled && override === 'bundle') return { source: 'bundle', reason: 'GJSIFY_OPENGL=bundle' };
860
+ if (host.wddmIcd)
861
+ return { source: 'system', reason: `the display driver provides an OpenGL ICD (${host.wddmIcd})` };
862
+ if (host.registryIcd) return { source: 'system', reason: `an OpenGL ICD is registered (${host.registryIcd})` };
863
+ if (!bundled) {
864
+ return {
865
+ source: 'system',
866
+ missing: true,
867
+ reason:
868
+ 'this host has no OpenGL driver (only the GDI generic OpenGL 1.1, which GTK rejects), so ' +
869
+ 'Gtk.GLArea fails and GSK renders with cairo. Add ' +
870
+ `${glRuntimePackageName('win32-x64')} as a dependency to ship Mesa for such hosts ` +
871
+ '(VMs, RDP sessions, CI), or set GJSIFY_OPENGL=system to accept this',
872
+ };
873
+ }
874
+ return { source: 'bundle', reason: 'the host has no OpenGL ICD, only the GDI generic OpenGL 1.1' };
875
+ }
876
+
877
+ let openGL = null;
878
+
879
+ /**
880
+ * Apply `decideOpenGLSource` to this process: preload the GL package's opengl32.dll when it
881
+ * wins. MUST run before anything loads gtk-4-1.dll, which imports OPENGL32 statically —
882
+ * index.js calls it right after the addon loads. Strict no-op off win32. Idempotent.
883
+ * @param {{ probeHostOpenGL?: () => HostOpenGL, preloadOpenGL?: (p: string) => string }} native
884
+ * @param {object} [deps] the host facts this reads, so a spec can drive the win32 advice path
885
+ * on a host that has neither win32 nor a bundle: `platform` (default `process.platform`),
886
+ * `bundled` (the GL package's opengl32.dll, default `resolveGlRuntime()`), `windowing`
887
+ * (default `hasWindowingRuntime()`). Every default is the real reading; no production caller
888
+ * passes this — it exists so the warning and the preload fallback are executed by a test at all.
889
+ * @returns {{ source: 'bundle' | 'system', reason: string, loadedFrom: string | null } | null}
890
+ */
891
+ export function activateBundledOpenGL(native, deps) {
892
+ if (openGL !== null) return openGL || null;
893
+ openGL = false;
894
+ const platform = deps?.platform ?? process.platform;
895
+ if (platform !== 'win32' || typeof native?.probeHostOpenGL !== 'function') return null;
896
+ const candidate = deps?.bundled !== undefined ? deps.bundled : resolveGlRuntime();
897
+ const host = native.probeHostOpenGL();
898
+ const decision = decideOpenGLSource({ bundled: candidate, host, override: process.env.GJSIFY_OPENGL });
899
+ if (decision.source === 'system') {
900
+ // Said once, at the point the process could still be told what to add — the
901
+ // alternative is a GLArea painting "No GL implementation is available" with nothing
902
+ // naming the package. Only where a WINDOWING runtime is present: a headless process
903
+ // never asks for GL, and this would be noise there.
904
+ const canOpenWindows = deps?.windowing ?? hasWindowingRuntime();
905
+ if (decision.missing && canOpenWindows) {
906
+ process.emitWarning(decision.reason, { code: 'GJSIFY_OPENGL_MISSING' });
907
+ }
908
+ openGL = { ...decision, loadedFrom: host.loadedFrom || null };
909
+ return openGL;
910
+ }
911
+ try {
912
+ openGL = { ...decision, loadedFrom: native.preloadOpenGL(candidate) };
913
+ } catch (error) {
914
+ // preloadOpenGL throws with the loader's own reason (a missing libgallium_wgl.dll, a
915
+ // blocked file). The process still runs — GTK falls back to cairo — so this is a
916
+ // warning with the cause, where silence would read as "the host has no GL".
917
+ const reason = `preloading the bundled OpenGL failed: ${error.message}`;
918
+ process.emitWarning(reason, { code: 'GJSIFY_OPENGL_PRELOAD' });
919
+ openGL = { source: 'system', reason, loadedFrom: null };
920
+ }
921
+ return openGL;
922
+ }
923
+
924
+ /** TEST-ONLY: drop the memoized decision so a spec can run the activation again. */
925
+ export function resetOpenGLForTests() {
926
+ openGL = null;
927
+ }
928
+
929
+ /** Whether the GTK in use can open windows — the bundle's windowing marker. */
930
+ function hasWindowingRuntime() {
931
+ return !!windowingBundle();
932
+ }
933
+
934
+ /** What `activateBundledOpenGL` decided for this process, or null when it did not apply. */
935
+ export function openGLActivation() {
936
+ return openGL || null;
937
+ }
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.53.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
  //
package/src/common.h CHANGED
@@ -176,6 +176,8 @@ const ForeignStructOps* ForeignOpsForInfo(GIBaseInfo* iface);
176
176
  // from addon.cc's Init; sets the `__cairo` export + registers Context/Surface/
177
177
  // Pattern foreign converters.
178
178
  void InitCairo(Napi::Env env, Napi::Object exports);
179
+ // win32: probeHostOpenGL / preloadOpenGL / currentGLStrings (opengl-win32.cc); a no-op elsewhere.
180
+ void InitOpenGLWin32(Napi::Env env, Napi::Object exports);
179
181
 
180
182
  // ---- GJS-exact 64-bit integer marshalling helpers (shared) -------------------
181
183
  //
@@ -413,6 +415,42 @@ bool IsSupportedContainerType(GITypeInfo* type, std::string* why, ContainerUse u
413
415
  // function-invoke path (calls.cc) and the vfunc chain-up path (class.cc).
414
416
  bool IsSupportedOutType(GITypeInfo* type, std::string* why);
415
417
  size_t CElementSize(GITypeInfo* elem);
418
+ // One call C makes INTO JS — a JS vfunc override (class.cc) or a GI callback
419
+ // (calls.cc) — marshalled in gjs's OUT/INOUT shape (see marshal.cc). Lives on the
420
+ // trampoline's stack under its HandleScope; `ci` is borrowed.
421
+ class CToJsCall {
422
+ public:
423
+ // `owner` is the vfunc's instance (class.cc passes args[0]'s GObject*); nullptr for a
424
+ // plain GI callback (calls.cc), which has no natural object. Only used to tie a
425
+ // transfer-none string ANSWER's lifetime to something bounded (StringForC,
426
+ // marshal.cc) — gjs's own fallback (g_intern_string, unbounded) when there is none.
427
+ CToJsCall(Napi::Env env, GICallableInfo* ci, std::string label, GObject* owner = nullptr);
428
+ ~CToJsCall();
429
+ CToJsCall(const CToJsCall&) = delete;
430
+ CToJsCall& operator=(const CToJsCall&) = delete;
431
+ // The JS arguments from the ffi args (`args[offset + i]` is declared arg i): IN as
432
+ // values, INOUT as their current value, pure OUT omitted. False = exception pending.
433
+ bool MarshalArgs(void** args, unsigned int offset, std::vector<napi_value>* jsArgs);
434
+ // Write JS's answer back: the return value into `result` (already zeroed by the
435
+ // caller), the OUT/INOUT through their pointers. A wrong shape or a failed
436
+ // conversion throws; every pure OUT left unanswered is zeroed, never uninitialised.
437
+ // `ret` is nullptr when the JS call itself failed.
438
+ void WriteAnswer(napi_value ret, void* result);
439
+
440
+ private:
441
+ struct Slot {
442
+ GIArgInfo* ai; // owned
443
+ GITypeInfo* ti; // owned
444
+ gpointer dest; // the caller's variable (or caller-allocated record); may be NULL
445
+ };
446
+ void ThrowUnsupported(const Slot& s) const;
447
+ bool WriteOut(const Slot& s, Napi::Value v);
448
+ Napi::Env env_;
449
+ GICallableInfo* ci_;
450
+ std::string label_;
451
+ GObject* owner_; // borrowed; see the constructor comment
452
+ std::vector<Slot> outs_;
453
+ };
416
454
  void WriteLengthValue(GITypeInfo* lenType, GIArgument* slot, long n);
417
455
 
418
456
  // One IN length-argument autofill, remembered so a SECOND array naming the same length