node-gtk 1.0.0 → 2.1.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/src/function.cc CHANGED
@@ -55,7 +55,9 @@ static void* AllocateArgument (GIBaseInfo *arg_info) {
55
55
 
56
56
  GIBaseInfo* base_info = g_type_info_get_interface (&arg_type);
57
57
  size_t size = Boxed::GetSize (base_info);
58
- void* pointer = g_malloc0(size);
58
+ GType gtype = g_registered_type_info_get_g_type (base_info);
59
+ // Match g_boxed_free's allocator for registered boxed types (#290, #213).
60
+ void* pointer = AllocateBoxed(gtype, size);
59
61
 
60
62
  g_base_info_unref(base_info);
61
63
  return pointer;
@@ -67,6 +69,35 @@ static bool IsMethod (GIBaseInfo *info) {
67
69
  (flags & GI_FUNCTION_IS_CONSTRUCTOR) == 0);
68
70
  }
69
71
 
72
+ static bool EndsWith (const char *str, const char *suffix) {
73
+ size_t str_len = strlen (str);
74
+ size_t suffix_len = strlen (suffix);
75
+ return str_len >= suffix_len && strcmp (str + str_len - suffix_len, suffix) == 0;
76
+ }
77
+
78
+ /* True for an instance method that deallocates the instance itself — a *_free
79
+ * or *_unref on a boxed/struct/union. node-gtk's wrapper owns the memory and
80
+ * frees it on GC, so after such a call the wrapper must disown it or GC will
81
+ * double-free (#429). Restricted to boxed-like containers: GObject lifetime is
82
+ * handled separately, and gtk_widget_destroy() etc. don't free the wrapper. */
83
+ static bool FreesInstance (GIFunctionInfo *info) {
84
+ if (!IsMethod (info))
85
+ return false;
86
+
87
+ const char *symbol = g_function_info_get_symbol (info);
88
+ if (symbol == NULL || !(EndsWith (symbol, "_free") || EndsWith (symbol, "_unref")))
89
+ return false;
90
+
91
+ GIBaseInfo *container = g_base_info_get_container (info);
92
+ if (container == NULL)
93
+ return false;
94
+
95
+ GIInfoType type = g_base_info_get_type (container);
96
+ return type == GI_INFO_TYPE_STRUCT
97
+ || type == GI_INFO_TYPE_UNION
98
+ || type == GI_INFO_TYPE_BOXED;
99
+ }
100
+
70
101
  static bool ShouldSkipReturn(GIBaseInfo *info, GITypeInfo *return_type) {
71
102
  return g_type_info_get_tag(return_type) == GI_TYPE_TAG_VOID
72
103
  || g_callable_info_skip_return(info) == TRUE;
@@ -134,6 +165,7 @@ bool FunctionInfo::Init() {
134
165
 
135
166
  is_method = IsMethod(info);
136
167
  can_throw = g_callable_info_can_throw_gerror (info);
168
+ frees_instance = FreesInstance(info);
137
169
 
138
170
  n_callable_args = g_callable_info_get_n_args (info);
139
171
  n_total_args = n_callable_args;
@@ -208,14 +240,14 @@ bool FunctionInfo::Init() {
208
240
  if (closure_i >= 0 && closure_i < n_callable_args)
209
241
  call_parameters[closure_i].type = ParameterType::kSKIP;
210
242
 
211
- if (destroy_i < i) {
243
+ if (destroy_i >= 0 && destroy_i < i) {
212
244
  if (IsDirectionIn(call_parameters[destroy_i].direction))
213
245
  n_in_args--;
214
246
  if (IsDirectionOut(call_parameters[destroy_i].direction))
215
247
  n_out_args--;
216
248
  }
217
249
 
218
- if (closure_i < i) {
250
+ if (closure_i >= 0 && closure_i < i) {
219
251
  if (IsDirectionIn(call_parameters[closure_i].direction))
220
252
  n_in_args--;
221
253
  if (IsDirectionOut(call_parameters[closure_i].direction))
@@ -388,16 +420,16 @@ Local<Value> FunctionCall (
388
420
  }
389
421
  else if (param.type == ParameterType::kCALLBACK) {
390
422
  Callback *callback;
391
- ffi_closure *closure;
423
+ gpointer callable; /* executable trampoline address (see Callback) */
392
424
 
393
425
  if (info[in_arg]->IsNullOrUndefined()) {
394
- closure = nullptr;
426
+ callable = nullptr;
395
427
  callback = nullptr;
396
428
  } else {
397
429
  GICallableInfo *callback_info = g_type_info_get_interface (&type_info);
398
430
  GIScopeType scope_type = g_arg_info_get_scope(&arg_info);
399
431
  callback = new Callback(info[in_arg].As<Function>(), callback_info, scope_type);
400
- closure = callback->closure;
432
+ callable = callback->native_address;
401
433
  g_base_info_unref (callback_info);
402
434
  }
403
435
 
@@ -414,7 +446,7 @@ Local<Value> FunctionCall (
414
446
  callable_arg_values[closure_i].v_pointer = callback;
415
447
  }
416
448
 
417
- callable_arg_values[i].v_pointer = closure;
449
+ callable_arg_values[i].v_pointer = callable;
418
450
  func->call_parameters[i].data.v_pointer = callback;
419
451
  }
420
452
 
@@ -440,6 +472,25 @@ Local<Value> FunctionCall (
440
472
  param.data.v_pointer = callable_arg_values[i].v_pointer;
441
473
  callable_arg_values[i].v_pointer = &param.data;
442
474
  }
475
+ // For a transfer-container IN GList/GSList/GHashTable the callee
476
+ // frees the container; snapshot the (caller-owned) element
477
+ // pointers now so they can be freed after the call (#399).
478
+ else if (IsTransferContainerInList(&type_info,
479
+ g_arg_info_get_ownership_transfer(&arg_info), direction)) {
480
+ param.data = {};
481
+ param.data.v_pointer = CaptureTransferContainerElements(
482
+ &type_info, callable_arg_values[i].v_pointer);
483
+ }
484
+ // For a transfer-full IN argument the callee takes ownership.
485
+ // Boxed: hand it a copy so the JS wrapper's own memory isn't
486
+ // double-freed when finalized (#409). GObject: add the reference
487
+ // the callee will own, so it isn't finalized out from under the
488
+ // callee once the wrapper is GC'd (#439).
489
+ else if (direction == GI_DIRECTION_IN
490
+ && g_arg_info_get_ownership_transfer(&arg_info) == GI_TRANSFER_EVERYTHING) {
491
+ CopyBoxedForTransferFullIn(&type_info, &callable_arg_values[i], param.length);
492
+ RefObjectForTransferFullIn(&type_info, &callable_arg_values[i]);
493
+ }
443
494
  }
444
495
 
445
496
  in_arg++;
@@ -538,6 +589,11 @@ Local<Value> FunctionCall (
538
589
  delete callback;
539
590
  }
540
591
  }
592
+ else if (IsTransferContainerInList (&arg_type, transfer, direction)) {
593
+ // The callee already freed the container structure; free only the
594
+ // captured (caller-owned) elements, never the freed container (#399).
595
+ FreeTransferContainerElements (&arg_type, param.data.v_pointer);
596
+ }
541
597
  else {
542
598
  if (direction == GI_DIRECTION_INOUT || (direction == GI_DIRECTION_OUT && !g_arg_info_is_caller_allocates (&arg_info)))
543
599
  FreeGIArgument (&arg_type, (GIArgument*)arg_value.v_pointer, transfer, direction);
@@ -546,6 +602,11 @@ Local<Value> FunctionCall (
546
602
  }
547
603
  }
548
604
 
605
+ // If this method freed the instance, drop node-gtk's ownership of it so the
606
+ // GC finalizer won't free it a second time (#429).
607
+ if (func->frees_instance && !didThrow)
608
+ DisownBoxed (info.This());
609
+
549
610
  #ifndef __linux__
550
611
  delete[] total_arg_values;
551
612
  #endif
package/src/function.h CHANGED
@@ -33,6 +33,7 @@ struct FunctionInfo {
33
33
 
34
34
  bool is_method;
35
35
  bool can_throw;
36
+ bool frees_instance; /* a *_free/*_unref method that deallocates the instance (#429) */
36
37
 
37
38
  int n_callable_args;
38
39
  int n_total_args;
package/src/gi.cc CHANGED
@@ -208,6 +208,49 @@ NAN_METHOD(ObjectPropertySetter) {
208
208
  RETURN(success.ToLocalChecked());
209
209
  }
210
210
 
211
+ // All members of a C union live at offset 0. Some GLib versions (the
212
+ // regression fixed in 2.86.1 — glib#3745) emit a bogus 0xffff offset for union
213
+ // field members in the compiled typelib, so g_field_info_get_field /
214
+ // g_field_info_set_field would read/write far out of bounds and crash. Detect
215
+ // union fields so they can be accessed directly at offset 0 instead (#376).
216
+ static bool FieldIsInUnion (GIFieldInfo *field) {
217
+ GIBaseInfo *container = g_base_info_get_container (field);
218
+ return container != NULL
219
+ && g_base_info_get_type (container) == GI_INFO_TYPE_UNION;
220
+ }
221
+
222
+ // Whether a field type is a simple (non-pointer) C value that can be read or
223
+ // written by copying its bytes — mirrors what g_field_info_get/set_field
224
+ // accept. Pointer-backed types (strings, nested structs, arrays, ...) return
225
+ // false and are rejected, as before.
226
+ static bool IsSimpleFieldType (GITypeInfo *type_info) {
227
+ switch (g_type_info_get_tag (type_info)) {
228
+ case GI_TYPE_TAG_BOOLEAN:
229
+ case GI_TYPE_TAG_INT8:
230
+ case GI_TYPE_TAG_UINT8:
231
+ case GI_TYPE_TAG_INT16:
232
+ case GI_TYPE_TAG_UINT16:
233
+ case GI_TYPE_TAG_INT32:
234
+ case GI_TYPE_TAG_UINT32:
235
+ case GI_TYPE_TAG_INT64:
236
+ case GI_TYPE_TAG_UINT64:
237
+ case GI_TYPE_TAG_FLOAT:
238
+ case GI_TYPE_TAG_DOUBLE:
239
+ case GI_TYPE_TAG_UNICHAR:
240
+ case GI_TYPE_TAG_GTYPE:
241
+ return true;
242
+ case GI_TYPE_TAG_INTERFACE: {
243
+ GIBaseInfo *iface = g_type_info_get_interface (type_info);
244
+ GIInfoType itype = g_base_info_get_type (iface);
245
+ bool simple = (itype == GI_INFO_TYPE_ENUM || itype == GI_INFO_TYPE_FLAGS);
246
+ g_base_info_unref (iface);
247
+ return simple;
248
+ }
249
+ default:
250
+ return false;
251
+ }
252
+ }
253
+
211
254
  NAN_METHOD(StructFieldSetter) {
212
255
  Local<Object> boxedWrapper = info[0].As<Object>();
213
256
  Local<Object> fieldInfo = info[1].As<Object>();
@@ -231,6 +274,17 @@ NAN_METHOD(StructFieldSetter) {
231
274
 
232
275
  RETURN (Nan::Undefined());
233
276
 
277
+ } else if (FieldIsInUnion(field)) {
278
+
279
+ // Union members are at offset 0; write directly to avoid the bogus
280
+ // introspected offset (glib#3745, #376).
281
+ if (IsSimpleFieldType(field_type)) {
282
+ memcpy(boxed, &arg, GNodeJS::GetTypeSize(field_type));
283
+ } else {
284
+ Nan::ThrowError("Unable to set field (complex types not allowed)");
285
+ RETURN (Nan::Undefined());
286
+ }
287
+
234
288
  } else {
235
289
 
236
290
  if (g_field_info_set_field(field, boxed, &arg) == FALSE) {
@@ -286,6 +340,19 @@ NAN_METHOD(StructFieldGetter) {
286
340
  GNodeJS::ResourceOwnership ownership = GNodeJS::kCopy;
287
341
  BaseInfo typeInfo = g_field_info_get_type(*fieldInfo);
288
342
 
343
+ if (FieldIsInUnion(*fieldInfo)) {
344
+ // Union members are at offset 0; read directly to avoid the bogus
345
+ // introspected offset (glib#3745, #376).
346
+ if (!IsSimpleFieldType(*typeInfo)) {
347
+ Nan::ThrowError("Converting non-primitive fields is not allowed");
348
+ return;
349
+ }
350
+ memset(&value, 0, sizeof(value));
351
+ memcpy(&value, boxed, GNodeJS::GetTypeSize(*typeInfo));
352
+ RETURN(GNodeJS::GIArgumentToV8(*typeInfo, &value, -1, ownership));
353
+ return;
354
+ }
355
+
289
356
  if (!g_field_info_get_field(*fieldInfo, boxed, &value)) {
290
357
  /* If g_field_info_get_field() failed, this is a non-primitive type */
291
358
 
@@ -313,6 +380,10 @@ NAN_METHOD(StartLoop) {
313
380
  GNodeJS::StartLoop ();
314
381
  }
315
382
 
383
+ NAN_METHOD(IsRunningMicrotasks) {
384
+ info.GetReturnValue().Set(Nan::New<Boolean>(GNodeJS::IsRunningMicrotasks ()));
385
+ }
386
+
316
387
  NAN_METHOD(GetBaseClass) {
317
388
  auto tpl = GNodeJS::GetBaseClassTemplate ();
318
389
  auto fn = Nan::GetFunction (tpl).ToLocalChecked();
@@ -358,6 +429,7 @@ void InitModule(Local<Object> exports, Local<Value> module, void *priv) {
358
429
  Nan::Export(exports, "ObjectPropertyGetter", ObjectPropertyGetter);
359
430
  Nan::Export(exports, "ObjectPropertySetter", ObjectPropertySetter);
360
431
  Nan::Export(exports, "StartLoop", StartLoop);
432
+ Nan::Export(exports, "IsRunningMicrotasks", IsRunningMicrotasks);
361
433
  Nan::Export(exports, "GetLoopStack", GetLoopStack);
362
434
  Nan::Export(exports, "RegisterClass", RegisterClass);
363
435
  Nan::Export(exports, "RegisterVFunc", RegisterVFunc);
package/src/gobject.cc CHANGED
@@ -34,7 +34,6 @@ namespace GNodeJS {
34
34
  static Nan::Persistent<FunctionTemplate> baseTemplate;
35
35
 
36
36
 
37
- static void GObjectDestroyed(const Nan::WeakCallbackInfo<GObject> &data);
38
37
  static MaybeLocal<FunctionTemplate> GetClassTemplate(GType gtype);
39
38
  static MaybeLocal<Function> GetClass(GType gtype);
40
39
  static void StoreVFunc(GType gtype, Callback *callback);
@@ -59,12 +58,18 @@ static GObject* CreateGObjectFromObject(GType gtype, Local<Value> object) {
59
58
 
60
59
  for (int i = 0; i < n_properties; i++) {
61
60
  Local<String> name = TO_STRING (Nan::Get(properties, i).ToLocalChecked());
62
- const char *name_string = g_strdup (*Nan::Utf8String(name));
61
+ // Accept camelCase property names (e.g. iconName) in addition to
62
+ // dashed/underscored ones; GObject canonicalizes '_' to '-' but not
63
+ // camelCase, so convert here (#320). The original spelling is kept so
64
+ // an unknown name is reported as the user wrote it.
65
+ Nan::Utf8String name_original (name);
66
+ char *name_string = Util::ToDashed (*name_original);
63
67
  Local<Value> value = Nan::Get(property_hash, name).ToLocalChecked();
64
68
 
65
69
  auto value_spec = g_object_class_find_property (G_OBJECT_CLASS (klass), name_string);
66
70
  if (value_spec == NULL) {
67
- Throw::InvalidPropertyName(name_string);
71
+ Throw::InvalidPropertyName(*name_original);
72
+ g_free(name_string);
68
73
  goto out;
69
74
  }
70
75
 
@@ -94,21 +99,66 @@ out:
94
99
  return gobject;
95
100
  }
96
101
 
102
+ struct GObjectWrapper;
103
+ static void GObjectDestroyedFirstPass(const v8::WeakCallbackInfo<GObjectWrapper> &data);
104
+ static void GObjectDestroyedSecondPass(const v8::WeakCallbackInfo<GObjectWrapper> &data);
105
+ static void GObjectFinalized(gpointer data, GObject *where_the_object_was);
106
+
107
+ struct GObjectWrapper {
108
+ Nan::Persistent<Object> persistent;
109
+ GObject *gobject;
110
+ /* Set to true the moment SetWeak is called. Between that point and the
111
+ * destroy callback actually running, the V8 handle is weak (and, once GC
112
+ * reclaims it, dead). If ToggleNotify fires in that window (because native
113
+ * code adjusts the refcount), touching the persistent crashes. Guard every
114
+ * persistent access with this flag. */
115
+ bool dying = false;
116
+ /* Set in the first-pass weak callback, i.e. the instant GC reclaims the
117
+ * wrapper, before any JS/GTK code resumes. While this is true the
118
+ * persistent is dead but the qdata still points here until the second-pass
119
+ * callback runs; WrapperFromGObject must build a fresh wrapper rather than
120
+ * resurrect this one. */
121
+ bool collected = false;
122
+ };
123
+
97
124
  static void ToggleNotify(gpointer user_data, GObject *gobject, gboolean toggle_down) {
98
125
  void *data = g_object_get_qdata (gobject, GNodeJS::object_quark());
99
126
 
100
127
  g_assert (data != NULL);
101
128
 
102
- auto *persistent = (Nan::Persistent<Object> *) data;
129
+ auto *wrapper = (GObjectWrapper *) data;
130
+
131
+ /* The V8 handle has already been reclaimed by GC (collected) — it is dead
132
+ * and can be made neither weak nor strong. If the object is marshalled
133
+ * again, WrapperFromGObject builds a fresh wrapper. */
134
+ if (wrapper->collected)
135
+ return;
103
136
 
104
137
  if (toggle_down) {
105
- /* We're dropping from 2 refs to 1 ref. We are the last holder. Make
106
- * sure that that our weak ref is installed. */
107
- persistent->SetWeak (gobject, GObjectDestroyed, v8::WeakCallbackType::kParameter);
138
+ /* We're dropping from 2 refs to 1 ref: we are the last holder, so the
139
+ * wrapper may be collected. Install the weak ref (unless it already is). */
140
+ if (wrapper->dying)
141
+ return;
142
+ wrapper->dying = true;
143
+ /* Two-pass weak callback: the first pass runs *during* GC (before any
144
+ * JS/GTK code resumes) and only flips a flag, so WrapperFromGObject can
145
+ * tell a reclaimed wrapper from a live one and never marshals a dead
146
+ * handle to JS. All GObject teardown happens in the second pass — a
147
+ * first-pass callback may not call into GObject. */
148
+ wrapper->persistent.v8::PersistentBase<Object>::SetWeak (
149
+ wrapper, GObjectDestroyedFirstPass, v8::WeakCallbackType::kParameter);
108
150
  } else {
109
- /* We're going from 1 ref to 2 refs. We can't let our wrapper be
110
- * collected, so make sure that our reference is persistent */
111
- persistent->ClearWeak ();
151
+ /* We're going from 1 ref to 2 refs: something other than us now holds
152
+ * the object, so the wrapper must stay alive (strong) until that ref is
153
+ * dropped again. Reviving here is essential — without it a wrapper that
154
+ * went weak once (e.g. a freshly constructed object at refcount 1) would
155
+ * never become strong again when GTK takes ownership, and GC could then
156
+ * collect a wrapper whose GObject is still in use (notably a subclassed
157
+ * widget owned by GTK, losing its overridden vfuncs and instance state). */
158
+ if (!wrapper->dying)
159
+ return;
160
+ wrapper->dying = false;
161
+ wrapper->persistent.ClearWeak ();
112
162
  }
113
163
  }
114
164
 
@@ -117,8 +167,10 @@ static void AssociateGObject(Local<Object> object, GObject *gobject, GType gtype
117
167
 
118
168
  SET_OBJECT_GTYPE(object, gtype);
119
169
 
120
- auto *persistent = new Nan::Persistent<Object>(object);
121
- g_object_set_qdata (gobject, GNodeJS::object_quark(), persistent);
170
+ auto *wrapper = new GObjectWrapper();
171
+ wrapper->gobject = gobject;
172
+ wrapper->persistent.Reset(object);
173
+ g_object_set_qdata (gobject, GNodeJS::object_quark(), wrapper);
122
174
 
123
175
  // Because we can't sink floating ref and add toggle ref at the same time,
124
176
  // first sink the floating ref, add the toggle ref, and then release the
@@ -126,6 +178,18 @@ static void AssociateGObject(Local<Object> object, GObject *gobject, GType gtype
126
178
  g_object_ref_sink (gobject);
127
179
  g_object_add_toggle_ref (gobject, ToggleNotify, NULL);
128
180
  g_object_unref (gobject);
181
+
182
+ // The toggle ref above is supposed to keep the GObject alive for as long as
183
+ // the wrapper exists. A weak ref guards against the case where it doesn't —
184
+ // e.g. a JS-subclassed instance whose refcount is driven to 0 from the GTK
185
+ // side while we still hold the toggle ref: GObjectFinalized then clears the
186
+ // dangling pointer so the destroy callbacks never touch freed memory.
187
+ g_object_weak_ref (gobject, GObjectFinalized, wrapper);
188
+ }
189
+
190
+ static void GObjectFinalized(gpointer data, GObject *where_the_object_was) {
191
+ auto *wrapper = (GObjectWrapper *) data;
192
+ wrapper->gobject = NULL;
129
193
  }
130
194
 
131
195
  static void GObjectConstructor(const FunctionCallbackInfo<Value> &info) {
@@ -177,18 +241,71 @@ static void GObjectConstructor(const FunctionCallbackInfo<Value> &info) {
177
241
  }
178
242
  }
179
243
 
180
- static void GObjectDestroyed(const Nan::WeakCallbackInfo<GObject> &data) {
181
- GObject *gobject = data.GetParameter ();
244
+ static void GObjectDestroyedFirstPass(const v8::WeakCallbackInfo<GObjectWrapper> &data) {
245
+ GObjectWrapper *wrapper = data.GetParameter ();
246
+
247
+ /* This runs *during* GC, where it is not legal to call into V8 (beyond
248
+ * resetting the handle, which the two-pass contract requires) or into
249
+ * GObject — the GObject is not safe to touch here, and doing so crashes in
250
+ * g_type_check_instance_is_fundamentally_a. So only flip a flag and reset
251
+ * the handle; the real teardown is deferred to the second pass.
252
+ *
253
+ * The flag lets WrapperFromGObject distinguish a reclaimed wrapper (whose
254
+ * persistent is now dead) from a live one during the window before the
255
+ * second pass nulls the qdata, so it builds a fresh wrapper instead of
256
+ * handing the dead handle to JS — which crashed on first property access. */
257
+ wrapper->collected = true;
258
+ wrapper->persistent.Reset ();
259
+
260
+ data.SetSecondPassCallback (GObjectDestroyedSecondPass);
261
+ }
182
262
 
183
- void *type_data = g_object_get_qdata (gobject, GNodeJS::object_quark());
184
- auto *persistent = (Nan::Persistent<Object> *) type_data;
185
- delete persistent;
263
+ static gboolean GObjectTeardownIdle(gpointer data) {
264
+ GObjectWrapper *wrapper = (GObjectWrapper *) data;
265
+ GObject *gobject = wrapper->gobject;
266
+
267
+ /* If the GObject was already finalized out from under us, GObjectFinalized
268
+ * cleared the pointer; there is nothing left to detach or unref. */
269
+ if (gobject != NULL) {
270
+ /* Drop the weak ref first so removing the toggle ref (which may finalize
271
+ * the object) doesn't re-enter GObjectFinalized. */
272
+ g_object_weak_unref (gobject, GObjectFinalized, wrapper);
273
+
274
+ /* Only detach the qdata if it still points at us — WrapperFromGObject
275
+ * may have resurrected this GObject with a fresh wrapper while we were
276
+ * pending, and we must not clobber it. */
277
+ if (g_object_get_qdata (gobject, GNodeJS::object_quark()) == wrapper)
278
+ g_object_set_qdata (gobject, GNodeJS::object_quark(), NULL);
279
+
280
+ /* Dropping the last toggle ref disposes the object, and GTK's dispose
281
+ * synchronously emits signals (e.g. ::destroy) into still-connected
282
+ * node-gtk closures — i.e. it re-enters arbitrary JS. That is only legal
283
+ * here because we run from a GLib idle on the main loop, not from the GC
284
+ * second-pass callback that scheduled us (see GObjectDestroyedSecondPass). */
285
+ g_object_remove_toggle_ref (gobject, &ToggleNotify, NULL);
286
+ }
186
287
 
187
- /* We're destroying the wrapper object, so make sure to clear out
188
- * the qdata that points back to us. */
189
- g_object_set_qdata (gobject, GNodeJS::object_quark(), NULL);
288
+ delete wrapper;
289
+ return G_SOURCE_REMOVE;
290
+ }
190
291
 
191
- g_object_remove_toggle_ref (gobject, &ToggleNotify, NULL);
292
+ static void GObjectDestroyedSecondPass(const v8::WeakCallbackInfo<GObjectWrapper> &data) {
293
+ GObjectWrapper *wrapper = data.GetParameter ();
294
+
295
+ /* Defer the actual teardown to a main-loop idle instead of running it here.
296
+ * This callback fires from V8's InvokeSecondPassPhantomCallbacks *during* a
297
+ * garbage collection. Dropping the toggle ref can take the GObject's refcount
298
+ * to zero, and GTK's dispose then emits signals into node-gtk closures,
299
+ * re-entering JS (Nan::Call) — which crashes when invoked mid-GC. Running the
300
+ * teardown from a GLib idle moves the ref drop (and any disposal/signal
301
+ * emission it triggers) to a point where calling into JS is safe again.
302
+ *
303
+ * The GObject stays alive across the window because we still hold the toggle
304
+ * ref; the wrapper (with its now-reset persistent and collected=true) is kept
305
+ * until the idle deletes it. WrapperFromGObject already handles a resurrected
306
+ * GObject during this window by building a fresh wrapper, and the idle's
307
+ * qdata check above won't clobber it. */
308
+ g_idle_add (GObjectTeardownIdle, wrapper);
192
309
  }
193
310
 
194
311
  static void GObjectClassDestroyed(const Nan::WeakCallbackInfo<GType> &info) {
@@ -333,8 +450,7 @@ static void StoreVFunc(GType gtype, Callback *callback) {
333
450
  static void DestroyVFuncs(GType gtype) {
334
451
  /* Destroy vfunc list, if any */
335
452
  GSList *list = (GSList *) g_type_get_qdata (gtype, GNodeJS::vfuncs_quark());
336
- GSList *item = list;
337
- while ((item = g_slist_next (item)) != NULL) {
453
+ for (GSList *item = list; item != NULL; item = item->next) {
338
454
  auto callback = (Callback *) item->data;
339
455
  delete callback;
340
456
  }
@@ -652,10 +768,15 @@ Local<Value> WrapperFromGObject(GObject *gobject) {
652
768
  void *data = g_object_get_qdata (gobject, GNodeJS::object_quark());
653
769
 
654
770
  if (data) {
655
- /* Easy case: we already have an object. */
656
- auto *persistent = (Nan::Persistent<Object> *) data;
657
- auto obj = New<Object> (*persistent);
658
- return obj;
771
+ auto *wrapper = (GObjectWrapper *) data;
772
+ /* Reuse the existing wrapper unless GC has already reclaimed it (its
773
+ * persistent is dead and only awaiting the second-pass teardown). In
774
+ * that case fall through and build a fresh wrapper; the stale one's
775
+ * second pass is guarded so it won't clobber the new qdata. */
776
+ if (!wrapper->collected) {
777
+ auto obj = New<Object> (wrapper->persistent);
778
+ return obj;
779
+ }
659
780
  }
660
781
 
661
782
  GType gtype = G_OBJECT_TYPE(gobject);
package/src/loop.cc CHANGED
@@ -26,6 +26,8 @@ static Nan::Persistent<Array> loopStack(Nan::New<Array> ());
26
26
  struct uv_loop_source {
27
27
  GSource source;
28
28
  uv_loop_t *loop;
29
+ gpointer fd_tag;
30
+ bool fd_polled;
29
31
  };
30
32
 
31
33
  static gboolean loop_source_prepare (GSource *base, int *timeout) {
@@ -45,6 +47,24 @@ static gboolean loop_source_prepare (GSource *base, int *timeout) {
45
47
 
46
48
  bool loop_alive = uv_loop_alive (source->loop);
47
49
 
50
+ /* Toggle whether GLib polls uv's backend fd. When the loop is dead we must
51
+ * stop polling it: an *unref'd* but still-active uv handle (e.g. the async
52
+ * eventfd left signaling by emscripten/WASM runtimes such as web-tree-sitter,
53
+ * or by worker_threads) keeps the backend epoll fd perpetually readable.
54
+ * uv_loop_alive() reports the loop dead (unref'd handles don't count), so we
55
+ * intend to sleep here, but a polled-and-ready fd makes GLib's poll() return
56
+ * immediately every iteration -> loop_source_dispatch() busy-spins at 100%
57
+ * CPU, starving GTK. Masking the fd lets GLib actually block until a GTK
58
+ * source wakes us; we restore polling as soon as the loop is alive again. */
59
+ #if !OS_WINDOWS
60
+ if (source->fd_tag != NULL && loop_alive != source->fd_polled) {
61
+ g_source_modify_unix_fd (&source->source, source->fd_tag,
62
+ loop_alive ? (GIOCondition) (G_IO_IN | G_IO_OUT | G_IO_ERR)
63
+ : (GIOCondition) 0);
64
+ source->fd_polled = loop_alive;
65
+ }
66
+ #endif
67
+
48
68
  /* If the loop is dead, we can simply sleep forever until a GTK+ source
49
69
  * (presumably) wakes us back up again. */
50
70
  if (!loop_alive)
@@ -92,13 +112,15 @@ static GSourceFuncs uv_loop_source_funcs = {
92
112
  static GSource *loop_source_new (uv_loop_t *loop) {
93
113
  struct uv_loop_source *source = (struct uv_loop_source *) g_source_new (&uv_loop_source_funcs, sizeof (*source));
94
114
  source->loop = loop;
115
+ source->fd_tag = NULL;
116
+ source->fd_polled = true;
95
117
  #if OS_WINDOWS
96
118
  // FIXME
97
119
  // https://github.com/nodejs/node/issues/36015
98
120
  #else
99
- g_source_add_unix_fd (&source->source,
100
- uv_backend_fd (loop),
101
- (GIOCondition) (G_IO_IN | G_IO_OUT | G_IO_ERR));
121
+ source->fd_tag = g_source_add_unix_fd (&source->source,
122
+ uv_backend_fd (loop),
123
+ (GIOCondition) (G_IO_IN | G_IO_OUT | G_IO_ERR));
102
124
  #endif
103
125
  return &source->source;
104
126
  }
@@ -145,6 +167,20 @@ void CallMicrotaskHandlers () {
145
167
  #endif
146
168
  }
147
169
 
170
+ bool IsRunningMicrotasks() {
171
+ /* True while V8 is draining the microtask queue. Under ES modules the
172
+ * top-level body executes as a microtask, so a synchronous blocking call
173
+ * (e.g. g_main_loop_run) made from it nests inside this drain. V8 refuses
174
+ * nested microtask checkpoints, so any Promise/async continuation queued
175
+ * by user code is stuck until the blocking call returns. Callers use this
176
+ * to detect that situation and defer the blocking call to a macrotask so
177
+ * the module's top-level microtask can return and the queue can drain.
178
+ *
179
+ * - https://github.com/romgrk/node-gtk/issues/442
180
+ */
181
+ return MicrotasksScope::IsRunningMicrotasks(Isolate::GetCurrent());
182
+ }
183
+
148
184
  void StartLoop() {
149
185
  GSource *source = loop_source_new (uv_default_loop ());
150
186
  g_source_attach (source, NULL);
package/src/loop.h CHANGED
@@ -12,6 +12,8 @@ namespace GNodeJS {
12
12
 
13
13
  void StartLoop();
14
14
 
15
+ bool IsRunningMicrotasks();
16
+
15
17
  void QuitLoopStack();
16
18
 
17
19
  Local<Array> GetLoopStack();
package/src/type.cc CHANGED
@@ -15,11 +15,12 @@ char *GetInfoName (GIBaseInfo* info) {
15
15
 
16
16
  char* name = g_strdup (info_name);
17
17
 
18
- GIBaseInfo *parent;
19
- while ((parent = g_base_info_get_container (info)) != NULL) {
18
+ GIBaseInfo *parent = g_base_info_get_container (info);
19
+ while (parent != NULL) {
20
20
  char *new_name = g_strconcat (g_base_info_get_name(parent), ".", name, NULL);
21
21
  g_free (name);
22
22
  name = new_name;
23
+ parent = g_base_info_get_container (parent);
23
24
  }
24
25
 
25
26
  char *new_name = g_strconcat (g_base_info_get_namespace(info), ".", name, NULL);