node-gtk 2.0.0 → 2.2.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
@@ -10,24 +10,31 @@
10
10
 
11
11
  <h1 align="center">node-gtk</h1>
12
12
  <p align="center">
13
- <b>GNOME Gtk+ bindings for NodeJS</b>
13
+ <b>GTK bindings for NodeJS</b>
14
14
  <br/>
15
15
  <img src="https://img.shields.io/npm/v/node-gtk" alt="Package Version" />
16
16
  </p>
17
17
 
18
- Node-Gtk is a [gobject-introspection](https://gi.readthedocs.io/en/latest) library for nodejs. It makes it possible to
19
- use any introspected library, such as Gtk+, usable. It is similar in essence to [GJS](https://wiki.gnome.org/action/show/Projects/Gjs) or [PyGObject](https://pygobject.readthedocs.io). Please note this project is currently in a _beta_ state and is being developed. Any contributors willing to help
20
- will be welcomed.
18
+ `node-gtk` is a [gobject-introspection](https://gi.readthedocs.io/en/latest) library
19
+ for nodejs. It makes it possible to use any introspected C library, such as GTK,
20
+ usable. It is similar in essence to [GJS](https://wiki.gnome.org/action/show/Projects/Gjs)
21
+ or [PyGObject](https://pygobject.readthedocs.io). Please note this project is
22
+ currently in a _alpha_ state.
21
23
 
22
24
  Supported Node.js versions: **20**, **22**, **24** (other versions may work but are untested)<br>
23
- Pre-built binaries available for: **Linux**, **macOS**
25
+ Supported platforms:
26
+ - **Linux** — prebuilt binaries available
27
+ - **macOS** — prebuilt binaries available
28
+ - **Windows** — prebuilt binaries available (but read [Windows](#windows))
29
+
24
30
 
25
31
  ### Table of contents
26
32
 
27
33
  - [Usage](#usage)
34
+ - [ES modules](#es-modules)
28
35
  - [Documentation](#documentation)
29
36
  - [TypeScript](#typescript)
30
- - [Installing and building](#installing-and-building)
37
+ - [Installing](#installing)
31
38
  - [Contributing](#contributing)
32
39
 
33
40
  ## Usage
@@ -80,9 +87,28 @@ You can also easily create custom applications:
80
87
  <img src="./img/system-monitor.png" style="width: 400px; height: auto;"/>
81
88
  </p>
82
89
 
83
- #### Other projects
90
+ ## ES modules
91
+
92
+ The Usage example above is CommonJS. node-gtk also works under ESM, but the
93
+ blocking main-loop calls (`GLib.MainLoop.run`, `Gio`/`Gtk.Application.run`,
94
+ `Gtk.main`) **return immediately** instead of blocking and **don't return a
95
+ value** — so make the run call the last statement and exit from your handler:
84
96
 
85
- The [react-gtk](https://github.com/codejamninja/react-gtk) project may also allow you to use GTK via React (unmaintained).
97
+ ```javascript
98
+ app.on('activate', () => {
99
+ // ...build the window...
100
+ window.on('close-request', () => (loop.quit(), app.quit(), false));
101
+ window.present();
102
+
103
+ gi.startLoop();
104
+ loop.run(); // returns immediately under ESM; do cleanup/exit in the handler
105
+ });
106
+
107
+ app.run([]); // not `process.exit(app.run([]))` — the return value is unavailable
108
+ ```
109
+
110
+ CommonJS (and signal callbacks) are unaffected. For the why and the design
111
+ trade-off, see [#449](https://github.com/romgrk/node-gtk/issues/449).
86
112
 
87
113
  ## Documentation
88
114
 
@@ -140,9 +166,44 @@ script so it regenerates on install:
140
166
 
141
167
  Run `npx node-gtk generate-types --help` for options.
142
168
 
143
- ## Installing and building
169
+ ## Installing
170
+
171
+ 1. Install `node-gtk` itself
172
+ 2. Install the native libraries you use (see examples per platform below)
173
+
174
+ ```sh
175
+ npm install node-gtk
176
+
177
+ # This installs a prebuilt binary when one is available for your platform and
178
+ # Node.js version, otherwise it falls back to building from source.
179
+ ```
180
+
181
+ #### Linux
182
+
183
+ ```sh
184
+ # archlinux
185
+ pacman -S gtk4
186
+
187
+ # ubuntu
188
+ apt install libgtk-4-1
189
+ ```
144
190
 
145
- See [Installing & building](./doc/installation.md) for prebuilt-binary notes, per-platform build instructions (Linux, macOS, Windows), and how to run the tests and examples.
191
+ #### macOS
192
+
193
+ ```sh
194
+ brew install gtk4
195
+ ```
196
+
197
+ #### Windows
198
+
199
+ Windows doesn't have the dependencies we need in a package manager, therefore
200
+ `node-gtk` ships prebuilt versions of GTK 4 / Adwaita runtime (DLLs, typelibs,
201
+ icons), so `npm install node-gtk` is all you need **if** your dependency is in
202
+ our [list of prebuilt libraries](./windows/runtime-libraries.txt).
203
+
204
+ ### build from source
205
+
206
+ Building from source, or contributing? See [Building from source](./doc/building.md).
146
207
 
147
208
  ## Contributing
148
209
 
@@ -153,6 +214,3 @@ for LSP to work nicely, you can use [bear](https://github.com/rizsotto/Bear) as
153
214
  - https://developer.gnome.org/gi/stable/index.html
154
215
  - https://v8docs.nodesource.com/
155
216
  - https://github.com/nodejs/nan#api
156
-
157
- There is a [Discord channel](https://discord.gg/r2VqPUV) but it receives little monitoring, use github issues or
158
- discussions preferably.
package/lib/loop.js CHANGED
@@ -6,6 +6,7 @@ const internal = require('./native.js')
6
6
 
7
7
  module.exports = {
8
8
  start,
9
+ runLoopEntry,
9
10
  }
10
11
 
11
12
 
@@ -32,6 +33,39 @@ function start() {
32
33
  }
33
34
 
34
35
 
36
+ /**
37
+ * Runs a blocking main-loop entry point (e.g. GLib.MainLoop.run,
38
+ * Gio.Application.run, Gtk.main) so that Promise/async continuations keep
39
+ * draining while it blocks.
40
+ *
41
+ * Under ES modules the top-level body executes as a V8 microtask, so a blocking
42
+ * call made directly from it nests inside V8's microtask drain. V8 refuses
43
+ * nested microtask checkpoints, so any pending Promise/async continuation is
44
+ * starved for the entire lifetime of the loop. Deferring the blocking call to a
45
+ * macrotask lets the module's top-level microtask return first, so the queue
46
+ * drains and the loop integration takes over from a clean (non-nested) state.
47
+ *
48
+ * Under CommonJS (and inside signal callbacks) we are not in a microtask, so we
49
+ * run synchronously and preserve the original blocking semantics exactly.
50
+ *
51
+ * https://github.com/romgrk/node-gtk/issues/442
52
+ *
53
+ * Returns the native call's result when run synchronously (so e.g.
54
+ * `const status = app.run()` keeps working under CommonJS); returns undefined
55
+ * when deferred, since the result is not yet available.
56
+ *
57
+ * @param {Function} run - performs the blocking native call
58
+ * @returns {*} the native return value, or undefined when deferred
59
+ */
60
+ function runLoopEntry(run) {
61
+ if (internal.IsRunningMicrotasks()) {
62
+ setImmediate(run)
63
+ return undefined
64
+ }
65
+ return run()
66
+ }
67
+
68
+
35
69
  // Helpers
36
70
 
37
71
  function wrappedLoopFunction(fn) {
package/lib/native.js CHANGED
@@ -4,10 +4,61 @@
4
4
 
5
5
  const binary = require('@mapbox/node-pre-gyp')
6
6
  const path = require('path')
7
+ const fs = require('fs')
7
8
 
8
9
  const packagePath = path.resolve(path.join(__dirname,'../package.json'))
9
10
  const bindingPath = binary.find(packagePath)
10
11
 
12
+ // On Windows, the prebuilt binary ships with its whole GTK runtime bundled in
13
+ // the same directory as the .node (DLLs, GObject-Introspection typelibs, and
14
+ // runtime data). Wire the process environment to that bundle BEFORE the addon
15
+ // is loaded, so a plain `npm install node-gtk` works with no MSYS2, no compiler
16
+ // and no manual PATH setup. Done in this file specifically because it must run
17
+ // before `require(bindingPath)` (the addon's DLL imports are resolved at load
18
+ // time) and before bootstrap.js requires the GIRepository typelib.
19
+ if (process.platform === 'win32')
20
+ setupBundledRuntime(path.dirname(bindingPath))
21
+
22
+ function setupBundledRuntime(bundleDir) {
23
+ // A self-contained prebuilt always bundles its typelibs; if that marker is
24
+ // absent the addon was built from source against a system GTK, so leave the
25
+ // environment alone.
26
+ const typelibDir = path.join(bundleDir, 'girepository-1.0')
27
+ if (!fs.existsSync(typelibDir))
28
+ return
29
+
30
+ const prepend = (name, value) => {
31
+ const cur = process.env[name]
32
+ process.env[name] = cur ? value + path.delimiter + cur : value
33
+ }
34
+ const exists = p => { try { return fs.existsSync(p) } catch (e) { return false } }
35
+
36
+ // 1) DLLs — both the addon's own imports and the namespace shared libraries
37
+ // GObject-Introspection loads at runtime via g_module_open().
38
+ prepend('PATH', bundleDir)
39
+
40
+ // 2) GI typelibs.
41
+ prepend('GI_TYPELIB_PATH', typelibDir)
42
+
43
+ // 3) Runtime data: icon themes, GtkSourceView languages/styles, schemas.
44
+ const share = path.join(bundleDir, 'share')
45
+ if (exists(share))
46
+ prepend('XDG_DATA_DIRS', share)
47
+ const schemas = path.join(share, 'glib-2.0', 'schemas')
48
+ if (exists(schemas) && !process.env.GSETTINGS_SCHEMA_DIR)
49
+ process.env.GSETTINGS_SCHEMA_DIR = schemas
50
+
51
+ // 4) gdk-pixbuf image loaders (made path-portable at bundle time). The
52
+ // loaders dir goes on PATH so g_module_open() of a bare loader name from
53
+ // the cache resolves.
54
+ const loadersDir = path.join(bundleDir, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders')
55
+ if (exists(loadersDir))
56
+ prepend('PATH', loadersDir)
57
+ const loaderCache = path.join(bundleDir, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders.cache')
58
+ if (exists(loaderCache) && !process.env.GDK_PIXBUF_MODULE_FILE)
59
+ process.env.GDK_PIXBUF_MODULE_FILE = loaderCache
60
+ }
61
+
11
62
  const binding = require(bindingPath)
12
63
 
13
64
  module.exports = binding
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  const internal = require('../native.js')
6
+ const { runLoopEntry } = require('../loop.js')
6
7
 
7
8
  exports.apply = (GLib) => {
8
9
 
@@ -10,17 +11,19 @@ exports.apply = (GLib) => {
10
11
  GLib.MainLoop.prototype._quit = GLib.MainLoop.prototype.quit
11
12
 
12
13
  GLib.MainLoop.prototype.run = function run() {
13
- /* Run before we enter the loop otherwise pending microtasks
14
- * are not run */
15
- process._tickCallback()
16
-
17
14
  const loopStack = internal.GetLoopStack()
18
-
19
15
  loopStack.push(() => this.quit())
20
- this._run()
21
- if (this._userQuit)
22
- loopStack.pop()
23
- delete this._userQuit
16
+
17
+ return runLoopEntry(() => {
18
+ /* Run before we enter the loop otherwise pending microtasks
19
+ * are not run */
20
+ process._tickCallback()
21
+
22
+ this._run()
23
+ if (this._userQuit)
24
+ loopStack.pop()
25
+ delete this._userQuit
26
+ })
24
27
  }
25
28
  GLib.MainLoop.prototype.quit = function quit() {
26
29
  this._userQuit = true
@@ -0,0 +1,26 @@
1
+ /*
2
+ * Gio-2.0.js
3
+ */
4
+
5
+ const { runLoopEntry } = require('../loop.js')
6
+
7
+ exports.apply = (Gio) => {
8
+
9
+ Gio.Application.prototype._run = Gio.Application.prototype.run
10
+
11
+ /* g_application_run() blocks until the application quits. Under ES modules
12
+ * this would starve Promise/async continuations; runLoopEntry() defers the
13
+ * blocking call when needed so they keep draining. See loop.js / #442.
14
+ *
15
+ * Note: when deferred (ESM), the exit status is not available synchronously,
16
+ * so `app.run()` returns undefined instead of the status code in that case. */
17
+ Gio.Application.prototype.run = function run(...args) {
18
+ return runLoopEntry(() => {
19
+ /* Run before we enter the loop otherwise pending microtasks
20
+ * are not run */
21
+ process._tickCallback()
22
+
23
+ return this._run(...args)
24
+ })
25
+ }
26
+ }
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  const internal = require('../native.js')
6
+ const { runLoopEntry } = require('../loop.js')
6
7
 
7
8
  /**
8
9
  * @typedef {Object} Dimension
@@ -27,10 +28,6 @@ exports.apply = (Gtk) => {
27
28
  let userCallingQuit = false
28
29
 
29
30
  Gtk.main = function main() {
30
- /* Run before we enter the loop otherwise pending microtasks
31
- * are not run */
32
- process._tickCallback()
33
-
34
31
  const loopStack = internal.GetLoopStack()
35
32
 
36
33
  /*
@@ -42,17 +39,25 @@ exports.apply = (Gtk) => {
42
39
 
43
40
  loopStack.push(originalQuit)
44
41
 
45
- originalMain()
42
+ /* runLoopEntry() defers the blocking call under ES modules so pending
43
+ * Promise/async continuations keep draining. See loop.js / #442. */
44
+ return runLoopEntry(() => {
45
+ /* Run before we enter the loop otherwise pending microtasks
46
+ * are not run */
47
+ process._tickCallback()
46
48
 
47
- if (userCallingQuit) {
48
- loopStack.pop()
49
- }
49
+ originalMain()
50
50
 
51
- userCallingQuit = false
51
+ if (userCallingQuit) {
52
+ loopStack.pop()
53
+ }
52
54
 
53
- if (Gtk.mainLevel() === 0) {
54
- placeholderIntervalID = clearInterval(placeholderIntervalID)
55
- }
55
+ userCallingQuit = false
56
+
57
+ if (Gtk.mainLevel() === 0) {
58
+ placeholderIntervalID = clearInterval(placeholderIntervalID)
59
+ }
60
+ })
56
61
  }
57
62
 
58
63
  Gtk.mainQuit = function mainQuit() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-gtk",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "GNOME Gtk+ bindings for NodeJS",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -0,0 +1,163 @@
1
+ #!/bin/bash
2
+ #
3
+ # windows-bundle-runtime.sh
4
+ #
5
+ # Make a freshly-built Windows prebuilt self-contained so it can be used WITHOUT
6
+ # MSYS2/MinGW or any compiler on the target machine. We copy the addon's entire
7
+ # transitive MinGW DLL closure next to the .node, plus the GObject-Introspection
8
+ # typelibs and runtime data it needs at runtime.
9
+ #
10
+ # The set of bundled libraries is driven by the curated seed list
11
+ # windows/runtime-libraries.txt (also a user-facing reference). The exact
12
+ # resolved DLL set is written to <binding-dir>/bundled-dlls.txt.
13
+ #
14
+ # Run inside the MINGW64 shell, after building.
15
+ #
16
+ # ./scripts/windows-bundle-runtime.sh lib/binding/node-v127-win32-x64
17
+ #
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
21
+
22
+ BINDING_DIR="${1:?usage: windows-bundle-runtime.sh <binding-dir> [runtime-libraries.txt]}"
23
+ # The curated seed list lives in a text file so it doubles as user-facing
24
+ # reference (windows/runtime-libraries.txt). Override with $2 if needed.
25
+ LIBS_FILE="${2:-$SCRIPT_DIR/../windows/runtime-libraries.txt}"
26
+ # MinGW bin dir the seed names resolve against.
27
+ MB="${MINGW_BIN:-/mingw64/bin}"
28
+
29
+ NODE_FILE="$BINDING_DIR/node_gtk.node"
30
+
31
+ if [ ! -f "$NODE_FILE" ]; then
32
+ echo "error: $NODE_FILE not found"
33
+ ls -la "$BINDING_DIR" || true
34
+ exit 1
35
+ fi
36
+ if [ ! -f "$LIBS_FILE" ]; then
37
+ echo "error: seed list $LIBS_FILE not found"
38
+ exit 1
39
+ fi
40
+
41
+ # GObject-Introspection loads each namespace's shared library at runtime via
42
+ # g_module_open() when you call gi.require('Gtk', ...). Those libraries
43
+ # (libgio, libgtk, libgdk, libpango, libgdk_pixbuf, ...) are NOT linked by
44
+ # node_gtk.node, so ntldd on the addon alone misses them. We therefore seed the
45
+ # closure from the addon AND from the GTK runtime libraries listed in LIBS_FILE.
46
+ echo "## Seed libraries from $LIBS_FILE"
47
+ ENTRY_LIBS=("$NODE_FILE")
48
+ while IFS= read -r line; do
49
+ # strip comments and surrounding whitespace; skip blanks
50
+ name="${line%%#*}"
51
+ name="$(echo "$name" | tr -d '[:space:]')"
52
+ [ -z "$name" ] && continue
53
+ if [ -f "$MB/$name" ]; then
54
+ ENTRY_LIBS+=("$MB/$name")
55
+ echo " - $name"
56
+ else
57
+ echo " (skip missing seed $name)"
58
+ fi
59
+ done < "$LIBS_FILE"
60
+
61
+ echo "## Computing recursive DLL closure for the addon + GTK runtime"
62
+ # ntldd -R prints every transitive dependency with its resolved Windows path:
63
+ # libgtk-3-0.dll => C:\msys64\mingw64\bin\libgtk-3-0.dll (0x...)
64
+ # Collect the union of every entry's closure, keep only MinGW-provided DLLs
65
+ # (skip C:\Windows\System32 OS DLLs), and copy them next to the .node.
66
+ : > /tmp/dll-closure.txt
67
+ for lib in "${ENTRY_LIBS[@]}"; do
68
+ [ -f "$lib" ] || { echo " (skip missing entry $lib)"; continue; }
69
+ # the entry library itself (when it is one of the GTK runtime DLLs)
70
+ case "$lib" in *mingw64*) echo "$lib" >> /tmp/dll-closure.txt ;; esac
71
+ ntldd -R "$lib" \
72
+ | sed -n 's/.* => \(.*\) (0x.*/\1/p' \
73
+ | while IFS= read -r winpath; do
74
+ [ -z "$winpath" ] && continue
75
+ u=$(cygpath -u "$winpath" 2>/dev/null || echo "$winpath")
76
+ case "$u" in *mingw64*) echo "$u" >> /tmp/dll-closure.txt ;; esac
77
+ done
78
+ done
79
+
80
+ copied=0
81
+ sort -u /tmp/dll-closure.txt | while IFS= read -r u; do
82
+ if [ -f "$u" ]; then
83
+ cp -f "$u" "$BINDING_DIR/"
84
+ echo " + $(basename "$u")"
85
+ copied=$((copied + 1))
86
+ fi
87
+ done
88
+ DLL_COUNT=$(ls "$BINDING_DIR"/*.dll 2>/dev/null | wc -l | tr -d ' ')
89
+ echo "## DLLs bundled into $BINDING_DIR ($DLL_COUNT total)"
90
+
91
+ # Write the exact set that shipped, as a reference manifest (ships in the
92
+ # bundle and the artifact). Generated from the curated seed list above.
93
+ MANIFEST="$BINDING_DIR/bundled-dlls.txt"
94
+ {
95
+ echo "# bundled-dlls.txt — Windows DLLs shipped with this node-gtk prebuilt."
96
+ echo "# GENERATED by scripts/windows-bundle-runtime.sh; do not edit by hand."
97
+ echo "# Edit the curated seed list windows/runtime-libraries.txt instead."
98
+ echo "# $DLL_COUNT DLLs, expanded from the seed list via 'ntldd -R'."
99
+ echo "#"
100
+ ( cd "$BINDING_DIR" && ls -1 *.dll 2>/dev/null | sort )
101
+ } > "$MANIFEST"
102
+ echo "## Wrote manifest $MANIFEST"
103
+
104
+ echo "## Bundling GObject-Introspection typelibs"
105
+ TYPELIB_SRC=$(pkg-config --variable=typelibdir gobject-introspection-1.0 2>/dev/null || true)
106
+ if [ -z "$TYPELIB_SRC" ] || [ ! -d "$TYPELIB_SRC" ]; then
107
+ TYPELIB_SRC=/mingw64/lib/girepository-1.0
108
+ fi
109
+ TYPELIB_DST="$BINDING_DIR/girepository-1.0"
110
+ mkdir -p "$TYPELIB_DST"
111
+ # Copy the full typelib set; it is small and guarantees every transitive
112
+ # namespace dependency (Gdk, Pango, cairo, GdkPixbuf, Graphene, ...) is present.
113
+ cp -f "$TYPELIB_SRC"/*.typelib "$TYPELIB_DST/"
114
+ echo "## Typelibs bundled from $TYPELIB_SRC -> $TYPELIB_DST"
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # Runtime DATA a real GTK4/Adwaita app needs at run time (beyond DLLs/typelibs).
118
+ # These are copied so we can MEASURE the realistic bundle size and ship a
119
+ # self-contained app. cp is best-effort: a missing source is not fatal.
120
+ # ---------------------------------------------------------------------------
121
+ copy_tree() { # src dst
122
+ if [ -d "$1" ]; then mkdir -p "$2"; cp -rf "$1"/. "$2"/ 2>/dev/null || true; fi
123
+ }
124
+
125
+ echo "## Bundling runtime data (loaders, schemas, icons, gtksourceview)"
126
+ # gdk-pixbuf image loaders (PNG/SVG/... — needed for icons/images)
127
+ copy_tree /mingw64/lib/gdk-pixbuf-2.0 "$BINDING_DIR/lib/gdk-pixbuf-2.0"
128
+ # compiled GSettings schemas (GTK4/Adw/GtkSourceView read settings)
129
+ copy_tree /mingw64/share/glib-2.0/schemas "$BINDING_DIR/share/glib-2.0/schemas"
130
+ # icon themes (Adwaita + hicolor fallback) — typically the biggest chunk
131
+ copy_tree /mingw64/share/icons/Adwaita "$BINDING_DIR/share/icons/Adwaita"
132
+ copy_tree /mingw64/share/icons/hicolor "$BINDING_DIR/share/icons/hicolor"
133
+ # GtkSourceView language definitions + style schemes
134
+ copy_tree /mingw64/share/gtksourceview-5 "$BINDING_DIR/share/gtksourceview-5"
135
+
136
+ # Make the gdk-pixbuf loaders cache path-portable: the build machine bakes in
137
+ # absolute paths to each loader DLL, which don't exist on the user's machine.
138
+ # Rewrite each loader path to its bare file name; lib/native.js puts the loaders
139
+ # dir on PATH so g_module_open() resolves the bare name at run time.
140
+ LOADER_CACHE="$BINDING_DIR/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"
141
+ if [ -f "$LOADER_CACHE" ]; then
142
+ sed -i -E 's#^"[^"]*[\\/]([^"\\/]+\.dll)"#"\1"#' "$LOADER_CACHE"
143
+ echo "## Rewrote $LOADER_CACHE to portable (bare) loader names"
144
+ fi
145
+
146
+ echo
147
+ echo "## ===== SIZE BREAKDOWN ====="
148
+ size() { # label dir-or-glob
149
+ local label="$1"; shift
150
+ local total
151
+ total=$(du -ch "$@" 2>/dev/null | tail -1 | cut -f1)
152
+ printf ' %-26s %s\n' "$label" "${total:-0}"
153
+ }
154
+ size "DLLs (load-critical)" "$BINDING_DIR"/*.dll
155
+ size "typelibs (load-critical)" "$TYPELIB_DST"
156
+ size " + .node" "$NODE_FILE"
157
+ size "gdk-pixbuf loaders" "$BINDING_DIR/lib/gdk-pixbuf-2.0"
158
+ size "glib schemas" "$BINDING_DIR/share/glib-2.0/schemas"
159
+ size "icons (Adwaita+hicolor)" "$BINDING_DIR/share/icons"
160
+ size "gtksourceview data" "$BINDING_DIR/share/gtksourceview-5"
161
+ echo " --------------------------------------"
162
+ size "TOTAL (uncompressed)" "$BINDING_DIR"
163
+ echo "## =========================="
@@ -0,0 +1,104 @@
1
+ /*
2
+ * windows-smoke-test.js
3
+ *
4
+ * Verifies that a Windows prebuilt + bundled GTK runtime can be loaded and used
5
+ * on a clean machine that has NO MSYS2/MinGW and NO compiler — i.e. exactly what
6
+ * a user gets from `npm install node-gtk`.
7
+ *
8
+ * CRITICAL: this test sets NO environment variables. Everything (DLL search
9
+ * path, GI typelib path, icon/schema/loader data) is wired up automatically by
10
+ * lib/native.js when it loads the bundled prebuilt. The workflow runs this with
11
+ * the runner's own MinGW stripped from PATH, so a pass proves the bundle is
12
+ * fully self-sufficient via the auto-wiring alone.
13
+ */
14
+
15
+ const path = require('path')
16
+ const fs = require('fs')
17
+
18
+ const abi = process.versions.modules
19
+ const bindingDir = path.join(__dirname, '..', 'lib', 'binding', `node-v${abi}-win32-x64`)
20
+ const typelibDir = path.join(bindingDir, 'girepository-1.0')
21
+
22
+ console.log('node:', process.version, '| abi:', abi)
23
+ console.log('binding dir:', bindingDir, '| exists:', fs.existsSync(bindingDir))
24
+ console.log('.node:', fs.existsSync(path.join(bindingDir, 'node_gtk.node')))
25
+ console.log('typelibs:', fs.existsSync(typelibDir))
26
+ if (fs.existsSync(bindingDir)) {
27
+ const dlls = fs.readdirSync(bindingDir).filter(f => f.endsWith('.dll'))
28
+ console.log(`bundled DLLs: ${dlls.length}`)
29
+ }
30
+
31
+ // Require the local package. lib/native.js resolves the prebuilt via
32
+ // node-pre-gyp's binary.find and auto-wires the bundled runtime — NO manual
33
+ // PATH / GI_TYPELIB_PATH / XDG_DATA_DIRS setup here on purpose.
34
+ const gi = require(path.join(__dirname, '..'))
35
+ console.log('OK: require(node-gtk) — prebuilt loaded and runtime auto-wired by native.js')
36
+
37
+ // The full quilx namespace set. Vte (3.91) has no Windows port, so it is
38
+ // expected to be unavailable; everything else must load.
39
+ const REQUIRED = [
40
+ ['GLib', '2.0'], ['GObject', '2.0'], ['Gio', '2.0'],
41
+ ['Pango', '1.0'], ['PangoCairo', '1.0'],
42
+ ['Gdk', '4.0'], ['GdkPixbuf', '2.0'], ['Graphene', '1.0'],
43
+ ['Gtk', '4.0'], ['Adw', '1'], ['GtkSource', '5'],
44
+ ]
45
+ const OPTIONAL = [['Vte', '3.91']]
46
+
47
+ const loaded = {}
48
+ function load(ns, version, optional) {
49
+ try {
50
+ const mod = gi.require(ns, version)
51
+ console.log(`OK: gi.require('${ns}', '${version}')`)
52
+ loaded[ns] = mod
53
+ return mod
54
+ } catch (e) {
55
+ console.log(`${optional ? 'note' : 'FAIL'}: gi.require('${ns}', '${version}') — ${e.message}`)
56
+ if (!optional) throw e
57
+ return null
58
+ }
59
+ }
60
+
61
+ for (const [ns, v] of REQUIRED) load(ns, v, false)
62
+ for (const [ns, v] of OPTIONAL) load(ns, v, true)
63
+
64
+ // Sanity: the typelibs really resolved their symbols.
65
+ if (typeof loaded.GLib.getMonotonicTime !== 'function')
66
+ throw new Error('GLib.getMonotonicTime missing — typelib not really loaded')
67
+ if (typeof loaded.Gtk.Window !== 'function')
68
+ throw new Error('Gtk.Window missing — Gtk4 typelib not really loaded')
69
+ if (typeof loaded.Adw.ApplicationWindow !== 'function')
70
+ throw new Error('Adw.ApplicationWindow missing — libadwaita not really loaded')
71
+
72
+ // Exercise a real GTK4 + Adwaita object graph (needs a display; the runner has one).
73
+ let appOk = false
74
+ try {
75
+ loaded.Gtk.init()
76
+ const win = new loaded.Adw.ApplicationWindow()
77
+ const buffer = new loaded.GtkSource.Buffer()
78
+ const view = new loaded.GtkSource.View()
79
+ view.setBuffer(buffer)
80
+ win.setContent(view)
81
+ console.log('OK: created Adw.ApplicationWindow + GtkSource.View')
82
+ appOk = true
83
+ } catch (e) {
84
+ console.log('note: live widget creation threw (likely no display):', e.message)
85
+ }
86
+ console.log('live GTK4/Adwaita widgets:', appOk ? 'ok' : 'skipped (no display)')
87
+
88
+ // Exercise the bundled gdk-pixbuf image loaders + (portable) loaders.cache by
89
+ // decoding a real PNG. This proves the loader subsystem works from the bundle.
90
+ const PNG_1x1 = Buffer.from(
91
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC',
92
+ 'base64')
93
+ const pngPath = path.join(__dirname, '..', 'smoke-test-pixel.png')
94
+ fs.writeFileSync(pngPath, PNG_1x1)
95
+ try {
96
+ const pixbuf = loaded.GdkPixbuf.Pixbuf.newFromFile(pngPath)
97
+ if (pixbuf.getWidth() !== 1 || pixbuf.getHeight() !== 1)
98
+ throw new Error(`unexpected pixbuf size ${pixbuf.getWidth()}x${pixbuf.getHeight()}`)
99
+ console.log('OK: GdkPixbuf decoded a PNG via the bundled loaders')
100
+ } finally {
101
+ try { fs.unlinkSync(pngPath) } catch (e) { /* ignore */ }
102
+ }
103
+
104
+ console.log('\n=== SMOKE TEST PASSED: GTK4/Adwaita prebuilt usable with NO compiler/MSYS2 ===')
package/src/closure.cc CHANGED
@@ -113,9 +113,19 @@ void Closure::Execute(GICallableInfo *info, guint signal_id,
113
113
  LoadGIArgumentFromPointer(&type_info, &argument);
114
114
  ownership = kNone;
115
115
  } else if (direction == GI_DIRECTION_OUT) {
116
- // Pure out: there is no meaningful incoming value.
117
- memset(&argument, 0, sizeof(argument));
118
- ownership = kNone;
116
+ if (g_arg_info_is_caller_allocates(&arg_info)) {
117
+ // Caller-allocated out (e.g. a GdkRectangle in
118
+ // GtkOverlay::get-child-position): the GValue already holds a
119
+ // pointer to caller (GLib/GTK) memory. Pass a live wrapper so the
120
+ // handler fills it in place — it is NOT written back from the
121
+ // return value (see the output loop below). #444.
122
+ ownership = kNone;
123
+ } else {
124
+ // Callee-allocated out: no meaningful incoming value; the handler
125
+ // supplies it through the return value.
126
+ memset(&argument, 0, sizeof(argument));
127
+ ownership = kNone;
128
+ }
119
129
  }
120
130
 
121
131
  js_args[i - 1] = GIArgumentToV8(&type_info, &argument, -1, ownership);
@@ -156,11 +166,14 @@ void Closure::Execute(GICallableInfo *info, guint signal_id,
156
166
  GIArgInfo out_arg_info;
157
167
  GITypeInfo out_type_info;
158
168
 
169
+ // Caller-allocated out structs are filled in place via the wrapper
170
+ // passed to the handler, so they are not part of the return value.
159
171
  int n_outputs = (g_return_value != NULL) ? 1 : 0;
160
172
  for (guint i = 1; i < n_param_values; i++) {
161
173
  g_callable_info_load_arg(info, i - 1, &out_arg_info);
162
174
  GIDirection d = g_arg_info_get_direction(&out_arg_info);
163
- if (d == GI_DIRECTION_OUT || d == GI_DIRECTION_INOUT)
175
+ if (d == GI_DIRECTION_INOUT ||
176
+ (d == GI_DIRECTION_OUT && !g_arg_info_is_caller_allocates(&out_arg_info)))
164
177
  n_outputs++;
165
178
  }
166
179
 
@@ -184,6 +197,10 @@ void Closure::Execute(GICallableInfo *info, guint signal_id,
184
197
  GIDirection d = g_arg_info_get_direction(&out_arg_info);
185
198
  if (d != GI_DIRECTION_OUT && d != GI_DIRECTION_INOUT)
186
199
  continue;
200
+ // Caller-allocated out structs were filled in place by the handler
201
+ // via their wrapper; nothing to write back from the return value.
202
+ if (d == GI_DIRECTION_OUT && g_arg_info_is_caller_allocates(&out_arg_info))
203
+ continue;
187
204
 
188
205
  g_arg_info_load_type(&out_arg_info, &out_type_info);
189
206
 
package/src/function.cc CHANGED
@@ -481,12 +481,15 @@ Local<Value> FunctionCall (
481
481
  param.data.v_pointer = CaptureTransferContainerElements(
482
482
  &type_info, callable_arg_values[i].v_pointer);
483
483
  }
484
- // For a transfer-full IN boxed (or array of boxed) the callee
485
- // frees the memory; hand it a copy so the JS wrapper's own
486
- // memory isn't double-freed when it's finalized (#409).
484
+ // For a transfer-full IN argument the callee takes ownership.
485
+ // Boxed: hand it a copy so the JS wrapper's own memory isn't
486
+ // double-freed when finalized (#409). GObject: add the reference
487
+ // the callee will own, so it isn't finalized out from under the
488
+ // callee once the wrapper is GC'd (#439).
487
489
  else if (direction == GI_DIRECTION_IN
488
490
  && g_arg_info_get_ownership_transfer(&arg_info) == GI_TRANSFER_EVERYTHING) {
489
491
  CopyBoxedForTransferFullIn(&type_info, &callable_arg_values[i], param.length);
492
+ RefObjectForTransferFullIn(&type_info, &callable_arg_values[i]);
490
493
  }
491
494
  }
492
495
 
package/src/gi.cc CHANGED
@@ -380,6 +380,10 @@ NAN_METHOD(StartLoop) {
380
380
  GNodeJS::StartLoop ();
381
381
  }
382
382
 
383
+ NAN_METHOD(IsRunningMicrotasks) {
384
+ info.GetReturnValue().Set(Nan::New<Boolean>(GNodeJS::IsRunningMicrotasks ()));
385
+ }
386
+
383
387
  NAN_METHOD(GetBaseClass) {
384
388
  auto tpl = GNodeJS::GetBaseClassTemplate ();
385
389
  auto fn = Nan::GetFunction (tpl).ToLocalChecked();
@@ -425,6 +429,7 @@ void InitModule(Local<Object> exports, Local<Value> module, void *priv) {
425
429
  Nan::Export(exports, "ObjectPropertyGetter", ObjectPropertyGetter);
426
430
  Nan::Export(exports, "ObjectPropertySetter", ObjectPropertySetter);
427
431
  Nan::Export(exports, "StartLoop", StartLoop);
432
+ Nan::Export(exports, "IsRunningMicrotasks", IsRunningMicrotasks);
428
433
  Nan::Export(exports, "GetLoopStack", GetLoopStack);
429
434
  Nan::Export(exports, "RegisterClass", RegisterClass);
430
435
  Nan::Export(exports, "RegisterVFunc", RegisterVFunc);
package/src/gobject.cc CHANGED
@@ -34,7 +34,6 @@ namespace GNodeJS {
34
34
  static Nan::Persistent<FunctionTemplate> baseTemplate;
35
35
 
36
36
 
37
- static void GObjectDestroyed(const Nan::WeakCallbackInfo<GObject> &data);
38
37
  static MaybeLocal<FunctionTemplate> GetClassTemplate(GType gtype);
39
38
  static MaybeLocal<Function> GetClass(GType gtype);
40
39
  static void StoreVFunc(GType gtype, Callback *callback);
@@ -100,21 +99,66 @@ out:
100
99
  return gobject;
101
100
  }
102
101
 
102
+ struct GObjectWrapper;
103
+ static void GObjectDestroyedFirstPass(const v8::WeakCallbackInfo<GObjectWrapper> &data);
104
+ static void GObjectDestroyedSecondPass(const v8::WeakCallbackInfo<GObjectWrapper> &data);
105
+ static void GObjectFinalized(gpointer data, GObject *where_the_object_was);
106
+
107
+ struct GObjectWrapper {
108
+ Nan::Persistent<Object> persistent;
109
+ GObject *gobject;
110
+ /* Set to true the moment SetWeak is called. Between that point and the
111
+ * destroy callback actually running, the V8 handle is weak (and, once GC
112
+ * reclaims it, dead). If ToggleNotify fires in that window (because native
113
+ * code adjusts the refcount), touching the persistent crashes. Guard every
114
+ * persistent access with this flag. */
115
+ bool dying = false;
116
+ /* Set in the first-pass weak callback, i.e. the instant GC reclaims the
117
+ * wrapper, before any JS/GTK code resumes. While this is true the
118
+ * persistent is dead but the qdata still points here until the second-pass
119
+ * callback runs; WrapperFromGObject must build a fresh wrapper rather than
120
+ * resurrect this one. */
121
+ bool collected = false;
122
+ };
123
+
103
124
  static void ToggleNotify(gpointer user_data, GObject *gobject, gboolean toggle_down) {
104
125
  void *data = g_object_get_qdata (gobject, GNodeJS::object_quark());
105
126
 
106
127
  g_assert (data != NULL);
107
128
 
108
- auto *persistent = (Nan::Persistent<Object> *) data;
129
+ auto *wrapper = (GObjectWrapper *) data;
130
+
131
+ /* The V8 handle has already been reclaimed by GC (collected) — it is dead
132
+ * and can be made neither weak nor strong. If the object is marshalled
133
+ * again, WrapperFromGObject builds a fresh wrapper. */
134
+ if (wrapper->collected)
135
+ return;
109
136
 
110
137
  if (toggle_down) {
111
- /* We're dropping from 2 refs to 1 ref. We are the last holder. Make
112
- * sure that that our weak ref is installed. */
113
- persistent->SetWeak (gobject, GObjectDestroyed, v8::WeakCallbackType::kParameter);
138
+ /* We're dropping from 2 refs to 1 ref: we are the last holder, so the
139
+ * wrapper may be collected. Install the weak ref (unless it already is). */
140
+ if (wrapper->dying)
141
+ return;
142
+ wrapper->dying = true;
143
+ /* Two-pass weak callback: the first pass runs *during* GC (before any
144
+ * JS/GTK code resumes) and only flips a flag, so WrapperFromGObject can
145
+ * tell a reclaimed wrapper from a live one and never marshals a dead
146
+ * handle to JS. All GObject teardown happens in the second pass — a
147
+ * first-pass callback may not call into GObject. */
148
+ wrapper->persistent.v8::PersistentBase<Object>::SetWeak (
149
+ wrapper, GObjectDestroyedFirstPass, v8::WeakCallbackType::kParameter);
114
150
  } else {
115
- /* We're going from 1 ref to 2 refs. We can't let our wrapper be
116
- * collected, so make sure that our reference is persistent */
117
- persistent->ClearWeak ();
151
+ /* We're going from 1 ref to 2 refs: something other than us now holds
152
+ * the object, so the wrapper must stay alive (strong) until that ref is
153
+ * dropped again. Reviving here is essential — without it a wrapper that
154
+ * went weak once (e.g. a freshly constructed object at refcount 1) would
155
+ * never become strong again when GTK takes ownership, and GC could then
156
+ * collect a wrapper whose GObject is still in use (notably a subclassed
157
+ * widget owned by GTK, losing its overridden vfuncs and instance state). */
158
+ if (!wrapper->dying)
159
+ return;
160
+ wrapper->dying = false;
161
+ wrapper->persistent.ClearWeak ();
118
162
  }
119
163
  }
120
164
 
@@ -123,8 +167,10 @@ static void AssociateGObject(Local<Object> object, GObject *gobject, GType gtype
123
167
 
124
168
  SET_OBJECT_GTYPE(object, gtype);
125
169
 
126
- auto *persistent = new Nan::Persistent<Object>(object);
127
- g_object_set_qdata (gobject, GNodeJS::object_quark(), persistent);
170
+ auto *wrapper = new GObjectWrapper();
171
+ wrapper->gobject = gobject;
172
+ wrapper->persistent.Reset(object);
173
+ g_object_set_qdata (gobject, GNodeJS::object_quark(), wrapper);
128
174
 
129
175
  // Because we can't sink floating ref and add toggle ref at the same time,
130
176
  // first sink the floating ref, add the toggle ref, and then release the
@@ -132,6 +178,18 @@ static void AssociateGObject(Local<Object> object, GObject *gobject, GType gtype
132
178
  g_object_ref_sink (gobject);
133
179
  g_object_add_toggle_ref (gobject, ToggleNotify, NULL);
134
180
  g_object_unref (gobject);
181
+
182
+ // The toggle ref above is supposed to keep the GObject alive for as long as
183
+ // the wrapper exists. A weak ref guards against the case where it doesn't —
184
+ // e.g. a JS-subclassed instance whose refcount is driven to 0 from the GTK
185
+ // side while we still hold the toggle ref: GObjectFinalized then clears the
186
+ // dangling pointer so the destroy callbacks never touch freed memory.
187
+ g_object_weak_ref (gobject, GObjectFinalized, wrapper);
188
+ }
189
+
190
+ static void GObjectFinalized(gpointer data, GObject *where_the_object_was) {
191
+ auto *wrapper = (GObjectWrapper *) data;
192
+ wrapper->gobject = NULL;
135
193
  }
136
194
 
137
195
  static void GObjectConstructor(const FunctionCallbackInfo<Value> &info) {
@@ -183,18 +241,71 @@ static void GObjectConstructor(const FunctionCallbackInfo<Value> &info) {
183
241
  }
184
242
  }
185
243
 
186
- static void GObjectDestroyed(const Nan::WeakCallbackInfo<GObject> &data) {
187
- GObject *gobject = data.GetParameter ();
244
+ static void GObjectDestroyedFirstPass(const v8::WeakCallbackInfo<GObjectWrapper> &data) {
245
+ GObjectWrapper *wrapper = data.GetParameter ();
246
+
247
+ /* This runs *during* GC, where it is not legal to call into V8 (beyond
248
+ * resetting the handle, which the two-pass contract requires) or into
249
+ * GObject — the GObject is not safe to touch here, and doing so crashes in
250
+ * g_type_check_instance_is_fundamentally_a. So only flip a flag and reset
251
+ * the handle; the real teardown is deferred to the second pass.
252
+ *
253
+ * The flag lets WrapperFromGObject distinguish a reclaimed wrapper (whose
254
+ * persistent is now dead) from a live one during the window before the
255
+ * second pass nulls the qdata, so it builds a fresh wrapper instead of
256
+ * handing the dead handle to JS — which crashed on first property access. */
257
+ wrapper->collected = true;
258
+ wrapper->persistent.Reset ();
259
+
260
+ data.SetSecondPassCallback (GObjectDestroyedSecondPass);
261
+ }
188
262
 
189
- void *type_data = g_object_get_qdata (gobject, GNodeJS::object_quark());
190
- auto *persistent = (Nan::Persistent<Object> *) type_data;
191
- delete persistent;
263
+ static gboolean GObjectTeardownIdle(gpointer data) {
264
+ GObjectWrapper *wrapper = (GObjectWrapper *) data;
265
+ GObject *gobject = wrapper->gobject;
266
+
267
+ /* If the GObject was already finalized out from under us, GObjectFinalized
268
+ * cleared the pointer; there is nothing left to detach or unref. */
269
+ if (gobject != NULL) {
270
+ /* Drop the weak ref first so removing the toggle ref (which may finalize
271
+ * the object) doesn't re-enter GObjectFinalized. */
272
+ g_object_weak_unref (gobject, GObjectFinalized, wrapper);
273
+
274
+ /* Only detach the qdata if it still points at us — WrapperFromGObject
275
+ * may have resurrected this GObject with a fresh wrapper while we were
276
+ * pending, and we must not clobber it. */
277
+ if (g_object_get_qdata (gobject, GNodeJS::object_quark()) == wrapper)
278
+ g_object_set_qdata (gobject, GNodeJS::object_quark(), NULL);
279
+
280
+ /* Dropping the last toggle ref disposes the object, and GTK's dispose
281
+ * synchronously emits signals (e.g. ::destroy) into still-connected
282
+ * node-gtk closures — i.e. it re-enters arbitrary JS. That is only legal
283
+ * here because we run from a GLib idle on the main loop, not from the GC
284
+ * second-pass callback that scheduled us (see GObjectDestroyedSecondPass). */
285
+ g_object_remove_toggle_ref (gobject, &ToggleNotify, NULL);
286
+ }
192
287
 
193
- /* We're destroying the wrapper object, so make sure to clear out
194
- * the qdata that points back to us. */
195
- g_object_set_qdata (gobject, GNodeJS::object_quark(), NULL);
288
+ delete wrapper;
289
+ return G_SOURCE_REMOVE;
290
+ }
196
291
 
197
- g_object_remove_toggle_ref (gobject, &ToggleNotify, NULL);
292
+ static void GObjectDestroyedSecondPass(const v8::WeakCallbackInfo<GObjectWrapper> &data) {
293
+ GObjectWrapper *wrapper = data.GetParameter ();
294
+
295
+ /* Defer the actual teardown to a main-loop idle instead of running it here.
296
+ * This callback fires from V8's InvokeSecondPassPhantomCallbacks *during* a
297
+ * garbage collection. Dropping the toggle ref can take the GObject's refcount
298
+ * to zero, and GTK's dispose then emits signals into node-gtk closures,
299
+ * re-entering JS (Nan::Call) — which crashes when invoked mid-GC. Running the
300
+ * teardown from a GLib idle moves the ref drop (and any disposal/signal
301
+ * emission it triggers) to a point where calling into JS is safe again.
302
+ *
303
+ * The GObject stays alive across the window because we still hold the toggle
304
+ * ref; the wrapper (with its now-reset persistent and collected=true) is kept
305
+ * until the idle deletes it. WrapperFromGObject already handles a resurrected
306
+ * GObject during this window by building a fresh wrapper, and the idle's
307
+ * qdata check above won't clobber it. */
308
+ g_idle_add (GObjectTeardownIdle, wrapper);
198
309
  }
199
310
 
200
311
  static void GObjectClassDestroyed(const Nan::WeakCallbackInfo<GType> &info) {
@@ -657,10 +768,15 @@ Local<Value> WrapperFromGObject(GObject *gobject) {
657
768
  void *data = g_object_get_qdata (gobject, GNodeJS::object_quark());
658
769
 
659
770
  if (data) {
660
- /* Easy case: we already have an object. */
661
- auto *persistent = (Nan::Persistent<Object> *) data;
662
- auto obj = New<Object> (*persistent);
663
- return obj;
771
+ auto *wrapper = (GObjectWrapper *) data;
772
+ /* Reuse the existing wrapper unless GC has already reclaimed it (its
773
+ * persistent is dead and only awaiting the second-pass teardown). In
774
+ * that case fall through and build a fresh wrapper; the stale one's
775
+ * second pass is guarded so it won't clobber the new qdata. */
776
+ if (!wrapper->collected) {
777
+ auto obj = New<Object> (wrapper->persistent);
778
+ return obj;
779
+ }
664
780
  }
665
781
 
666
782
  GType gtype = G_OBJECT_TYPE(gobject);
package/src/loop.cc CHANGED
@@ -26,6 +26,8 @@ static Nan::Persistent<Array> loopStack(Nan::New<Array> ());
26
26
  struct uv_loop_source {
27
27
  GSource source;
28
28
  uv_loop_t *loop;
29
+ gpointer fd_tag;
30
+ bool fd_polled;
29
31
  };
30
32
 
31
33
  static gboolean loop_source_prepare (GSource *base, int *timeout) {
@@ -45,6 +47,24 @@ static gboolean loop_source_prepare (GSource *base, int *timeout) {
45
47
 
46
48
  bool loop_alive = uv_loop_alive (source->loop);
47
49
 
50
+ /* Toggle whether GLib polls uv's backend fd. When the loop is dead we must
51
+ * stop polling it: an *unref'd* but still-active uv handle (e.g. the async
52
+ * eventfd left signaling by emscripten/WASM runtimes such as web-tree-sitter,
53
+ * or by worker_threads) keeps the backend epoll fd perpetually readable.
54
+ * uv_loop_alive() reports the loop dead (unref'd handles don't count), so we
55
+ * intend to sleep here, but a polled-and-ready fd makes GLib's poll() return
56
+ * immediately every iteration -> loop_source_dispatch() busy-spins at 100%
57
+ * CPU, starving GTK. Masking the fd lets GLib actually block until a GTK
58
+ * source wakes us; we restore polling as soon as the loop is alive again. */
59
+ #if !OS_WINDOWS
60
+ if (source->fd_tag != NULL && loop_alive != source->fd_polled) {
61
+ g_source_modify_unix_fd (&source->source, source->fd_tag,
62
+ loop_alive ? (GIOCondition) (G_IO_IN | G_IO_OUT | G_IO_ERR)
63
+ : (GIOCondition) 0);
64
+ source->fd_polled = loop_alive;
65
+ }
66
+ #endif
67
+
48
68
  /* If the loop is dead, we can simply sleep forever until a GTK+ source
49
69
  * (presumably) wakes us back up again. */
50
70
  if (!loop_alive)
@@ -92,13 +112,15 @@ static GSourceFuncs uv_loop_source_funcs = {
92
112
  static GSource *loop_source_new (uv_loop_t *loop) {
93
113
  struct uv_loop_source *source = (struct uv_loop_source *) g_source_new (&uv_loop_source_funcs, sizeof (*source));
94
114
  source->loop = loop;
115
+ source->fd_tag = NULL;
116
+ source->fd_polled = true;
95
117
  #if OS_WINDOWS
96
118
  // FIXME
97
119
  // https://github.com/nodejs/node/issues/36015
98
120
  #else
99
- g_source_add_unix_fd (&source->source,
100
- uv_backend_fd (loop),
101
- (GIOCondition) (G_IO_IN | G_IO_OUT | G_IO_ERR));
121
+ source->fd_tag = g_source_add_unix_fd (&source->source,
122
+ uv_backend_fd (loop),
123
+ (GIOCondition) (G_IO_IN | G_IO_OUT | G_IO_ERR));
102
124
  #endif
103
125
  return &source->source;
104
126
  }
@@ -145,6 +167,20 @@ void CallMicrotaskHandlers () {
145
167
  #endif
146
168
  }
147
169
 
170
+ bool IsRunningMicrotasks() {
171
+ /* True while V8 is draining the microtask queue. Under ES modules the
172
+ * top-level body executes as a microtask, so a synchronous blocking call
173
+ * (e.g. g_main_loop_run) made from it nests inside this drain. V8 refuses
174
+ * nested microtask checkpoints, so any Promise/async continuation queued
175
+ * by user code is stuck until the blocking call returns. Callers use this
176
+ * to detect that situation and defer the blocking call to a macrotask so
177
+ * the module's top-level microtask can return and the queue can drain.
178
+ *
179
+ * - https://github.com/romgrk/node-gtk/issues/442
180
+ */
181
+ return MicrotasksScope::IsRunningMicrotasks(Isolate::GetCurrent());
182
+ }
183
+
148
184
  void StartLoop() {
149
185
  GSource *source = loop_source_new (uv_default_loop ());
150
186
  g_source_attach (source, NULL);
package/src/loop.h CHANGED
@@ -12,6 +12,8 @@ namespace GNodeJS {
12
12
 
13
13
  void StartLoop();
14
14
 
15
+ bool IsRunningMicrotasks();
16
+
15
17
  void QuitLoopStack();
16
18
 
17
19
  Local<Array> GetLoopStack();
package/src/value.cc CHANGED
@@ -1650,6 +1650,30 @@ void CopyBoxedForTransferFullIn (GITypeInfo *type_info, GIArgument *arg, long le
1650
1650
  }
1651
1651
  }
1652
1652
 
1653
+ void RefObjectForTransferFullIn (GITypeInfo *type_info, GIArgument *arg) {
1654
+ if (arg->v_pointer == NULL)
1655
+ return;
1656
+
1657
+ if (g_type_info_get_tag(type_info) != GI_TYPE_TAG_INTERFACE)
1658
+ return;
1659
+
1660
+ GIBaseInfo *iface = g_type_info_get_interface(type_info);
1661
+ GIInfoType itype = g_base_info_get_type(iface);
1662
+
1663
+ // The callee owns one reference after the call (transfer-full). node-gtk
1664
+ // keeps only its toggle ref, so without this the callee would effectively
1665
+ // "own" the toggle ref: the refcount never accounts for the new owner, no
1666
+ // toggle-up fires, the wrapper stays weak, and once GC collects it the
1667
+ // destroy callback finalizes the object while the callee still uses it
1668
+ // (e.g. a controller passed to gtk_widget_add_controller). The G_IS_OBJECT
1669
+ // guard skips boxed/fundamental interface types (handled elsewhere).
1670
+ if ((itype == GI_INFO_TYPE_OBJECT || itype == GI_INFO_TYPE_INTERFACE)
1671
+ && G_IS_OBJECT(arg->v_pointer))
1672
+ g_object_ref(arg->v_pointer);
1673
+
1674
+ g_base_info_unref(iface);
1675
+ }
1676
+
1653
1677
 
1654
1678
  /*
1655
1679
  * GValue conversion functions
package/src/value.h CHANGED
@@ -46,6 +46,12 @@ void FreeTransferContainerElements (GITypeInfo *type_info, gpointer capt
46
46
  // See #409.
47
47
  void CopyBoxedForTransferFullIn (GITypeInfo *type_info, GIArgument *arg, long length);
48
48
 
49
+ // For a transfer-full IN GObject argument the callee takes ownership of one
50
+ // reference. node-gtk only holds a toggle ref on the wrapper, so add the
51
+ // reference the callee expects — otherwise the object is finalized out from
52
+ // under the callee once the wrapper is GC'd. See #439.
53
+ void RefObjectForTransferFullIn (GITypeInfo *type_info, GIArgument *arg);
54
+
49
55
  bool CanConvertV8ToGIArgument (GITypeInfo *type_info, Local<Value> value, bool may_be_null);
50
56
 
51
57
  bool V8ToGValue(GValue *gvalue, Local<Value> value, ResourceOwnership ownership = kNone);