@gjsify/node-gi 0.58.0 → 0.59.1

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/src/loop.cc CHANGED
@@ -1,7 +1,9 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // libuv <-> GLib main loop bridge: startMainLoop (uv-in-GLib GSource) +
3
3
  // iterateMainContext + the uv-driven GLib auto-pump (GLib-in-libuv, the
4
- // non-blocking case).
4
+ // non-blocking case). Where there is no libuv (Android/NativeScript,
5
+ // NODE_GI_HAS_LIBUV off) the auto-pump rides the thread's ALooper instead; both
6
+ // halves implement the same Pump* interface and only one is compiled in.
5
7
 
6
8
  #include "common.h"
7
9
 
@@ -18,8 +20,36 @@
18
20
  #include <glib-unix.h>
19
21
  #endif
20
22
 
23
+ #if !NODE_GI_HAS_LIBUV && defined(__ANDROID__)
24
+ // The libuv-less half of the auto-pump rides the thread's ALooper instead of a
25
+ // uv loop, with a timerfd carrying GLib's next deadline. ALooper_* comes from
26
+ // libandroid, so this build needs -landroid — the NativeScript runtime links it
27
+ // already; the standalone cross-build passes it explicitly.
28
+ #include <algorithm>
29
+
30
+ #include <android/looper.h>
31
+ #include <sys/timerfd.h>
32
+ #include <unistd.h>
33
+ #endif
34
+
21
35
  namespace nodegi {
22
36
 
37
+ static napi_env g_loop_env = nullptr; // captured at startMainLoop (main thread)
38
+ static gboolean g_loop_started = FALSE;
39
+
40
+ // TRUE while THIS addon's auto-pump is inside its own GLib work — draining the
41
+ // context or running the prepare/query hint pass. On Node the UvLoopSource
42
+ // consults it to PARK itself (mask the backend fd, report not-ready, skip
43
+ // uv_run): a pump-driven context iteration must never dispatch the uv-in-GLib
44
+ // source, or it would nest uv_run inside the very uv callback that drives the
45
+ // pump (uv_run is not reentrant) — and recursively re-enter the pump itself. On
46
+ // every host it additionally keeps the drain and the hint pass out of each
47
+ // other, which is all it does on the libuv-less Android pump (no co-pump there,
48
+ // so nothing to park). Name kept for the incident trail in common.h.
49
+ static gboolean g_in_uv_pump = FALSE;
50
+
51
+ #if NODE_GI_HAS_LIBUV
52
+
23
53
  // ---- libuv <-> GLib main loop bridge (milestone: mainloop) ----
24
54
  //
25
55
  // Port of node-gtk's src/loop.cc (romgrk and contributors, MIT) to N-API. Nests
@@ -43,19 +73,9 @@ struct UvLoopSource {
43
73
  #endif
44
74
  };
45
75
 
46
- static napi_env g_loop_env = nullptr; // captured at startMainLoop (main thread)
47
- static gboolean g_loop_started = FALSE;
48
76
  static napi_ref g_process_ref = nullptr; // process
49
77
  static napi_ref g_tick_callback_ref = nullptr; // process._tickCallback
50
78
 
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
79
  // Drain Node's nextTick queue + run a microtask checkpoint. process._tickCallback
60
80
  // invoked through napi_make_callback runs the tick queue, and the surrounding
61
81
  // callback scope's close performs the microtask checkpoint — the N-API analogue
@@ -92,6 +112,8 @@ static void DrainMicrotasks() {
92
112
  napi_close_handle_scope(env, scope);
93
113
  }
94
114
 
115
+ #endif // NODE_GI_HAS_LIBUV (UvLoopSource + the Node nextTick/microtask drain)
116
+
95
117
  // ---- cross-runtime microtask checkpoint (Bun/Deno) --------------------------
96
118
  //
97
119
  // Node's napi_make_callback performs the nextTick + microtask checkpoint when
@@ -176,6 +198,7 @@ void NodeGiMaybeDrainMicrotasks(napi_env env) {
176
198
  napi_close_handle_scope(env, scope);
177
199
  }
178
200
 
201
+ #if NODE_GI_HAS_LIBUV
179
202
  #ifndef _WIN32
180
203
  static gboolean uv_source_prepare(GSource* base, gint* timeout) {
181
204
  UvLoopSource* s = reinterpret_cast<UvLoopSource*>(base);
@@ -316,6 +339,7 @@ static GSourceFuncs uv_source_funcs = {
316
339
  uv_source_prepare, uv_source_check, uv_source_dispatch, nullptr, nullptr, nullptr,
317
340
  };
318
341
  #endif // _WIN32 (UvLoopSource: POSIX fd-driven / Windows readiness-bounded co-pump)
342
+ #endif // NODE_GI_HAS_LIBUV
319
343
 
320
344
  // iterateMainContext(mayBlock?) -> boolean
321
345
  //
@@ -334,13 +358,139 @@ Napi::Value IterateMainContext(const Napi::CallbackInfo& info) {
334
358
  return Napi::Boolean::New(env, dispatched == TRUE);
335
359
  }
336
360
 
337
- // ---- uv-driven GLib auto-pump (the non-blocking case) -----------------------
361
+ // ---- the GLib auto-pump (the non-blocking case) -----------------------------
338
362
  //
339
- // The inverse co-pump of UvLoopSource: with NO blocking GLib loop running, a
340
- // plain `node bundle.mjs` still needs pending GLib sources (Gio async
363
+ // With NO blocking GLib loop running, a plain `node bundle.mjs` (or a
364
+ // NativeScript Android app) still needs pending GLib sources (Gio async
341
365
  // completions, GLib timeouts/idles, DBus) to dispatch — under GJS the GLib loop
342
- // IS the process loop, so they always do. Here Node's libuv loop drives the
343
- // default GLib main context instead:
366
+ // IS the process loop, so they always do. The HOST's own event loop drives the
367
+ // default GLib main context instead, and the two halves of that are the same on
368
+ // every host:
369
+ //
370
+ // • DRAIN — dispatch everything the context has ready right now
371
+ // (PumpDrainContext), run from the host loop with a valid napi scope;
372
+ // • WAKE — ask the context when its earliest timer is due and which fds it
373
+ // polls (PumpQueryWakeups), then mirror both into whatever primitive the
374
+ // host wakes on (PumpArmWakeups, the one host-specific half: libuv
375
+ // timer/poll watchers on Node, a timerfd + ALooper_addFd on Android).
376
+ //
377
+ // Both of those must never run while a blocking GLib loop owns the context
378
+ // (g_main_depth() > 0) or re-entrantly from our own GLib work (g_in_uv_pump).
379
+
380
+ static gboolean g_pump_inited = FALSE;
381
+ static gboolean g_pump_context_acquired = FALSE;
382
+ static std::vector<GPollFD>* g_pump_fds = nullptr; // query scratch (reused)
383
+ static int g_pump_async_pending = 0; // in-flight scope=async GI callbacks
384
+
385
+ // Host-specific: mirror the queried deadline + fds into the host's wake-up
386
+ // primitives. Defined once per host below.
387
+ static void PumpArmWakeups();
388
+
389
+ // Drain every currently-ready GLib source (bounded). Each iteration is a full,
390
+ // self-contained GLib cycle, so readiness (fds, timers, idles) is re-evaluated
391
+ // natively — the host's watchers are pure wake-ups. No-op while a blocking GLib
392
+ // loop is dispatching (g_main_depth() > 0) or when re-entered from a nested
393
+ // host-loop turn inside our own dispatch (g_in_uv_pump).
394
+ static void PumpDrainContext() {
395
+ if (g_main_depth() > 0 || g_in_uv_pump) return;
396
+ GMainContext* ctx = g_main_context_default();
397
+ // The drain runs from a bare host-loop C callback — there is NO ambient V8
398
+ // HandleScope (unlike a blocking run(), which is entered through a
399
+ // JS-initiated N-API frame). Anything the dispatched sources do that creates
400
+ // JS handles outside its own scope (the toggle-ref bridge's
401
+ // napi_get_reference_value on a GTask teardown unref, most prominently) would
402
+ // abort with "Cannot create a handle without a HandleScope" — so provide one
403
+ // for the whole drain.
404
+ napi_handle_scope scope = nullptr;
405
+ if (g_loop_env != nullptr) napi_open_handle_scope(g_loop_env, &scope);
406
+ g_in_uv_pump = TRUE;
407
+ int guard = 0;
408
+ while (g_main_context_iteration(ctx, FALSE) && ++guard < 1000) {
409
+ }
410
+ g_in_uv_pump = FALSE;
411
+ if (scope != nullptr) napi_close_handle_scope(g_loop_env, scope);
412
+ }
413
+
414
+ // The prepare+query+check HINT pass: learn GLib's earliest-timer deadline and
415
+ // which fds it polls, leaving the queried set in g_pump_fds[0..n). Returns the
416
+ // fd count, or -1 when the pass must not run. `*timeout` is GLib's
417
+ // milliseconds-to-next-deadline: 0 = work due now, -1 = no deadline. The
418
+ // g_in_uv_pump flag is held across it so UvLoopSource stays parked (masking
419
+ // uv's backend fd out of the queried set — it must not be watched, and uv's own
420
+ // backend timeout must not feed back into the deadline hint).
421
+ //
422
+ // The CHECK is not cosmetic and must never be dropped again: a GSource may hold
423
+ // a LOCK between its prepare() and its check(), and leaving the context prepared
424
+ // deadlocks the process on the next call into that library. GDK's Wayland event
425
+ // source is exactly that shape — its prepare() takes libwayland's designated-reader
426
+ // slot (`wl_display_prepare_read_queue`) and only its check() releases it
427
+ // (`wl_display_read_events` / `wl_display_cancel_read`). Measured (gjsify #1145,
428
+ // gtk 4.22 / Fedora 44 / Mesa Vulkan): with the hint pass ending at query, the very
429
+ // next `gtk_window_present()` blocked forever inside `wl_display_roundtrip_queue`
430
+ // under `gsk_renderer_new_for_surface_full`, so the app never finished showing its
431
+ // window while its GDBus worker thread kept answering Peer.Ping and Introspect —
432
+ // a live process at ~0.5 % CPU that read as "devtools hangs". Only Wayland shows
433
+ // it (GdkX11's source holds nothing across the pair), only node (bun/deno pump
434
+ // through complete `g_main_context_iteration`s), and only once GTK was initialised
435
+ // at g_main_depth() == 0 — `Gio.Application.register()` before `runAsync()` does
436
+ // exactly that, which is why an app that only ever calls `run()` never saw it.
437
+ // Passing revents = 0 is the right reading: this pass did NOT poll, so no fd is
438
+ // reported ready and every source that took something in prepare() gives it back.
439
+ //
440
+ // No dispatch pairs with the check: g_main_context_prepare() clears
441
+ // pending_dispatches on entry (glib gmain.c), and PumpDrainContext() runs on the
442
+ // same host-loop turn, so anything this marks ready is dispatched there rather
443
+ // than being stranded.
444
+ static int PumpQueryWakeups(gint* timeout) {
445
+ if (!g_pump_inited || g_main_depth() > 0 || g_in_uv_pump) return -1;
446
+ GMainContext* ctx = g_main_context_default();
447
+ gint prio = 0;
448
+ *timeout = -1;
449
+ g_in_uv_pump = TRUE;
450
+ g_main_context_prepare(ctx, &prio);
451
+ int nfds = g_main_context_query(ctx, prio, timeout, g_pump_fds->data(),
452
+ static_cast<gint>(g_pump_fds->size()));
453
+ while (nfds > static_cast<int>(g_pump_fds->size())) {
454
+ g_pump_fds->resize(nfds);
455
+ nfds = g_main_context_query(ctx, prio, timeout, g_pump_fds->data(),
456
+ static_cast<gint>(g_pump_fds->size()));
457
+ }
458
+ // g_main_context_query fills fd + events and leaves revents untouched, so the
459
+ // previous pass's values would otherwise be replayed as fresh readiness.
460
+ for (int i = 0; i < nfds; i++) (*g_pump_fds)[i].revents = 0;
461
+ g_main_context_check(ctx, prio, g_pump_fds->data(), nfds);
462
+ g_in_uv_pump = FALSE;
463
+ return nfds;
464
+ }
465
+
466
+ // Own the default context on this (the JS) thread. Required so a cross-thread
467
+ // g_source_attach (a completing GTask worker) signals the context's wakeup
468
+ // eventfd — which the host's fd watcher turns into a host-loop wake. Same-thread
469
+ // recursive acquisition by a later blocking g_main_loop_run() still works.
470
+ static void PumpAcquireContext() {
471
+ g_pump_fds = new std::vector<GPollFD>(16);
472
+ g_pump_context_acquired = g_main_context_acquire(g_main_context_default());
473
+ if (!g_pump_context_acquired) {
474
+ g_printerr("(node-gi) warning: default GMainContext is owned by another thread; "
475
+ "cross-thread wakeups may be delayed\n");
476
+ }
477
+ }
478
+
479
+ static void PumpReleaseContext() {
480
+ delete g_pump_fds;
481
+ g_pump_fds = nullptr;
482
+ if (g_pump_context_acquired) {
483
+ g_main_context_release(g_main_context_default());
484
+ g_pump_context_acquired = FALSE;
485
+ }
486
+ }
487
+
488
+ #if NODE_GI_HAS_LIBUV
489
+
490
+ // ---- the libuv-driven half (Node) -------------------------------------------
491
+ //
492
+ // The inverse co-pump of UvLoopSource: Node's libuv loop drives the default GLib
493
+ // main context.
344
494
  //
345
495
  // • a uv_prepare + uv_check handle pair drains every READY GLib source once
346
496
  // per libuv loop turn (a bounded `g_main_context_iteration(ctx, FALSE)`
@@ -384,15 +534,11 @@ struct PumpPoll {
384
534
  gboolean started; // whether the watcher is currently armed (see PumpPollCb)
385
535
  };
386
536
 
387
- static gboolean g_pump_inited = FALSE;
388
- static gboolean g_pump_context_acquired = FALSE;
389
537
  static uv_prepare_t g_pump_prepare;
390
538
  static uv_check_t g_pump_check;
391
539
  static uv_timer_t g_pump_timer;
392
540
  static gboolean g_pump_timer_reffed = FALSE;
393
541
  static std::unordered_map<int, PumpPoll*>* g_pump_polls = nullptr;
394
- static std::vector<GPollFD>* g_pump_fds = nullptr; // query scratch (reused)
395
- static int g_pump_async_pending = 0; // in-flight scope=async GI callbacks
396
542
  static gboolean g_pump_prepare_reffed = FALSE;
397
543
  static gboolean g_pump_poll_warned = FALSE;
398
544
 
@@ -508,78 +654,12 @@ static gboolean SyncPumpPolls(int nfds) {
508
654
  #endif // _WIN32
509
655
  }
510
656
 
511
- // Drain every currently-ready GLib source (bounded). Each iteration is a full,
512
- // self-contained GLib cycle, so readiness (fds, timers, idles) is re-evaluated
513
- // natively — the uv watchers are pure wake-ups. No-op while a blocking GLib
514
- // loop is dispatching (g_main_depth() > 0) or when re-entered from a nested
515
- // uv_run inside our own dispatch (g_in_uv_pump).
516
- static void PumpDrainContext() {
517
- if (g_main_depth() > 0 || g_in_uv_pump) return;
518
- GMainContext* ctx = g_main_context_default();
519
- // The drain runs from a bare uv_prepare/uv_check C callback — there is NO
520
- // ambient V8 HandleScope (unlike a blocking run(), which is entered through a
521
- // JS-initiated N-API frame). Anything the dispatched sources do that creates
522
- // JS handles outside its own scope (the toggle-ref bridge's
523
- // napi_get_reference_value on a GTask teardown unref, most prominently) would
524
- // abort with "Cannot create a handle without a HandleScope" — so provide one
525
- // for the whole drain.
526
- napi_handle_scope scope = nullptr;
527
- if (g_loop_env != nullptr) napi_open_handle_scope(g_loop_env, &scope);
528
- g_in_uv_pump = TRUE;
529
- int guard = 0;
530
- while (g_main_context_iteration(ctx, FALSE) && ++guard < 1000) {
531
- }
532
- g_in_uv_pump = FALSE;
533
- if (scope != nullptr) napi_close_handle_scope(g_loop_env, scope);
534
- }
535
-
536
- // The prepare+query+check HINT pass: learn GLib's earliest-timer deadline + poll
537
- // fds and mirror them into the uv timer / uv_poll watchers, so libuv's poll sleep
538
- // ends exactly when GLib next has work. Runs only at g_main_depth() == 0; the
539
- // g_in_uv_pump flag is held across it so UvLoopSource stays parked (masking uv's
540
- // backend fd out of the queried set — it must not be uv_poll'd, and uv's own
541
- // backend timeout must not feed back into the timer hint).
542
- //
543
- // The CHECK is not cosmetic and must never be dropped again: a GSource may hold a
544
- // LOCK between its prepare() and its check(), and leaving the context prepared
545
- // deadlocks the process on the next call into that library. GDK's Wayland event
546
- // source is exactly that shape — its prepare() takes libwayland's designated-reader
547
- // slot (`wl_display_prepare_read_queue`) and only its check() releases it
548
- // (`wl_display_read_events` / `wl_display_cancel_read`). Measured (gjsify #1145,
549
- // gtk 4.22 / Fedora 44 / Mesa Vulkan): with the hint pass ending at query, the very
550
- // next `gtk_window_present()` blocked forever inside `wl_display_roundtrip_queue`
551
- // under `gsk_renderer_new_for_surface_full`, so the app never finished showing its
552
- // window while its GDBus worker thread kept answering Peer.Ping and Introspect —
553
- // a live process at ~0.5 % CPU that read as "devtools hangs". Only Wayland shows
554
- // it (GdkX11's source holds nothing across the pair), only node (bun/deno pump
555
- // through complete `g_main_context_iteration`s), and only once GTK was initialised
556
- // at g_main_depth() == 0 — `Gio.Application.register()` before `runAsync()` does
557
- // exactly that, which is why an app that only ever calls `run()` never saw it.
558
- // Passing revents = 0 is the right reading: this pass did NOT poll, so no fd is
559
- // reported ready and every source that took something in prepare() gives it back.
657
+ // Mirror GLib's queried deadline + fds into the uv timer / uv_poll watchers, so
658
+ // libuv's poll sleep ends exactly when GLib next has work.
560
659
  static void PumpArmWakeups() {
561
- if (!g_pump_inited || g_main_depth() > 0 || g_in_uv_pump) return;
562
- GMainContext* ctx = g_main_context_default();
563
- gint prio = 0;
564
660
  gint timeout = -1;
565
- g_in_uv_pump = TRUE;
566
- g_main_context_prepare(ctx, &prio);
567
- int nfds = g_main_context_query(ctx, prio, &timeout, g_pump_fds->data(),
568
- static_cast<gint>(g_pump_fds->size()));
569
- while (nfds > static_cast<int>(g_pump_fds->size())) {
570
- g_pump_fds->resize(nfds);
571
- nfds = g_main_context_query(ctx, prio, &timeout, g_pump_fds->data(),
572
- static_cast<gint>(g_pump_fds->size()));
573
- }
574
- // g_main_context_query fills fd + events and leaves revents untouched, so the
575
- // previous pass's values would otherwise be replayed as fresh readiness.
576
- for (int i = 0; i < nfds; i++) (*g_pump_fds)[i].revents = 0;
577
- // No dispatch pairs with this check: g_main_context_prepare() clears
578
- // pending_dispatches on entry (glib gmain.c), and PumpDrainContext() — a full
579
- // iteration — runs on the same libuv turn, so anything this marks ready is
580
- // dispatched there rather than being stranded.
581
- g_main_context_check(ctx, prio, g_pump_fds->data(), nfds);
582
- g_in_uv_pump = FALSE;
661
+ const int nfds = PumpQueryWakeups(&timeout);
662
+ if (nfds < 0) return;
583
663
 
584
664
  if (!SyncPumpPolls(nfds)) {
585
665
  // Degraded wake path: poll at a short cadence instead of on readiness. This is
@@ -641,6 +721,244 @@ static void PumpCheckCb(uv_check_t* /*h*/) {
641
721
  PumpArmWakeups();
642
722
  }
643
723
 
724
+ static void PumpInit(uv_loop_t* loop) {
725
+ if (g_pump_inited) return;
726
+ g_pump_polls = new std::unordered_map<int, PumpPoll*>();
727
+ PumpAcquireContext();
728
+
729
+ uv_prepare_init(loop, &g_pump_prepare);
730
+ uv_prepare_start(&g_pump_prepare, PumpPrepareCb);
731
+ uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_prepare));
732
+
733
+ uv_check_init(loop, &g_pump_check);
734
+ uv_check_start(&g_pump_check, PumpCheckCb);
735
+ uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_check));
736
+
737
+ uv_timer_init(loop, &g_pump_timer);
738
+ uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_timer));
739
+
740
+ g_pump_inited = TRUE;
741
+ }
742
+
743
+ // Close the pump's uv handles so the loop can wind down cleanly.
744
+ static void PumpShutdownPlatform() {
745
+ uv_prepare_stop(&g_pump_prepare);
746
+ uv_check_stop(&g_pump_check);
747
+ uv_timer_stop(&g_pump_timer);
748
+ uv_close(reinterpret_cast<uv_handle_t*>(&g_pump_prepare), nullptr);
749
+ uv_close(reinterpret_cast<uv_handle_t*>(&g_pump_check), nullptr);
750
+ uv_close(reinterpret_cast<uv_handle_t*>(&g_pump_timer), nullptr);
751
+ if (g_pump_polls != nullptr) {
752
+ for (auto& it : *g_pump_polls) {
753
+ uv_poll_stop(&it.second->handle);
754
+ uv_close(reinterpret_cast<uv_handle_t*>(&it.second->handle), PumpPollCloseCb);
755
+ }
756
+ delete g_pump_polls;
757
+ g_pump_polls = nullptr;
758
+ }
759
+ }
760
+
761
+ #elif defined(__ANDROID__)
762
+
763
+ // ---- the ALooper-driven half (Android / NativeScript) -----------------------
764
+ //
765
+ // NativeScript's V8 runtime is a Node-API host with NO libuv of its own, so the
766
+ // whole uv half above is compiled out (NODE_GI_HAS_LIBUV 0 — common.h explains
767
+ // why a mere REFERENCE to a uv symbol is fatal there, not just a call). The
768
+ // host's event loop is Android's own ALooper on the thread that runs JS, and the
769
+ // pump mirrors GLib onto it with the two halves named at the top of this
770
+ // section:
771
+ //
772
+ // • WAKE — every fd GLib queries is registered with ALooper_addFd, and GLib's
773
+ // next deadline is armed on a timerfd registered the same way. The looper
774
+ // then wakes for GLib I/O, for a cross-thread wakeup (the context's own
775
+ // eventfd, signalled by a completing GTask worker) and for GLib timers
776
+ // alike, instead of for a uv_poll/uv_timer.
777
+ // • DRAIN — a looper callback runs from the host's poll, OUTSIDE any JS frame
778
+ // or handle scope, so it must not touch napi or JS at all. It only posts a
779
+ // threadsafe function; the TSFN's call_js runs on the JS thread inside a
780
+ // proper handle + callback scope and does the drain plus the re-arm.
781
+ //
782
+ // No keep-alive half: Android's looper belongs to the app and outlives every
783
+ // pump, so there is nothing to ref or unref. g_pump_async_pending is still
784
+ // maintained (calls.cc does it unconditionally) and still answers
785
+ // mainContextHasPending(); it just holds nothing open here.
786
+ //
787
+ // NOT supported on this host: a BLOCKING GLib.MainLoop.run() / Gio.Application
788
+ // .run() on the UI thread. It would park Android's own looper — no input, no
789
+ // frames, an ANR — and there is no co-pump to borrow (UvLoopSource embeds uv's
790
+ // backend fd in a GSource; a looper exposes no such fd). Nothing refuses it at
791
+ // build or link time, since the boxed GLib.MainLoop surface is host-independent;
792
+ // it simply freezes the thread it is called on. An app drives GLib through this
793
+ // pump instead.
794
+
795
+ static ALooper* g_looper = nullptr;
796
+ static napi_threadsafe_function g_pump_tsfn = nullptr;
797
+ static int g_pump_timerfd = -1;
798
+ // The GLib poll fds currently registered with the looper (not the timerfd, which
799
+ // is added once and stays). Small by nature — a linear sweep beats a hash map.
800
+ static std::vector<int>* g_looper_fds = nullptr;
801
+ // A drain post is already in flight: one looper poll can report several fds
802
+ // ready for a single GLib cycle, and one drain answers all of them. Written only
803
+ // on the JS thread (every poster runs there), so no atomic.
804
+ static gboolean g_pump_posted = FALSE;
805
+
806
+ // Ask for a drain on the JS thread. Callable from a looper callback: the only
807
+ // napi function it touches is the non-blocking TSFN post, which is the one
808
+ // documented to be legal from any thread at any time.
809
+ static void PumpPostTsfn() {
810
+ if (g_pump_tsfn == nullptr || g_pump_posted) return;
811
+ if (napi_call_threadsafe_function(g_pump_tsfn, nullptr, napi_tsfn_nonblocking) == napi_ok) {
812
+ g_pump_posted = TRUE;
813
+ }
814
+ }
815
+
816
+ // A GLib poll fd became ready. Wake-only, and the wake is an EDGE for the same
817
+ // reason as on libuv (#1912): ALooper is level-triggered and this callback reads
818
+ // nothing, so an fd that STAYS ready — the context's wakeup eventfd, which
819
+ // block_source() re-signals on every dispatch, or a dead socket at POLLHUP —
820
+ // would spin the host loop at 100 % CPU. Returning 0 removes the registration;
821
+ // PumpArmWakeups re-adds whatever GLib still queries.
822
+ static int PumpLooperFdCb(int /*fd*/, int /*events*/, void* /*data*/) {
823
+ PumpPostTsfn();
824
+ return 0;
825
+ }
826
+
827
+ // The timerfd expired. This one KEEPS its registration (returns 1): read() is
828
+ // what clears the expiry, so it cannot stay spuriously ready, and an
829
+ // armed-again timer needs only a new itimerspec, not a new watcher.
830
+ static int PumpTimerFdCb(int fd, int /*events*/, void* /*data*/) {
831
+ uint64_t expirations = 0;
832
+ while (read(fd, &expirations, sizeof(expirations)) == sizeof(expirations)) {
833
+ }
834
+ PumpPostTsfn();
835
+ return 1;
836
+ }
837
+
838
+ // Register every fd GLib queries, and drop the ones it no longer does.
839
+ // ALooper_addFd REPLACES an existing registration for the same fd, which is how
840
+ // a self-removed watcher (PumpLooperFdCb returning 0) comes back.
841
+ static void SyncLooperFds(int nfds) {
842
+ std::vector<int> wanted;
843
+ wanted.reserve(static_cast<size_t>(nfds));
844
+ for (int i = 0; i < nfds; i++) {
845
+ const GPollFD& pfd = (*g_pump_fds)[i];
846
+ int events = 0;
847
+ if (pfd.events & (G_IO_IN | G_IO_HUP | G_IO_ERR)) events |= ALOOPER_EVENT_INPUT;
848
+ if (pfd.events & G_IO_OUT) events |= ALOOPER_EVENT_OUTPUT;
849
+ if (events == 0) continue;
850
+ if (ALooper_addFd(g_looper, pfd.fd, ALOOPER_POLL_CALLBACK, events, PumpLooperFdCb,
851
+ nullptr) == 1) {
852
+ wanted.push_back(pfd.fd);
853
+ }
854
+ }
855
+ for (int fd : *g_looper_fds) {
856
+ if (std::find(wanted.begin(), wanted.end(), fd) == wanted.end()) {
857
+ ALooper_removeFd(g_looper, fd);
858
+ }
859
+ }
860
+ *g_looper_fds = std::move(wanted);
861
+ }
862
+
863
+ // Mirror GLib's deadline onto the timerfd. An all-zero it_value DISARMS a
864
+ // timerfd rather than firing immediately, so a 0 ms deadline ("work is due now")
865
+ // has to be answered by posting the drain directly; that drain re-arms.
866
+ static void ArmTimerfd(gint timeout) {
867
+ if (g_pump_timerfd < 0) return;
868
+ if (timeout == 0) {
869
+ PumpPostTsfn();
870
+ timeout = -1;
871
+ }
872
+ struct itimerspec its = {};
873
+ if (timeout > 0) {
874
+ its.it_value.tv_sec = timeout / 1000;
875
+ its.it_value.tv_nsec = static_cast<long>(timeout % 1000) * 1000000L;
876
+ }
877
+ timerfd_settime(g_pump_timerfd, 0, &its, nullptr);
878
+ }
879
+
880
+ static void PumpArmWakeups() {
881
+ gint timeout = -1;
882
+ const int nfds = PumpQueryWakeups(&timeout);
883
+ if (nfds < 0) return;
884
+ SyncLooperFds(nfds);
885
+ ArmTimerfd(timeout);
886
+ }
887
+
888
+ // The drain, on the JS thread, inside the scopes the TSFN infra provides.
889
+ static void PumpTsfnCb(napi_env /*env*/, napi_value /*js_cb*/, void* /*context*/,
890
+ void* /*data*/) {
891
+ g_pump_posted = FALSE;
892
+ if (!g_pump_inited) return;
893
+ PumpDrainContext();
894
+ PumpArmWakeups();
895
+ }
896
+
897
+ static void PumpInit(napi_env env, ALooper* looper) {
898
+ if (g_pump_inited) return;
899
+ g_looper = looper;
900
+ g_looper_fds = new std::vector<int>();
901
+ PumpAcquireContext();
902
+
903
+ // js_func null — PumpTsfnCb IS the callback, and the TSFN infra invokes it
904
+ // inside a handle + callback scope, so a dispatched GLib source re-entering JS
905
+ // runs as a proper N-API callback. max_queue_size 1 coalesces bursts;
906
+ // initial_thread_count 1 keeps it alive for the env's lifetime, so a looper
907
+ // callback may post without acquiring first. Same shape as toggle.cc's drain.
908
+ napi_value name = nullptr;
909
+ napi_create_string_utf8(env, "node-gi:glib-pump", NAPI_AUTO_LENGTH, &name);
910
+ if (napi_create_threadsafe_function(env, nullptr, nullptr, name, /*max_queue_size*/ 1,
911
+ /*initial_thread_count*/ 1, nullptr, nullptr, nullptr,
912
+ PumpTsfnCb, &g_pump_tsfn) != napi_ok) {
913
+ g_pump_tsfn = nullptr;
914
+ } else {
915
+ // The pump must not hold the host loop open by itself (the uv_unref
916
+ // equivalent). Best-effort: NativeScript no-ops TSFN ref/unref, and its
917
+ // looper does not end anyway.
918
+ napi_unref_threadsafe_function(env, g_pump_tsfn);
919
+ }
920
+
921
+ // NONBLOCK because PumpTimerFdCb read()s until EAGAIN; CLOEXEC so no
922
+ // Gio.Subprocess child inherits it.
923
+ g_pump_timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
924
+ if (g_pump_timerfd >= 0) {
925
+ ALooper_addFd(g_looper, g_pump_timerfd, ALOOPER_POLL_CALLBACK, ALOOPER_EVENT_INPUT,
926
+ PumpTimerFdCb, nullptr);
927
+ }
928
+
929
+ g_pump_inited = TRUE;
930
+ // Arm the first wake-ups now. The uv half gets this for free from its
931
+ // always-active prepare handle, which runs on the very next loop turn; here
932
+ // nothing would ever ask until something already woke the looper.
933
+ PumpArmWakeups();
934
+ }
935
+
936
+ // Unregister everything the pump put on the looper. Reached only through an env
937
+ // cleanup hook, which NativeScript never runs for the main env — so on the live
938
+ // host this is dead code, kept because the pump must not leak a watcher into a
939
+ // looper that outlives it on any host that DOES tear an env down.
940
+ static void PumpShutdownPlatform() {
941
+ if (g_pump_tsfn != nullptr) {
942
+ napi_release_threadsafe_function(g_pump_tsfn, napi_tsfn_release);
943
+ g_pump_tsfn = nullptr;
944
+ }
945
+ if (g_looper_fds != nullptr) {
946
+ for (int fd : *g_looper_fds) ALooper_removeFd(g_looper, fd);
947
+ delete g_looper_fds;
948
+ g_looper_fds = nullptr;
949
+ }
950
+ if (g_pump_timerfd >= 0) {
951
+ ALooper_removeFd(g_looper, g_pump_timerfd);
952
+ close(g_pump_timerfd);
953
+ g_pump_timerfd = -1;
954
+ }
955
+ g_looper = nullptr;
956
+ }
957
+
958
+ #else
959
+ #error "no GLib auto-pump for this host: it has neither libuv nor an ALooper"
960
+ #endif // NODE_GI_HAS_LIBUV / __ANDROID__ (the host-specific pump half)
961
+
644
962
  // pumpKick() -> void — drain ready GLib sources + re-arm the wake hints NOW.
645
963
  // The L1 layer calls this from a `process.on('beforeExit')` hook: with only
646
964
  // UNREF'd handles active, uv_run(UV_RUN_DEFAULT) returns without ever running
@@ -656,6 +974,14 @@ Napi::Value PumpKick(const Napi::CallbackInfo& info) {
656
974
  if (g_pump_inited && static_cast<napi_env>(info.Env()) == g_loop_env) {
657
975
  PumpDrainContext();
658
976
  PumpArmWakeups();
977
+ #if !NODE_GI_HAS_LIBUV && defined(__ANDROID__)
978
+ // Both calls above no-op while a blocking GLib loop owns the context or we
979
+ // are re-entered from our own dispatch, and on this host nothing else will
980
+ // come back for the work: the uv half has an always-active prepare handle
981
+ // that retries every loop turn, the looper only wakes for something it
982
+ // watches. The post defers the kick to a later JS-thread turn instead.
983
+ PumpPostTsfn();
984
+ #endif
659
985
  }
660
986
  return info.Env().Undefined();
661
987
  }
@@ -668,35 +994,6 @@ NodeGiPumpJsDispatchScope::NodeGiPumpJsDispatchScope() : saved_(g_in_uv_pump) {
668
994
  }
669
995
  NodeGiPumpJsDispatchScope::~NodeGiPumpJsDispatchScope() { g_in_uv_pump = saved_; }
670
996
 
671
- static void PumpInit(uv_loop_t* loop) {
672
- if (g_pump_inited) return;
673
- g_pump_polls = new std::unordered_map<int, PumpPoll*>();
674
- g_pump_fds = new std::vector<GPollFD>(16);
675
-
676
- // Own the default context on this (the JS) thread. Required so a cross-thread
677
- // g_source_attach (a completing GTask worker) signals the context's wakeup
678
- // eventfd — which our uv_poll watcher turns into a libuv wake. Same-thread
679
- // recursive acquisition by a later blocking g_main_loop_run() still works.
680
- g_pump_context_acquired = g_main_context_acquire(g_main_context_default());
681
- if (!g_pump_context_acquired) {
682
- g_printerr("(node-gi) warning: default GMainContext is owned by another thread; "
683
- "cross-thread wakeups may be delayed\n");
684
- }
685
-
686
- uv_prepare_init(loop, &g_pump_prepare);
687
- uv_prepare_start(&g_pump_prepare, PumpPrepareCb);
688
- uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_prepare));
689
-
690
- uv_check_init(loop, &g_pump_check);
691
- uv_check_start(&g_pump_check, PumpCheckCb);
692
- uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_check));
693
-
694
- uv_timer_init(loop, &g_pump_timer);
695
- uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_timer));
696
-
697
- g_pump_inited = TRUE;
698
- }
699
-
700
997
  // mainContextHasPending() -> boolean
701
998
  //
702
999
  // Is there JS-armed GLib work a *pumping* runtime must stay alive for? True
@@ -762,49 +1059,51 @@ Napi::Value MakePumpPendingCount(const Napi::CallbackInfo& info) {
762
1059
  bool NodeGiPumpAsyncBegin(napi_env env) {
763
1060
  if (g_loop_env != nullptr && env != g_loop_env) return false;
764
1061
  ++g_pump_async_pending;
1062
+ #if NODE_GI_HAS_LIBUV
765
1063
  if (g_pump_inited && g_pump_async_pending == 1 && !g_pump_prepare_reffed) {
766
1064
  uv_ref(reinterpret_cast<uv_handle_t*>(&g_pump_prepare));
767
1065
  g_pump_prepare_reffed = TRUE;
768
1066
  }
1067
+ #else
1068
+ // Nothing to hold open here (§ the ALooper half), but the work just armed may
1069
+ // be a SAME-THREAD source — a GLib idle or timeout added from JS — which does
1070
+ // not signal the context's wakeup eventfd, so no fd the looper watches
1071
+ // changed and the looper would sleep straight past it. Ask for a drain, which
1072
+ // re-arms the timerfd to the new deadline. (The uv half needs no equivalent:
1073
+ // its prepare handle re-queries on the very next loop turn.)
1074
+ //
1075
+ // This covers same-thread sources armed FROM JS only. One armed inside a C
1076
+ // library on this thread — GDK's repeating timeout is the known case, and only
1077
+ // track C of ADR 0104 reaches it — still waits for the next drain, because
1078
+ // nothing re-queries the context per looper turn the way uv's prepare does.
1079
+ if (g_pump_inited) PumpPostTsfn();
1080
+ #endif
769
1081
  return true;
770
1082
  }
771
1083
 
772
1084
  void NodeGiPumpAsyncEnd() {
773
1085
  if (g_pump_async_pending == 0) return;
774
1086
  --g_pump_async_pending;
1087
+ #if NODE_GI_HAS_LIBUV
775
1088
  if (g_pump_inited && g_pump_async_pending == 0 && g_pump_prepare_reffed) {
776
1089
  uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_prepare));
777
1090
  g_pump_prepare_reffed = FALSE;
778
1091
  }
1092
+ #endif
779
1093
  }
780
1094
 
781
- // Env-teardown: close the pump's uv handles so the loop can wind down cleanly.
782
- // Only the env that armed the pump (the main env) tears it down.
1095
+ // Env-teardown: release the pump's host handles so the host loop can wind down
1096
+ // cleanly, then give the context back. Only the env that armed the pump (the
1097
+ // main env) tears it down.
783
1098
  void NodeGiPumpShutdown(napi_env env) {
784
1099
  if (!g_pump_inited || env != g_loop_env) return;
785
1100
  g_pump_inited = FALSE;
786
- uv_prepare_stop(&g_pump_prepare);
787
- uv_check_stop(&g_pump_check);
788
- uv_timer_stop(&g_pump_timer);
789
- uv_close(reinterpret_cast<uv_handle_t*>(&g_pump_prepare), nullptr);
790
- uv_close(reinterpret_cast<uv_handle_t*>(&g_pump_check), nullptr);
791
- uv_close(reinterpret_cast<uv_handle_t*>(&g_pump_timer), nullptr);
792
- if (g_pump_polls != nullptr) {
793
- for (auto& it : *g_pump_polls) {
794
- uv_poll_stop(&it.second->handle);
795
- uv_close(reinterpret_cast<uv_handle_t*>(&it.second->handle), PumpPollCloseCb);
796
- }
797
- delete g_pump_polls;
798
- g_pump_polls = nullptr;
799
- }
800
- delete g_pump_fds;
801
- g_pump_fds = nullptr;
802
- if (g_pump_context_acquired) {
803
- g_main_context_release(g_main_context_default());
804
- g_pump_context_acquired = FALSE;
805
- }
1101
+ PumpShutdownPlatform();
1102
+ PumpReleaseContext();
806
1103
  }
807
1104
 
1105
+ #if NODE_GI_HAS_LIBUV
1106
+
808
1107
  // startMainLoop() -> void
809
1108
  // Attach the libuv-backed GSource to the default GLib main context (idempotent).
810
1109
  // Harmless until a GLib main loop actually runs — it adds no uv handle, so it
@@ -892,4 +1191,42 @@ Napi::Value StartMainLoop(const Napi::CallbackInfo& info) {
892
1191
  return env.Undefined();
893
1192
  }
894
1193
 
1194
+ #elif defined(__ANDROID__)
1195
+
1196
+ // startMainLoop() -> void
1197
+ // Arm the ALooper-driven GLib pump on the calling thread (idempotent). There is
1198
+ // no uv-in-GLib bridge to attach alongside it: NativeScript's V8 host has no
1199
+ // libuv loop to co-pump, so the pump is the whole bridge here — which also means
1200
+ // a blocking GLib.MainLoop.run() has no co-pump to keep the host alive (see the
1201
+ // ALooper half above).
1202
+ Napi::Value StartMainLoop(const Napi::CallbackInfo& info) {
1203
+ Napi::Env env = info.Env();
1204
+ if (g_loop_started) return env.Undefined();
1205
+
1206
+ // The looper of the thread that called in — whichever JS thread that is. The
1207
+ // UI thread always has one, and so does a NativeScript Worker (its Java side
1208
+ // runs Looper.prepare() + Looper.loop() around the runtime, see the host's
1209
+ // WorkerWrapper). A Worker is in fact the better owner for GTK: GTK's Android
1210
+ // glue blocks the UI thread on a default-GMainContext idle (GlibContext
1211
+ // .blockForMain), which only returns if some OTHER thread iterates that
1212
+ // context. A null answer means the host called from a bare pthread that never
1213
+ // prepared a looper, and there is then nothing for GLib to be pumped by.
1214
+ ALooper* looper = ALooper_forThread();
1215
+ if (looper == nullptr) {
1216
+ Napi::Error::New(env,
1217
+ "no ALooper on this thread: the GLib pump needs a looper-backed "
1218
+ "thread (call startMainLoop from the JS thread)")
1219
+ .ThrowAsJavaScriptException();
1220
+ return env.Undefined();
1221
+ }
1222
+
1223
+ g_loop_env = env;
1224
+ PumpInit(env, looper);
1225
+
1226
+ g_loop_started = TRUE;
1227
+ return env.Undefined();
1228
+ }
1229
+
1230
+ #endif // NODE_GI_HAS_LIBUV / __ANDROID__ (startMainLoop)
1231
+
895
1232
  } // namespace nodegi