@gjsify/node-gi 0.23.0 → 0.24.2

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.d.ts CHANGED
@@ -93,7 +93,7 @@ export interface GObjectNamespace extends GiNamespace {
93
93
  ParamSpec: ParamSpecFactories;
94
94
  /**
95
95
  * The FULL introspected `GParamFlags` bitfield (READABLE/WRITABLE/READWRITE/
96
- * CONSTRUCT/CONSTRUCT_ONLY/LAX_VALIDATION/STATIC_*/EXPLICIT_NOTIFY/DEPRECATED/
96
+ * CONSTRUCT/CONSTRUCT_ONLY/LAX_VALIDATION/STATIC_.../EXPLICIT_NOTIFY/DEPRECATED/
97
97
  * …) — the named members below are the common convenience bits; the index
98
98
  * signature covers the remaining introspected members.
99
99
  */
package/globals.d.ts CHANGED
@@ -32,17 +32,11 @@ export interface GjsImports {
32
32
  }
33
33
 
34
34
  declare global {
35
- // eslint-disable-next-line no-var
36
35
  var print: (...args: unknown[]) => void;
37
- // eslint-disable-next-line no-var
38
36
  var printerr: (...args: unknown[]) => void;
39
- // eslint-disable-next-line no-var
40
37
  var log: (...args: unknown[]) => void;
41
- // eslint-disable-next-line no-var
42
38
  var logError: (error: unknown, prefix?: string) => void;
43
- // eslint-disable-next-line no-var
44
39
  var ARGV: string[];
45
- // eslint-disable-next-line no-var
46
40
  var imports: GjsImports;
47
41
  }
48
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.23.0",
3
+ "version": "0.24.2",
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/marshal.cc CHANGED
@@ -153,6 +153,43 @@ BoxedHandle* TryGetBoxedHandle(Napi::Value v) {
153
153
  return ext.Data();
154
154
  }
155
155
 
156
+ // The pointer to hand a callee for a boxed/struct IN argument, honouring the
157
+ // arg's TRANSFER annotation.
158
+ //
159
+ // A transfer-full (or -container) IN arg is ADOPTED by the callee: it will free
160
+ // the pointer. Handing over the very block this handle still owns makes both
161
+ // sides free it — the finalizer's g_boxed_free then hits already-released
162
+ // memory (`free(): invalid pointer`, SIGABRT). The canonical case is
163
+ // `GstWebRTC.WebRTCSessionDescription.new(type, sdp)`, whose `sdp` param is
164
+ // `(transfer full)`: the fresh `GstSDPMessage` from
165
+ // `GstSdp.SDPMessage.new_from_text()` (an OUT `(transfer full)`, so our handle
166
+ // owns it) got freed once by `gst_webrtc_session_description_free` and again by
167
+ // its own finalizer.
168
+ //
169
+ // gjs solves this by COPYING on transfer — refs/gjs/gi/wrapperutils.h
170
+ // `GIWrapperBase::transfer_to_gi_argument` (`GI_DIRECTION_IN &&
171
+ // transfer != GI_TRANSFER_NOTHING` → `Instance::copy_ptr`), with
172
+ // refs/gjs/gi/struct.h `StructInstance::copy_ptr` = `g_variant_ref` for
173
+ // GVariant else refs/gjs/gi/boxed.h `BoxedInstance::copy_ptr` = `g_boxed_copy`.
174
+ // Mirror that: the callee owns an independent instance, the handle keeps (and
175
+ // still owns) its own. Transfer-NOTHING is a plain borrow — unchanged.
176
+ //
177
+ // A handle with no copy function (a plain, non-boxed C struct — G_TYPE_INVALID,
178
+ // or a `rawOwned` caller-allocates OUT block) cannot be duplicated. gjs throws
179
+ // there; we instead RELINQUISH ownership so the finalizer stays out of the
180
+ // callee's way — a leak-free hand-over is impossible either way, and not
181
+ // double-freeing is strictly safer than the alternative.
182
+ gpointer TransferBoxedIn(BoxedHandle* h, GITransfer transfer) {
183
+ if (h == nullptr) return nullptr;
184
+ if (transfer == GI_TRANSFER_NOTHING || h->ptr == nullptr) return h->ptr;
185
+ if (h->gtype == G_TYPE_VARIANT) return g_variant_ref(static_cast<GVariant*>(h->ptr));
186
+ if (h->gtype != G_TYPE_INVALID && G_TYPE_IS_BOXED(h->gtype))
187
+ return g_boxed_copy(h->gtype, h->ptr);
188
+ h->owns = false;
189
+ h->rawOwned = false;
190
+ return h->ptr;
191
+ }
192
+
156
193
  // Read a boxed handle's pointer if `v` is one (tag-checked; no deref of ptr).
157
194
  bool TryGetBoxedPtr(Napi::Value v, gpointer* out) {
158
195
  if (!v.IsExternal()) return false;
@@ -440,7 +477,7 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
440
477
  .ThrowAsJavaScriptException();
441
478
  return false;
442
479
  }
443
- out->v_pointer = h->ptr;
480
+ out->v_pointer = TransferBoxedIn(h, transfer);
444
481
  handled = true;
445
482
  }
446
483
  }
package/src/object.cc CHANGED
@@ -258,7 +258,32 @@ Napi::Value GValueToJs(Napi::Env env, const GValue* v) {
258
258
  // NOT special-cased here: gjs leaves it as a GLib.Bytes boxed handle
259
259
  // (value.cpp has no GBytes case → the generic boxed branch), which is what the
260
260
  // MakeBoxedHandle below already produces (verified: a GBytes signal param →
261
- // `instanceof GLib.Bytes`). A GStrv-as-array read stays a follow-up.
261
+ // `instanceof GLib.Bytes`).
262
+ //
263
+ // G_TYPE_STRV (a NULL-terminated gchar**, e.g. Gtk.Widget:css-classes,
264
+ // Gio.ThemedIcon:names, or any Vala `public string[] x { get; }` property —
265
+ // valac emits g_param_spec_boxed(…, G_TYPE_STRV) for those). This is the READ
266
+ // half of the pair JsToGValue's GStrv branch opened: without it a GStrv
267
+ // property fell through to MakeBoxedHandle and JS got an opaque boxed handle
268
+ // instead of an array — SILENTLY, because a handle is a truthy object whose
269
+ // `.length` is undefined, so `arr ?? []` keeps it and every index/`for` over it
270
+ // yields nothing (this is how @gjsify/http lost every request header on the
271
+ // node-gi bridge: `bridgeReq.header_pairs` became a handle and the pair loop
272
+ // ran zero times). Mirrors GJS gjs_value_from_g_value_internal
273
+ // (refs/gjs/gi/value.cpp:1082 `gtype == G_TYPE_STRV` → gjs_array_from_strv).
274
+ // NULL → an EMPTY ARRAY, not null: GJS explicitly excludes G_TYPE_STRV from its
275
+ // "pointer-valued NULL → JS null" pre-check (value.cpp:1023) and documents the
276
+ // choice in gjs_array_from_strv (arg.cpp:2267) — "clients would need to always
277
+ // check for both an empty array and null" otherwise. g_value_get_boxed BORROWS;
278
+ // every string is copied into JS, so nothing is owned here.
279
+ if (G_VALUE_HOLDS(v, G_TYPE_STRV)) {
280
+ const gchar* const* strv = static_cast<const gchar* const*>(g_value_get_boxed(v));
281
+ guint len = 0;
282
+ while (strv != nullptr && strv[len] != nullptr) len++;
283
+ Napi::Array arr = Napi::Array::New(env, len);
284
+ for (guint i = 0; i < len; i++) arr.Set(i, Napi::String::New(env, strv[i]));
285
+ return arr;
286
+ }
262
287
  if (G_VALUE_HOLDS(v, G_TYPE_BYTE_ARRAY)) {
263
288
  GByteArray* ba = static_cast<GByteArray*>(g_value_get_boxed(v));
264
289
  if (ba == nullptr) return env.Null();
package/system.js CHANGED
@@ -20,25 +20,61 @@
20
20
  // returning the same shape GJS does — they exist so GJS source that calls them
21
21
  // keeps running unmodified, not to reproduce SpiderMonkey internals.
22
22
 
23
+ // The RUNTIME's own `process`, bound ONCE at module evaluation.
24
+ //
25
+ // `globalThis.process` is NOT a safe source for this module: a GJS program run
26
+ // through the reverse bridge routinely installs the gjsify `@gjsify/process`
27
+ // polyfill over it (`@gjsify/node-globals/register` defines it unconditionally),
28
+ // and that polyfill's `exit()` takes its GJS path whenever `imports.system.exit`
29
+ // exists — i.e. it delegates back into THIS module. Reading `globalThis.process`
30
+ // in `exit()` closes that loop into an infinite, GLib-idle-driven recursion
31
+ // (exit → polyfill → system.exit → polyfill → …) that spins at 100 % CPU and
32
+ // never terminates. `getBuiltinModule('node:process')` (Node ≥ 22.3 / Bun /
33
+ // Deno 2) returns the real builtin regardless of what the global currently is;
34
+ // the module-evaluation capture is the fallback for hosts without it — this
35
+ // module is loaded from `@gjsify/node-gi/globals`, before any bundle body (and
36
+ // therefore before any register side effect) runs.
37
+ const runtimeProcess = (() => {
38
+ const hostProcess = typeof process !== 'undefined' ? process : undefined;
39
+ try {
40
+ const builtin = hostProcess?.getBuiltinModule?.('node:process');
41
+ if (builtin && typeof builtin.exit === 'function') return builtin;
42
+ } catch {
43
+ // getBuiltinModule missing or refusing the specifier — use the captured one.
44
+ }
45
+ return hostProcess;
46
+ })();
47
+
48
+ const runtimeExit = typeof runtimeProcess?.exit === 'function' ? runtimeProcess.exit.bind(runtimeProcess) : undefined;
49
+
23
50
  // `programArgs` mirrors GJS's ARGV: the script arguments, excluding the
24
51
  // interpreter (argv[0]) and the script path (argv[1]).
25
52
  function readProgramArgs() {
26
- return typeof process !== 'undefined' && Array.isArray(process.argv) ? process.argv.slice(2) : [];
53
+ return Array.isArray(runtimeProcess?.argv) ? runtimeProcess.argv.slice(2) : [];
27
54
  }
28
55
 
29
56
  // `programInvocationName` / `programPath` track the running script — Node's
30
57
  // `process.argv[1]`.
31
58
  function readProgramInvocationName() {
32
- return (typeof process !== 'undefined' && process.argv[1]) || '';
59
+ return runtimeProcess?.argv?.[1] || '';
33
60
  }
34
61
 
35
62
  function readProgramPath() {
36
- return (typeof process !== 'undefined' && process.argv[1]) || null;
63
+ return runtimeProcess?.argv?.[1] || null;
37
64
  }
38
65
 
39
- /** Exit the process (GJS `System.exit`). */
66
+ /**
67
+ * Exit the process (GJS `System.exit`).
68
+ *
69
+ * Terminal by contract: under gjs this is the `exit()` syscall — it never
70
+ * returns and nothing a program installs can intercept it. The Node/Bun/Deno
71
+ * equivalent is the RUNTIME's own `process.exit`, resolved above; routing
72
+ * through `globalThis.process` instead is what produced the never-exiting
73
+ * reverse-bridge process (see the `runtimeProcess` note and
74
+ * `conformance/programs/system-exit-lifetime.conf.mjs`).
75
+ */
40
76
  export function exit(code) {
41
- if (typeof process !== 'undefined') process.exit(code ?? 0);
77
+ if (runtimeExit !== undefined) runtimeExit(code ?? 0);
42
78
  }
43
79
 
44
80
  /** Trigger a garbage collection if the host exposed `globalThis.gc` (`--expose-gc`). */