@gjsify/node-gi 0.51.1 → 0.53.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.51.1",
3
+ "version": "0.53.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",
@@ -88,23 +88,23 @@
88
88
  "build:prebuild": "node-gyp rebuild && node scripts/stage-prebuild.mjs",
89
89
  "rebuild": "node-gyp rebuild",
90
90
  "clean": "node-gyp clean",
91
- "test": "LC_ALL=C NODE_GI_NATIVE=build node --test",
92
- "test:node": "LC_ALL=C NODE_GI_NATIVE=build node --test",
93
- "test:gc": "LC_ALL=C NODE_GI_NATIVE=build node --test --expose-gc",
94
- "test:dbus": "dbus-run-session --config-file=test/session.conf -- env LC_ALL=C NODE_GI_NATIVE=build node --test --expose-gc test/dbus.test.mjs test/dbus-async.test.mjs",
91
+ "test": "node scripts/pinned-env.mjs --test",
92
+ "test:node": "node scripts/pinned-env.mjs --test",
93
+ "test:gc": "node scripts/pinned-env.mjs --test --expose-gc",
94
+ "test:dbus": "dbus-run-session --config-file=test/session.conf -- node scripts/pinned-env.mjs --test --expose-gc test/dbus.test.mjs test/dbus-async.test.mjs",
95
95
  "test:bun": "node scripts/cross-runtime.mjs bun",
96
96
  "test:conformance": "node scripts/conformance.mjs",
97
97
  "test:conformance:gjs-napi": "node scripts/conformance.mjs --runtimes=gjs-napi",
98
98
  "test:gimarshalling": "node scripts/gimarshalling.mjs",
99
99
  "test:deno": "node scripts/cross-runtime.mjs deno",
100
- "test:cairo": "LC_ALL=C NODE_GI_NATIVE=build node --test test/cairo.test.mjs",
101
- "test:cairo-canvas2d": "LC_ALL=C NODE_GI_NATIVE=build node --test test/cairo-canvas2d.test.mjs",
102
- "test:gtk": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-smoke.test.mjs",
103
- "test:gtk-cairo": "LC_ALL=C NODE_GI_NATIVE=build node --test test/cairo-drawfunc.test.mjs",
104
- "test:adw": "LC_ALL=C NODE_GI_NATIVE=build node --test test/adw-smoke.test.mjs",
105
- "test:gtk-template": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-template.test.mjs",
106
- "test:gtk-template-callbacks": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-template-callbacks.test.mjs",
107
- "test:gtk-template-signals": "LC_ALL=C NODE_GI_NATIVE=build node --test test/gtk-template-signals.test.mjs"
100
+ "test:cairo": "node scripts/pinned-env.mjs --test test/cairo.test.mjs",
101
+ "test:cairo-canvas2d": "node scripts/pinned-env.mjs --test test/cairo-canvas2d.test.mjs",
102
+ "test:gtk": "node scripts/pinned-env.mjs --test test/gtk-smoke.test.mjs",
103
+ "test:gtk-cairo": "node scripts/pinned-env.mjs --test test/cairo-drawfunc.test.mjs",
104
+ "test:adw": "node scripts/pinned-env.mjs --test test/adw-smoke.test.mjs",
105
+ "test:gtk-template": "node scripts/pinned-env.mjs --test test/gtk-template.test.mjs",
106
+ "test:gtk-template-callbacks": "node scripts/pinned-env.mjs --test test/gtk-template-callbacks.test.mjs",
107
+ "test:gtk-template-signals": "node scripts/pinned-env.mjs --test test/gtk-template-signals.test.mjs"
108
108
  },
109
109
  "dependencies": {
110
110
  "node-addon-api": "^8.0.0"
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/addon.cc CHANGED
@@ -155,6 +155,7 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
155
155
  exports.Set("localeCategories", Napi::Function::New(env, LocaleCategories));
156
156
  // The native cairo binding + foreign-struct registration (the `__cairo` export).
157
157
  InitCairo(env, exports);
158
+ InitOpenGLWin32(env, exports);
158
159
  return exports;
159
160
  }
160
161
 
package/src/calls.cc CHANGED
@@ -223,58 +223,34 @@ static void NodeGiCallbackTrampoline(ffi_cif* /*cif*/, void* result, void** args
223
223
  if (cb->scope != GI_SCOPE_TYPE_CALL) depthReset = std::make_unique<SyncEmitDepthReset>();
224
224
 
225
225
  GICallableInfo* ci = cb->info;
226
- unsigned int n = gi_callable_info_get_n_args(ci);
226
+ // Arguments and the answer in gjs's OUT/INOUT shape, shared with the vfunc
227
+ // trampoline (CToJsCall, marshal.cc): a pure OUT is not a JS argument and is
228
+ // written back from the return value. ffi hands each argument's storage as
229
+ // args[i] (a user_data/void IN arg marshals to undefined, which JS ignores).
230
+ CToJsCall call(napiEnv, ci, std::string("callback '") +
231
+ gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ci)) + "'");
227
232
  std::vector<napi_value> jsArgs;
228
- jsArgs.reserve(n);
229
- bool ok = true;
230
- for (unsigned int i = 0; i < n; i++) {
231
- GIArgInfo* ai = gi_callable_info_get_arg(ci, i);
232
- GITypeInfo* ti = gi_arg_info_get_type_info(ai);
233
- // ffi hands each argument's storage as args[i]; reinterpret as a GIArgument
234
- // union (a user_data/void arg marshals to undefined, which JS ignores).
235
- Napi::Value v = GIArgumentToJs(napiEnv, ti, static_cast<GIArgument*>(args[i]), GI_TRANSFER_NOTHING);
236
- gi_base_info_unref(ti);
237
- gi_base_info_unref(ai);
238
- if (napiEnv.IsExceptionPending()) {
239
- ok = false;
240
- break;
241
- }
242
- jsArgs.push_back(v);
243
- }
233
+ bool ok = call.MarshalArgs(args, 0, &jsArgs);
244
234
 
245
235
  // Zero the result slot first (it is >= ffi_arg wide; narrow returns leave the
246
236
  // upper bytes indeterminate otherwise).
247
237
  if (result != nullptr) static_cast<GIArgument*>(result)->v_uint64 = 0;
248
238
 
249
- GITypeInfo* retType = gi_callable_info_get_return_type(ci);
239
+ napi_value ret = nullptr;
250
240
  if (ok) {
251
241
  napi_value fn = nullptr;
252
242
  if (napi_get_reference_value(env, cb->jsFn, &fn) == napi_ok && fn != nullptr) {
253
243
  napi_value global = nullptr;
254
244
  napi_get_global(env, &global);
255
- napi_value ret = nullptr;
256
245
  // napi_make_callback drains nextTick/microtasks around the call (Node; on
257
246
  // Bun/Deno the checkpoint is run by NodeGiMaybeDrainMicrotasks below).
258
247
  g_loopDispatchDepth++;
259
248
  napi_status st = napi_make_callback(env, nullptr, global, fn, jsArgs.size(), jsArgs.data(), &ret);
260
249
  g_loopDispatchDepth--;
261
- if (st == napi_ok && result != nullptr) {
262
- GITypeTag rtag = gi_type_info_get_tag(retType);
263
- if (rtag == GI_TYPE_TAG_UTF8 || rtag == GI_TYPE_TAG_FILENAME) {
264
- // Hand the caller an owned copy — a JsToGIArgument string would point
265
- // into a std::string that dies with this frame.
266
- Napi::Value rv(env, ret);
267
- static_cast<GIArgument*>(result)->v_string =
268
- rv.IsString() ? g_strdup(rv.As<Napi::String>().Utf8Value().c_str()) : nullptr;
269
- } else if (rtag != GI_TYPE_TAG_VOID) {
270
- std::string held;
271
- JsToGIArgument(napiEnv, Napi::Value(env, ret), retType, static_cast<GIArgument*>(result),
272
- &held);
273
- }
274
- }
250
+ if (st != napi_ok) ret = nullptr;
275
251
  }
276
252
  }
277
- gi_base_info_unref(retType);
253
+ call.WriteAnswer(ret, result);
278
254
  // A CALL-scope callback (e.g. a list `foreach`) runs synchronously inside a
279
255
  // JS-initiated GI invoke, so a pending exception must propagate to that JS
280
256
  // caller — leave it to surface at the invoke's N-API boundary. A NOTIFIED/ASYNC
package/src/class.cc CHANGED
@@ -223,37 +223,27 @@ static void NodeGiVFuncTrampoline(ffi_cif* /*cif*/, void* result, void** args,
223
223
 
224
224
  GICallableInfo* ci = reinterpret_cast<GICallableInfo*>(vf->info);
225
225
  // args[0] is the instance; declared args follow at args[1..].
226
- Napi::Value recv = WrapGObject(
227
- napiEnv, static_cast<GObject*>(static_cast<GIArgument*>(args[0])->v_pointer),
228
- GI_TRANSFER_NOTHING);
229
-
230
- unsigned int n = gi_callable_info_get_n_args(ci);
226
+ GObject* instance = static_cast<GObject*>(static_cast<GIArgument*>(args[0])->v_pointer);
227
+ Napi::Value recv = WrapGObject(napiEnv, instance, GI_TRANSFER_NOTHING);
228
+
229
+ // Arguments and the answer in gjs's OUT/INOUT shape, shared with the GI-callback
230
+ // trampoline (CToJsCall, marshal.cc). args[0] is the instance, so the declared
231
+ // arguments start at offset 1. `instance` is also the owner a transfer-none string
232
+ // answer ties its lifetime to (StringForC, marshal.cc) instead of interning it
233
+ // forever — a real vfunc can answer a fresh string every call (e.g.
234
+ // Gtk.Editable's vfunc_get_text on a live-updating buffer).
235
+ CToJsCall call(napiEnv, ci, "vfunc '" + vf->name + "'", instance);
231
236
  std::vector<napi_value> jsArgs;
232
- jsArgs.reserve(n);
233
- bool ok = true;
234
- for (unsigned int i = 0; i < n; i++) {
235
- GIArgInfo* ai = gi_callable_info_get_arg(ci, i);
236
- GITypeInfo* ti = gi_arg_info_get_type_info(ai);
237
- Napi::Value v =
238
- GIArgumentToJs(napiEnv, ti, static_cast<GIArgument*>(args[i + 1]), GI_TRANSFER_NOTHING);
239
- gi_base_info_unref(ti);
240
- gi_base_info_unref(ai);
241
- if (napiEnv.IsExceptionPending()) {
242
- ok = false;
243
- break;
244
- }
245
- jsArgs.push_back(v);
246
- }
237
+ bool ok = call.MarshalArgs(args, 1, &jsArgs);
247
238
 
248
239
  // Zero the result slot first (it is >= ffi_arg wide; narrow returns leave the
249
240
  // upper bytes indeterminate otherwise).
250
241
  if (result != nullptr) static_cast<GIArgument*>(result)->v_uint64 = 0;
251
242
 
252
- GITypeInfo* retType = gi_callable_info_get_return_type(ci);
243
+ napi_value ret = nullptr;
253
244
  if (ok) {
254
245
  napi_value fn = nullptr;
255
246
  if (napi_get_reference_value(env, vf->fn, &fn) == napi_ok && fn != nullptr) {
256
- napi_value ret = nullptr;
257
247
  // napi_make_callback drains nextTick/microtasks around the call (Node; on
258
248
  // Bun/Deno the checkpoint is run by NodeGiMaybeDrainMicrotasks below); the
259
249
  // wrapped instance is the receiver (`this`).
@@ -261,23 +251,10 @@ static void NodeGiVFuncTrampoline(ffi_cif* /*cif*/, void* result, void** args,
261
251
  napi_status st =
262
252
  napi_make_callback(env, nullptr, recv, fn, jsArgs.size(), jsArgs.data(), &ret);
263
253
  g_loopDispatchDepth--;
264
- if (st == napi_ok && result != nullptr) {
265
- GITypeTag rtag = gi_type_info_get_tag(retType);
266
- if (rtag == GI_TYPE_TAG_UTF8 || rtag == GI_TYPE_TAG_FILENAME) {
267
- // Hand the caller an owned copy — a JsToGIArgument string would point
268
- // into a std::string that dies with this frame.
269
- Napi::Value rv(env, ret);
270
- static_cast<GIArgument*>(result)->v_string =
271
- rv.IsString() ? g_strdup(rv.As<Napi::String>().Utf8Value().c_str()) : nullptr;
272
- } else if (rtag != GI_TYPE_TAG_VOID) {
273
- std::string held;
274
- JsToGIArgument(napiEnv, Napi::Value(env, ret), retType, static_cast<GIArgument*>(result),
275
- &held);
276
- }
277
- }
254
+ if (st != napi_ok) ret = nullptr;
278
255
  }
279
256
  }
280
- gi_base_info_unref(retType);
257
+ call.WriteAnswer(ret, result);
281
258
  // A pending JS exception surfaces at the next N-API boundary (e.g. when the
282
259
  // constructType / method call that triggered this vfunc returns).
283
260
  //
package/src/common.h CHANGED
@@ -176,6 +176,8 @@ const ForeignStructOps* ForeignOpsForInfo(GIBaseInfo* iface);
176
176
  // from addon.cc's Init; sets the `__cairo` export + registers Context/Surface/
177
177
  // Pattern foreign converters.
178
178
  void InitCairo(Napi::Env env, Napi::Object exports);
179
+ // win32: probeHostOpenGL / preloadOpenGL / currentGLStrings (opengl-win32.cc); a no-op elsewhere.
180
+ void InitOpenGLWin32(Napi::Env env, Napi::Object exports);
179
181
 
180
182
  // ---- GJS-exact 64-bit integer marshalling helpers (shared) -------------------
181
183
  //
@@ -413,6 +415,42 @@ bool IsSupportedContainerType(GITypeInfo* type, std::string* why, ContainerUse u
413
415
  // function-invoke path (calls.cc) and the vfunc chain-up path (class.cc).
414
416
  bool IsSupportedOutType(GITypeInfo* type, std::string* why);
415
417
  size_t CElementSize(GITypeInfo* elem);
418
+ // One call C makes INTO JS — a JS vfunc override (class.cc) or a GI callback
419
+ // (calls.cc) — marshalled in gjs's OUT/INOUT shape (see marshal.cc). Lives on the
420
+ // trampoline's stack under its HandleScope; `ci` is borrowed.
421
+ class CToJsCall {
422
+ public:
423
+ // `owner` is the vfunc's instance (class.cc passes args[0]'s GObject*); nullptr for a
424
+ // plain GI callback (calls.cc), which has no natural object. Only used to tie a
425
+ // transfer-none string ANSWER's lifetime to something bounded (StringForC,
426
+ // marshal.cc) — gjs's own fallback (g_intern_string, unbounded) when there is none.
427
+ CToJsCall(Napi::Env env, GICallableInfo* ci, std::string label, GObject* owner = nullptr);
428
+ ~CToJsCall();
429
+ CToJsCall(const CToJsCall&) = delete;
430
+ CToJsCall& operator=(const CToJsCall&) = delete;
431
+ // The JS arguments from the ffi args (`args[offset + i]` is declared arg i): IN as
432
+ // values, INOUT as their current value, pure OUT omitted. False = exception pending.
433
+ bool MarshalArgs(void** args, unsigned int offset, std::vector<napi_value>* jsArgs);
434
+ // Write JS's answer back: the return value into `result` (already zeroed by the
435
+ // caller), the OUT/INOUT through their pointers. A wrong shape or a failed
436
+ // conversion throws; every pure OUT left unanswered is zeroed, never uninitialised.
437
+ // `ret` is nullptr when the JS call itself failed.
438
+ void WriteAnswer(napi_value ret, void* result);
439
+
440
+ private:
441
+ struct Slot {
442
+ GIArgInfo* ai; // owned
443
+ GITypeInfo* ti; // owned
444
+ gpointer dest; // the caller's variable (or caller-allocated record); may be NULL
445
+ };
446
+ void ThrowUnsupported(const Slot& s) const;
447
+ bool WriteOut(const Slot& s, Napi::Value v);
448
+ Napi::Env env_;
449
+ GICallableInfo* ci_;
450
+ std::string label_;
451
+ GObject* owner_; // borrowed; see the constructor comment
452
+ std::vector<Slot> outs_;
453
+ };
416
454
  void WriteLengthValue(GITypeInfo* lenType, GIArgument* slot, long n);
417
455
 
418
456
  // One IN length-argument autofill, remembered so a SECOND array naming the same length
package/src/loop.cc CHANGED
@@ -379,8 +379,9 @@ Napi::Value IterateMainContext(const Napi::CallbackInfo& info) {
379
379
  struct PumpPoll {
380
380
  uv_poll_t handle;
381
381
  int fd;
382
- int events; // currently-subscribed uv event mask
383
- gboolean seen; // mark/sweep flag for SyncPumpPolls
382
+ int events; // currently-subscribed uv event mask
383
+ gboolean seen; // mark/sweep flag for SyncPumpPolls
384
+ gboolean started; // whether the watcher is currently armed (see PumpPollCb)
384
385
  };
385
386
 
386
387
  static gboolean g_pump_inited = FALSE;
@@ -398,7 +399,26 @@ static gboolean g_pump_poll_warned = FALSE;
398
399
  // Wake-only callbacks: readiness/expiry just ends libuv's poll sleep; the actual
399
400
  // GLib dispatch happens in the check phase of the same loop turn (PumpCheckCb).
400
401
  static void PumpTimerCb(uv_timer_t* /*t*/) {}
401
- static void PumpPollCb(uv_poll_t* /*p*/, int /*status*/, int /*events*/) {}
402
+
403
+ // A wake-up is an EDGE, so the watcher DISARMS itself on the first fire and
404
+ // PumpArmWakeups re-arms it on the next libuv turn. uv_poll is level-triggered
405
+ // and this callback reads nothing, so a GLib fd that stays ready — a dead D-Bus
406
+ // socket stuck at POLLHUP, or the context's own wakeup eventfd, which
407
+ // `block_source()` re-signals on every dispatch — leaves uv's backend fd
408
+ // PERPETUALLY readable. That fd is embedded in UvLoopSource, so GLib then reads
409
+ // "Node has work" on every prepare, and any foreign drain loop of the shape
410
+ // `while (g_main_context_iteration(ctx, FALSE));` never terminates: measured in
411
+ // `e_source_registry_new_sync`'s failure path (no D-Bus reachable), 6.9 M
412
+ // dispatches in 20 s at 100 % CPU with `uv_backend_timeout()` sitting at ~4.6 s,
413
+ // i.e. never once because Node actually had work. The pump's own drain would
414
+ // have cleared the GLib side, but it no-ops at `g_main_depth() > 0` — exactly
415
+ // where a foreign nested iteration runs. Regression
416
+ // `test/foreign-context-drain.test.mjs`.
417
+ static void PumpPollCb(uv_poll_t* p, int /*status*/, int /*events*/) {
418
+ PumpPoll* pp = reinterpret_cast<PumpPoll*>(p->data);
419
+ uv_poll_stop(p);
420
+ pp->started = FALSE;
421
+ }
402
422
 
403
423
  static void PumpPollCloseCb(uv_handle_t* h) {
404
424
  delete reinterpret_cast<PumpPoll*>(h->data);
@@ -440,9 +460,12 @@ static gboolean SyncPumpPolls(int nfds) {
440
460
  if (it != g_pump_polls->end()) {
441
461
  PumpPoll* pp = it->second;
442
462
  pp->seen = TRUE;
443
- if (pp->events != ev) {
463
+ // Re-arm a watcher that disarmed itself on its last fire, as well as one
464
+ // whose event mask changed.
465
+ if (pp->events != ev || !pp->started) {
444
466
  if (uv_poll_start(&pp->handle, ev, PumpPollCb) == 0) {
445
467
  pp->events = ev;
468
+ pp->started = TRUE;
446
469
  uv_unref(reinterpret_cast<uv_handle_t*>(&pp->handle));
447
470
  } else {
448
471
  all_ok = FALSE;
@@ -466,6 +489,7 @@ static gboolean SyncPumpPolls(int nfds) {
466
489
  all_ok = FALSE;
467
490
  continue;
468
491
  }
492
+ pp->started = TRUE;
469
493
  uv_unref(reinterpret_cast<uv_handle_t*>(&pp->handle));
470
494
  (*g_pump_polls)[fd] = pp;
471
495
  }