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 +71 -13
- package/lib/binding/node-v127-linux-x64/node_gtk.node +0 -0
- package/lib/loop.js +34 -0
- package/lib/native.js +51 -0
- package/lib/overrides/GLib-2.0.js +12 -9
- package/lib/overrides/Gio-2.0.js +26 -0
- package/lib/overrides/Gtk-3.0.js +17 -12
- package/package.json +1 -1
- package/scripts/windows-bundle-runtime.sh +163 -0
- package/scripts/windows-smoke-test.js +104 -0
- package/src/closure.cc +21 -4
- package/src/function.cc +6 -3
- package/src/gi.cc +5 -0
- package/src/gobject.cc +139 -23
- package/src/loop.cc +39 -3
- package/src/loop.h +2 -0
- package/src/value.cc +24 -0
- package/src/value.h +6 -0
- package/lib/binding/node-v131-linux-x64/node_gtk.node +0 -0
- package/lib/binding/node-v137-linux-x64/node_gtk.node +0 -0
- package/lib/binding/node-v147-linux-x64/node_gtk.node +0 -0
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>
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
Binary file
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|
package/lib/overrides/Gtk-3.0.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
48
|
-
loopStack.pop()
|
|
49
|
-
}
|
|
49
|
+
originalMain()
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
if (userCallingQuit) {
|
|
52
|
+
loopStack.pop()
|
|
53
|
+
}
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
@@ -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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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 ==
|
|
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
|
|
485
|
-
//
|
|
486
|
-
//
|
|
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 *
|
|
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
|
|
112
|
-
*
|
|
113
|
-
|
|
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
|
|
116
|
-
*
|
|
117
|
-
|
|
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 *
|
|
127
|
-
|
|
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
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
288
|
+
delete wrapper;
|
|
289
|
+
return G_SOURCE_REMOVE;
|
|
290
|
+
}
|
|
196
291
|
|
|
197
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
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);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|