Skip to content

Commit c873a85

Browse files
committed
Fix intern runtime explosions
1 parent 96fbd78 commit c873a85

File tree

1 file changed

+56
-110
lines changed

1 file changed

+56
-110
lines changed

pkg/core/intern/intern.go

+56-110
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import "C"
1010

1111
import (
1212
"fmt"
13+
"log"
1314
"runtime"
1415
"runtime/debug"
1516
"runtime/pprof"
@@ -24,7 +25,7 @@ import (
2425
_ "go4.org/unsafe/assume-no-moving-gc"
2526
)
2627

27-
const maxTypesAllowed = 3
28+
const maxTypesAllowed = 2
2829

2930
var knownTypes uint32 = 0
3031

@@ -36,7 +37,7 @@ type BoxedType[T any] struct {
3637
func RegisterType[T any](ctor func(*Box) *T) BoxedType[T] {
3738
t := BoxedType[T]{
3839
ctor: ctor,
39-
id: atomic.AddUint32(&knownTypes, 1),
40+
id: atomic.AddUint32(&knownTypes, 1) - 1,
4041
}
4142
if t.id > maxTypesAllowed {
4243
panic("BoxedType ID overflow")
@@ -81,18 +82,29 @@ func (t *BoxedType[T]) Delete(box *Box) {
8182

8283
// Box is an opaque type holding extra data.
8384
type Box struct {
84-
dummy *unsafe.Pointer
85-
data [maxTypesAllowed]unsafe.Pointer
85+
gobject unsafe.Pointer
86+
dummy *boxDummy
87+
data [maxTypesAllowed]unsafe.Pointer
88+
}
89+
90+
type boxDummy struct {
91+
gobject unsafe.Pointer
8692
}
8793

8894
// Object returns Box's C GObject pointer.
8995
func (b *Box) GObject() unsafe.Pointer {
90-
return atomic.LoadPointer(&b.data[0])
96+
return b.gobject
9197
}
9298

9399
// Hack to force an object on the heap.
94100
var never bool
95-
var sink interface{}
101+
var sink_ interface{}
102+
103+
func sink(v interface{}) {
104+
if never {
105+
sink_ = v
106+
}
107+
}
96108

97109
var (
98110
traceObjects = gdebug.NewDebugLoggerNullable("trace-objects")
@@ -113,11 +125,12 @@ func objInfo(obj unsafe.Pointer) string {
113125
// newBox creates a zero-value instance of Box.
114126
func newBox(obj unsafe.Pointer) *Box {
115127
box := &Box{}
116-
box.data[0] = obj
128+
box.gobject = obj
117129

118130
// Cheat Go's GC by adding a finalizer to a dummy pointer that is inside Box
119131
// but is not Box itself.
120-
box.dummy = &obj
132+
box.dummy = &boxDummy{gobject: obj}
133+
sink(box.dummy)
121134
runtime.SetFinalizer(box.dummy, finalizeBox)
122135

123136
if objectProfile != nil {
@@ -131,9 +144,7 @@ func newBox(obj unsafe.Pointer) *Box {
131144
// Force box on the heap. Objects on the stack can move, but not objects on
132145
// the heap. At least not for now; the assume-no-moving-gc import will
133146
// guard against that.
134-
if never {
135-
sink = box
136-
}
147+
sink(box)
137148

138149
return box
139150
}
@@ -170,10 +181,7 @@ func Get(gobject unsafe.Pointer, take bool) *Box {
170181
// If the registry is currently strongly referenced, then we must move it to
171182
// a weak reference.
172183

173-
shared.mu.RLock()
174-
box, _ := gets(gobject)
175-
shared.mu.RUnlock()
176-
184+
box := TryGet(gobject)
177185
if box != nil {
178186
return box
179187
}
@@ -196,14 +204,14 @@ func Get(gobject unsafe.Pointer, take bool) *Box {
196204
//
197205
shared.strong[gobject] = box
198206

199-
shared.mu.Unlock()
200-
201207
if toggleRefs != nil {
202208
toggleRefs.Println(objInfo(gobject),
203209
"Get: will introduce new box, current ref =",
204210
C.g_atomic_int_get((*C.gint)(unsafe.Pointer(&(*C.GObject)(gobject).ref_count))))
205211
}
206212

213+
shared.mu.Unlock()
214+
207215
C.g_object_add_toggle_ref(
208216
(*C.GObject)(gobject),
209217
(*[0]byte)(C.goToggleNotify), nil,
@@ -236,132 +244,71 @@ func Get(gobject unsafe.Pointer, take bool) *Box {
236244

237245
// Free explicitly frees the box permanently. It must not be resurrected after
238246
// this.
247+
//
248+
// Deprecated: this function is no longer needed.
239249
func Free(box *Box) {
240-
obj := box.GObject()
241-
if obj == nil {
242-
panic("bug: Free called on already freed object")
243-
}
244-
245-
shared.mu.Lock()
246-
delete(shared.strong, obj)
247-
delete(shared.weak, obj)
248-
for i := range box.data {
249-
atomic.StorePointer(&box.data[i], nil)
250-
}
251-
shared.mu.Unlock()
252-
253-
C.g_object_remove_toggle_ref(
254-
(*C.GObject)(unsafe.Pointer(obj)),
255-
(*[0]byte)(C.goToggleNotify), nil,
256-
)
257-
258-
if toggleRefs != nil {
259-
toggleRefs.Println(objInfo(obj), "Free: explicitly removed toggle ref")
260-
}
261-
262-
if objectProfile != nil {
263-
objectProfile.Remove(obj)
264-
}
250+
panic("not implemented")
265251
}
266252

267-
var finalizing atomic.Bool
268-
269253
// finalizeBox only delays its finalization until GLib notifies us a toggle. It
270254
// does so for as long as an object is stored only in the Go heap. Once the
271255
// object is also shared, the toggle notifier will strongly reference the Box.
272-
func finalizeBox(dummy *unsafe.Pointer) {
273-
// obj := box.GObject()
274-
// if obj == nil {
275-
// return
276-
// }
277-
278-
// var objInfoRes string
279-
// if toggleRefs != nil {
280-
// objInfoRes = objInfo(obj)
281-
// toggleRefs.Println(objInfoRes, "finalizeBox: acquiring lock...")
282-
// }
256+
func finalizeBox(dummy *boxDummy) {
257+
if dummy == nil {
258+
panic("bug: finalizeBox called with nil dummy")
259+
}
283260

284261
shared.mu.Lock()
285262

286-
if !finalizing.CompareAndSwap(false, true) {
287-
panic("impossible: finalizeBox called while finalizing")
288-
}
289-
defer finalizing.Store(false)
290-
291-
box, _ := gets(*dummy)
263+
box, strong := gets(dummy.gobject)
292264
if box == nil {
293-
panic("bug: finalizeBox called on already freed object")
265+
log.Print("gotk4: intern: finalizer got unknown gobject ", dummy.gobject, ", ignoring")
266+
shared.mu.Unlock()
267+
return
294268
}
295269

296-
obj := box.GObject()
297-
298270
var objInfoRes string
299271
if toggleRefs != nil {
300-
objInfoRes = objInfo(obj)
272+
objInfoRes = objInfo(dummy.gobject)
301273
toggleRefs.Println(objInfoRes, "finalizeBox: acquiring lock...")
302274
}
303275

304-
if !freeBox(box) {
276+
if strong {
277+
// If the closures are strong-referenced, then they might still be
278+
// referenced from the C side, and those closures might access this
279+
// object. Don't free.
280+
305281
// Delegate finalizing to the next cycle.
306-
runtime.SetFinalizer(box.dummy, finalizeBox)
282+
runtime.SetFinalizer(dummy, finalizeBox)
283+
307284
shared.mu.Unlock()
308285

309286
if toggleRefs != nil {
310287
toggleRefs.Println(objInfoRes, "finalizeBox: moving finalize to next GC cycle")
311288
}
312289
} else {
290+
// If the closures are weak-referenced, then the object reference hasn't
291+
// been toggled yet. Since the object is going away and we're still
292+
// weakly referenced, we can wipe the closures away.
293+
delete(shared.weak, dummy.gobject)
294+
295+
shared.mu.Unlock()
296+
313297
// Unreference the object. This will potentially free the object as
314298
// well. The closures are definitely gone at this point.
315299
C.g_object_remove_toggle_ref(
316-
(*C.GObject)(unsafe.Pointer(obj)),
300+
(*C.GObject)(unsafe.Pointer(dummy.gobject)),
317301
(*[0]byte)(C.goToggleNotify), nil,
318302
)
319-
shared.mu.Unlock()
320303

321304
if toggleRefs != nil {
322305
toggleRefs.Println(objInfoRes, "finalizeBox: removed toggle ref during GC")
323306
}
324307

325308
if objectProfile != nil {
326-
objectProfile.Remove(obj)
327-
}
328-
}
329-
}
330-
331-
// freeBox must only be called during finalizing of a box. It's used to know if
332-
// a box should be freed or not during finalization. If false is returned, then
333-
// the object must not be freed yet.
334-
//
335-
//go:nocheckptr
336-
func freeBox(box *Box) bool {
337-
b, ok := shared.strong[box.GObject()]
338-
if ok {
339-
if b != box {
340-
panic("bug: multiple Box found for same GObject")
341-
}
342-
// If the closures are strong-referenced, then they might still be
343-
// referenced from the C side, and those closures might access this
344-
// object. Don't free.
345-
return false
346-
}
347-
348-
_, ok = shared.weak[box.GObject()]
349-
if ok {
350-
// If the closures are weak-referenced, then the object reference hasn't
351-
// been toggled yet. Since the object is going away and we're still
352-
// weakly referenced, we can wipe the closures away.
353-
delete(shared.weak, box.GObject())
354-
355-
// By setting *box to a zero-value of closures, we're nilling out the
356-
// maps, which will signal to Go that these cyclical objects can be
357-
// freed altogether.
358-
for i := range box.data {
359-
atomic.StorePointer(&box.data[i], nil)
309+
objectProfile.Remove(dummy.gobject)
360310
}
361311
}
362-
363-
// We can proceed to free the object.
364-
return true
365312
}
366313

367314
// goToggleNotify is called by GLib on each toggle notification. It doesn't
@@ -373,10 +320,7 @@ func goToggleNotify(_ C.gpointer, obj *C.GObject, isLastInt C.gboolean) {
373320
gobject := unsafe.Pointer(obj)
374321
isLast := isLastInt != C.FALSE
375322

376-
if !finalizing.Load() {
377-
shared.mu.Lock()
378-
defer shared.mu.Unlock()
379-
}
323+
shared.mu.Lock()
380324

381325
if isLast {
382326
// delete(shared.sharing, gobject)
@@ -386,6 +330,8 @@ func goToggleNotify(_ C.gpointer, obj *C.GObject, isLastInt C.gboolean) {
386330
makeStrong(gobject)
387331
}
388332

333+
shared.mu.Unlock()
334+
389335
if toggleRefs != nil {
390336
toggleRefs.Println(objInfo(unsafe.Pointer(obj)), "goToggleNotify: is last =", isLast)
391337
}

0 commit comments

Comments
 (0)