node-gtk 2.0.0 → 2.1.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 +42 -0
- package/lib/loop.js +34 -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/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-v127-linux-x64/node_gtk.node +0 -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
|
@@ -66,6 +66,48 @@ process.exit(app.run([]));
|
|
|
66
66
|
<img src="./img/hello-world.png" style="width: 290px; height: auto;"/>
|
|
67
67
|
</p>
|
|
68
68
|
|
|
69
|
+
#### ES modules
|
|
70
|
+
|
|
71
|
+
The example above is CommonJS. node-gtk also works under ES modules, but with one
|
|
72
|
+
behavioral difference around the blocking main-loop calls (`GLib.MainLoop.run`,
|
|
73
|
+
`Gio`/`Gtk.Application.run`, `Gtk.main`).
|
|
74
|
+
|
|
75
|
+
Under ESM, a module's top-level body runs as a V8 *microtask*. If a blocking
|
|
76
|
+
loop call ran synchronously from there, it would nest inside V8's microtask
|
|
77
|
+
drain and starve every `Promise`/`async` continuation (so `await fetch(...)`,
|
|
78
|
+
`fs/promises`, etc. would never settle) for the entire lifetime of the loop —
|
|
79
|
+
see [#442](https://github.com/romgrk/node-gtk/issues/442). To avoid this,
|
|
80
|
+
node-gtk defers the blocking call to the next event-loop tick when it detects it
|
|
81
|
+
is being invoked from within a microtask. Two consequences for ESM code:
|
|
82
|
+
|
|
83
|
+
- The run call **returns immediately** instead of blocking until quit, so any
|
|
84
|
+
code placed *after* it executes *before* the loop. Make the run call the last
|
|
85
|
+
statement, and do cleanup/exit from your quit/`close-request` handler.
|
|
86
|
+
- Its **return value is not available** (e.g. the application's exit status), so
|
|
87
|
+
don't wrap it like `process.exit(app.run([]))` — call `app.run([])` on its own
|
|
88
|
+
and exit from the handler instead.
|
|
89
|
+
|
|
90
|
+
Under CommonJS (and inside signal callbacks) nothing changes: the run calls block
|
|
91
|
+
synchronously and return their value exactly as before.
|
|
92
|
+
|
|
93
|
+
> **Design note (may be reconsidered).** Two approaches were considered for #442:
|
|
94
|
+
>
|
|
95
|
+
> - **Transparent auto-defer (chosen).** node-gtk detects the microtask context
|
|
96
|
+
> and defers the blocking call automatically, so existing ESM code keeps
|
|
97
|
+
> working with no changes. The cost is the leaky behavior above: under ESM the
|
|
98
|
+
> run call no longer blocks, which is surprising and silently breaks idioms
|
|
99
|
+
> like `process.exit(app.run([]))`.
|
|
100
|
+
> - **Explicit async API (alternative).** Keep the blocking calls strictly
|
|
101
|
+
> synchronous and add awaitable variants (e.g. `await loop.runAsync()`),
|
|
102
|
+
> leaving ESM users to opt in. This has clean, predictable semantics and no
|
|
103
|
+
> hidden return-immediately behavior, but it does *not* transparently fix
|
|
104
|
+
> existing ESM code — users must change their code, and plain `loop.run()`
|
|
105
|
+
> would still starve microtasks under ESM (or would need to throw/warn).
|
|
106
|
+
>
|
|
107
|
+
> The transparent approach was chosen to fix existing code out of the box, but
|
|
108
|
+
> given the leaky semantics this trade-off may be revisited — possibly moving to
|
|
109
|
+
> (or also offering) the explicit async API.
|
|
110
|
+
|
|
69
111
|
You can also easily create custom applications:
|
|
70
112
|
|
|
71
113
|
[A web browser (using WebKit2GTK)](./examples/browser.js)
|
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) {
|
|
@@ -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
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
|
|
Binary file
|