@gjsify/node-gi 0.13.0 → 0.20.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 +818 -5
- package/binding.gyp +55 -19
- package/cairo.d.ts +187 -0
- package/cairo.js +570 -0
- package/gettext.d.ts +65 -0
- package/gettext.js +128 -0
- package/gi.d.ts +178 -0
- package/gi.js +2018 -23
- package/globals.d.ts +7 -33
- package/globals.js +40 -51
- package/gtk-runtime.d.ts +17 -0
- package/gtk-runtime.js +245 -0
- package/index.d.ts +306 -12
- package/index.js +447 -14
- package/overrides/_signals.js +167 -0
- package/overrides/gio-dbus.js +749 -0
- package/overrides/mainloop.js +60 -0
- package/package.json +45 -4
- package/src/addon.cc +98 -2074
- package/src/cairo.cc +926 -0
- package/src/calls.cc +1425 -0
- package/src/class.cc +1170 -0
- package/src/common.h +624 -0
- package/src/loop.cc +766 -0
- package/src/marshal.cc +1738 -0
- package/src/object.cc +814 -0
- package/src/private.cc +351 -0
- package/src/repo.cc +297 -0
- package/src/signals.cc +535 -0
- package/src/template.cc +116 -0
- package/src/toggle.cc +718 -0
- package/src/variant.cc +587 -0
- package/system.d.ts +49 -0
- package/system.js +116 -0
package/src/loop.cc
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// libuv <-> GLib main loop bridge: startMainLoop (uv-in-GLib GSource) +
|
|
3
|
+
// iterateMainContext + the uv-driven GLib auto-pump (GLib-in-libuv, the
|
|
4
|
+
// non-blocking case).
|
|
5
|
+
|
|
6
|
+
#include "common.h"
|
|
7
|
+
|
|
8
|
+
#include <unordered_map>
|
|
9
|
+
|
|
10
|
+
#ifndef _WIN32
|
|
11
|
+
// g_source_add_unix_fd / g_source_modify_unix_fd embed libuv's backend fd into a
|
|
12
|
+
// GLib GSource — POSIX-only (declared in glib-unix.h; absent on Windows, where
|
|
13
|
+
// GLib splits platform APIs into -Unix/-Win32 namespaces and libuv uses IOCP, not
|
|
14
|
+
// an fd-polling backend). ONLY the UvLoopSource path (the BLOCKING-GLib-main-loop
|
|
15
|
+
// co-pump) is guarded out on Windows; the uv-driven pump below (prepare/check/timer
|
|
16
|
+
// → g_main_context_iteration) has no unix_fd dependency and stays active, so async
|
|
17
|
+
// Gio/timers/DBus still work on Windows (degraded to timed polling — see SyncPumpPolls).
|
|
18
|
+
#include <glib-unix.h>
|
|
19
|
+
#endif
|
|
20
|
+
|
|
21
|
+
namespace nodegi {
|
|
22
|
+
|
|
23
|
+
// ---- libuv <-> GLib main loop bridge (milestone: mainloop) ----
|
|
24
|
+
//
|
|
25
|
+
// Port of node-gtk's src/loop.cc (romgrk and contributors, MIT) to N-API. Nests
|
|
26
|
+
// Node's libuv loop inside GLib's main loop: a GSource polls libuv's backend fd
|
|
27
|
+
// and runs uv_run(UV_RUN_NOWAIT) on dispatch, so a blocking GLib main loop
|
|
28
|
+
// (GLib.MainLoop.run / GApplication.run) keeps Node timers/promises/IO alive —
|
|
29
|
+
// matching GJS, where the GLib loop IS the process loop. Nesting GLib inside uv
|
|
30
|
+
// is impractical (uv exposes no external prepare/check hook), so we nest the
|
|
31
|
+
// other way, exactly as node-gtk does.
|
|
32
|
+
//
|
|
33
|
+
// Main-thread only (worker_threads would need a per-context source); the GLib
|
|
34
|
+
// default context is iterated on the same thread Node runs on.
|
|
35
|
+
struct UvLoopSource {
|
|
36
|
+
GSource source;
|
|
37
|
+
uv_loop_t* loop;
|
|
38
|
+
gpointer fd_tag; // POSIX: the uv_backend_fd poll tag (unused on Windows)
|
|
39
|
+
gboolean fd_polled; // POSIX: whether the backend fd is currently polled
|
|
40
|
+
#ifdef _WIN32
|
|
41
|
+
gint64 win_next_wake_us; // Windows: monotonic time of the next mandatory uv co-pump
|
|
42
|
+
// (G_MAXINT64 = parked: uv is idle, sleep until a GLib source wakes us)
|
|
43
|
+
#endif
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
static napi_env g_loop_env = nullptr; // captured at startMainLoop (main thread)
|
|
47
|
+
static gboolean g_loop_started = FALSE;
|
|
48
|
+
static napi_ref g_process_ref = nullptr; // process
|
|
49
|
+
static napi_ref g_tick_callback_ref = nullptr; // process._tickCallback
|
|
50
|
+
|
|
51
|
+
// TRUE while the uv-driven auto-pump (below) is inside its own GLib work —
|
|
52
|
+
// draining the context or running the prepare/query hint pass. The UvLoopSource
|
|
53
|
+
// consults it to PARK itself (mask the backend fd, report not-ready, skip
|
|
54
|
+
// uv_run): a pump-driven context iteration must never dispatch the uv-in-GLib
|
|
55
|
+
// source, or it would nest uv_run inside the very uv callback that drives the
|
|
56
|
+
// pump (uv_run is not reentrant) — and recursively re-enter the pump itself.
|
|
57
|
+
static gboolean g_in_uv_pump = FALSE;
|
|
58
|
+
|
|
59
|
+
// Drain Node's nextTick queue + run a microtask checkpoint. process._tickCallback
|
|
60
|
+
// invoked through napi_make_callback runs the tick queue, and the surrounding
|
|
61
|
+
// callback scope's close performs the microtask checkpoint — the N-API analogue
|
|
62
|
+
// of node-gtk's CallMicrotaskHandlers (process._tickCallback +
|
|
63
|
+
// Isolate::PerformMicrotaskCheckpoint). Best-effort: skipped if a JS exception is
|
|
64
|
+
// already pending (it will surface when the blocking run() returns).
|
|
65
|
+
//
|
|
66
|
+
// Limitation (node-gtk #442/#121): when the blocking run() is nested inside an
|
|
67
|
+
// outer async callback scope (node:test, an await, a signal handler), V8 defers
|
|
68
|
+
// the checkpoint to that outer scope, so promise continuations queued before the
|
|
69
|
+
// run() do not drain until run() returns. nextTick still drains; timers/I/O the
|
|
70
|
+
// loop dispatches are unaffected. The robust fix lives in L1 (defer the run() to
|
|
71
|
+
// a macrotask when a microtask checkpoint is in progress).
|
|
72
|
+
//
|
|
73
|
+
// Cross-platform: process._tickCallback drains BOTH the nextTick queue AND the
|
|
74
|
+
// microtask checkpoint explicitly (node's task_queues.js calls runMicrotasks() at
|
|
75
|
+
// its end), so this settles an async DBus reply / GLib-timeout-resolved await
|
|
76
|
+
// continuation regardless of callback-scope depth. On Windows it is the timer-driven
|
|
77
|
+
// UvLoopSource (below) that calls this during a blocking GLib loop.
|
|
78
|
+
static void DrainMicrotasks() {
|
|
79
|
+
if (g_loop_env == nullptr || g_tick_callback_ref == nullptr || g_process_ref == nullptr) return;
|
|
80
|
+
napi_env env = g_loop_env;
|
|
81
|
+
bool pending = false;
|
|
82
|
+
if (napi_is_exception_pending(env, &pending) != napi_ok || pending) return;
|
|
83
|
+
|
|
84
|
+
napi_handle_scope scope;
|
|
85
|
+
if (napi_open_handle_scope(env, &scope) != napi_ok) return;
|
|
86
|
+
napi_value process_v = nullptr, tick = nullptr, result = nullptr;
|
|
87
|
+
if (napi_get_reference_value(env, g_process_ref, &process_v) == napi_ok &&
|
|
88
|
+
napi_get_reference_value(env, g_tick_callback_ref, &tick) == napi_ok &&
|
|
89
|
+
process_v != nullptr && tick != nullptr) {
|
|
90
|
+
napi_make_callback(env, nullptr, process_v, tick, 0, nullptr, &result);
|
|
91
|
+
}
|
|
92
|
+
napi_close_handle_scope(env, scope);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- cross-runtime microtask checkpoint (Bun/Deno) --------------------------
|
|
96
|
+
//
|
|
97
|
+
// Node's napi_make_callback performs the nextTick + microtask checkpoint when
|
|
98
|
+
// its callback scope closes, so promise continuations queued by a
|
|
99
|
+
// loop-dispatched GLib→JS callback drain at the callback boundary even while a
|
|
100
|
+
// blocking GLib loop owns the thread. Bun's and Deno's N-API implementations do
|
|
101
|
+
// NOT (Deno's napi_make_callback is a plain Function::Call — refs/deno
|
|
102
|
+
// ext/napi/node_api.rs; Deno's own FFI trampoline drains explicitly for exactly
|
|
103
|
+
// this reason, refs/deno ext/ffi/callback.rs), and with the runtime's event
|
|
104
|
+
// loop paused for the lifetime of a blocking run() the queue never drains — an
|
|
105
|
+
// async (Promise-returning) DBus method handler never sent its reply
|
|
106
|
+
// (client-side "Timeout was reached"), while GJS drains the promise-job queue
|
|
107
|
+
// whenever the last JS frame exits.
|
|
108
|
+
//
|
|
109
|
+
// Portable fix: N-API exposes no engine microtask checkpoint, but both runtimes
|
|
110
|
+
// expose their OWN drain primitive to JS (Bun: `bun:jsc` drainMicrotasks —
|
|
111
|
+
// JSC's VM drain, callable mid-stack by design; Deno: core.runMicrotasks →
|
|
112
|
+
// Isolate::PerformMicrotaskCheckpoint via a reentrant op). index.js registers
|
|
113
|
+
// it here on non-Node runtimes only; the loop-dispatched trampolines
|
|
114
|
+
// (signals.cc / calls.cc / class.cc) invoke NodeGiMaybeDrainMicrotasks at their
|
|
115
|
+
// OUTERMOST boundary (g_loopDispatchDepth == 0). Node never registers — its
|
|
116
|
+
// checkpoint already runs natively — so this is a guaranteed no-op there.
|
|
117
|
+
//
|
|
118
|
+
// NOT drained from a GSource prepare phase: running JS mid-iteration from
|
|
119
|
+
// prepare broke the GDK frame clock (the Excalibur stall — see signals.cc);
|
|
120
|
+
// the drain runs strictly AFTER the dispatched handler returned.
|
|
121
|
+
|
|
122
|
+
// TRUE while the registered drain runs: a drained microtask that re-enters the
|
|
123
|
+
// loop (a nested blocking run dispatching further callbacks) must not recurse
|
|
124
|
+
// into another drain — the engines' own checkpoints refuse re-entry anyway
|
|
125
|
+
// (V8 no-ops while IsRunningMicrotasks; matches Node, where a checkpoint
|
|
126
|
+
// cannot re-enter itself).
|
|
127
|
+
static bool g_in_microtask_drain = false;
|
|
128
|
+
|
|
129
|
+
// setMicrotaskDrain(drain) — register the runtime-native microtask drain for
|
|
130
|
+
// this env. Called by index.js on Bun/Deno only (never on Node).
|
|
131
|
+
Napi::Value SetMicrotaskDrain(const Napi::CallbackInfo& info) {
|
|
132
|
+
Napi::Env env = info.Env();
|
|
133
|
+
if (info.Length() < 1 || !info[0].IsFunction()) {
|
|
134
|
+
Napi::TypeError::New(env, "setMicrotaskDrain(drain: function)").ThrowAsJavaScriptException();
|
|
135
|
+
return env.Undefined();
|
|
136
|
+
}
|
|
137
|
+
NodeGiEnvData* d = EnvData(env);
|
|
138
|
+
if (d == nullptr) return env.Undefined();
|
|
139
|
+
if (d->microtaskDrain != nullptr) {
|
|
140
|
+
napi_delete_reference(env, d->microtaskDrain);
|
|
141
|
+
d->microtaskDrain = nullptr;
|
|
142
|
+
}
|
|
143
|
+
napi_create_reference(env, info[0], 1, &d->microtaskDrain);
|
|
144
|
+
return env.Undefined();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
void NodeGiMaybeDrainMicrotasks(napi_env env) {
|
|
148
|
+
NodeGiEnvData* d = EnvData(env);
|
|
149
|
+
if (d == nullptr || d->microtaskDrain == nullptr) return; // Node: never registered
|
|
150
|
+
if (g_in_microtask_drain) return;
|
|
151
|
+
// A failed callback skips the queues (Node parity: an InternalCallbackScope
|
|
152
|
+
// marked failed skips its task-queue processing) — and NodeGiJsAvailable also
|
|
153
|
+
// covers env teardown, where JS must not be entered at all.
|
|
154
|
+
if (!NodeGiJsAvailable(env)) return;
|
|
155
|
+
napi_handle_scope scope = nullptr;
|
|
156
|
+
if (napi_open_handle_scope(env, &scope) != napi_ok) return;
|
|
157
|
+
napi_value fn = nullptr;
|
|
158
|
+
if (napi_get_reference_value(env, d->microtaskDrain, &fn) == napi_ok && fn != nullptr) {
|
|
159
|
+
napi_value undef = nullptr;
|
|
160
|
+
napi_get_undefined(env, &undef);
|
|
161
|
+
napi_value result = nullptr;
|
|
162
|
+
g_in_microtask_drain = true;
|
|
163
|
+
napi_status st = napi_call_function(env, undef, fn, 0, nullptr, &result);
|
|
164
|
+
g_in_microtask_drain = false;
|
|
165
|
+
// The drain primitive itself must never wedge the loop on a pending
|
|
166
|
+
// exception (microtask exceptions are reported inside the engines' own
|
|
167
|
+
// checkpoints, so this only fires on a broken registration).
|
|
168
|
+
if (st != napi_ok) SurfacePendingException(env, "microtask drain");
|
|
169
|
+
}
|
|
170
|
+
napi_close_handle_scope(env, scope);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#ifndef _WIN32
|
|
174
|
+
static gboolean uv_source_prepare(GSource* base, gint* timeout) {
|
|
175
|
+
UvLoopSource* s = reinterpret_cast<UvLoopSource*>(base);
|
|
176
|
+
|
|
177
|
+
// Parked while the uv-driven auto-pump iterates the context: report not-ready
|
|
178
|
+
// with an infinite timeout and mask the backend fd (the `alive == FALSE` shape
|
|
179
|
+
// below), so a pump-driven iteration never dispatches this source — libuv is
|
|
180
|
+
// already live and running us; co-pumping it from inside itself would nest
|
|
181
|
+
// uv_run reentrantly. Also: while parked the uv backend timeout must not leak
|
|
182
|
+
// into the pump's g_main_context_query() timeout hint (a feedback loop).
|
|
183
|
+
if (g_in_uv_pump) {
|
|
184
|
+
if (s->fd_tag != nullptr && s->fd_polled) {
|
|
185
|
+
g_source_modify_unix_fd(&s->source, s->fd_tag, static_cast<GIOCondition>(0));
|
|
186
|
+
s->fd_polled = FALSE;
|
|
187
|
+
}
|
|
188
|
+
*timeout = -1;
|
|
189
|
+
return FALSE;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
uv_update_time(s->loop);
|
|
193
|
+
DrainMicrotasks();
|
|
194
|
+
|
|
195
|
+
gboolean alive = uv_loop_alive(s->loop);
|
|
196
|
+
// Toggle whether GLib polls uv's backend fd: an unref'd-but-active uv handle
|
|
197
|
+
// keeps the backend fd perpetually ready, which would busy-spin GLib at 100%
|
|
198
|
+
// CPU when the loop is otherwise dead. Mask the fd while dead so GLib actually
|
|
199
|
+
// blocks until a GLib source wakes us; restore it the moment uv is alive again.
|
|
200
|
+
if (s->fd_tag != nullptr && alive != s->fd_polled) {
|
|
201
|
+
g_source_modify_unix_fd(
|
|
202
|
+
&s->source, s->fd_tag,
|
|
203
|
+
alive ? static_cast<GIOCondition>(G_IO_IN | G_IO_OUT | G_IO_ERR) : static_cast<GIOCondition>(0));
|
|
204
|
+
s->fd_polled = alive;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!alive) {
|
|
208
|
+
*timeout = -1; // sleep until a GLib source wakes us
|
|
209
|
+
return FALSE;
|
|
210
|
+
}
|
|
211
|
+
int t = uv_backend_timeout(s->loop);
|
|
212
|
+
*timeout = t;
|
|
213
|
+
return t == 0; // ready immediately when uv has work due now
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
static gboolean uv_source_dispatch(GSource* base, GSourceFunc /*callback*/, gpointer /*user_data*/) {
|
|
217
|
+
// Belt-and-braces for the pump parking above: a stale unmasked-fd readiness
|
|
218
|
+
// from an earlier blocking-loop epoch could still dispatch us once — never
|
|
219
|
+
// nest uv_run inside a pump-driven iteration.
|
|
220
|
+
if (g_in_uv_pump) return G_SOURCE_CONTINUE;
|
|
221
|
+
UvLoopSource* s = reinterpret_cast<UvLoopSource*>(base);
|
|
222
|
+
uv_run(s->loop, UV_RUN_NOWAIT);
|
|
223
|
+
DrainMicrotasks();
|
|
224
|
+
return G_SOURCE_CONTINUE;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
static GSourceFuncs uv_source_funcs = {
|
|
228
|
+
uv_source_prepare, nullptr, uv_source_dispatch, nullptr, nullptr, nullptr,
|
|
229
|
+
};
|
|
230
|
+
#else // _WIN32 — the UvLoopSource, readiness-bounded timer instead of uv-backend-fd-driven.
|
|
231
|
+
//
|
|
232
|
+
// Windows/IOCP has no uv backend fd to embed in a GLib GSource, so the co-pump of
|
|
233
|
+
// Node's loop during a BLOCKING GLib main loop can't wake on backend-fd readiness the
|
|
234
|
+
// way POSIX does. Instead the source schedules its next dispatch off uv's OWN state,
|
|
235
|
+
// using only PUBLIC libuv API (uv_loop_alive + uv_backend_timeout — the same two calls
|
|
236
|
+
// the POSIX prepare uses):
|
|
237
|
+
// • uv idle (no active handles/reqs) → PARK: win_next_wake_us = G_MAXINT64, sleep -1
|
|
238
|
+
// until a GLib source wakes us. The Windows twin of the POSIX backend-fd mask, so a
|
|
239
|
+
// blocking GLib loop with an otherwise-quiet Node loop no longer spins every 5 ms.
|
|
240
|
+
// • uv alive → dispatch after min(uv_backend_timeout, cap): a Node timer due in < cap
|
|
241
|
+
// ms wakes at its actual deadline (not rounded up to a fixed 5 ms), while the CAP
|
|
242
|
+
// bounds the wait so IOCP I/O completions — which arrive on a HANDLE we can NOT embed
|
|
243
|
+
// in GLib's poll (uv_backend_fd() == -1, and loop->iocp is a private libuv struct
|
|
244
|
+
// field we must not depend on) — are still serviced by an unconditional uv_run within
|
|
245
|
+
// cap ms. Full zero-latency IOCP readiness would need that private HANDLE, so the cap
|
|
246
|
+
// stays as the I/O-completion poll (see the honesty note in StartMainLoop).
|
|
247
|
+
// Each dispatch runs uv_run(UV_RUN_NOWAIT) (Node timers/I/O) + DrainMicrotasks() (nextTick
|
|
248
|
+
// + microtask checkpoint via process._tickCallback); the DRAIN settles an async DBus reply
|
|
249
|
+
// (or any GLib-timeout-resolved `await`) posted through a Promise `.then`, exactly as the
|
|
250
|
+
// POSIX fd-driven source does. Parked while the uv-driven pump owns the context (g_in_uv_pump).
|
|
251
|
+
static const gint NODE_GI_WIN_UV_POLL_CAP_MS = 5;
|
|
252
|
+
|
|
253
|
+
// Next mandatory-poll deadline from uv's own next-due time, capped so unwatchable IOCP I/O
|
|
254
|
+
// completions are still serviced within the cap. G_MAXINT64 when uv is idle (park the
|
|
255
|
+
// metronome — sleep until a GLib source wakes us). Public libuv API only.
|
|
256
|
+
static gint64 WinNextUvWake(UvLoopSource* s, gint64 now) {
|
|
257
|
+
if (!uv_loop_alive(s->loop)) return G_MAXINT64;
|
|
258
|
+
int t = uv_backend_timeout(s->loop); // ms to uv's next due work: 0 = now, > 0 = timer, < 0 = I/O-wait
|
|
259
|
+
gint interval = (t >= 0 && t < NODE_GI_WIN_UV_POLL_CAP_MS) ? t : NODE_GI_WIN_UV_POLL_CAP_MS;
|
|
260
|
+
return now + static_cast<gint64>(interval) * 1000;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static gboolean uv_source_prepare(GSource* base, gint* timeout) {
|
|
264
|
+
UvLoopSource* s = reinterpret_cast<UvLoopSource*>(base);
|
|
265
|
+
if (g_in_uv_pump) {
|
|
266
|
+
*timeout = -1;
|
|
267
|
+
return FALSE;
|
|
268
|
+
}
|
|
269
|
+
uv_update_time(s->loop);
|
|
270
|
+
DrainMicrotasks(); // drain before we (maybe) sleep — settles a pending async reply
|
|
271
|
+
const gint64 now = g_source_get_time(base);
|
|
272
|
+
// Un-park if uv became live again since the last dispatch (a GLib-dispatched callback
|
|
273
|
+
// may have armed a Node timer / started Node I/O). While parked, sleep until a GLib
|
|
274
|
+
// source wakes us instead of spinning on a fixed cadence.
|
|
275
|
+
if (s->win_next_wake_us == G_MAXINT64) {
|
|
276
|
+
s->win_next_wake_us = WinNextUvWake(s, now);
|
|
277
|
+
if (s->win_next_wake_us == G_MAXINT64) {
|
|
278
|
+
*timeout = -1;
|
|
279
|
+
return FALSE;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (now >= s->win_next_wake_us) {
|
|
283
|
+
*timeout = 0;
|
|
284
|
+
return TRUE; // due now — dispatch runs uv_run + drain
|
|
285
|
+
}
|
|
286
|
+
gint64 remaining_ms = (s->win_next_wake_us - now + 999) / 1000;
|
|
287
|
+
*timeout = remaining_ms > NODE_GI_WIN_UV_POLL_CAP_MS ? NODE_GI_WIN_UV_POLL_CAP_MS
|
|
288
|
+
: static_cast<gint>(remaining_ms);
|
|
289
|
+
return FALSE;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
static gboolean uv_source_check(GSource* base) {
|
|
293
|
+
UvLoopSource* s = reinterpret_cast<UvLoopSource*>(base);
|
|
294
|
+
if (g_in_uv_pump) return FALSE;
|
|
295
|
+
return g_source_get_time(base) >= s->win_next_wake_us; // never ready while parked (G_MAXINT64)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
static gboolean uv_source_dispatch(GSource* base, GSourceFunc /*callback*/, gpointer /*user_data*/) {
|
|
299
|
+
if (g_in_uv_pump) return G_SOURCE_CONTINUE; // never nest uv_run under a pump iteration
|
|
300
|
+
UvLoopSource* s = reinterpret_cast<UvLoopSource*>(base);
|
|
301
|
+
uv_run(s->loop, UV_RUN_NOWAIT);
|
|
302
|
+
DrainMicrotasks();
|
|
303
|
+
// Re-schedule off uv's post-run state (idle → park; else min(next-due, cap)).
|
|
304
|
+
uv_update_time(s->loop);
|
|
305
|
+
s->win_next_wake_us = WinNextUvWake(s, g_source_get_time(base));
|
|
306
|
+
return G_SOURCE_CONTINUE;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
static GSourceFuncs uv_source_funcs = {
|
|
310
|
+
uv_source_prepare, uv_source_check, uv_source_dispatch, nullptr, nullptr, nullptr,
|
|
311
|
+
};
|
|
312
|
+
#endif // _WIN32 (UvLoopSource: POSIX fd-driven / Windows readiness-bounded co-pump)
|
|
313
|
+
|
|
314
|
+
// iterateMainContext(mayBlock?) -> boolean
|
|
315
|
+
//
|
|
316
|
+
// Iterate the default GLib main context once, dispatching any ready sources (GIO
|
|
317
|
+
// async callbacks, GLib timeouts/idles, DBus). Pure GLib — touches NO libuv — so
|
|
318
|
+
// it is the PORTABLE main-loop primitive on Bun/Deno, where the uv-nesting bridge
|
|
319
|
+
// (startMainLoop) can't run: Deno exports no libuv symbols and Bun panics on
|
|
320
|
+
// uv_backend_fd. The L1 layer drives it from a JS timer (pumpMainContext), so GLib
|
|
321
|
+
// co-pumps while the runtime's own event loop stays in control — GJS's non-blocking
|
|
322
|
+
// main loop reached the other way around. Returns true if a source was dispatched.
|
|
323
|
+
Napi::Value IterateMainContext(const Napi::CallbackInfo& info) {
|
|
324
|
+
Napi::Env env = info.Env();
|
|
325
|
+
bool may_block = info.Length() > 0 && info[0].ToBoolean().Value();
|
|
326
|
+
gboolean dispatched =
|
|
327
|
+
g_main_context_iteration(g_main_context_default(), may_block ? TRUE : FALSE);
|
|
328
|
+
return Napi::Boolean::New(env, dispatched == TRUE);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---- uv-driven GLib auto-pump (the non-blocking case) -----------------------
|
|
332
|
+
//
|
|
333
|
+
// The inverse co-pump of UvLoopSource: with NO blocking GLib loop running, a
|
|
334
|
+
// plain `node bundle.mjs` still needs pending GLib sources (Gio async
|
|
335
|
+
// completions, GLib timeouts/idles, DBus) to dispatch — under GJS the GLib loop
|
|
336
|
+
// IS the process loop, so they always do. Here Node's libuv loop drives the
|
|
337
|
+
// default GLib main context instead:
|
|
338
|
+
//
|
|
339
|
+
// • a uv_prepare + uv_check handle pair drains every READY GLib source once
|
|
340
|
+
// per libuv loop turn (a bounded `g_main_context_iteration(ctx, FALSE)`
|
|
341
|
+
// loop — self-contained prepare/query/poll(0)/check/dispatch cycles, so no
|
|
342
|
+
// cross-phase GLib protocol state is held), then
|
|
343
|
+
// • runs a prepare+query HINT pass to learn (a) when GLib's earliest timer is
|
|
344
|
+
// due — mirrored into a uv_timer so libuv wakes for it — and (b) which fds
|
|
345
|
+
// GLib polls (including the context's cross-thread wakeup eventfd, which a
|
|
346
|
+
// completing GTask worker signals) — mirrored into uv_poll watchers so I/O
|
|
347
|
+
// readiness and cross-thread wakeups end libuv's poll sleep.
|
|
348
|
+
//
|
|
349
|
+
// Keep-alive policy (what keeps the Node process alive, matching what a Node
|
|
350
|
+
// developer expects from in-flight work):
|
|
351
|
+
// • the mirrored uv_timer is REF'd while armed — a due GLib timeout behaves
|
|
352
|
+
// like a due `setTimeout` (a REPEATING GLib timeout therefore keeps Node
|
|
353
|
+
// alive like `setInterval`; under `gjs -m` the process would instead exit
|
|
354
|
+
// once the module settles — the one deliberate lifetime divergence);
|
|
355
|
+
// • one-shot in-flight async operations (a GI scope=async callback such as a
|
|
356
|
+
// GAsyncReadyCallback, counted via NodeGiPumpAsyncBegin/End) REF the pump
|
|
357
|
+
// while pending — an in-flight `read_async` behaves like in-flight Node I/O;
|
|
358
|
+
// • everything else (the prepare/check pair, the fd watchers) stays UNREF'd —
|
|
359
|
+
// the pump alone never keeps a finished program alive, so a purely-sync
|
|
360
|
+
// node-gi program still exits immediately. Consequence: a *passive* GLib fd
|
|
361
|
+
// source with no pending async op (e.g. only a listening Gio.SocketService)
|
|
362
|
+
// does not keep the process alive on its own.
|
|
363
|
+
//
|
|
364
|
+
// Guards: the pump only touches the context at g_main_depth() == 0 — during a
|
|
365
|
+
// blocking GLib.MainLoop.run()/Application.run() (which iterates the context
|
|
366
|
+
// itself, with UvLoopSource co-pumping libuv) every pump callback is a no-op, so
|
|
367
|
+
// there is no double-dispatch. g_in_uv_pump parks UvLoopSource during pump-driven
|
|
368
|
+
// iterations (see uv_source_prepare) so the two co-pumps never nest uv_run.
|
|
369
|
+
//
|
|
370
|
+
// Main-thread only, armed once by StartMainLoop (Node only — Bun/Deno have no
|
|
371
|
+
// usable libuv; they keep the L1 startMainContextPump timer pump).
|
|
372
|
+
|
|
373
|
+
struct PumpPoll {
|
|
374
|
+
uv_poll_t handle;
|
|
375
|
+
int fd;
|
|
376
|
+
int events; // currently-subscribed uv event mask
|
|
377
|
+
gboolean seen; // mark/sweep flag for SyncPumpPolls
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
static gboolean g_pump_inited = FALSE;
|
|
381
|
+
static gboolean g_pump_context_acquired = FALSE;
|
|
382
|
+
static uv_prepare_t g_pump_prepare;
|
|
383
|
+
static uv_check_t g_pump_check;
|
|
384
|
+
static uv_timer_t g_pump_timer;
|
|
385
|
+
static gboolean g_pump_timer_reffed = FALSE;
|
|
386
|
+
static std::unordered_map<int, PumpPoll*>* g_pump_polls = nullptr;
|
|
387
|
+
static std::vector<GPollFD>* g_pump_fds = nullptr; // query scratch (reused)
|
|
388
|
+
static int g_pump_async_pending = 0; // in-flight scope=async GI callbacks
|
|
389
|
+
static gboolean g_pump_prepare_reffed = FALSE;
|
|
390
|
+
static gboolean g_pump_poll_warned = FALSE;
|
|
391
|
+
|
|
392
|
+
// Wake-only callbacks: readiness/expiry just ends libuv's poll sleep; the actual
|
|
393
|
+
// GLib dispatch happens in the check phase of the same loop turn (PumpCheckCb).
|
|
394
|
+
static void PumpTimerCb(uv_timer_t* /*t*/) {}
|
|
395
|
+
static void PumpPollCb(uv_poll_t* /*p*/, int /*status*/, int /*events*/) {}
|
|
396
|
+
|
|
397
|
+
static void PumpPollCloseCb(uv_handle_t* h) {
|
|
398
|
+
delete reinterpret_cast<PumpPoll*>(h->data);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Mirror the queried GLib poll fds into uv_poll watchers (mark/sweep diff — the
|
|
402
|
+
// set rarely changes). Duplicate fds are merged; fds with no events (e.g. uv's
|
|
403
|
+
// own backend fd, masked by the parked UvLoopSource) are skipped. On a watcher
|
|
404
|
+
// failure (fd not pollable / already watched elsewhere in this loop) fall back
|
|
405
|
+
// to a short timer so readiness is still discovered, just later.
|
|
406
|
+
static gboolean SyncPumpPolls(int nfds) {
|
|
407
|
+
#ifdef _WIN32
|
|
408
|
+
// uv_poll requires a SOCKET on Windows, but GLib's queried poll fds are HANDLEs
|
|
409
|
+
// (g_poll uses MsgWaitForMultipleObjects), not pollable via libuv. Report "not
|
|
410
|
+
// fully watched" so PumpArmWakeups falls back to timed main-context polling — the
|
|
411
|
+
// pump still drains ready GLib sources each loop turn (async Gio/timers/DBus
|
|
412
|
+
// work), just discovering readiness on a short cadence instead of on the fd. A
|
|
413
|
+
// HANDLE/IOCP fd-watch integration via the Win32 GLib surface is a follow-up.
|
|
414
|
+
(void)nfds;
|
|
415
|
+
return FALSE;
|
|
416
|
+
#else
|
|
417
|
+
gboolean all_ok = TRUE;
|
|
418
|
+
for (auto& it : *g_pump_polls) it.second->seen = FALSE;
|
|
419
|
+
|
|
420
|
+
// Merge events per fd first (a context can poll one fd from several sources).
|
|
421
|
+
std::unordered_map<int, int> wanted;
|
|
422
|
+
for (int i = 0; i < nfds; i++) {
|
|
423
|
+
const GPollFD& p = (*g_pump_fds)[i];
|
|
424
|
+
if (p.fd < 0 || p.events == 0) continue;
|
|
425
|
+
int ev = 0;
|
|
426
|
+
if (p.events & (G_IO_IN | G_IO_HUP | G_IO_ERR)) ev |= UV_READABLE;
|
|
427
|
+
if (p.events & G_IO_OUT) ev |= UV_WRITABLE;
|
|
428
|
+
if (p.events & G_IO_PRI) ev |= UV_PRIORITIZED;
|
|
429
|
+
if (ev != 0) wanted[p.fd] |= ev;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
for (const auto& [fd, ev] : wanted) {
|
|
433
|
+
auto it = g_pump_polls->find(fd);
|
|
434
|
+
if (it != g_pump_polls->end()) {
|
|
435
|
+
PumpPoll* pp = it->second;
|
|
436
|
+
pp->seen = TRUE;
|
|
437
|
+
if (pp->events != ev) {
|
|
438
|
+
if (uv_poll_start(&pp->handle, ev, PumpPollCb) == 0) {
|
|
439
|
+
pp->events = ev;
|
|
440
|
+
uv_unref(reinterpret_cast<uv_handle_t*>(&pp->handle));
|
|
441
|
+
} else {
|
|
442
|
+
all_ok = FALSE;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
PumpPoll* pp = new PumpPoll();
|
|
448
|
+
pp->fd = fd;
|
|
449
|
+
pp->events = ev;
|
|
450
|
+
pp->seen = TRUE;
|
|
451
|
+
pp->handle.data = pp;
|
|
452
|
+
uv_loop_t* loop = g_pump_prepare.loop;
|
|
453
|
+
if (uv_poll_init(loop, &pp->handle, fd) != 0) {
|
|
454
|
+
delete pp;
|
|
455
|
+
all_ok = FALSE;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (uv_poll_start(&pp->handle, ev, PumpPollCb) != 0) {
|
|
459
|
+
uv_close(reinterpret_cast<uv_handle_t*>(&pp->handle), PumpPollCloseCb);
|
|
460
|
+
all_ok = FALSE;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
uv_unref(reinterpret_cast<uv_handle_t*>(&pp->handle));
|
|
464
|
+
(*g_pump_polls)[fd] = pp;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
for (auto it = g_pump_polls->begin(); it != g_pump_polls->end();) {
|
|
468
|
+
if (it->second->seen) {
|
|
469
|
+
++it;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
PumpPoll* pp = it->second;
|
|
473
|
+
uv_poll_stop(&pp->handle);
|
|
474
|
+
uv_close(reinterpret_cast<uv_handle_t*>(&pp->handle), PumpPollCloseCb);
|
|
475
|
+
it = g_pump_polls->erase(it);
|
|
476
|
+
}
|
|
477
|
+
return all_ok;
|
|
478
|
+
#endif // _WIN32
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Drain every currently-ready GLib source (bounded). Each iteration is a full,
|
|
482
|
+
// self-contained GLib cycle, so readiness (fds, timers, idles) is re-evaluated
|
|
483
|
+
// natively — the uv watchers are pure wake-ups. No-op while a blocking GLib
|
|
484
|
+
// loop is dispatching (g_main_depth() > 0) or when re-entered from a nested
|
|
485
|
+
// uv_run inside our own dispatch (g_in_uv_pump).
|
|
486
|
+
static void PumpDrainContext() {
|
|
487
|
+
if (g_main_depth() > 0 || g_in_uv_pump) return;
|
|
488
|
+
GMainContext* ctx = g_main_context_default();
|
|
489
|
+
// The drain runs from a bare uv_prepare/uv_check C callback — there is NO
|
|
490
|
+
// ambient V8 HandleScope (unlike a blocking run(), which is entered through a
|
|
491
|
+
// JS-initiated N-API frame). Anything the dispatched sources do that creates
|
|
492
|
+
// JS handles outside its own scope (the toggle-ref bridge's
|
|
493
|
+
// napi_get_reference_value on a GTask teardown unref, most prominently) would
|
|
494
|
+
// abort with "Cannot create a handle without a HandleScope" — so provide one
|
|
495
|
+
// for the whole drain.
|
|
496
|
+
napi_handle_scope scope = nullptr;
|
|
497
|
+
if (g_loop_env != nullptr) napi_open_handle_scope(g_loop_env, &scope);
|
|
498
|
+
g_in_uv_pump = TRUE;
|
|
499
|
+
int guard = 0;
|
|
500
|
+
while (g_main_context_iteration(ctx, FALSE) && ++guard < 1000) {
|
|
501
|
+
}
|
|
502
|
+
g_in_uv_pump = FALSE;
|
|
503
|
+
if (scope != nullptr) napi_close_handle_scope(g_loop_env, scope);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// The prepare+query HINT pass: learn GLib's earliest-timer deadline + poll fds
|
|
507
|
+
// and mirror them into the uv timer / uv_poll watchers, so libuv's poll sleep
|
|
508
|
+
// ends exactly when GLib next has work. The context is left "prepared" without a
|
|
509
|
+
// matching check/dispatch — safe: the next g_main_context_iteration() (ours or a
|
|
510
|
+
// blocking loop's) simply re-prepares. Runs only at g_main_depth() == 0; the
|
|
511
|
+
// g_in_uv_pump flag is held across it so UvLoopSource stays parked (masking uv's
|
|
512
|
+
// backend fd out of the queried set — it must not be uv_poll'd, and uv's own
|
|
513
|
+
// backend timeout must not feed back into the timer hint).
|
|
514
|
+
static void PumpArmWakeups() {
|
|
515
|
+
if (!g_pump_inited || g_main_depth() > 0 || g_in_uv_pump) return;
|
|
516
|
+
GMainContext* ctx = g_main_context_default();
|
|
517
|
+
gint prio = 0;
|
|
518
|
+
gint timeout = -1;
|
|
519
|
+
g_in_uv_pump = TRUE;
|
|
520
|
+
g_main_context_prepare(ctx, &prio);
|
|
521
|
+
int nfds = g_main_context_query(ctx, prio, &timeout, g_pump_fds->data(),
|
|
522
|
+
static_cast<gint>(g_pump_fds->size()));
|
|
523
|
+
while (nfds > static_cast<int>(g_pump_fds->size())) {
|
|
524
|
+
g_pump_fds->resize(nfds);
|
|
525
|
+
nfds = g_main_context_query(ctx, prio, &timeout, g_pump_fds->data(),
|
|
526
|
+
static_cast<gint>(g_pump_fds->size()));
|
|
527
|
+
}
|
|
528
|
+
g_in_uv_pump = FALSE;
|
|
529
|
+
|
|
530
|
+
if (!SyncPumpPolls(nfds)) {
|
|
531
|
+
// Degraded wake path: poll at a short cadence instead of on readiness. This is
|
|
532
|
+
// the ALWAYS case on Windows (GLib's poll fds are HANDLEs, not uv_poll-able) and
|
|
533
|
+
// an occasional one on Linux (an fd that isn't uv_poll-able). Force the cadence
|
|
534
|
+
// ONLY while async work is in-flight — its completion arrives on an fd we can't
|
|
535
|
+
// watch, so we must poll to catch it. When nothing is pending, keep the query's
|
|
536
|
+
// own timeout (-1 when GLib is idle) so the loop goes to sleep / the process
|
|
537
|
+
// exits instead of a ref'd timer spinning forever — otherwise a purely-sync
|
|
538
|
+
// program (and every finished conformance test) would never exit on Windows.
|
|
539
|
+
if (!g_pump_poll_warned) {
|
|
540
|
+
g_pump_poll_warned = TRUE;
|
|
541
|
+
g_printerr("(node-gi) warning: could not watch a GLib poll fd from libuv; "
|
|
542
|
+
"falling back to timed main-context polling\n");
|
|
543
|
+
}
|
|
544
|
+
if (g_pump_async_pending > 0 && (timeout < 0 || timeout > 32)) timeout = 32;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (timeout >= 0) {
|
|
548
|
+
uv_timer_start(&g_pump_timer, PumpTimerCb, static_cast<uint64_t>(timeout), 0);
|
|
549
|
+
if (!g_pump_timer_reffed) {
|
|
550
|
+
uv_ref(reinterpret_cast<uv_handle_t*>(&g_pump_timer));
|
|
551
|
+
g_pump_timer_reffed = TRUE;
|
|
552
|
+
}
|
|
553
|
+
} else if (g_pump_timer_reffed) {
|
|
554
|
+
uv_timer_stop(&g_pump_timer);
|
|
555
|
+
uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_timer));
|
|
556
|
+
g_pump_timer_reffed = FALSE;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
static void PumpPrepareCb(uv_prepare_t* /*h*/) {
|
|
561
|
+
PumpDrainContext();
|
|
562
|
+
PumpArmWakeups();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
static void PumpCheckCb(uv_check_t* /*h*/) {
|
|
566
|
+
PumpDrainContext();
|
|
567
|
+
// Re-arm after the drain: the dispatched callbacks may have added/removed
|
|
568
|
+
// sources, and a due-now source must schedule another loop turn (the 0-ms
|
|
569
|
+
// ref'd timer) even when Node itself has nothing left pending.
|
|
570
|
+
PumpArmWakeups();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// pumpKick() -> void — drain ready GLib sources + re-arm the wake hints NOW.
|
|
574
|
+
// The L1 layer calls this from a `process.on('beforeExit')` hook: with only
|
|
575
|
+
// UNREF'd handles active, uv_run(UV_RUN_DEFAULT) returns without ever running
|
|
576
|
+
// the prepare/check callbacks (uv__loop_alive is false at entry), so an
|
|
577
|
+
// otherwise-empty loop would never arm the GLib timer/fd wake-ups and a pending
|
|
578
|
+
// top-level await on, say, a GLib timeout would exit with an unsettled-TLA error
|
|
579
|
+
// before the timeout could fire. beforeExit is emitted exactly at that point;
|
|
580
|
+
// the kick dispatches what is ready and — when GLib still has scheduled work —
|
|
581
|
+
// arms the REF'd uv timer, reviving the loop.
|
|
582
|
+
Napi::Value PumpKick(const Napi::CallbackInfo& info) {
|
|
583
|
+
// Owner env only: a worker's beforeExit must not drive the MAIN loop's GLib
|
|
584
|
+
// context or its uv handles from the worker thread.
|
|
585
|
+
if (g_pump_inited && static_cast<napi_env>(info.Env()) == g_loop_env) {
|
|
586
|
+
PumpDrainContext();
|
|
587
|
+
PumpArmWakeups();
|
|
588
|
+
}
|
|
589
|
+
return info.Env().Undefined();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// The C→JS dispatch window (see common.h): JS run from a pump-driven iteration
|
|
593
|
+
// must not observe g_in_uv_pump, or a blocking loop it starts would keep the
|
|
594
|
+
// UvLoopSource co-pump parked (frozen Node timers for the whole run).
|
|
595
|
+
NodeGiPumpJsDispatchScope::NodeGiPumpJsDispatchScope() : saved_(g_in_uv_pump) {
|
|
596
|
+
g_in_uv_pump = FALSE;
|
|
597
|
+
}
|
|
598
|
+
NodeGiPumpJsDispatchScope::~NodeGiPumpJsDispatchScope() { g_in_uv_pump = saved_; }
|
|
599
|
+
|
|
600
|
+
static void PumpInit(uv_loop_t* loop) {
|
|
601
|
+
if (g_pump_inited) return;
|
|
602
|
+
g_pump_polls = new std::unordered_map<int, PumpPoll*>();
|
|
603
|
+
g_pump_fds = new std::vector<GPollFD>(16);
|
|
604
|
+
|
|
605
|
+
// Own the default context on this (the JS) thread. Required so a cross-thread
|
|
606
|
+
// g_source_attach (a completing GTask worker) signals the context's wakeup
|
|
607
|
+
// eventfd — which our uv_poll watcher turns into a libuv wake. Same-thread
|
|
608
|
+
// recursive acquisition by a later blocking g_main_loop_run() still works.
|
|
609
|
+
g_pump_context_acquired = g_main_context_acquire(g_main_context_default());
|
|
610
|
+
if (!g_pump_context_acquired) {
|
|
611
|
+
g_printerr("(node-gi) warning: default GMainContext is owned by another thread; "
|
|
612
|
+
"cross-thread wakeups may be delayed\n");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
uv_prepare_init(loop, &g_pump_prepare);
|
|
616
|
+
uv_prepare_start(&g_pump_prepare, PumpPrepareCb);
|
|
617
|
+
uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_prepare));
|
|
618
|
+
|
|
619
|
+
uv_check_init(loop, &g_pump_check);
|
|
620
|
+
uv_check_start(&g_pump_check, PumpCheckCb);
|
|
621
|
+
uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_check));
|
|
622
|
+
|
|
623
|
+
uv_timer_init(loop, &g_pump_timer);
|
|
624
|
+
uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_timer));
|
|
625
|
+
|
|
626
|
+
g_pump_inited = TRUE;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// In-flight scope=async keep-alive: while any GI scope=async callback (a
|
|
630
|
+
// GAsyncReadyCallback) is pending, REF the (always-active) prepare handle so the
|
|
631
|
+
// libuv loop stays alive until the completion arrives — the exact analogue of
|
|
632
|
+
// Node's own in-flight I/O keeping the process alive, and the reason a plain
|
|
633
|
+
// top-level `await` on a Gio async op settles instead of exiting with an
|
|
634
|
+
// unsettled-TLA error. Main-thread only; counted only for the pump-owning env.
|
|
635
|
+
bool NodeGiPumpAsyncBegin(napi_env env) {
|
|
636
|
+
if (!g_pump_inited || env != g_loop_env) return false;
|
|
637
|
+
if (++g_pump_async_pending == 1 && !g_pump_prepare_reffed) {
|
|
638
|
+
uv_ref(reinterpret_cast<uv_handle_t*>(&g_pump_prepare));
|
|
639
|
+
g_pump_prepare_reffed = TRUE;
|
|
640
|
+
}
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
void NodeGiPumpAsyncEnd() {
|
|
645
|
+
if (!g_pump_inited || g_pump_async_pending == 0) return;
|
|
646
|
+
if (--g_pump_async_pending == 0 && g_pump_prepare_reffed) {
|
|
647
|
+
uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_prepare));
|
|
648
|
+
g_pump_prepare_reffed = FALSE;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Env-teardown: close the pump's uv handles so the loop can wind down cleanly.
|
|
653
|
+
// Only the env that armed the pump (the main env) tears it down.
|
|
654
|
+
void NodeGiPumpShutdown(napi_env env) {
|
|
655
|
+
if (!g_pump_inited || env != g_loop_env) return;
|
|
656
|
+
g_pump_inited = FALSE;
|
|
657
|
+
uv_prepare_stop(&g_pump_prepare);
|
|
658
|
+
uv_check_stop(&g_pump_check);
|
|
659
|
+
uv_timer_stop(&g_pump_timer);
|
|
660
|
+
uv_close(reinterpret_cast<uv_handle_t*>(&g_pump_prepare), nullptr);
|
|
661
|
+
uv_close(reinterpret_cast<uv_handle_t*>(&g_pump_check), nullptr);
|
|
662
|
+
uv_close(reinterpret_cast<uv_handle_t*>(&g_pump_timer), nullptr);
|
|
663
|
+
if (g_pump_polls != nullptr) {
|
|
664
|
+
for (auto& it : *g_pump_polls) {
|
|
665
|
+
uv_poll_stop(&it.second->handle);
|
|
666
|
+
uv_close(reinterpret_cast<uv_handle_t*>(&it.second->handle), PumpPollCloseCb);
|
|
667
|
+
}
|
|
668
|
+
delete g_pump_polls;
|
|
669
|
+
g_pump_polls = nullptr;
|
|
670
|
+
}
|
|
671
|
+
delete g_pump_fds;
|
|
672
|
+
g_pump_fds = nullptr;
|
|
673
|
+
if (g_pump_context_acquired) {
|
|
674
|
+
g_main_context_release(g_main_context_default());
|
|
675
|
+
g_pump_context_acquired = FALSE;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// startMainLoop() -> void
|
|
680
|
+
// Attach the libuv-backed GSource to the default GLib main context (idempotent).
|
|
681
|
+
// Harmless until a GLib main loop actually runs — it adds no uv handle, so it
|
|
682
|
+
// neither keeps Node alive nor runs uv on its own; it only pumps uv while a GLib
|
|
683
|
+
// loop is iterating. The L1 layer calls this once when a namespace is required.
|
|
684
|
+
// Also arms the uv-driven GLib auto-pump (above), so pending GLib sources keep
|
|
685
|
+
// dispatching WITHOUT a blocking GLib loop — the plain `node bundle.mjs` case.
|
|
686
|
+
Napi::Value StartMainLoop(const Napi::CallbackInfo& info) {
|
|
687
|
+
Napi::Env env = info.Env();
|
|
688
|
+
if (g_loop_started) return env.Undefined();
|
|
689
|
+
|
|
690
|
+
uv_loop_t* loop = nullptr;
|
|
691
|
+
if (napi_get_uv_event_loop(env, &loop) != napi_ok || loop == nullptr) {
|
|
692
|
+
Napi::Error::New(env, "failed to obtain the libuv event loop").ThrowAsJavaScriptException();
|
|
693
|
+
return env.Undefined();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Capture env + process._tickCallback for nextTick/microtask draining.
|
|
697
|
+
g_loop_env = env;
|
|
698
|
+
napi_value global = nullptr, process_v = nullptr, tick = nullptr;
|
|
699
|
+
if (napi_get_global(env, &global) == napi_ok &&
|
|
700
|
+
napi_get_named_property(env, global, "process", &process_v) == napi_ok &&
|
|
701
|
+
process_v != nullptr) {
|
|
702
|
+
napi_create_reference(env, process_v, 1, &g_process_ref);
|
|
703
|
+
if (napi_get_named_property(env, process_v, "_tickCallback", &tick) == napi_ok) {
|
|
704
|
+
napi_valuetype vt;
|
|
705
|
+
if (napi_typeof(env, tick, &vt) == napi_ok && vt == napi_function) {
|
|
706
|
+
napi_create_reference(env, tick, 1, &g_tick_callback_ref);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// UvLoopSource co-pumps Node's timers/I/O + drains its microtasks during a
|
|
712
|
+
// BLOCKING GLib main loop (GLib.MainLoop.run / Gio.Application.run). POSIX embeds
|
|
713
|
+
// uv's backend fd into the GSource (readiness-driven); Windows/IOCP has no backend
|
|
714
|
+
// fd, so the Windows variant schedules off uv_loop_alive + uv_backend_timeout (see
|
|
715
|
+
// uv_source_funcs above) — same dispatch (uv_run + DrainMicrotasks), woken at uv's
|
|
716
|
+
// next-due time bounded by a 5 ms IOCP-completion poll cap instead of on the fd.
|
|
717
|
+
// PumpInit below is the separate NON-blocking case (bare `node bundle.mjs` async).
|
|
718
|
+
#ifndef _WIN32
|
|
719
|
+
GSource* source = g_source_new(&uv_source_funcs, sizeof(UvLoopSource));
|
|
720
|
+
UvLoopSource* s = reinterpret_cast<UvLoopSource*>(source);
|
|
721
|
+
s->loop = loop;
|
|
722
|
+
s->fd_polled = TRUE;
|
|
723
|
+
// uv_backend_fd is the epoll/kqueue fd on POSIX.
|
|
724
|
+
s->fd_tag = g_source_add_unix_fd(source, uv_backend_fd(loop),
|
|
725
|
+
static_cast<GIOCondition>(G_IO_IN | G_IO_OUT | G_IO_ERR));
|
|
726
|
+
// PRIORITY — below GDK_PRIORITY_REDRAW (G_PRIORITY_HIGH_IDLE + 20), above
|
|
727
|
+
// G_PRIORITY_DEFAULT_IDLE. At the g_source_new default (G_PRIORITY_DEFAULT, 0)
|
|
728
|
+
// a busy Node loop STARVES GTK painting: GLib dispatches only the
|
|
729
|
+
// highest-priority READY band per iteration, so a libuv timer that is due on
|
|
730
|
+
// every prepare — a 1 ms metronome like Excalibur's requestIdleCallback
|
|
731
|
+
// polyfill (`setTimeout(cb, 1)` re-armed from its own callback, run
|
|
732
|
+
// perpetually by its GarbageCollector) — keeps this source ready at priority
|
|
733
|
+
// 0 forever and GDK's frame-clock paint/update sources (GDK_PRIORITY_REDRAW,
|
|
734
|
+
// G_PRIORITY_HIGH_IDLE + 20) never run again: ticks, GLArea renders and rAF
|
|
735
|
+
// all freeze while plain GLib timeouts (priority 0) keep firing — the
|
|
736
|
+
// Excalibur-on-node-gi stall. Under GJS there is no extra source competing
|
|
737
|
+
// with GTK at default priority; the co-pump must not introduce one. Redraw
|
|
738
|
+
// wins over Node work (browser-like: rendering outranks timers); Node I/O
|
|
739
|
+
// still runs in every frame gap.
|
|
740
|
+
g_source_set_priority(source, G_PRIORITY_HIGH_IDLE + 30);
|
|
741
|
+
g_source_attach(source, nullptr); // default GLib main context
|
|
742
|
+
g_source_unref(source); // the context holds the surviving ref
|
|
743
|
+
#else // _WIN32 — readiness-bounded UvLoopSource (no uv backend fd on IOCP).
|
|
744
|
+
GSource* source = g_source_new(&uv_source_funcs, sizeof(UvLoopSource));
|
|
745
|
+
UvLoopSource* s = reinterpret_cast<UvLoopSource*>(source);
|
|
746
|
+
s->loop = loop;
|
|
747
|
+
s->fd_tag = nullptr;
|
|
748
|
+
s->fd_polled = FALSE;
|
|
749
|
+
s->win_next_wake_us = 0; // due immediately on the first iteration (dispatch re-schedules)
|
|
750
|
+
// Same priority rationale as POSIX (below GDK redraw, above default idle) so a
|
|
751
|
+
// future GTK-on-Windows paint is not starved by the co-pump.
|
|
752
|
+
g_source_set_priority(source, G_PRIORITY_HIGH_IDLE + 30);
|
|
753
|
+
g_source_attach(source, nullptr); // default GLib main context
|
|
754
|
+
g_source_unref(source); // the context holds the surviving ref
|
|
755
|
+
#endif // _WIN32 (UvLoopSource blocking-loop co-pump)
|
|
756
|
+
|
|
757
|
+
// Cross-platform: the uv-driven GLib pump (prepare/check/timer → g_main_context_
|
|
758
|
+
// iteration) drains async Gio/timers/DBus without uv's backend fd — active on
|
|
759
|
+
// Windows too (degraded to timed polling; see SyncPumpPolls).
|
|
760
|
+
PumpInit(loop);
|
|
761
|
+
|
|
762
|
+
g_loop_started = TRUE;
|
|
763
|
+
return env.Undefined();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
} // namespace nodegi
|