node-gtk 4.0.1 → 4.1.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/binding.gyp CHANGED
@@ -36,6 +36,7 @@
36
36
  "src/gobject.cc",
37
37
  "src/loop.cc",
38
38
  "src/param_spec.cc",
39
+ "src/toggle_queue.cc",
39
40
  "src/type.cc",
40
41
  "src/util.cc",
41
42
  "src/value.cc",
package/lib/bootstrap.js CHANGED
@@ -2,9 +2,21 @@
2
2
  * bootstrap.js
3
3
  */
4
4
 
5
- const camelCase = require('lodash.camelcase')
5
+ const camelCaseRaw = require('lodash.camelcase')
6
6
  const internal = require('./native.js')
7
7
 
8
+ // Method/field/property names repeat heavily across classes (get_name,
9
+ // set_visible, ...): memoize the case conversion.
10
+ const camelCaseCache = new Map()
11
+ function camelCase(name) {
12
+ let result = camelCaseCache.get(name)
13
+ if (result === undefined) {
14
+ result = camelCaseRaw(name)
15
+ camelCaseCache.set(name, result)
16
+ }
17
+ return result
18
+ }
19
+
8
20
  // The bootstrap from C here contains functions and methods for each object,
9
21
  // namespaced with underscores. See gi.cc for more information.
10
22
  const GI = internal.Bootstrap();
@@ -15,6 +27,7 @@ module.exports = {
15
27
  GI,
16
28
  makeInfo,
17
29
  getInfoName,
30
+ defineLazyInfos,
18
31
  }
19
32
 
20
33
  // The GIRepository API is fairly poor, and contains methods on classes,
@@ -341,6 +354,84 @@ function applyInterfaceMethods(constructor, interfaceRefs) {
341
354
 
342
355
  internal.SetInterfaceMethodsApplier(applyInterfaceMethods)
343
356
 
357
+ /*
358
+ * Lazy type materialization. Namespaces expose thousands of top-level infos
359
+ * (6,600+ for Gtk-4.0 with its dependencies) but an app touches a few dozen:
360
+ * giRequire() defines one lazy accessor per info instead of building every
361
+ * class eagerly, and the first access runs makeInfo() and replaces the
362
+ * accessor with the result. Types reached from C before JS ever names them (a
363
+ * method return value, a signal argument) are materialized through the C++
364
+ * template-creation hook (SetTypeMaterializer below), so their prototypes are
365
+ * decorated before any wrapper is handed out.
366
+ */
367
+
368
+ const materializing = new Set()
369
+
370
+ function defineLazyInfos(module, ns) {
371
+ // One native call per namespace: a flat [name, infoType, index, ...] array
372
+ // of the infos makeInfo() can handle (enumerating via GI.* costs ~3 FFI
373
+ // round-trips per info, 48ms for Gtk-4.0 + dependencies).
374
+ const entries = internal.GetInfoEntries(ns)
375
+
376
+ for (let i = 0; i < entries.length; i += 3) {
377
+ const type = entries[i + 1]
378
+ const name = type === GI.InfoType.FUNCTION ? camelCase(entries[i]) : entries[i]
379
+ defineLazyInfo(module, ns, name, entries[i + 2])
380
+ }
381
+ }
382
+
383
+ function defineLazyInfo(module, ns, name, index) {
384
+ const key = ns + '.' + name
385
+
386
+ Object.defineProperty(module, name, {
387
+ configurable: true,
388
+ enumerable: true,
389
+ get() {
390
+ /* Re-entered while materializing: the C++ hook fires for the class
391
+ * makeInfo() is currently building, and MakeBoxedClass() consults the
392
+ * module cache for the G_TYPE_NONE struct it is building. Report
393
+ * undefined; the outer call installs the final value. */
394
+ if (materializing.has(key))
395
+ return undefined
396
+ materializing.add(key)
397
+ try {
398
+ const repo = GI.Repository_get_default()
399
+ const info = GI.Repository_get_info.call(repo, ns, index)
400
+ const value = makeInfo(info)
401
+ Object.defineProperty(module, name, {
402
+ configurable: true, enumerable: true, writable: true, value,
403
+ })
404
+ return value
405
+ } finally {
406
+ materializing.delete(key)
407
+ }
408
+ },
409
+ /* Plain assignments must keep working (getInterface() and the overrides
410
+ * write straight into the module). */
411
+ set(value) {
412
+ Object.defineProperty(module, name, {
413
+ configurable: true, enumerable: true, writable: true, value,
414
+ })
415
+ },
416
+ })
417
+ }
418
+
419
+ /*
420
+ * Called from C++ when a class template is first created for an
421
+ * introspectable type (GetClassTemplate/GetBoxedFunction/
422
+ * GetFundamentalTemplate), so that a type reached from C first has its
423
+ * prototype decorated. Triggering the accessor is enough; the accessor makes
424
+ * this idempotent and re-entrancy-safe.
425
+ */
426
+ function materializeType(ns, name) {
427
+ const module = moduleCache[ns]
428
+ if (module === undefined)
429
+ return
430
+ void module[name]
431
+ }
432
+
433
+ internal.SetTypeMaterializer(materializeType)
434
+
344
435
  function makeBoxed(info) {
345
436
  if (getType(info) == GI.InfoType.UNION) {
346
437
  return makeUnion(info)
package/lib/module.js CHANGED
@@ -7,7 +7,7 @@ const util = require('util')
7
7
  const readdir = util.promisify(fs.readdir)
8
8
 
9
9
  const internal = require('./native.js')
10
- const { GI, makeInfo, getInfoName } = require('./bootstrap.js')
10
+ const { GI, defineLazyInfos } = require('./bootstrap.js')
11
11
 
12
12
  const moduleCache = internal.GetModuleCache();
13
13
 
@@ -37,14 +37,11 @@ function giRequire(ns, version) {
37
37
 
38
38
  loadDependencies(ns, version)
39
39
 
40
- const nInfos = GI.Repository_get_n_infos.call(repo, ns);
41
- for (let i = 0; i < nInfos; i++) {
42
- const info = GI.Repository_get_info.call(repo, ns, i);
43
- const item = makeInfo(info);
44
-
45
- if (item !== undefined)
46
- module[getInfoName(info)] = item
47
- }
40
+ // Top-level infos materialize on first access (see defineLazyInfos in
41
+ // bootstrap.js): building every class of a namespace and its dependencies
42
+ // eagerly used to cost ~160ms for Gtk-4.0, for the few dozen classes a
43
+ // typical app touches.
44
+ defineLazyInfos(module, ns)
48
45
 
49
46
  // Apply overrides, if present
50
47
  let override
package/lib/native.js CHANGED
@@ -2,12 +2,22 @@
2
2
  * native.js
3
3
  */
4
4
 
5
- const binary = require('@mapbox/node-pre-gyp')
6
5
  const path = require('path')
7
6
  const fs = require('fs')
8
7
 
9
- const packagePath = path.resolve(path.join(__dirname,'../package.json'))
10
- const bindingPath = binary.find(packagePath)
8
+ // Resolve the compiled addon directly rather than through node-pre-gyp:
9
+ // requiring node-pre-gyp costs ~16ms of startup for what amounts to
10
+ // evaluating the module_path template of package.json (`binary.find`). Keep
11
+ // it as a fallback for any layout the shorthand doesn't cover (e.g. a
12
+ // non-node runtime with a different ABI naming scheme).
13
+ const bindingName = `node-v${process.versions.modules}-${process.platform}-${process.arch}`
14
+ let bindingPath = path.join(__dirname, 'binding', bindingName, 'node_gtk.node')
15
+
16
+ if (!fs.existsSync(bindingPath)) {
17
+ const binary = require('@mapbox/node-pre-gyp')
18
+ const packagePath = path.resolve(path.join(__dirname, '../package.json'))
19
+ bindingPath = binary.find(packagePath)
20
+ }
11
21
 
12
22
  // On Windows, the prebuilt binary ships with its whole GTK runtime bundled in
13
23
  // the same directory as the .node (DLLs, GObject-Introspection typelibs, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-gtk",
3
- "version": "4.0.1",
3
+ "version": "4.1.1",
4
4
  "description": "GNOME Gtk+ bindings for NodeJS",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "homepage": "https://github.com/romgrk/node-gtk#readme",
53
53
  "dependencies": {
54
- "@mapbox/node-pre-gyp": "^1.0.10",
54
+ "@mapbox/node-pre-gyp": "^2.0.3",
55
55
  "lodash.camelcase": "4.3.0",
56
56
  "lodash.isequal": "4.5.0",
57
57
  "lodash.snakecase": "^4.1.1",
@@ -62,11 +62,26 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "assert": "^1.5.0",
65
- "aws-sdk": "^2.452.0",
65
+ "aws-sdk": "^2.1692.0",
66
66
  "chalk": "^2.4.2",
67
- "mocha": "^7.1.0",
68
- "nid-parser": "0.0.5",
69
- "node-pre-gyp-github": "^1.4.5"
67
+ "mocha": "^11.7.0",
68
+ "nid-parser": "0.0.5"
69
+ },
70
+ "overrides": {
71
+ "nid-parser": {
72
+ "js-yaml": "^3.15.0",
73
+ "extend": "^3.0.2"
74
+ },
75
+ "optimist": {
76
+ "minimist": "^1.2.8"
77
+ },
78
+ "aws-sdk": {
79
+ "uuid": "^11.1.1"
80
+ },
81
+ "mocha": {
82
+ "diff": "^8.0.3",
83
+ "serialize-javascript": "^7.0.5"
84
+ }
70
85
  },
71
86
  "files": [
72
87
  "/bin",
@@ -74,7 +89,9 @@
74
89
  "/src",
75
90
  "/scripts",
76
91
  "/tools",
77
- "binding.gyp"
92
+ "binding.gyp",
93
+ "!lib/binding",
94
+ "!**/*.node"
78
95
  ],
79
96
  "binary": {
80
97
  "module_name": "node_gtk",
package/scripts/ci.sh CHANGED
@@ -46,12 +46,9 @@ function npm_test() {
46
46
  # fixture build does not run automatically; do it here. Best-effort:
47
47
  # marshalling tests skip if fixtures cannot be produced on macOS.
48
48
  npm run build:test-fixtures || true;
49
- npx mocha \
50
- --skip=callback \
51
- tests/__run__.js
49
+ NODE_GTK_TEST_SKIP=callback npx mocha tests/__run__.js
52
50
  else
53
- xvfb-run -a npm test -- \
54
- --skip=callback;
51
+ NODE_GTK_TEST_SKIP=callback xvfb-run -a npm test;
55
52
  fi;
56
53
  }
57
54
 
package/src/boxed.cc CHANGED
@@ -409,6 +409,14 @@ Local<Function> GetBoxedFunction(GIBaseInfo *info, GType gtype) {
409
409
 
410
410
  g_type_set_qdata(gtype, GNodeJS::function_quark(), persistent);
411
411
 
412
+ /* Modules hold lazy accessors: a registered boxed type reached from C
413
+ * first (method return value, signal argument) must be materialized so
414
+ * makeBoxed() in JS attaches its methods/fields before a wrapper is
415
+ * handed out. Fired after the qdata above so the re-entrant
416
+ * MakeBoxedClass() from JS finds this same function. Idempotent and
417
+ * re-entrancy-safe on the JS side. */
418
+ GNodeJS::MaterializeType(info);
419
+
412
420
  return fn;
413
421
  }
414
422
 
@@ -430,10 +438,12 @@ Local<Function> MakeBoxedClass(GIBaseInfo *info) {
430
438
  if (Nan::HasOwnProperty(moduleCache, ns).FromMaybe(false)) {
431
439
  auto module = TO_OBJECT (Nan::Get(moduleCache, ns).ToLocalChecked());
432
440
 
433
- if (Nan::HasOwnProperty(module, name).FromMaybe(false)) {
434
- auto constructor = TO_OBJECT (Nan::Get(module, name).ToLocalChecked());
441
+ /* The Get can run a lazy accessor. It returns the materialized
442
+ * class, or undefined when re-entered from the accessor's own
443
+ * makeBoxed() call — fall through and build the class then. */
444
+ auto constructor = Nan::Get(module, name).ToLocalChecked();
445
+ if (constructor->IsFunction())
435
446
  return Local<Function>::Cast (constructor);
436
- }
437
447
  }
438
448
  }
439
449
 
package/src/callback.cc CHANGED
@@ -23,6 +23,9 @@ namespace GNodeJS {
23
23
 
24
24
  static guint callbackLevel = 0;
25
25
  static GSList* notifiedCallbacks = NULL;
26
+ /* DestroyNotify can fire on any thread (e.g. a GTask freeing its callback
27
+ * data on a pool thread); the list must not race the JS thread's AsyncFree. */
28
+ static GMutex notifiedCallbacksLock;
26
29
 
27
30
  static Local<Object> GetSelfInstance(GIArgument **args) {
28
31
  return WrapperFromGObject((GObject *)args[0]->v_pointer).As<Object>();
@@ -66,26 +69,27 @@ Callback::~Callback() {
66
69
  * we can't free the resources here. They'll be freed in Callback::AsyncFree.
67
70
  */
68
71
  void Callback::DestroyNotify (void* user_data) {
72
+ g_mutex_lock (&notifiedCallbacksLock);
69
73
  notifiedCallbacks = g_slist_prepend (notifiedCallbacks, user_data);
74
+ g_mutex_unlock (&notifiedCallbacksLock);
70
75
  }
71
76
 
72
77
  /**
73
78
  * Frees the callbacks that have been destroy-notified
74
79
  */
75
80
  void Callback::AsyncFree () {
76
- if (notifiedCallbacks == NULL || callbackLevel > 0)
81
+ if (callbackLevel > 0)
77
82
  return;
78
83
 
79
- GSList* current = notifiedCallbacks;
80
-
81
- while (current != NULL) {
82
- Callback* callback = static_cast<Callback*>(current->data);
83
- delete callback;
84
+ g_mutex_lock (&notifiedCallbacksLock);
85
+ GSList* list = notifiedCallbacks;
86
+ notifiedCallbacks = NULL;
87
+ g_mutex_unlock (&notifiedCallbacksLock);
84
88
 
85
- current = current->next;
86
- }
89
+ for (GSList* current = list; current != NULL; current = current->next)
90
+ delete static_cast<Callback*>(current->data);
87
91
 
88
- notifiedCallbacks = NULL;
92
+ g_slist_free (list);
89
93
  }
90
94
 
91
95
  /**
@@ -220,6 +224,18 @@ void Callback::Execute (GIArgument *result, GIArgument **args, Callback *callbac
220
224
  Throw::InvalidReturnValue (&return_type_info, jsReturnValue);
221
225
  goto out;
222
226
  }
227
+
228
+ // When the callback's return value is transfer-full, the C caller takes
229
+ // ownership of it (e.g. GtkTreeListModelCreateModelFunc → GListModel).
230
+ // V8ToGIArgument only unwrapped the pointer, leaving node-gtk holding
231
+ // just its toggle ref; without the owning reference the caller "owns"
232
+ // that toggle ref, so no toggle-up fires, the wrapper stays weak, and
233
+ // once GC collects it the object is finalized while the caller still
234
+ // uses it — a use-after-free (e.g. gtk_tree_list_model_finalize
235
+ // disconnecting its items-changed handler from a freed child model).
236
+ // This is the return counterpart of the transfer-full IN argument.
237
+ if (g_callable_info_get_caller_owns(callback->info) == GI_TRANSFER_EVERYTHING)
238
+ TakeOwnershipForTransferFull(&return_type_info, result, -1);
223
239
  }
224
240
 
225
241
  // TODO: assess transferness of arguments & check if we need to free them
package/src/function.cc CHANGED
@@ -527,10 +527,7 @@ Local<Value> FunctionCall (
527
527
  // GskRenderNode): add the reference the callee will own (#468).
528
528
  else if (direction == GI_DIRECTION_IN
529
529
  && g_arg_info_get_ownership_transfer(&arg_info) == GI_TRANSFER_EVERYTHING) {
530
- CopyBoxedForTransferFullIn(&type_info, &callable_arg_values[i], param.length);
531
- RefObjectForTransferFullIn(&type_info, &callable_arg_values[i]);
532
- RefFundamentalForTransferFullIn(&type_info, &callable_arg_values[i]);
533
- RefVariantForTransferFullIn(&type_info, &callable_arg_values[i]);
530
+ TakeOwnershipForTransferFull(&type_info, &callable_arg_values[i], param.length);
534
531
  }
535
532
  }
536
533
 
@@ -239,6 +239,16 @@ static Local<FunctionTemplate> GetFundamentalTemplate (GIObjectInfo *info, GType
239
239
  }
240
240
  }
241
241
 
242
+ /* The parent recursion above runs JS (each ancestor's template fires the
243
+ * type materializer below), so JS may have re-entered here and created
244
+ * this very template already. Keep that one: its function is the one JS
245
+ * has started decorating. */
246
+ data = g_type_get_qdata (gtype, GNodeJS::template_quark ());
247
+ if (data) {
248
+ auto *persistent = (Persistent<FunctionTemplate> *) data;
249
+ return New<FunctionTemplate> (*persistent);
250
+ }
251
+
242
252
  auto *persistentTpl = new Persistent<FunctionTemplate> (tpl);
243
253
  auto *persistentFn = new Persistent<Function> (Nan::GetFunction (tpl).ToLocalChecked ());
244
254
  persistentTpl->SetWeak (
@@ -247,6 +257,11 @@ static Local<FunctionTemplate> GetFundamentalTemplate (GIObjectInfo *info, GType
247
257
  g_type_set_qdata (gtype, GNodeJS::template_quark (), persistentTpl);
248
258
  g_type_set_qdata (gtype, GNodeJS::function_quark (), persistentFn);
249
259
 
260
+ /* Modules hold lazy accessors: a fundamental type reached from C first
261
+ * must be materialized so makeObject() in JS attaches its methods before
262
+ * a wrapper is handed out (see gi.h). */
263
+ GNodeJS::MaterializeType (info);
264
+
250
265
  return tpl;
251
266
  }
252
267
 
@@ -405,6 +420,17 @@ static Local<FunctionTemplate> GetVariantTemplate () {
405
420
  g_type_set_qdata (gtype, GNodeJS::template_quark (), persistentTpl);
406
421
  g_type_set_qdata (gtype, GNodeJS::function_quark (), persistentFn);
407
422
 
423
+ /* Modules hold lazy accessors: a variant reached from C first (e.g. a
424
+ * signal argument, #465) must have GLib.Variant materialized so
425
+ * makeBoxed() in JS attaches its methods before a wrapper is handed out. */
426
+ if (g_irepository_is_registered (NULL, "GLib", NULL)) {
427
+ GIBaseInfo *variant_info = g_irepository_find_by_name (NULL, "GLib", "Variant");
428
+ if (variant_info) {
429
+ GNodeJS::MaterializeType (variant_info);
430
+ g_base_info_unref (variant_info);
431
+ }
432
+ }
433
+
408
434
  return tpl;
409
435
  }
410
436
 
package/src/gi.cc CHANGED
@@ -32,9 +32,37 @@ namespace GNodeJS {
32
32
 
33
33
  Nan::Persistent<Object> moduleCache(Nan::New<Object>());
34
34
 
35
+ GThread *js_thread = NULL;
36
+
35
37
  Local<Object> GetModuleCache() {
36
38
  return Nan::New<Object>(GNodeJS::moduleCache);
37
39
  }
40
+
41
+ static Nan::Persistent<v8::Function> typeMaterializer;
42
+
43
+ void SetTypeMaterializerInternal(Local<v8::Function> fn) {
44
+ typeMaterializer.Reset(fn);
45
+ }
46
+
47
+ void MaterializeType(GIBaseInfo *info) {
48
+ if (typeMaterializer.IsEmpty() || info == NULL)
49
+ return;
50
+
51
+ Local<v8::Function> fn = Nan::New<v8::Function>(typeMaterializer);
52
+ Local<Value> argv[] = {
53
+ UTF8(g_base_info_get_namespace(info)),
54
+ UTF8(g_base_info_get_name(info)),
55
+ };
56
+ Nan::TryCatch tryCatch;
57
+ Nan::Call(fn, Nan::GetCurrentContext()->Global(), 2, argv);
58
+ if (tryCatch.HasCaught()) {
59
+ Nan::Utf8String message(tryCatch.Exception());
60
+ g_warning("node-gtk: could not materialize type %s.%s: %s",
61
+ g_base_info_get_namespace(info),
62
+ g_base_info_get_name(info),
63
+ *message);
64
+ }
65
+ }
38
66
  }
39
67
 
40
68
 
@@ -121,6 +149,71 @@ NAN_METHOD(Bootstrap) {
121
149
  info.GetReturnValue().Set(module_obj);
122
150
  }
123
151
 
152
+ /*
153
+ * Enumerate a loaded namespace's top-level infos in one native call, so that
154
+ * lib/module.js can define lazy accessors without paying ~3 FFI round-trips
155
+ * per info (48ms for Gtk-4.0 + dependencies, vs ~1ms here). Returns a flat
156
+ * array of [name, infoType, index, ...] triplets. Infos that makeInfo() cannot
157
+ * handle (callbacks, gtype structs, etc.) are filtered out here, mirroring the
158
+ * `item !== undefined` check the eager loop used to do.
159
+ */
160
+ NAN_METHOD(GetInfoEntries) {
161
+ Nan::Utf8String ns(info[0]);
162
+ GIRepository *repo = g_irepository_get_default();
163
+
164
+ int n = g_irepository_get_n_infos(repo, *ns);
165
+ Local<Array> result = Nan::New<Array>();
166
+ uint32_t position = 0;
167
+
168
+ for (int i = 0; i < n; i++) {
169
+ BaseInfo baseInfo(g_irepository_get_info(repo, *ns, i));
170
+ GIInfoType type = baseInfo.type();
171
+
172
+ switch (type) {
173
+ case GI_INFO_TYPE_FUNCTION:
174
+ case GI_INFO_TYPE_BOXED:
175
+ case GI_INFO_TYPE_ENUM:
176
+ case GI_INFO_TYPE_FLAGS:
177
+ case GI_INFO_TYPE_OBJECT:
178
+ case GI_INFO_TYPE_INTERFACE:
179
+ case GI_INFO_TYPE_CONSTANT:
180
+ case GI_INFO_TYPE_UNION:
181
+ break;
182
+ case GI_INFO_TYPE_STRUCT:
183
+ if (g_struct_info_is_gtype_struct(baseInfo.info()))
184
+ continue;
185
+ break;
186
+ default:
187
+ continue;
188
+ }
189
+
190
+ /* Register the GType with the GObject runtime NOW, even though the JS
191
+ * class stays lazy. The eager loop did this as a side effect
192
+ * (MakeObjectClass/MakeBoxedClass call get_g_type() for every class)
193
+ * and code depends on it: by-name lookups such as
194
+ * GObject.typeFromName('GFileInfo') or type names in GtkBuilder XML
195
+ * must resolve without JS ever touching the class. Registration is a
196
+ * few µs per type; class initialization stays lazy either way. */
197
+ switch (type) {
198
+ case GI_INFO_TYPE_STRUCT:
199
+ case GI_INFO_TYPE_BOXED:
200
+ case GI_INFO_TYPE_UNION:
201
+ case GI_INFO_TYPE_OBJECT:
202
+ case GI_INFO_TYPE_INTERFACE:
203
+ g_registered_type_info_get_g_type((GIRegisteredTypeInfo *) baseInfo.info());
204
+ break;
205
+ default:
206
+ break;
207
+ }
208
+
209
+ Nan::Set(result, position++, UTF8(baseInfo.name()));
210
+ Nan::Set(result, position++, Nan::New<v8::Int32>(type));
211
+ Nan::Set(result, position++, Nan::New<v8::Int32>(i));
212
+ }
213
+
214
+ info.GetReturnValue().Set(result);
215
+ }
216
+
124
217
  NAN_METHOD(GetConstantValue) {
125
218
  GIBaseInfo *gi_info = (GIBaseInfo *) GNodeJS::PointerFromWrapper (info[0]);
126
219
  GITypeInfo *type_info = g_constant_info_get_type(gi_info);
@@ -418,6 +511,10 @@ NAN_METHOD(SetInterfaceMethodsApplier) {
418
511
  GNodeJS::SetInterfaceMethodsApplier(info);
419
512
  }
420
513
 
514
+ NAN_METHOD(SetTypeMaterializer) {
515
+ GNodeJS::SetTypeMaterializerInternal(info[0].As<v8::Function>());
516
+ }
517
+
421
518
  NAN_METHOD(RegisterClass) {
422
519
  GNodeJS::ObjectClass::RegisterClass(info);
423
520
  }
@@ -431,6 +528,8 @@ NAN_METHOD(CallVFunc) {
431
528
  }
432
529
 
433
530
  void InitModule(Local<Object> exports, Local<Value> module, void *priv) {
531
+ GNodeJS::js_thread = g_thread_self();
532
+
434
533
  GNodeJS::AsyncCallEnvironment::Initialize();
435
534
 
436
535
  Nan::Export(exports, "Bootstrap", Bootstrap);
@@ -438,6 +537,8 @@ void InitModule(Local<Object> exports, Local<Value> module, void *priv) {
438
537
  Nan::Export(exports, "GetBaseClass", GetBaseClass);
439
538
  Nan::Export(exports, "GetTypeSize", GetTypeSize);
440
539
  Nan::Export(exports, "GetConstantValue", GetConstantValue);
540
+ Nan::Export(exports, "GetInfoEntries", GetInfoEntries);
541
+ Nan::Export(exports, "SetTypeMaterializer", SetTypeMaterializer);
441
542
  Nan::Export(exports, "MakeBoxedClass", MakeBoxedClass);
442
543
  Nan::Export(exports, "MakeObjectClass", MakeObjectClass);
443
544
  Nan::Export(exports, "MakeFunction", MakeFunction);
package/src/gi.h CHANGED
@@ -24,8 +24,29 @@ namespace GNodeJS {
24
24
 
25
25
  extern Nan::Persistent<Object> moduleCache;
26
26
 
27
+ /*
28
+ * The thread running V8/libuv (recorded at module init). V8 objects — global
29
+ * handles in particular — may only be touched from this thread.
30
+ */
31
+ extern GThread *js_thread;
32
+
27
33
  Local<Object> GetModuleCache();
28
34
 
35
+ /*
36
+ * Lazy type materialization: modules are populated with lazy accessors
37
+ * (lib/module.js), and prototypes only get their methods/properties when a
38
+ * class is first touched. Types can also be reached from C first (a method
39
+ * return value, a signal argument): the template-creation paths in
40
+ * gobject.cc/boxed.cc/fundamental.cc call MaterializeType so the JS side
41
+ * decorates the prototype before any wrapper is handed out. The callback is
42
+ * installed from bootstrap.js via SetTypeMaterializer and is idempotent and
43
+ * re-entrancy-safe on the JS side.
44
+ */
45
+
46
+ void MaterializeType(GIBaseInfo *info);
47
+
48
+ void SetTypeMaterializerInternal(Local<v8::Function> fn);
49
+
29
50
 
30
51
 
31
52
  /*
package/src/gobject.cc CHANGED
@@ -11,6 +11,7 @@
11
11
  #include "gi.h"
12
12
  #include "gobject.h"
13
13
  #include "macros.h"
14
+ #include "toggle_queue.h"
14
15
  #include "util.h"
15
16
  #include "value.h"
16
17
 
@@ -133,10 +134,28 @@ struct GObjectWrapper {
133
134
  bool collected = false;
134
135
  };
135
136
 
136
- static void ToggleNotify(gpointer user_data, GObject *gobject, gboolean toggle_down) {
137
+ /* Reconcile the wrapper's persistent with the state the object's current
138
+ * refcount calls for. Main thread only — called inline by ToggleNotify on the
139
+ * JS thread, and by toggleQueue's drain for deferred off-thread notifications.
140
+ *
141
+ * Weak when the toggle ref is the only reference left: we are the last
142
+ * holder, so the wrapper may be collected. The two-pass weak callback's first
143
+ * pass runs *during* GC (before any JS/GTK code resumes) and only flips a
144
+ * flag, so WrapperFromGObject can tell a reclaimed wrapper from a live one
145
+ * and never marshals a dead handle to JS. All GObject teardown happens in the
146
+ * second pass — a first-pass callback may not call into GObject.
147
+ *
148
+ * Strong otherwise: something other than us holds the object, so the wrapper
149
+ * must stay alive until that ref is dropped again. Reviving is essential —
150
+ * without it a wrapper that went weak once (e.g. a freshly constructed object
151
+ * at refcount 1) would never become strong again when GTK takes ownership,
152
+ * and GC could then collect a wrapper whose GObject is still in use (notably
153
+ * a subclassed widget owned by GTK, losing its overridden vfuncs and instance
154
+ * state). */
155
+ void SynchronizeToggleState(GObject *gobject) {
137
156
  void *data = g_object_get_qdata (gobject, GNodeJS::object_quark());
138
-
139
- g_assert (data != NULL);
157
+ if (data == NULL)
158
+ return;
140
159
 
141
160
  auto *wrapper = (GObjectWrapper *) data;
142
161
 
@@ -146,34 +165,37 @@ static void ToggleNotify(gpointer user_data, GObject *gobject, gboolean toggle_d
146
165
  if (wrapper->collected)
147
166
  return;
148
167
 
149
- if (toggle_down) {
150
- /* We're dropping from 2 refs to 1 ref: we are the last holder, so the
151
- * wrapper may be collected. Install the weak ref (unless it already is). */
152
- if (wrapper->dying)
153
- return;
168
+ bool only_toggle_ref =
169
+ g_atomic_int_get ((const gint *) &gobject->ref_count) <= 1;
170
+
171
+ if (only_toggle_ref && !wrapper->dying) {
154
172
  wrapper->dying = true;
155
- /* Two-pass weak callback: the first pass runs *during* GC (before any
156
- * JS/GTK code resumes) and only flips a flag, so WrapperFromGObject can
157
- * tell a reclaimed wrapper from a live one and never marshals a dead
158
- * handle to JS. All GObject teardown happens in the second pass — a
159
- * first-pass callback may not call into GObject. */
160
173
  wrapper->persistent.v8::PersistentBase<Object>::SetWeak (
161
174
  wrapper, GObjectDestroyedFirstPass, v8::WeakCallbackType::kParameter);
162
- } else {
163
- /* We're going from 1 ref to 2 refs: something other than us now holds
164
- * the object, so the wrapper must stay alive (strong) until that ref is
165
- * dropped again. Reviving here is essential — without it a wrapper that
166
- * went weak once (e.g. a freshly constructed object at refcount 1) would
167
- * never become strong again when GTK takes ownership, and GC could then
168
- * collect a wrapper whose GObject is still in use (notably a subclassed
169
- * widget owned by GTK, losing its overridden vfuncs and instance state). */
170
- if (!wrapper->dying)
171
- return;
175
+ } else if (!only_toggle_ref && wrapper->dying) {
172
176
  wrapper->dying = false;
173
177
  wrapper->persistent.ClearWeak ();
174
178
  }
175
179
  }
176
180
 
181
+ /* Fires synchronously on whatever thread crosses the 1<->2 refcount boundary
182
+ * — e.g. GLib's worker thread dropping the ref it held on a GSubprocess while
183
+ * waiting for the child to exit. V8 global handles may only be touched from
184
+ * the JS thread: a SetWeak/ClearWeak from a worker raced the scavenger's
185
+ * weak-handle processing and corrupted the global-handle list (fatal "Check
186
+ * failed: Heap::InFromPage(heap_object)" in a later scavenge, or random
187
+ * SIGSEGV). Off-thread notifications are deferred to the main context; the
188
+ * notified direction is not forwarded because the deferred reconciliation
189
+ * re-derives it from the refcount (see toggle_queue.h). */
190
+ static void ToggleNotify(gpointer user_data, GObject *gobject, gboolean toggle_down) {
191
+ if (G_UNLIKELY (g_thread_self () != GNodeJS::js_thread)) {
192
+ toggleQueue.Synchronize (gobject);
193
+ return;
194
+ }
195
+
196
+ SynchronizeToggleState (gobject);
197
+ }
198
+
177
199
  static void AssociateGObject(Local<Object> object, GObject *gobject, GType gtype) {
178
200
  Nan::SetInternalFieldPointer(object, 0, gobject);
179
201
 
@@ -202,6 +224,9 @@ static void AssociateGObject(Local<Object> object, GObject *gobject, GType gtype
202
224
  static void GObjectFinalized(gpointer data, GObject *where_the_object_was) {
203
225
  auto *wrapper = (GObjectWrapper *) data;
204
226
  wrapper->gobject = NULL;
227
+ /* A deferred off-thread toggle may still be queued for this object; the
228
+ * drain must not touch freed memory. */
229
+ toggleQueue.Cancel (where_the_object_was);
205
230
  }
206
231
 
207
232
  static void GObjectConstructor(const FunctionCallbackInfo<Value> &info) {
@@ -308,6 +333,11 @@ static gboolean GObjectTeardownIdle(gpointer data) {
308
333
  /* If the GObject was already finalized out from under us, GObjectFinalized
309
334
  * cleared the pointer; there is nothing left to detach or unref. */
310
335
  if (gobject != NULL) {
336
+ /* The weak ref that would cancel a queued off-thread toggle on
337
+ * finalize is removed below, so cancel any pending entry now — the
338
+ * toggle ref drop at the end may be the object's last reference. */
339
+ toggleQueue.Cancel (gobject);
340
+
311
341
  /* Drop the weak ref first so removing the toggle ref (which may finalize
312
342
  * the object) doesn't re-enter GObjectFinalized. */
313
343
  g_object_weak_unref (gobject, GObjectFinalized, wrapper);
@@ -878,6 +908,17 @@ static MaybeLocal<FunctionTemplate> GetClassTemplate(GType gtype) {
878
908
  if (maybeTpl.IsEmpty())
879
909
  return MaybeLocal<FunctionTemplate> ();
880
910
 
911
+ /* NewClassTemplate() runs JS while building the parent chain (each
912
+ * ancestor's template fires the type materializer below), so JS may have
913
+ * re-entered here and created this very template already. Keep that one:
914
+ * its function is the one JS has started decorating. */
915
+ data = g_type_get_qdata (gtype, GNodeJS::template_quark());
916
+ if (data) {
917
+ auto *persistent = (Nan::Persistent<FunctionTemplate> *) data;
918
+ auto existingTpl = New<FunctionTemplate> (*persistent);
919
+ return existingTpl;
920
+ }
921
+
881
922
  auto tpl = maybeTpl.ToLocalChecked();
882
923
  auto fn = Nan::GetFunction (tpl).ToLocalChecked();
883
924
  auto persistentTpl = new Nan::Persistent<FunctionTemplate>(tpl);
@@ -892,14 +933,19 @@ static MaybeLocal<FunctionTemplate> GetClassTemplate(GType gtype) {
892
933
  g_type_set_qdata(gtype, GNodeJS::template_quark(), persistentTpl);
893
934
  g_type_set_qdata(gtype, GNodeJS::function_quark(), persistentFn);
894
935
 
895
- // Introspectable object types have their interface methods installed by
896
- // makeObject() in JS. Private concrete types are never seen there, so mix
897
- // in their interface methods now (issue #441).
936
+ // Introspectable object types have their methods/properties installed by
937
+ // makeObject() in JS. Modules hold lazy accessors, so a type reached from
938
+ // C first (method return value, signal argument) must be materialized here
939
+ // or its wrappers would expose a bare prototype. Private concrete types
940
+ // are invisible to makeObject(), so mix in their interface methods
941
+ // instead (issue #441).
898
942
  GIBaseInfo *own_info = g_irepository_find_by_gtype(NULL, gtype);
899
- if (own_info == NULL)
943
+ if (own_info == NULL) {
900
944
  ApplyInterfaceMethods(fn, gtype);
901
- else
945
+ } else {
946
+ GNodeJS::MaterializeType(own_info);
902
947
  g_base_info_unref(own_info);
948
+ }
903
949
 
904
950
  return MaybeLocal<FunctionTemplate> (tpl);
905
951
  }
package/src/gobject.h CHANGED
@@ -18,6 +18,11 @@ namespace GNodeJS {
18
18
  MaybeLocal<Function> MakeClass (GIBaseInfo *info);
19
19
  Local<Value> WrapperFromGObject (GObject *object);
20
20
  GObject * GObjectFromWrapper (Local<Value> value);
21
+
22
+ /* Reconcile the wrapper's V8 persistent (weak or strong) with the GObject's
23
+ * current refcount. Main thread only; called inline by ToggleNotify on the JS
24
+ * thread and by toggleQueue's drain for deferred off-thread notifications. */
25
+ void SynchronizeToggleState (GObject *gobject);
21
26
  Local<Value> GetSignalHandler (GObject *gobject, guint index);
22
27
  Local<FunctionTemplate> GetBaseClassTemplate ();
23
28
  MaybeLocal<Value> GetGObjectProperty (GObject * gobject, const char *prop_name);
@@ -0,0 +1,44 @@
1
+
2
+ #include "toggle_queue.h"
3
+ #include "gobject.h"
4
+
5
+ namespace GNodeJS {
6
+
7
+ ToggleQueue toggleQueue;
8
+
9
+ void ToggleQueue::Synchronize (GObject *gobject) {
10
+ g_mutex_lock (&lock);
11
+ if (g_slist_find (objects, gobject) == NULL)
12
+ objects = g_slist_prepend (objects, gobject);
13
+ if (source == 0)
14
+ source = g_idle_add (ToggleQueue::Drain, this);
15
+ g_mutex_unlock (&lock);
16
+ }
17
+
18
+ void ToggleQueue::Cancel (GObject *gobject) {
19
+ g_mutex_lock (&lock);
20
+ objects = g_slist_remove (objects, gobject);
21
+ g_mutex_unlock (&lock);
22
+ }
23
+
24
+ /* Runs on the main context. The lock is held across the drain so a (buggy,
25
+ * over-unreffed) off-thread finalize cancelling its entry cannot race the
26
+ * entry being processed. SynchronizeToggleState never re-enters the queue,
27
+ * so this cannot deadlock. */
28
+ gboolean ToggleQueue::Drain (gpointer user_data) {
29
+ auto *self = static_cast<ToggleQueue *> (user_data);
30
+
31
+ g_mutex_lock (&self->lock);
32
+ GSList *objects = self->objects;
33
+ self->objects = NULL;
34
+ self->source = 0;
35
+
36
+ for (GSList *l = objects; l != NULL; l = l->next)
37
+ SynchronizeToggleState ((GObject *) l->data);
38
+
39
+ g_slist_free (objects);
40
+ g_mutex_unlock (&self->lock);
41
+ return G_SOURCE_REMOVE;
42
+ }
43
+
44
+ }; /* GNodeJS */
@@ -0,0 +1,53 @@
1
+
2
+ #pragma once
3
+
4
+ #include <glib-object.h>
5
+
6
+ namespace GNodeJS {
7
+
8
+ /*
9
+ * Defers toggle-state synchronization to the main context.
10
+ *
11
+ * "Synchronize" is meant as reconcile — bring the wrapper's V8 persistent
12
+ * (weak or strong) in line with the GObject's current refcount — not as
13
+ * synchronous execution: requests may come from any thread, and the
14
+ * reconciliation itself always runs later, from an idle on the main context.
15
+ *
16
+ * A toggle notification runs on whatever thread crosses the 1<->2 refcount
17
+ * boundary, but V8 global handles may only be touched from the JS thread
18
+ * (see ToggleNotify in gobject.cc). Off-thread notifications land here.
19
+ *
20
+ * The queue stores no direction: by the time the idle runs, the notified
21
+ * direction is stale (the refcount may have crossed the boundary again, in
22
+ * either order). Each drained object is instead reconciled from its current
23
+ * refcount (SynchronizeToggleState), which makes the deferral idempotent and
24
+ * ordering-insensitive.
25
+ *
26
+ * Entries hold no reference (taking one from inside a toggle handler would
27
+ * recursively fire the opposite toggle). A live wrapper's toggle ref keeps
28
+ * the object's refcount >= 1, so a queued object cannot be finalized under
29
+ * normal operation; as a safety net the GObjectFinalized weak callback and
30
+ * the wrapper teardown Cancel() any pending entry.
31
+ */
32
+ class ToggleQueue {
33
+ public:
34
+ /* Request a deferred synchronization of the object's toggle state.
35
+ * Callable from any thread. */
36
+ void Synchronize (GObject *gobject);
37
+
38
+ /* Drop the object's pending synchronization, if any. Callable from any
39
+ * thread. Must be called before the object can be finalized, so the
40
+ * drain never touches freed memory. */
41
+ void Cancel (GObject *gobject);
42
+
43
+ private:
44
+ static gboolean Drain (gpointer user_data);
45
+
46
+ GMutex lock;
47
+ GSList *objects;
48
+ guint source;
49
+ };
50
+
51
+ extern ToggleQueue toggleQueue;
52
+
53
+ }; /* GNodeJS */
package/src/value.cc CHANGED
@@ -1293,15 +1293,24 @@ void FreeGIArgument(GITypeInfo *type_info, GIArgument *arg, GITransfer transfer,
1293
1293
  if (free_elements) {
1294
1294
  GITypeInfo *element_info = g_type_info_get_param_type(type_info, 0);
1295
1295
 
1296
- GITransfer element_transfer = GI_TRANSFER_EVERYTHING;
1297
- GIDirection element_direction = GI_DIRECTION_OUT;
1296
+ /* Free elements in the direction they were allocated: an
1297
+ * OUT/EVERYTHING list transferred us one reference per element,
1298
+ * released here (the wrappers hold their own). An IN list was
1299
+ * built by V8ToGIArgument, which allocates strings but borrows
1300
+ * GObject/boxed pointers straight from their wrappers — freeing
1301
+ * with IN/NOTHING releases the strings and leaves the borrowed
1302
+ * pointers alone (mirrors FreeGIArgumentArray). Unreffing IN
1303
+ * elements stole a reference per element from their real owners
1304
+ * (e.g. Gdk.FileList.newFromList: the GFiles were finalized while
1305
+ * the GdkFileList's deep copy still pointed at them). */
1306
+ GITransfer element_transfer = is_in ? GI_TRANSFER_NOTHING : GI_TRANSFER_EVERYTHING;
1298
1307
  GIArgument element_arg;
1299
1308
 
1300
1309
  GSList* list = (GSList *)arg->v_pointer;
1301
1310
 
1302
1311
  for (; list != NULL; list = list->next) {
1303
1312
  element_arg.v_pointer = list->data;
1304
- FreeGIArgument(element_info, &element_arg, element_transfer, element_direction);
1313
+ FreeGIArgument(element_info, &element_arg, element_transfer, direction);
1305
1314
  }
1306
1315
 
1307
1316
  g_base_info_unref(element_info);
@@ -1702,6 +1711,14 @@ void RefObjectForTransferFullIn (GITypeInfo *type_info, GIArgument *arg) {
1702
1711
  g_base_info_unref(iface);
1703
1712
  }
1704
1713
 
1714
+ // Documented on the declaration in value.h.
1715
+ void TakeOwnershipForTransferFull (GITypeInfo *type_info, GIArgument *arg, long length) {
1716
+ CopyBoxedForTransferFullIn(type_info, arg, length);
1717
+ RefObjectForTransferFullIn(type_info, arg);
1718
+ RefFundamentalForTransferFullIn(type_info, arg);
1719
+ RefVariantForTransferFullIn(type_info, arg);
1720
+ }
1721
+
1705
1722
 
1706
1723
  /*
1707
1724
  * GValue conversion functions
package/src/value.h CHANGED
@@ -52,6 +52,16 @@ void CopyBoxedForTransferFullIn (GITypeInfo *type_info, GIArgument *arg,
52
52
  // under the callee once the wrapper is GC'd. See #439.
53
53
  void RefObjectForTransferFullIn (GITypeInfo *type_info, GIArgument *arg);
54
54
 
55
+ // Hand a transfer-full value its own reference for its new owner. A value whose
56
+ // ownership transfers full — a transfer-full IN argument, or a transfer-full
57
+ // callable return — is exactly one of boxed / GObject / fundamental / GVariant,
58
+ // and the four per-kind helpers (CopyBoxedForTransferFullIn / RefObjectFor... /
59
+ // RefFundamentalFor... / RefVariantFor...) each no-op for the other kinds. Runs
60
+ // them all so the single call covers every kind. `length` is the element count
61
+ // for a transfer-full C array of boxed pointers (-1 when not such an array).
62
+ // See #439/#409/#468.
63
+ void TakeOwnershipForTransferFull (GITypeInfo *type_info, GIArgument *arg, long length);
64
+
55
65
  bool CanConvertV8ToGIArgument (GITypeInfo *type_info, Local<Value> value, bool may_be_null);
56
66
 
57
67
  bool V8ToGValue(GValue *gvalue, Local<Value> value, ResourceOwnership ownership = kNone);