node-gtk 4.1.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-gtk",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "GNOME Gtk+ bindings for NodeJS",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
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;
84
+ g_mutex_lock (&notifiedCallbacksLock);
85
+ GSList* list = notifiedCallbacks;
86
+ notifiedCallbacks = NULL;
87
+ g_mutex_unlock (&notifiedCallbacksLock);
80
88
 
81
- while (current != NULL) {
82
- Callback* callback = static_cast<Callback*>(current->data);
83
- delete callback;
89
+ for (GSList* current = list; current != NULL; current = current->next)
90
+ delete static_cast<Callback*>(current->data);
84
91
 
85
- current = current->next;
86
- }
87
-
88
- notifiedCallbacks = NULL;
92
+ g_slist_free (list);
89
93
  }
90
94
 
91
95
  /**
package/src/gi.cc CHANGED
@@ -32,6 +32,8 @@ 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
  }
@@ -526,6 +528,8 @@ NAN_METHOD(CallVFunc) {
526
528
  }
527
529
 
528
530
  void InitModule(Local<Object> exports, Local<Value> module, void *priv) {
531
+ GNodeJS::js_thread = g_thread_self();
532
+
529
533
  GNodeJS::AsyncCallEnvironment::Initialize();
530
534
 
531
535
  Nan::Export(exports, "Bootstrap", Bootstrap);
package/src/gi.h CHANGED
@@ -24,6 +24,12 @@ 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
 
29
35
  /*
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);
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);