Skip to content

Commit 4219107

Browse files
committed
Fix crash after regressions from #126
Fixes #130
1 parent 4ab948c commit 4219107

File tree

5 files changed

+130
-53
lines changed

5 files changed

+130
-53
lines changed

pkg/core/glib/connect.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func closureNew(v *Object, f interface{}) *C.GClosure {
6666
closures := closure.RegistryType.Get(v.box)
6767
closures.Register(unsafe.Pointer(gclosure), fs)
6868

69-
// C.g_object_watch_closure(v.native(), gclosure)
69+
C.g_object_watch_closure(v.native(), gclosure)
7070
C.g_closure_set_meta_marshal(gclosure, C.gpointer(v.Native()), (*[0]byte)(C._gotk4_goMarshal))
7171
C.g_closure_add_finalize_notifier(gclosure, C.gpointer(v.Native()), (*[0]byte)(C._gotk4_removeClosure))
7272

pkg/core/intern/intern.c

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#include "intern.h"
2+
3+
const gchar *gotk4_object_type_name(gpointer obj) {
4+
return G_OBJECT_TYPE_NAME(obj);
5+
};
6+
7+
gboolean gotk4_intern_remove_toggle_ref(gpointer obj) {
8+
// First, remove the toggle reference. This forces the object to be freed,
9+
// calling any necessary finalizers.
10+
g_object_remove_toggle_ref(G_OBJECT(obj), (GToggleNotify)goToggleNotify,
11+
NULL);
12+
13+
// Only once the object is freed, we can remove it from the weak reference
14+
// registry, since now the finalizers will not be called anymore.
15+
goFinishRemovingToggleRef(obj);
16+
17+
return FALSE;
18+
}

pkg/core/intern/intern.go

+44-50
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,7 @@
22
package intern
33

44
// #cgo pkg-config: gobject-2.0
5-
// #include <glib-object.h>
6-
//
7-
// extern void goToggleNotify(gpointer, GObject*, gboolean);
8-
// static const gchar* gotk4_object_type_name(gpointer obj) { return G_OBJECT_TYPE_NAME(obj); };
9-
//
10-
// gboolean gotk4_intern_remove_toggle_ref(gpointer obj) {
11-
// g_object_remove_toggle_ref(G_OBJECT(obj), (GToggleNotify)goToggleNotify, NULL);
12-
// }
5+
// #include "intern.h"
136
import "C"
147

158
import (
@@ -89,6 +82,7 @@ type Box struct {
8982
gobject unsafe.Pointer
9083
dummy *boxDummy
9184
data [maxTypesAllowed]unsafe.Pointer
85+
done bool
9286
}
9387

9488
type boxDummy struct {
@@ -178,7 +172,14 @@ var shared = struct {
178172
//go:nosplit
179173
func TryGet(gobject unsafe.Pointer) *Box {
180174
shared.mu.RLock()
175+
181176
box, _ := gets(gobject)
177+
if box != nil && box.done {
178+
log.Panicf(
179+
"gotk4: critical: %s TryGet called on a finalized object",
180+
objInfo(gobject))
181+
}
182+
182183
shared.mu.RUnlock()
183184
return box
184185
}
@@ -285,64 +286,57 @@ func finalizeBox(dummy *boxDummy) {
285286
}
286287

287288
shared.mu.Lock()
289+
defer shared.mu.Unlock()
288290

289291
box, strong := gets(dummy.gobject)
290292
if box == nil {
291293
log.Print("gotk4: intern: finalizer got unknown gobject ", dummy.gobject, ", ignoring")
292-
shared.mu.Unlock()
293294
return
294295
}
295296

296-
var objInfoRes string
297-
if toggleRefs != nil {
298-
objInfoRes = objInfo(dummy.gobject)
299-
toggleRefs.Println(objInfoRes, "finalizeBox: acquiring lock...")
300-
}
297+
// Always delegate the finalization to the next cycle.
298+
// This won't be the case once goFinishRemovingToggleRef is called.
299+
runtime.SetFinalizer(dummy, finalizeBox)
301300

302-
if strong {
303-
// If the closures are strong-referenced, then they might still be
304-
// referenced from the C side, and those closures might access this
301+
if box.done || strong {
302+
// If box.done:
303+
// The finalizer is again called before goFinishRemovingToggleRef gets
304+
// the chance to run. Set the finalizer again and move on.
305+
//
306+
// If strong: the closures are strong-referenced, then they might still
307+
// be referenced from the C side, and those closures might access this
305308
// object. Don't free.
306309

307-
// Delegate finalizing to the next cycle.
308-
runtime.SetFinalizer(dummy, finalizeBox)
309-
310-
shared.mu.Unlock()
311-
312310
if toggleRefs != nil {
313-
toggleRefs.Println(objInfoRes, "finalizeBox: moving finalize to next GC cycle")
311+
if box.done {
312+
toggleRefs.Println(
313+
objInfo(dummy.gobject),
314+
"finalizeBox: finalizeBox called on already finalized object")
315+
} else {
316+
toggleRefs.Println(
317+
objInfo(dummy.gobject),
318+
"finalizeBox: moving finalize to next GC cycle since object is still strong")
319+
}
314320
}
315-
} else {
316-
// If the closures are weak-referenced, then the object reference hasn't
317-
// been toggled yet. Since the object is going away and we're still
318-
// weakly referenced, we can wipe the closures away.
319-
delete(shared.weak, dummy.gobject)
320321

321-
shared.mu.Unlock()
322+
return
323+
}
322324

323-
// Unreference the object. This will potentially free the object as
324-
// well. The closures are definitely gone at this point.
325-
// C.g_object_remove_toggle_ref(
326-
// (*C.GObject)(unsafe.Pointer(dummy.gobject)),
327-
// (*[0]byte)(C.goToggleNotify), nil,
328-
// )
329-
330-
// Do this in the main loop instead. This is because finalizers are
331-
// called in a finalizer thread, and our remove_toggle_ref might be
332-
// destroying other main loop objects.
333-
C.g_main_context_invoke(
334-
nil, // nil means the default main context
335-
(*[0]byte)(C.gotk4_intern_remove_toggle_ref),
336-
C.gpointer(dummy.gobject))
325+
// Mark the box as being removed "done" so that we don't double-free it.
326+
box.done = true
337327

338-
if toggleRefs != nil {
339-
toggleRefs.Println(objInfoRes,
340-
"finalizeBox: remove_toggle_ref queued for next main loop iteration")
341-
}
328+
// Do this in the main loop instead. This is because finalizers are
329+
// called in a finalizer thread, and our remove_toggle_ref might be
330+
// destroying other main loop objects.
331+
C.g_main_context_invoke(
332+
nil, // nil means the default main context
333+
(*[0]byte)(C.gotk4_intern_remove_toggle_ref),
334+
C.gpointer(dummy.gobject))
342335

343-
if objectProfile != nil {
344-
objectProfile.Remove(dummy.gobject)
345-
}
336+
if toggleRefs != nil {
337+
toggleRefs.Println(
338+
objInfo(dummy.gobject),
339+
"finalizeBox: remove_toggle_ref queued for next main loop iteration")
346340
}
347341
}
348342

pkg/core/intern/intern.h

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#include <glib-object.h>
2+
3+
extern void goToggleNotify(gpointer, GObject *, gboolean);
4+
extern void goFinishRemovingToggleRef(gpointer);
5+
const gchar *gotk4_object_type_name(gpointer obj);
6+
gboolean gotk4_intern_remove_toggle_ref(gpointer obj);

pkg/core/intern/intern_export.go

+61-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package intern
22

33
// #cgo pkg-config: gobject-2.0
4-
// #include <glib-object.h>
4+
// #include "intern.h"
55
import "C"
66

7-
import "unsafe"
7+
import (
8+
"log"
9+
"runtime"
10+
"unsafe"
11+
)
812

913
// goToggleNotify is called by GLib on each toggle notification. It doesn't
1014
// actually free anything and relies on Box's finalizer to free both the box and
@@ -32,3 +36,58 @@ func goToggleNotify(_ C.gpointer, obj *C.GObject, isLastInt C.gboolean) {
3236
toggleRefs.Println(objInfo(unsafe.Pointer(obj)), "goToggleNotify: is last =", isLast)
3337
}
3438
}
39+
40+
// finishRemovingToggleRef is called after the toggle reference removal routine
41+
// is dispatched in the main loop. It removes the GObject from the strong and
42+
// weak global maps and unsets the finalizer.
43+
//
44+
//go:nosplit
45+
//export goFinishRemovingToggleRef
46+
func goFinishRemovingToggleRef(gobject unsafe.Pointer) {
47+
if toggleRefs != nil {
48+
toggleRefs.Printf("goFinishRemovingToggleRef: called on %p", gobject)
49+
}
50+
51+
shared.mu.Lock()
52+
defer shared.mu.Unlock()
53+
54+
box, strong := gets(gobject)
55+
if box == nil {
56+
// Extremely weird error. This should never happen.
57+
log.Printf(
58+
"gotk4: critical: %p: finishRemovingToggleRef called on unknown object",
59+
gobject)
60+
return
61+
}
62+
63+
if strong {
64+
// Panic here, else we're memory leaking.
65+
log.Panicf(
66+
"gotk4: critical: %p: finishRemovingToggleRef cannot be called on strongly-referenced object (unexpectedly resurrected?)",
67+
gobject)
68+
}
69+
70+
if !box.done {
71+
log.Panicf(
72+
"gotk4: critical: %p: finishRemovingToggleRef cannot be called with finalizer still set",
73+
gobject)
74+
}
75+
76+
// If the closures are weak-referenced, then the object reference hasn't
77+
// been toggled yet. Since the object is going away and we're still
78+
// weakly referenced, we can wipe the closures away.
79+
//
80+
// Finally clear the object data off the registry.
81+
delete(shared.weak, gobject)
82+
83+
// Clear the finalizer.
84+
runtime.SetFinalizer(box.dummy, nil)
85+
86+
// Keep the box alive until the end of the function just in case the
87+
// finalizer is called again.
88+
runtime.KeepAlive(box.dummy)
89+
90+
if objectProfile != nil {
91+
objectProfile.Remove(gobject)
92+
}
93+
}

0 commit comments

Comments
 (0)