@gjsify/node-gi 0.24.2 → 0.26.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/gi.js +127 -13
- package/index.d.ts +23 -0
- package/index.js +54 -4
- package/package.json +1 -1
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/src/addon.cc +2 -0
- package/src/calls.cc +35 -9
- package/src/common.h +42 -3
- package/src/loop.cc +93 -12
- package/src/marshal.cc +146 -2
package/gi.js
CHANGED
|
@@ -19,7 +19,18 @@ import * as native from './index.js';
|
|
|
19
19
|
// setImmediate is a Node global but NOT a Deno global (Deno requires the explicit
|
|
20
20
|
// node:timers import; Bun/Node re-export it there too), so import it for the
|
|
21
21
|
// macrotask-deferred runAsync to work on all three runtimes.
|
|
22
|
-
|
|
22
|
+
//
|
|
23
|
+
// setInterval/clearInterval come from here for a second, load-bearing reason:
|
|
24
|
+
// the RUNTIME's timers, never `globalThis.setInterval`. A GJS source running
|
|
25
|
+
// through the bridge routinely replaces the global timers with GLib-backed ones
|
|
26
|
+
// (`@gjsify/node-globals/register` swaps them for `GLib.timeout_add` wrappers) —
|
|
27
|
+
// driving the GLib pump off a GLib timeout is a deadlock: the timer that is
|
|
28
|
+
// supposed to dispatch GLib sources is itself a GLib source nobody dispatches.
|
|
29
|
+
import { setImmediate, setInterval, clearInterval } from 'node:timers';
|
|
30
|
+
// Same reasoning for `process`: `globalThis.process` may be the `@gjsify/process`
|
|
31
|
+
// polyfill, whose EventEmitter never emits the runtime's 'beforeExit'. The
|
|
32
|
+
// loop-liveness hooks below must attach to the real one or they silently never fire.
|
|
33
|
+
import runtimeProcess from 'node:process';
|
|
23
34
|
// The GJS Gio DBus surface (Gio.DBus.session/system, own_name/watch_name,
|
|
24
35
|
// Gio.DBusProxy.makeProxyWrapper, Gio.DBusExportedObject) — ported to the L1 model.
|
|
25
36
|
import { createGioDBus } from './overrides/gio-dbus.js';
|
|
@@ -977,11 +988,94 @@ function applicationRunAsync(handle) {
|
|
|
977
988
|
// concurrent runAsync calls share one timer.
|
|
978
989
|
let pumpTimer = null;
|
|
979
990
|
let pumpRefCount = 0;
|
|
991
|
+
// How many of those refs belong to the AUTO-armed pump (requireGi's permanent
|
|
992
|
+
// hold on Bun/Deno). `pumpRefCount > pumpAutoRefs` therefore means an EXPLICIT
|
|
993
|
+
// `startMainContextPump()` caller currently holds the pump.
|
|
994
|
+
let pumpAutoRefs = 0;
|
|
995
|
+
// Whether the pump timer currently HOLDS the runtime's event loop open (ref'd).
|
|
996
|
+
let pumpHoldsRuntime = false;
|
|
997
|
+
|
|
998
|
+
/** Dispatch every currently-ready GLib source, then return. */
|
|
999
|
+
function drainReadySources() {
|
|
1000
|
+
let guard = 0;
|
|
1001
|
+
while (native.iterateMainContext(false) && guard++ < 100000) {
|
|
1002
|
+
/* drain ready sources */
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Zero-N-API view over the JS-armed-work counter — `pendingView[0] > 0` is
|
|
1007
|
+
// `mainContextHasPending()` as a plain V8 memory read. The IDLE beat must not
|
|
1008
|
+
// enter the addon at all: on Deno, a single napi call from the pump tick during
|
|
1009
|
+
// the between-test-files GC window reproduces the #47 boxed-handle teardown
|
|
1010
|
+
// SIGSEGV (measured on the gtk-smoke leg: a query-only tick crashed 3/3, a tick
|
|
1011
|
+
// that never touches the addon exited 0, the full dispatching tick crashed
|
|
1012
|
+
// 10/10). So every read on the beat's idle path goes through this view.
|
|
1013
|
+
// Created LAZILY on the first pump arm (Bun/Deno only): the @gjsify/napi shim
|
|
1014
|
+
// loud-stubs external arraybuffers, and the gjs host never arms the pump.
|
|
1015
|
+
let pendingView = null;
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* One pump beat: dispatch if the program owns GLib work, then re-evaluate the
|
|
1019
|
+
* keep-alive hold. Both the gate and the hold read the SAME counter, so the
|
|
1020
|
+
* auto-armed pump dispatches exactly when it also holds the loop open.
|
|
1021
|
+
*
|
|
1022
|
+
* DISPATCH is gated like the hold — on the program's OWN GLib work. An EXPLICIT
|
|
1023
|
+
* `startMainContextPump()` holder gets unconditional dispatch (its documented
|
|
1024
|
+
* contract: co-pump the context, whatever is in it). The AUTO-armed pump
|
|
1025
|
+
* dispatches only while a JS-armed GI callback is outstanding (scope=async
|
|
1026
|
+
* completions + scope=notified sources), so a FINISHED program stops touching
|
|
1027
|
+
* the addon entirely instead of grinding through GDK/GIO's C-armed housekeeping
|
|
1028
|
+
* sources forever.
|
|
1029
|
+
*
|
|
1030
|
+
* That silence is load-bearing on Deno: the documented #47 N-API teardown race
|
|
1031
|
+
* is armed by the pump ENTERING the addon between test files (see pendingView
|
|
1032
|
+
* above), while stopping/unref'ing the pump at teardown does NOT avoid it.
|
|
1033
|
+
* Nothing is lost for a RUNNING program: any JS-observable progress path is
|
|
1034
|
+
* counted (a Gio completion is scope=async, a `GLib.timeout_add` is
|
|
1035
|
+
* scope=notified), and while one is outstanding the beat drains ALL ready
|
|
1036
|
+
* sources, C-armed ones included.
|
|
1037
|
+
*/
|
|
1038
|
+
function pumpBeat() {
|
|
1039
|
+
if (pumpRefCount > pumpAutoRefs || pendingView[0] > 0) {
|
|
1040
|
+
drainReadySources();
|
|
1041
|
+
}
|
|
1042
|
+
syncPumpKeepAlive();
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Keep-alive accounting — the portable twin of the uv auto-pump's ref/unref
|
|
1047
|
+
* policy (`src/loop.cc`): scheduled GLib work (an armed timeout/idle) or an
|
|
1048
|
+
* in-flight scope=async GI callback holds the runtime's event loop open;
|
|
1049
|
+
* anything else lets it drain.
|
|
1050
|
+
*
|
|
1051
|
+
* Both halves are load-bearing. Permanently UNREF'd (the pre-fix behaviour) a
|
|
1052
|
+
* program whose only pending work lives in GLib dies mid-flight — a bare
|
|
1053
|
+
* `await` on a GLib timeout never settles and the runtime exits 0 with the
|
|
1054
|
+
* program half-run. Permanently REF'd, a sync-only program would never exit.
|
|
1055
|
+
*
|
|
1056
|
+
* Consequence, identical to Node's auto-pump: a REPEATING GLib timeout has
|
|
1057
|
+
* `setInterval` semantics and keeps the process alive, where `gjs -m` exits once
|
|
1058
|
+
* the module settles. That is the one deliberate lifetime divergence from gjs,
|
|
1059
|
+
* and it is now the SAME on node, bun and deno rather than node-only.
|
|
1060
|
+
*/
|
|
1061
|
+
function syncPumpKeepAlive() {
|
|
1062
|
+
if (pumpTimer === null) return;
|
|
1063
|
+
const hold = pendingView[0] > 0; // zero-N-API — see pendingView above
|
|
1064
|
+
if (hold === pumpHoldsRuntime) return;
|
|
1065
|
+
if (hold) pumpTimer.ref?.();
|
|
1066
|
+
else pumpTimer.unref?.();
|
|
1067
|
+
pumpHoldsRuntime = hold;
|
|
1068
|
+
}
|
|
980
1069
|
|
|
981
1070
|
/**
|
|
982
1071
|
* Start co-pumping the default GLib main context from a runtime timer (Bun/Deno).
|
|
983
1072
|
* No-op on Node, where the libuv↔GLib bridge already co-pumps. Reference-counted;
|
|
984
1073
|
* returns a disposer that decrements the count (clearing the timer at zero).
|
|
1074
|
+
*
|
|
1075
|
+
* `requireGi` arms this automatically on Bun/Deno (the Node twin of
|
|
1076
|
+
* `native.startMainLoop()`), so a plain `bun bundle.mjs` / `deno run bundle.mjs`
|
|
1077
|
+
* dispatches GLib sources with no explicit loop — calling it by hand is only
|
|
1078
|
+
* needed to hold the pump across a scope that must not depend on that.
|
|
985
1079
|
* @returns {() => void}
|
|
986
1080
|
*/
|
|
987
1081
|
export function startMainContextPump() {
|
|
@@ -991,18 +1085,17 @@ export function startMainContextPump() {
|
|
|
991
1085
|
if (native.isNodeRuntime || native.RUNTIME === 'gjs') return () => {};
|
|
992
1086
|
pumpRefCount++;
|
|
993
1087
|
if (pumpTimer === null) {
|
|
994
|
-
|
|
1088
|
+
if (pendingView === null) pendingView = native.makePumpPendingCount();
|
|
1089
|
+
// ~4 ms cadence: low latency without busy-spinning. Each beat drains every
|
|
995
1090
|
// currently-ready source (iterateMainContext(false) returns false when none
|
|
996
|
-
// remain, bounding the inner loop)
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
// (Node/Bun expose Timeout.unref; Deno's numeric handle has none).
|
|
1005
|
-
if (typeof pumpTimer.unref === 'function') pumpTimer.unref();
|
|
1091
|
+
// remain, bounding the inner loop) — gated on the program's own GLib work
|
|
1092
|
+
// for the auto-armed pump, see pumpBeat — then re-evaluates the hold.
|
|
1093
|
+
pumpTimer = setInterval(pumpBeat, 4);
|
|
1094
|
+
// Start UNREF'd: the pump alone must never keep a finished program alive.
|
|
1095
|
+
// syncPumpKeepAlive() refs it for exactly as long as GLib has work.
|
|
1096
|
+
pumpTimer.unref?.();
|
|
1097
|
+
pumpHoldsRuntime = false;
|
|
1098
|
+
syncPumpKeepAlive();
|
|
1006
1099
|
}
|
|
1007
1100
|
let disposed = false;
|
|
1008
1101
|
return () => {
|
|
@@ -1019,6 +1112,7 @@ export function stopMainContextPump() {
|
|
|
1019
1112
|
if (pumpRefCount === 0 && pumpTimer !== null) {
|
|
1020
1113
|
clearInterval(pumpTimer);
|
|
1021
1114
|
pumpTimer = null;
|
|
1115
|
+
pumpHoldsRuntime = false;
|
|
1022
1116
|
}
|
|
1023
1117
|
}
|
|
1024
1118
|
|
|
@@ -2309,7 +2403,27 @@ export function requireGi(namespace, version) {
|
|
|
2309
2403
|
// 'beforeExit' fires exactly when the loop runs empty: drain what is
|
|
2310
2404
|
// ready and re-arm — if GLib still has scheduled work, the armed (ref'd)
|
|
2311
2405
|
// uv timer revives the loop; otherwise the process exits normally.
|
|
2312
|
-
|
|
2406
|
+
runtimeProcess.on('beforeExit', () => native.pumpKick());
|
|
2407
|
+
} else if (native.RUNTIME !== 'gjs') {
|
|
2408
|
+
// Bun/Deno: no libuv to hook, so the portable timer pump IS the auto-pump.
|
|
2409
|
+
// Armed HERE, not left to the caller, so that `bun bundle.mjs` /
|
|
2410
|
+
// `deno run bundle.mjs` behaves like `node bundle.mjs` and `gjs -m`:
|
|
2411
|
+
// GLib sources dispatch with no explicit loop anywhere, and the pump's
|
|
2412
|
+
// keep-alive accounting decides when the process may exit. Leaving it
|
|
2413
|
+
// opt-in is what made the same bundle exit 0 MID-suite on bun/deno while
|
|
2414
|
+
// node ran it to completion.
|
|
2415
|
+
startMainContextPump(); // permanent +1 — the auto-pump is never disposed
|
|
2416
|
+
// Tag that hold as the AUTO pump's: pumpBeat dispatches for it only while
|
|
2417
|
+
// a JS-armed GI callback is outstanding, so a finished program produces
|
|
2418
|
+
// no teardown churn (the deno #47 window) — explicit callers keep
|
|
2419
|
+
// unconditional dispatch.
|
|
2420
|
+
pumpAutoRefs++;
|
|
2421
|
+
// The portable analogue of the Node pump's beforeExit kick. The runtime's
|
|
2422
|
+
// loop just ran empty, which is exactly the moment an unref'd pump would
|
|
2423
|
+
// let the process die with GLib work still outstanding: run one beat —
|
|
2424
|
+
// if the program still owns GLib work it drains + re-refs (reviving the
|
|
2425
|
+
// loop); if not, the beat stays silent and the process exits.
|
|
2426
|
+
runtimeProcess.on('beforeExit', pumpBeat);
|
|
2313
2427
|
}
|
|
2314
2428
|
loopAttached = true;
|
|
2315
2429
|
}
|
package/index.d.ts
CHANGED
|
@@ -418,6 +418,27 @@ export function startMainLoop(): void;
|
|
|
418
418
|
*/
|
|
419
419
|
export function iterateMainContext(mayBlock?: boolean): boolean;
|
|
420
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Whether the default GLib main context holds work a *pumping* runtime must stay
|
|
423
|
+
* alive for: a scheduled source (an armed timeout/idle) or an in-flight
|
|
424
|
+
* scope=async GI callback. Pure GLib — the query half of the keep-alive contract
|
|
425
|
+
* the L1 `startMainContextPump` uses on Bun/Deno to decide whether its timer
|
|
426
|
+
* holds the runtime's event loop open. Node reads the same facts inline in the
|
|
427
|
+
* uv auto-pump.
|
|
428
|
+
*/
|
|
429
|
+
export function mainContextHasPending(): boolean;
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Create an `Int32Array(1)` view over the same JS-armed-work counter
|
|
433
|
+
* `mainContextHasPending()` reads (`view[0] > 0` is equivalent). Reading the
|
|
434
|
+
* view is a plain memory access — no call into the addon — which the Bun/Deno
|
|
435
|
+
* pump's idle beat depends on: on Deno, entering the addon from a timer tick
|
|
436
|
+
* during the between-test-files GC window reproduces the #47 boxed-handle
|
|
437
|
+
* teardown SIGSEGV. Lazy factory: the @gjsify/napi shim loud-stubs external
|
|
438
|
+
* arraybuffers and never arms the portable pump, so it must not run at Init.
|
|
439
|
+
*/
|
|
440
|
+
export function makePumpPendingCount(): Int32Array;
|
|
441
|
+
|
|
421
442
|
/**
|
|
422
443
|
* Drain ready GLib sources + re-arm the auto-pump's libuv wake-ups now (no-op
|
|
423
444
|
* unless {@link startMainLoop} armed the pump on this env). The L1 layer wires
|
|
@@ -548,6 +569,8 @@ declare const native: {
|
|
|
548
569
|
isVariantHandle: typeof isVariantHandle;
|
|
549
570
|
startMainLoop: typeof startMainLoop;
|
|
550
571
|
iterateMainContext: typeof iterateMainContext;
|
|
572
|
+
mainContextHasPending: typeof mainContextHasPending;
|
|
573
|
+
makePumpPendingCount: typeof makePumpPendingCount;
|
|
551
574
|
pumpKick: typeof pumpKick;
|
|
552
575
|
setMicrotaskDrain: typeof setMicrotaskDrain;
|
|
553
576
|
connectSignal: typeof connectSignal;
|
package/index.js
CHANGED
|
@@ -179,11 +179,32 @@ if (!isNodeRuntime && RUNTIME !== 'gjs') {
|
|
|
179
179
|
// (Bun's own processTicksAndRejections calls it from JS).
|
|
180
180
|
({ drainMicrotasks: drain } = require('bun:jsc'));
|
|
181
181
|
} else if (typeof globalThis.Deno?.internal === 'symbol') {
|
|
182
|
-
// V8: core.runMicrotasks
|
|
183
|
-
// Isolate::PerformMicrotaskCheckpoint
|
|
184
|
-
//
|
|
182
|
+
// V8: the checkpoint needs BOTH queues. core.runMicrotasks is only
|
|
183
|
+
// Isolate::PerformMicrotaskCheckpoint; Deno's node-compat
|
|
184
|
+
// process.nextTick queue is a SEPARATE deno_core queue
|
|
185
|
+
// (ext:deno_node/_next_tick.ts → core.queueNextTick) that only the
|
|
186
|
+
// runtime's own event loop drains — and that loop is paused for the
|
|
187
|
+
// whole lifetime of a blocking run(). node:stream delivers stream
|
|
188
|
+
// 'end' via process.nextTick (endReadableNT), so with a
|
|
189
|
+
// microtask-only drain any body consumption over a Readable
|
|
190
|
+
// (@gjsify/fetch consumeBody → XHR arrayBuffer/text) got its chunks
|
|
191
|
+
// (microtask-delivered) but never the end: every asset load hung at
|
|
192
|
+
// readyState 3 forever on Deno while the SAME bundle settled on Bun,
|
|
193
|
+
// whose nextTick rides JSC's microtask queue (bun:jsc
|
|
194
|
+
// drainMicrotasks covers it). core.runNextTicks mirrors node's
|
|
195
|
+
// task_queues.js runNextTicks: microtask checkpoint when no ticks
|
|
196
|
+
// are scheduled, full processTicksAndRejections (ticks + microtasks
|
|
197
|
+
// interleaved) when there are — one call, both queues. Regression:
|
|
198
|
+
// test/blocking-run-checkpoint.test.mjs.
|
|
185
199
|
const core = globalThis.Deno[globalThis.Deno.internal]?.core;
|
|
186
|
-
if (typeof core?.
|
|
200
|
+
if (typeof core?.runNextTicks === 'function') {
|
|
201
|
+
drain = () => core.runNextTicks();
|
|
202
|
+
} else if (typeof core?.runMicrotasks === 'function') {
|
|
203
|
+
// Older deno_core without runNextTicks: microtask-only drain
|
|
204
|
+
// (pre-fix behaviour — promise continuations settle, nextTick
|
|
205
|
+
// consumers like node:stream 'end' still starve).
|
|
206
|
+
drain = () => core.runMicrotasks();
|
|
207
|
+
}
|
|
187
208
|
}
|
|
188
209
|
if (typeof drain === 'function') native.setMicrotaskDrain(drain);
|
|
189
210
|
} catch {
|
|
@@ -640,6 +661,35 @@ export const startMainLoop = native.startMainLoop;
|
|
|
640
661
|
*/
|
|
641
662
|
export const iterateMainContext = native.iterateMainContext;
|
|
642
663
|
|
|
664
|
+
/**
|
|
665
|
+
* Whether the default GLib main context holds work a *pumping* runtime must stay
|
|
666
|
+
* alive for: a scheduled source (an armed timeout/idle — the prepare/query
|
|
667
|
+
* timeout hint is >= 0) or an in-flight scope=async GI callback. Pure GLib —
|
|
668
|
+
* the companion of {@link iterateMainContext} and the query half of the
|
|
669
|
+
* keep-alive contract the L1 `startMainContextPump` applies on Bun/Deno (Node
|
|
670
|
+
* reads the same facts inline in the uv auto-pump). A `false` answer is what
|
|
671
|
+
* lets a sync-only program exit immediately.
|
|
672
|
+
* @returns {boolean}
|
|
673
|
+
*/
|
|
674
|
+
export const mainContextHasPending = native.mainContextHasPending;
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Create an `Int32Array(1)` view over the SAME JS-armed-work counter
|
|
678
|
+
* {@link mainContextHasPending} reads — `view[0] > 0` ⇔
|
|
679
|
+
* `mainContextHasPending()`. The zero-N-API spelling: reading the view is a
|
|
680
|
+
* plain V8 memory access, no call into the addon. Load-bearing for the Bun/Deno
|
|
681
|
+
* pump's IDLE beat — on Deno, merely ENTERING the addon from a timer tick
|
|
682
|
+
* during the between-test-files GC window reproduces the #47 boxed-handle
|
|
683
|
+
* teardown SIGSEGV (measured: a query-only tick crashed 3/3 where a tick that
|
|
684
|
+
* never touches the addon exited 0), so the beat must be able to answer "is
|
|
685
|
+
* there work?" without a single native call. A lazy FACTORY rather than an
|
|
686
|
+
* Init-time export: the @gjsify/napi shim (the gjs-napi conformance oracle)
|
|
687
|
+
* loud-stubs `napi_create_external_arraybuffer`, and the gjs host never arms
|
|
688
|
+
* the portable pump — called only on the first Bun/Deno pump arm.
|
|
689
|
+
* @returns {Int32Array}
|
|
690
|
+
*/
|
|
691
|
+
export const makePumpPendingCount = native.makePumpPendingCount;
|
|
692
|
+
|
|
643
693
|
/**
|
|
644
694
|
* Drain ready GLib sources + re-arm the auto-pump's libuv wake-ups NOW (no-op
|
|
645
695
|
* unless {@link startMainLoop} armed the pump, i.e. Node's main env). The L1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/node-gi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "GObject-Introspection runtime for Node.js — load gi:// namespaces (GLib/GObject/Gio/…) with GJS-compatible semantics. Vendored + rewritten from node-gtk under MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
Binary file
|
|
Binary file
|
package/src/addon.cc
CHANGED
|
@@ -113,6 +113,8 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
113
113
|
exports.Set("isVariantHandle", Napi::Function::New(env, IsVariantHandle));
|
|
114
114
|
exports.Set("startMainLoop", Napi::Function::New(env, StartMainLoop));
|
|
115
115
|
exports.Set("iterateMainContext", Napi::Function::New(env, IterateMainContext));
|
|
116
|
+
exports.Set("mainContextHasPending", Napi::Function::New(env, MainContextHasPending));
|
|
117
|
+
exports.Set("makePumpPendingCount", Napi::Function::New(env, MakePumpPendingCount));
|
|
116
118
|
exports.Set("pumpKick", Napi::Function::New(env, PumpKick));
|
|
117
119
|
exports.Set("setMicrotaskDrain", Napi::Function::New(env, SetMicrotaskDrain));
|
|
118
120
|
exports.Set("connectSignal", Napi::Function::New(env, ConnectSignal));
|
package/src/calls.cc
CHANGED
|
@@ -28,10 +28,23 @@ struct NodeGiCallback {
|
|
|
28
28
|
ffi_closure* closure; // from gi_callable_info_create_closure
|
|
29
29
|
gpointer native; // executable trampoline address
|
|
30
30
|
GIScopeType scope;
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
31
|
+
// Counted in the auto-pump's in-flight keep-alive — the process stays alive
|
|
32
|
+
// while JS-armed GLib work is outstanding, like in-flight Node I/O.
|
|
33
|
+
// • scope=async — a one-shot completion (GAsyncReadyCallback), released
|
|
34
|
+
// at the trampoline's single invocation.
|
|
35
|
+
// • scope=notified — a callback GLib owns until its GDestroyNotify fires:
|
|
36
|
+
// `GLib.timeout_add` / `GLib.idle_add` and friends. Released by
|
|
37
|
+
// NodeGiCallbackFree, i.e. exactly when GLib drops the source (the
|
|
38
|
+
// callback returned SOURCE_REMOVE, or `GLib.Source.remove` killed it).
|
|
39
|
+
// Either way released exactly once, also at free if it never fired.
|
|
40
|
+
//
|
|
41
|
+
// Counting the JS-ARMED sources is the point: a source armed from JS is
|
|
42
|
+
// program work the process must survive for, while a source armed inside C —
|
|
43
|
+
// GDK's frame clock, GIO's portal/a11y retries — is library plumbing that
|
|
44
|
+
// `gjs -m` would never stay alive for either. Keying the keep-alive on "the
|
|
45
|
+
// context has ANY scheduled source" instead made a finished GTK program
|
|
46
|
+
// immortal: GDK leaves a ~1 s repeating timeout armed, so the context reports
|
|
47
|
+
// work forever while nothing is ever ready to dispatch.
|
|
35
48
|
bool pumpCounted = false;
|
|
36
49
|
};
|
|
37
50
|
|
|
@@ -301,10 +314,14 @@ static NodeGiCallback* CreateCallback(Napi::Env env, Napi::Function fn, GICallab
|
|
|
301
314
|
cb->env = env;
|
|
302
315
|
cb->info = reinterpret_cast<GICallableInfo*>(gi_base_info_ref(callbackInfo));
|
|
303
316
|
cb->scope = scope;
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
|
|
317
|
+
// Count the JS-armed GLib work in the auto-pump keep-alive so the process
|
|
318
|
+
// stays alive until it completes: a one-shot in-flight operation
|
|
319
|
+
// (GAsyncReadyCallback et al, scope=async) or a source GLib owns until its
|
|
320
|
+
// destroy-notify (`GLib.timeout_add`/`idle_add`, scope=notified). See the
|
|
321
|
+
// `pumpCounted` note above for why JS-armed is the right unit.
|
|
322
|
+
if (scope == GI_SCOPE_TYPE_ASYNC || scope == GI_SCOPE_TYPE_NOTIFIED) {
|
|
323
|
+
cb->pumpCounted = NodeGiPumpAsyncBegin(env);
|
|
324
|
+
}
|
|
308
325
|
napi_create_reference(env, fn, 1, &cb->jsFn);
|
|
309
326
|
cb->closure = gi_callable_info_create_closure(callbackInfo, &cb->cif, NodeGiCallbackTrampoline, cb);
|
|
310
327
|
cb->native = gi_callable_info_get_closure_native_address(callbackInfo, cb->closure);
|
|
@@ -545,6 +562,7 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
|
|
|
545
562
|
// the invoke (the callee ref'd the GBytes if it kept it), transfer-full refs
|
|
546
563
|
// belong to the callee (dropped only when the callee never ran).
|
|
547
564
|
CreatedBytes createdBytes;
|
|
565
|
+
CreatedValues createdValues;
|
|
548
566
|
bool ok = true;
|
|
549
567
|
size_t jsCursor = 0;
|
|
550
568
|
for (unsigned int i = 0; i < n_args && ok; i++) {
|
|
@@ -800,7 +818,7 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
|
|
|
800
818
|
} else {
|
|
801
819
|
GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
|
|
802
820
|
ok = JsToGIArgument(env, v, ti, &in_args[inPos[i]], &held[i], tr, &ownedInStrings,
|
|
803
|
-
&createdClosures, &createdBytes);
|
|
821
|
+
&createdClosures, &createdBytes, &createdValues);
|
|
804
822
|
}
|
|
805
823
|
}
|
|
806
824
|
|
|
@@ -818,6 +836,9 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
|
|
|
818
836
|
// Likewise every fresh GBytes built from a JS typed array — never adopted.
|
|
819
837
|
for (GBytes* b : createdBytes.transferNone) g_bytes_unref(b);
|
|
820
838
|
for (GBytes* b : createdBytes.transferFull) g_bytes_unref(b);
|
|
839
|
+
// Likewise every GValue boxed from a plain JS value — never adopted.
|
|
840
|
+
for (GValue* gv : createdValues.transferNone) NodeGiFreeBoxedGValue(gv);
|
|
841
|
+
for (GValue* gv : createdValues.transferFull) NodeGiFreeBoxedGValue(gv);
|
|
821
842
|
// Likewise the transfer-full-instance ref: the callee never consumed it.
|
|
822
843
|
if (instanceRefTaken) g_object_unref(static_cast<GObject*>(instance));
|
|
823
844
|
for (gpointer s : ownedInStrings) g_free(s); // never reached the callee (#658)
|
|
@@ -857,6 +878,11 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
|
|
|
857
878
|
// paths). A callee that kept the bytes (GdkPixbuf.Pixbuf.new_from_bytes) holds
|
|
858
879
|
// its own ref. Transfer-full GBytes were adopted by the callee — no release.
|
|
859
880
|
for (GBytes* b : createdBytes.transferNone) g_bytes_unref(b);
|
|
881
|
+
// Transfer-none GValue IN-args boxed from plain JS values: the callee copied
|
|
882
|
+
// whatever it needed (g_object_set_property copies into the property), so the
|
|
883
|
+
// box is ours to free — success AND error paths, mirroring the GBytes rule.
|
|
884
|
+
// Transfer-full GValues were adopted by the callee — no release.
|
|
885
|
+
for (GValue* gv : createdValues.transferNone) NodeGiFreeBoxedGValue(gv);
|
|
860
886
|
if (!success) {
|
|
861
887
|
for (const InContainer& c : inContainers) FreeInContainer(c);
|
|
862
888
|
// A failed invoke did not adopt the transfer-full IN/INOUT strings we g_strdup'd
|
package/src/common.h
CHANGED
|
@@ -295,6 +295,33 @@ struct CreatedBytes {
|
|
|
295
295
|
std::vector<GBytes*> transferFull; // callee adopts; unref only when the callee never ran
|
|
296
296
|
};
|
|
297
297
|
|
|
298
|
+
// A plain JS value handed to a `GObject.Value` IN-arg is BOXED into a fresh
|
|
299
|
+
// GValue whose GType is guessed from the JS value — exactly what GJS does
|
|
300
|
+
// (refs/gjs/gi/arg-cache.cpp GValueIn::in → gjs_value_to_g_value, whose
|
|
301
|
+
// gjs_value_guess_g_type supplies the type for an uninitialized GValue). Without
|
|
302
|
+
// it, the single most common GObject call in existence —
|
|
303
|
+
// `obj.set_property('name', 0.5)`, whose second GI argument is a GValue — fails
|
|
304
|
+
// with "Unsupported interface IN argument", because a JS number is not a boxed
|
|
305
|
+
// handle. An existing GObject.Value boxed handle still routes through the
|
|
306
|
+
// boxed-handle path and is passed through untouched.
|
|
307
|
+
//
|
|
308
|
+
// Lifetime mirrors CreatedBytes: the box is freed after the invoke for
|
|
309
|
+
// transfer-none (the callee copied whatever it needed — g_object_set_property
|
|
310
|
+
// copies into the property), and left with the callee for transfer-full.
|
|
311
|
+
struct CreatedValues {
|
|
312
|
+
std::vector<GValue*> transferNone; // free after the invoke (callee borrowed)
|
|
313
|
+
std::vector<GValue*> transferFull; // callee adopts; free only when it never ran
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Release a GValue this layer boxed: unset the held value (dropping the ref an
|
|
317
|
+
// object/boxed/string GValue took) and free the struct. A plain `g_free` would
|
|
318
|
+
// leak whatever the GValue owns.
|
|
319
|
+
inline void NodeGiFreeBoxedGValue(GValue* gv) {
|
|
320
|
+
if (gv == nullptr) return;
|
|
321
|
+
if (G_IS_VALUE(gv)) g_value_unset(gv);
|
|
322
|
+
g_free(gv);
|
|
323
|
+
}
|
|
324
|
+
|
|
298
325
|
// Build the (floating) marshaled GClosure for a JS function (signals.cc — shares
|
|
299
326
|
// the battle-tested JsClosure marshal/finalize machinery with `.connect()`).
|
|
300
327
|
GClosure* NodeGiMakeGenericJsClosure(Napi::Env env, Napi::Value fn);
|
|
@@ -309,12 +336,16 @@ GClosure* NodeGiMakeGenericJsClosure(Napi::Env env, Napi::Value fn);
|
|
|
309
336
|
// `bytes` (optional): enables the JS-bytes→GBytes IN-arg marshalling above;
|
|
310
337
|
// nullptr (the default) keeps the previous behaviour (typed array → TypeError) on
|
|
311
338
|
// paths that cannot release a created GBytes.
|
|
339
|
+
// `values` (optional): enables the JS-value→GValue IN-arg boxing above; nullptr
|
|
340
|
+
// (the default) keeps the previous behaviour (plain value → TypeError) on paths
|
|
341
|
+
// that cannot release a created GValue.
|
|
312
342
|
bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument* out,
|
|
313
343
|
std::string* heldString,
|
|
314
344
|
GITransfer transfer = GI_TRANSFER_NOTHING,
|
|
315
345
|
std::vector<gpointer>* ownedStrings = nullptr,
|
|
316
346
|
CreatedClosures* closures = nullptr,
|
|
317
|
-
CreatedBytes* bytes = nullptr
|
|
347
|
+
CreatedBytes* bytes = nullptr,
|
|
348
|
+
CreatedValues* values = nullptr);
|
|
318
349
|
|
|
319
350
|
// ---- IN container building -----------------------------------------
|
|
320
351
|
//
|
|
@@ -604,6 +635,12 @@ Napi::Value BindingGroupBindFull(const Napi::CallbackInfo& info);
|
|
|
604
635
|
// loop.cc
|
|
605
636
|
Napi::Value StartMainLoop(const Napi::CallbackInfo& info);
|
|
606
637
|
Napi::Value IterateMainContext(const Napi::CallbackInfo& info);
|
|
638
|
+
Napi::Value MainContextHasPending(const Napi::CallbackInfo& info);
|
|
639
|
+
// Factory for an Int32Array(1) view over the JS-armed-work counter (see
|
|
640
|
+
// loop.cc) — lets the L1 pump answer "does the program own GLib work?" WITHOUT
|
|
641
|
+
// a napi call. Lazy (a factory, not an Init-time export): the @gjsify/napi shim
|
|
642
|
+
// loud-stubs napi_create_external_arraybuffer and never arms the pump.
|
|
643
|
+
Napi::Value MakePumpPendingCount(const Napi::CallbackInfo& info);
|
|
607
644
|
Napi::Value PumpKick(const Napi::CallbackInfo& info);
|
|
608
645
|
Napi::Value SetMicrotaskDrain(const Napi::CallbackInfo& info);
|
|
609
646
|
|
|
@@ -612,8 +649,10 @@ Napi::Value SetMicrotaskDrain(const Napi::CallbackInfo& info);
|
|
|
612
649
|
// In-flight scope=async keep-alive: while a GI scope=async callback (e.g. a
|
|
613
650
|
// GAsyncReadyCallback) is pending, the pump holds a libuv ref so a plain Node
|
|
614
651
|
// script survives until the completion dispatches (the top-level-await case).
|
|
615
|
-
//
|
|
616
|
-
//
|
|
652
|
+
// The counter itself is runtime-independent (Bun/Deno's portable pump reads it
|
|
653
|
+
// via MainContextHasPending); only the libuv ref is Node-only. Begin returns
|
|
654
|
+
// whether the callback was counted (false only for a foreign env, i.e. a worker
|
|
655
|
+
// thread once Node's main env owns the pump); the caller must call End exactly
|
|
617
656
|
// once for each counted callback. Main-thread only.
|
|
618
657
|
bool NodeGiPumpAsyncBegin(napi_env env);
|
|
619
658
|
void NodeGiPumpAsyncEnd();
|
package/src/loop.cc
CHANGED
|
@@ -108,8 +108,14 @@ static void DrainMicrotasks() {
|
|
|
108
108
|
//
|
|
109
109
|
// Portable fix: N-API exposes no engine microtask checkpoint, but both runtimes
|
|
110
110
|
// expose their OWN drain primitive to JS (Bun: `bun:jsc` drainMicrotasks —
|
|
111
|
-
// JSC's VM drain, callable mid-stack by design
|
|
112
|
-
//
|
|
111
|
+
// JSC's VM drain, callable mid-stack by design, and Bun's process.nextTick
|
|
112
|
+
// rides that same queue; Deno: core.runNextTicks — node's task_queues.js
|
|
113
|
+
// runNextTicks shape, draining the node-compat process.nextTick queue AND the
|
|
114
|
+
// V8 microtask checkpoint. core.runMicrotasks alone is NOT enough on Deno:
|
|
115
|
+
// its nextTick queue is separate from V8's microtasks, and node:stream's
|
|
116
|
+
// 'end' is nextTick-delivered (endReadableNT), so a microtask-only drain hung
|
|
117
|
+
// every fetch/XHR body consumption at readyState 3 during a blocking run —
|
|
118
|
+
// see test/blocking-run-checkpoint.test.mjs). index.js registers
|
|
113
119
|
// it here on non-Node runtimes only; the loop-dispatched trampolines
|
|
114
120
|
// (signals.cc / calls.cc / class.cc) invoke NodeGiMaybeDrainMicrotasks at their
|
|
115
121
|
// OUTERMOST boundary (g_loopDispatchDepth == 0). Node never registers — its
|
|
@@ -544,14 +550,31 @@ static void PumpArmWakeups() {
|
|
|
544
550
|
if (g_pump_async_pending > 0 && (timeout < 0 || timeout > 32)) timeout = 32;
|
|
545
551
|
}
|
|
546
552
|
|
|
553
|
+
// WAKE-UP and HOLD are separate decisions.
|
|
554
|
+
//
|
|
555
|
+
// Wake-up: whenever GLib has a deadline, arm the mirrored uv timer for it, so
|
|
556
|
+
// libuv's poll sleep ends exactly when GLib next has work.
|
|
557
|
+
//
|
|
558
|
+
// Hold (uv_ref): ONLY while JS-armed GLib work is outstanding — the
|
|
559
|
+
// scope=async/notified counter. Ref'ing whenever a deadline exists (what this
|
|
560
|
+
// used to do) makes any C-armed toolkit timer immortalize the process: GDK
|
|
561
|
+
// keeps a ~1 s repeating timeout armed for the process's whole life, so
|
|
562
|
+
// `node --test test/gtk-smoke.test.mjs` never exited once the GTK stack had
|
|
563
|
+
// been initialized. `gjs -m` exits when the module settles regardless of what
|
|
564
|
+
// GLib still has scheduled; keying the hold on the program's OWN outstanding
|
|
565
|
+
// GLib work is as close to that as a libuv-driven process can get, and it is
|
|
566
|
+
// what keeps a top-level `await` on a GLib timeout alive (the timeout's
|
|
567
|
+
// GSourceFunc is a scope=notified callback, counted until GLib drops it).
|
|
547
568
|
if (timeout >= 0) {
|
|
548
569
|
uv_timer_start(&g_pump_timer, PumpTimerCb, static_cast<uint64_t>(timeout), 0);
|
|
549
|
-
|
|
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) {
|
|
570
|
+
} else {
|
|
554
571
|
uv_timer_stop(&g_pump_timer);
|
|
572
|
+
}
|
|
573
|
+
const gboolean hold = g_pump_async_pending > 0 && timeout >= 0;
|
|
574
|
+
if (hold && !g_pump_timer_reffed) {
|
|
575
|
+
uv_ref(reinterpret_cast<uv_handle_t*>(&g_pump_timer));
|
|
576
|
+
g_pump_timer_reffed = TRUE;
|
|
577
|
+
} else if (!hold && g_pump_timer_reffed) {
|
|
555
578
|
uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_timer));
|
|
556
579
|
g_pump_timer_reffed = FALSE;
|
|
557
580
|
}
|
|
@@ -626,15 +649,72 @@ static void PumpInit(uv_loop_t* loop) {
|
|
|
626
649
|
g_pump_inited = TRUE;
|
|
627
650
|
}
|
|
628
651
|
|
|
652
|
+
// mainContextHasPending() -> boolean
|
|
653
|
+
//
|
|
654
|
+
// Is there JS-armed GLib work a *pumping* runtime must stay alive for? True
|
|
655
|
+
// while a GI callback the program handed to GLib is outstanding — a scope=async
|
|
656
|
+
// completion (a GAsyncReadyCallback) or a scope=notified source
|
|
657
|
+
// (`GLib.timeout_add`/`idle_add`), counted in calls.cc and released when GLib
|
|
658
|
+
// drops it. The query half of the keep-alive contract for the Bun/Deno pump
|
|
659
|
+
// (L1 `startMainContextPump`); Node reads the same counter inline in
|
|
660
|
+
// PumpArmWakeups and never calls this.
|
|
661
|
+
//
|
|
662
|
+
// It deliberately does NOT ask whether the context has any scheduled source.
|
|
663
|
+
// That probe (`g_main_context_prepare` + `_query`, timeout >= 0) reports the
|
|
664
|
+
// timers a TOOLKIT arms in C as well, and GDK keeps a ~1 s repeating timeout
|
|
665
|
+
// armed for the process's whole life: a finished GTK program then answers
|
|
666
|
+
// "pending" forever while nothing is ever ready to dispatch, and never exits.
|
|
667
|
+
// `gjs -m` would not stay alive for those either — it exits once the module
|
|
668
|
+
// settles. A `false` answer is also what lets a sync-only program exit
|
|
669
|
+
// immediately: the pump alone must never keep a finished program alive.
|
|
670
|
+
Napi::Value MainContextHasPending(const Napi::CallbackInfo& info) {
|
|
671
|
+
Napi::Env env = info.Env();
|
|
672
|
+
return Napi::Boolean::New(env, g_pump_async_pending > 0);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// makePumpPendingCount() -> Int32Array(1) OVER g_pump_async_pending (external,
|
|
676
|
+
// zero copy). The JS pump's beat reads this view instead of calling
|
|
677
|
+
// MainContextHasPending(): on Deno, merely ENTERING the addon from the pump's
|
|
678
|
+
// timer tick during the between-test-files GC window reproduces the #47
|
|
679
|
+
// boxed-handle teardown SIGSEGV — measured on the gtk-smoke leg, a query-only
|
|
680
|
+
// tick (one napi call, no dispatch) crashed 3/3 while a tick that never touches
|
|
681
|
+
// the addon exited 0. A typed-array read is a plain JS memory access, so the
|
|
682
|
+
// beat's idle path stays native-silent. No atomics: the counter is written only
|
|
683
|
+
// from the JS thread (Begin/End are main-thread only) and read from JS.
|
|
684
|
+
//
|
|
685
|
+
// A FACTORY, deliberately not created at addon Init: the @gjsify/napi shim
|
|
686
|
+
// (the gjs-napi conformance oracle runs node-gi under it) loud-stubs
|
|
687
|
+
// napi_create_external_arraybuffer, and the gjs host never arms the portable
|
|
688
|
+
// pump — L1 calls this lazily on the first Bun/Deno pump arm, so the stub is
|
|
689
|
+
// never reached where the view is never needed.
|
|
690
|
+
Napi::Value MakePumpPendingCount(const Napi::CallbackInfo& info) {
|
|
691
|
+
Napi::Env env = info.Env();
|
|
692
|
+
static_assert(sizeof(g_pump_async_pending) == sizeof(int32_t),
|
|
693
|
+
"the JS Int32Array view requires a 32-bit counter");
|
|
694
|
+
// External buffer over static storage — no finalizer needed, the counter
|
|
695
|
+
// outlives every env.
|
|
696
|
+
Napi::ArrayBuffer buf =
|
|
697
|
+
Napi::ArrayBuffer::New(env, &g_pump_async_pending, sizeof(g_pump_async_pending));
|
|
698
|
+
return Napi::TypedArrayOf<int32_t>::New(env, 1, buf, 0, napi_int32_array);
|
|
699
|
+
}
|
|
700
|
+
|
|
629
701
|
// In-flight scope=async keep-alive: while any GI scope=async callback (a
|
|
630
702
|
// GAsyncReadyCallback) is pending, REF the (always-active) prepare handle so the
|
|
631
703
|
// libuv loop stays alive until the completion arrives — the exact analogue of
|
|
632
704
|
// Node's own in-flight I/O keeping the process alive, and the reason a plain
|
|
633
705
|
// top-level `await` on a Gio async op settles instead of exiting with an
|
|
634
|
-
// unsettled-TLA error.
|
|
706
|
+
// unsettled-TLA error.
|
|
707
|
+
//
|
|
708
|
+
// The COUNTER is maintained on every runtime, not just Node: it is the
|
|
709
|
+
// "in-flight async work" half of the keep-alive contract, and Bun/Deno's
|
|
710
|
+
// portable pump reads it through MainContextHasPending() to decide whether its
|
|
711
|
+
// timer holds the runtime's event loop open. Only the uv ref/unref half is
|
|
712
|
+
// Node-only (it needs the uv handles the uv pump owns). Main-thread only —
|
|
713
|
+
// where a pump env is known (Node), a foreign env is not counted.
|
|
635
714
|
bool NodeGiPumpAsyncBegin(napi_env env) {
|
|
636
|
-
if (
|
|
637
|
-
|
|
715
|
+
if (g_loop_env != nullptr && env != g_loop_env) return false;
|
|
716
|
+
++g_pump_async_pending;
|
|
717
|
+
if (g_pump_inited && g_pump_async_pending == 1 && !g_pump_prepare_reffed) {
|
|
638
718
|
uv_ref(reinterpret_cast<uv_handle_t*>(&g_pump_prepare));
|
|
639
719
|
g_pump_prepare_reffed = TRUE;
|
|
640
720
|
}
|
|
@@ -642,8 +722,9 @@ bool NodeGiPumpAsyncBegin(napi_env env) {
|
|
|
642
722
|
}
|
|
643
723
|
|
|
644
724
|
void NodeGiPumpAsyncEnd() {
|
|
645
|
-
if (
|
|
646
|
-
|
|
725
|
+
if (g_pump_async_pending == 0) return;
|
|
726
|
+
--g_pump_async_pending;
|
|
727
|
+
if (g_pump_inited && g_pump_async_pending == 0 && g_pump_prepare_reffed) {
|
|
647
728
|
uv_unref(reinterpret_cast<uv_handle_t*>(&g_pump_prepare));
|
|
648
729
|
g_pump_prepare_reffed = FALSE;
|
|
649
730
|
}
|
package/src/marshal.cc
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#include "common.h"
|
|
5
5
|
|
|
6
|
+
#include <cmath>
|
|
6
7
|
#include <unordered_map>
|
|
7
8
|
#include <utility>
|
|
8
9
|
|
|
@@ -190,6 +191,101 @@ gpointer TransferBoxedIn(BoxedHandle* h, GITransfer transfer) {
|
|
|
190
191
|
return h->ptr;
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
// Whether `v` is a boxed handle that ALREADY holds a GValue — the one shape a
|
|
195
|
+
// `GObject.Value` IN-arg takes as-is (gjs GValueIn::in passes it through when the
|
|
196
|
+
// JS object's own gtype is G_TYPE_VALUE, and boxes EVERYTHING else, including
|
|
197
|
+
// other boxed handles: a GVariant handed to `set_property('state', …)` becomes a
|
|
198
|
+
// G_TYPE_VARIANT GValue, it does not become the GValue).
|
|
199
|
+
static bool NodeGiIsGValueHandle(Napi::Value v) {
|
|
200
|
+
BoxedHandle* h = TryGetBoxedHandle(v);
|
|
201
|
+
return h != nullptr && h->gtype != G_TYPE_INVALID && g_type_is_a(h->gtype, G_TYPE_VALUE);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Initialise `out` from a plain JS value, guessing the GType the way gjs does
|
|
205
|
+
// for an uninitialized GValue (refs/gjs/gi/value.cpp gjs_value_guess_g_type +
|
|
206
|
+
// gjs_value_to_g_value_internal). Returns false with a pending JS exception when
|
|
207
|
+
// the value has no sensible GValue representation.
|
|
208
|
+
//
|
|
209
|
+
// The int32-vs-double split matches gjs exactly (`value.isInt32()` →
|
|
210
|
+
// `G_TYPE_INT`, `value.isDouble()` → `G_TYPE_DOUBLE`); JS has no such tag at the
|
|
211
|
+
// N-API boundary, so an integral number in int32 range stands in for isInt32.
|
|
212
|
+
// That distinction is not load-bearing for correctness — `g_object_set_property`
|
|
213
|
+
// transforms between the numeric fundamentals — but staying bit-for-bit with gjs
|
|
214
|
+
// keeps a property that reads the GValue's own type behaving identically.
|
|
215
|
+
static bool JsToFreshGValue(Napi::Env env, Napi::Value v, GValue* out) {
|
|
216
|
+
if (v.IsNull() || v.IsUndefined()) {
|
|
217
|
+
// gjs guesses G_TYPE_POINTER for null. A real GValue holding NULL is also
|
|
218
|
+
// the only thing a callee can safely dereference — passing a NULL GValue*
|
|
219
|
+
// (the plain boxed-null path) would crash g_object_set_property.
|
|
220
|
+
g_value_init(out, G_TYPE_POINTER);
|
|
221
|
+
g_value_set_pointer(out, nullptr);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (v.IsBoolean()) {
|
|
225
|
+
g_value_init(out, G_TYPE_BOOLEAN);
|
|
226
|
+
g_value_set_boolean(out, v.As<Napi::Boolean>().Value() ? TRUE : FALSE);
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
if (v.IsString()) {
|
|
230
|
+
g_value_init(out, G_TYPE_STRING);
|
|
231
|
+
g_value_set_string(out, v.As<Napi::String>().Utf8Value().c_str());
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (v.IsBigInt()) {
|
|
235
|
+
bool lossless = false;
|
|
236
|
+
int64_t i64 = v.As<Napi::BigInt>().Int64Value(&lossless);
|
|
237
|
+
if (lossless || i64 < 0) {
|
|
238
|
+
g_value_init(out, G_TYPE_INT64);
|
|
239
|
+
g_value_set_int64(out, i64);
|
|
240
|
+
} else {
|
|
241
|
+
bool ulossless = false;
|
|
242
|
+
g_value_init(out, G_TYPE_UINT64);
|
|
243
|
+
g_value_set_uint64(out, v.As<Napi::BigInt>().Uint64Value(&ulossless));
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if (v.IsNumber()) {
|
|
248
|
+
double d = v.As<Napi::Number>().DoubleValue();
|
|
249
|
+
if (d == std::trunc(d) && d >= static_cast<double>(G_MININT32) &&
|
|
250
|
+
d <= static_cast<double>(G_MAXINT32)) {
|
|
251
|
+
g_value_init(out, G_TYPE_INT);
|
|
252
|
+
g_value_set_int(out, static_cast<gint>(d));
|
|
253
|
+
} else {
|
|
254
|
+
g_value_init(out, G_TYPE_DOUBLE);
|
|
255
|
+
g_value_set_double(out, d);
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
if (v.IsExternal()) {
|
|
260
|
+
// A GObject handle → a G_TYPE_OBJECT GValue carrying its own GType, so a
|
|
261
|
+
// property typed against a subclass still accepts it.
|
|
262
|
+
Napi::External<GObject> objExt = v.As<Napi::External<GObject>>();
|
|
263
|
+
if (objExt.CheckTypeTag(&kGObjectHandleTag)) {
|
|
264
|
+
GObject* obj = objExt.Data();
|
|
265
|
+
GType gt = obj != nullptr ? G_OBJECT_TYPE(obj) : G_TYPE_OBJECT;
|
|
266
|
+
g_value_init(out, gt);
|
|
267
|
+
g_value_set_object(out, obj);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
// A boxed handle → its own registered GType (GVariant has its own
|
|
271
|
+
// fundamental; a non-registered C struct has no GType to name and is
|
|
272
|
+
// rejected rather than guessed at).
|
|
273
|
+
BoxedHandle* h = TryGetBoxedHandle(v);
|
|
274
|
+
if (h != nullptr && h->gtype != G_TYPE_INVALID) {
|
|
275
|
+
g_value_init(out, h->gtype);
|
|
276
|
+
if (h->gtype == G_TYPE_VARIANT) g_value_set_variant(out, static_cast<GVariant*>(h->ptr));
|
|
277
|
+
else if (G_TYPE_IS_BOXED(h->gtype)) g_value_set_boxed(out, h->ptr);
|
|
278
|
+
else g_value_set_pointer(out, h->ptr);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
Napi::TypeError::New(
|
|
283
|
+
env, "Cannot convert this value to a GObject.Value (expected a boolean, number, bigint, "
|
|
284
|
+
"string, null, or a GObject/boxed handle)")
|
|
285
|
+
.ThrowAsJavaScriptException();
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
193
289
|
// Read a boxed handle's pointer if `v` is one (tag-checked; no deref of ptr).
|
|
194
290
|
bool TryGetBoxedPtr(Napi::Value v, gpointer* out) {
|
|
195
291
|
if (!v.IsExternal()) return false;
|
|
@@ -337,7 +433,8 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
|
|
|
337
433
|
GITransfer transfer,
|
|
338
434
|
std::vector<gpointer>* ownedStrings,
|
|
339
435
|
CreatedClosures* closures,
|
|
340
|
-
CreatedBytes* bytes
|
|
436
|
+
CreatedBytes* bytes,
|
|
437
|
+
CreatedValues* values) {
|
|
341
438
|
if (v.IsEmpty()) {
|
|
342
439
|
// Residue of a swallowed napi failure (a fallible Get()/coercion upstream
|
|
343
440
|
// failed on a terminating env, or a throwing getter left the exception
|
|
@@ -399,7 +496,31 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
|
|
|
399
496
|
out->v_pointer = nullptr;
|
|
400
497
|
handled = true;
|
|
401
498
|
} else if (v.IsExternal()) {
|
|
402
|
-
|
|
499
|
+
GObject* obj = v.As<Napi::External<GObject>>().Data();
|
|
500
|
+
// A (transfer full) GObject IN arg CONSUMES a reference: the callee
|
|
501
|
+
// takes over the one it is handed. Passing the handle's own ref
|
|
502
|
+
// makes the callee and the JS wrapper share a single ref that BOTH
|
|
503
|
+
// release — the wrapper's finalize then frees an object the callee
|
|
504
|
+
// still owns, and the callee is left dereferencing freed memory.
|
|
505
|
+
//
|
|
506
|
+
// `gtk_widget_add_controller()` is the canonical case and the one
|
|
507
|
+
// that surfaced it: GTK keeps the controller in the widget's list,
|
|
508
|
+
// the JS wrapper for it goes out of scope, and the next pointer
|
|
509
|
+
// motion over the widget hits
|
|
510
|
+
//
|
|
511
|
+
// Gtk-CRITICAL **: gtk_event_controller_handle_crossing:
|
|
512
|
+
// assertion 'GTK_IS_EVENT_CONTROLLER (controller)' failed
|
|
513
|
+
//
|
|
514
|
+
// NONDETERMINISTICALLY — it needs a GC between the add and the
|
|
515
|
+
// event, which is why it looked unrelated to anything nearby.
|
|
516
|
+
//
|
|
517
|
+
// gjs takes the ref here (wrapperutils.h
|
|
518
|
+
// `transfer_to_gi_argument`: `GI_DIRECTION_IN && transfer !=
|
|
519
|
+
// GI_TRANSFER_NOTHING` → `copy_ptr` → `g_object_ref`), and this
|
|
520
|
+
// file already does the equivalent for BOXED args via
|
|
521
|
+
// `TransferBoxedIn`. Objects were simply never given the same rule.
|
|
522
|
+
if (transfer != GI_TRANSFER_NOTHING && obj != nullptr) g_object_ref(obj);
|
|
523
|
+
out->v_pointer = obj;
|
|
403
524
|
handled = true;
|
|
404
525
|
}
|
|
405
526
|
} else if (GI_IS_ENUM_INFO(iface) || GI_IS_FLAGS_INFO(iface)) {
|
|
@@ -454,6 +575,29 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
|
|
|
454
575
|
if (transfer == GI_TRANSFER_NOTHING) bytes->transferNone.push_back(b);
|
|
455
576
|
else bytes->transferFull.push_back(b);
|
|
456
577
|
handled = true;
|
|
578
|
+
} else if (values != nullptr && !NodeGiIsGValueHandle(v) &&
|
|
579
|
+
gi_registered_type_info_get_g_type(
|
|
580
|
+
reinterpret_cast<GIRegisteredTypeInfo*>(iface)) == G_TYPE_VALUE) {
|
|
581
|
+
// A plain JS value for a `GObject.Value` IN-arg → a fresh GValue whose
|
|
582
|
+
// GType is guessed from the value, exactly as gjs (refs/gjs
|
|
583
|
+
// gi/arg-cache.cpp GValueIn::in → gjs_value_to_g_value). This is what
|
|
584
|
+
// makes `obj.set_property('volume', 0.5)` — whose GI signature takes a
|
|
585
|
+
// GValue — work at all; a JS number is not a boxed handle, so it used
|
|
586
|
+
// to fall through to the "Unsupported interface IN argument" throw.
|
|
587
|
+
//
|
|
588
|
+
// An EXISTING GObject.Value handle is excluded here and routes through
|
|
589
|
+
// the boxed-handle branch below untouched, matching gjs's pass-through
|
|
590
|
+
// for an already-typed GValue.
|
|
591
|
+
GValue* boxed = g_new0(GValue, 1);
|
|
592
|
+
if (!JsToFreshGValue(env, v, boxed)) {
|
|
593
|
+
g_free(boxed);
|
|
594
|
+
gi_base_info_unref(iface);
|
|
595
|
+
return false; // JsToFreshGValue already threw
|
|
596
|
+
}
|
|
597
|
+
out->v_pointer = boxed;
|
|
598
|
+
if (transfer == GI_TRANSFER_NOTHING) values->transferNone.push_back(boxed);
|
|
599
|
+
else values->transferFull.push_back(boxed);
|
|
600
|
+
handled = true;
|
|
457
601
|
} else if (v.IsNull() || v.IsUndefined()) {
|
|
458
602
|
// Boxed/struct IN args arrive as boxed handles; null/undefined maps to
|
|
459
603
|
// a NULL pointer (e.g. GLib.MainLoop.new(null, false)).
|