Skip to content

Commit 70ac799

Browse files
committed
Experimental memory leak fix 🎉
This commit experiments with refactoring package `core/intern` to use the newly added `runtime.AddCleanup` and `weak.Pointer` APIs from Go 1.24. This commit has been tested against issues #106 and #126 to ensure stability. It is still not recommended to use this in production until further testing has been done, however.
1 parent db82236 commit 70ac799

File tree

15 files changed

+247
-211
lines changed

15 files changed

+247
-211
lines changed

‎gir/cmd/gir-generate/gendata/gendata.go

+9-12
Original file line numberDiff line numberDiff line change
@@ -404,11 +404,10 @@ func GLibVariantIter(nsgen *girgen.NamespaceGenerator) error {
404404
}
405405
406406
_variant := (*Variant)(gextras.NewStructNative(unsafe.Pointer(_cret)))
407-
runtime.SetFinalizer(
407+
runtime.AddCleanup(
408408
gextras.StructIntern(unsafe.Pointer(_variant)),
409-
func(intern *struct{ C unsafe.Pointer }) {
410-
C.g_variant_unref((*C.GVariant)(intern.C))
411-
},
409+
C.g_variant_unref,
410+
(*C.GVariant)(unsafe.Pointer(_cret)),
412411
)
413412
return _variant, nil
414413
}
@@ -428,11 +427,10 @@ func GLibVariantIter(nsgen *girgen.NamespaceGenerator) error {
428427
}
429428
430429
variant := (*Variant)(gextras.NewStructNative(unsafe.Pointer(item)))
431-
runtime.SetFinalizer(
430+
runtime.AddCleanup(
432431
gextras.StructIntern(unsafe.Pointer(variant)),
433-
func(intern *struct{ C unsafe.Pointer }) {
434-
C.g_variant_unref((*C.GVariant)(intern.C))
435-
},
432+
C.g_variant_unref,
433+
(*C.GVariant)(unsafe.Pointer(item)),
436434
)
437435
438436
return variant
@@ -563,11 +561,10 @@ func GioArrayUseBytes(nsgen *girgen.NamespaceGenerator) error {
563561
)
564562
565563
_bytes := (*Bytes)(gextras.NewStructNative(unsafe.Pointer(v)))
566-
runtime.SetFinalizer(
564+
runtime.AddCleanup(
567565
gextras.StructIntern(unsafe.Pointer(_bytes)),
568-
func(intern *struct{ C unsafe.Pointer }) {
569-
C.g_bytes_unref((*C.GBytes)(intern.C))
570-
},
566+
C.g_bytes_unref,
567+
(*C.GBytes)(unsafe.Pointer(v)),
571568
)
572569
573570
return _bytes

‎gir/girgen/generators/union.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,12 @@ func (ug *UnionGenerator) Use(union *gir.Union) bool {
187187
p.Linef("original := (*C.%s)(%s.underlying%s())", union.CType, ug.Recv(), ug.GoName)
188188
p.Linef("copied := C.%s(original)", copyMethod.CIdentifier)
189189
p.Linef("dst := (*%s)(gextras.NewStructNative(unsafe.Pointer(copied)))", ug.GoName)
190-
p.Linef("runtime.SetFinalizer(")
190+
p.Linef("runtime.AddCleanup(")
191191
p.Linef(" gextras.StructIntern(unsafe.Pointer(dst)),")
192-
p.Linef(" func(intern *struct{ C unsafe.Pointer }) {")
193-
p.Linef(types.RecordPrintFree(ug.gen, &typRecord, "intern.C"))
194-
p.Linef("},")
192+
p.Linef(" func(ptr unsafe.Pointer) {")
193+
p.Linef(" %s", types.RecordPrintFree(ug.gen, &typRecord, "ptr"))
194+
p.Linef(" },")
195+
p.Linef(" unsafe.Pointer(copied),")
195196
p.Linef(")")
196197
p.Linef("return dst")
197198

@@ -264,12 +265,12 @@ func (ug *UnionGenerator) Use(union *gir.Union) bool {
264265
ug.hdr.Import("unsafe")
265266
ug.hdr.ImportCore("gextras")
266267

267-
p.Linef("runtime.SetFinalizer(")
268-
// dst is ASSUMED TO BE A POINTER.
268+
p.Linef("runtime.AddCleanup(")
269269
p.Linef(" gextras.StructIntern(unsafe.Pointer(dst)),")
270-
p.Linef(" func(intern *struct{ C unsafe.Pointer }) {")
271-
p.Linef(types.RecordPrintFree(ug.gen, &typRecord, "intern.C"))
272-
p.Linef("},")
270+
p.Linef(" func(ptr unsafe.Pointer) {")
271+
p.Linef(" %s", types.RecordPrintFree(ug.gen, &typRecord, "ptr"))
272+
p.Linef(" },")
273+
p.Linef(" unsafe.Pointer(cpy),")
273274
p.Linef(")")
274275
}
275276

‎gir/girgen/types/typeconv/c-go.go

+14-23
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,11 @@ func (conv *Converter) cgoArrayConverter(value *ValueConverted) bool {
222222
value.p.Linef("var len C.gsize")
223223
// If we're fully getting the backing array, then we can just steal
224224
// it (since we own it now), which is less copying.
225-
value.p.Linef("p := C.g_byte_array_steal(&%s, &len)", value.In.Name)
226-
value.p.Linef("%s = unsafe.Slice((*byte)(p), uint(len))", value.Out.Set)
227-
value.p.Linef("runtime.SetFinalizer(&%s, func(v *[]byte) {", value.OutName)
228-
value.p.Linef(" C.free(unsafe.Pointer(&(*v)[0]))")
229-
value.p.Linef("})")
225+
value.p.Linef("p := (*byte)(C.g_byte_array_steal(&%s, &len))", value.In.Name)
226+
value.p.Linef("%s = unsafe.Slice(p, uint(len))", value.Out.Set)
227+
value.p.Linef("runtime.AddCleanup(&%s, func(v *[]byte) {", value.OutName)
228+
value.p.Linef(" C.free(unsafe.Pointer(p))")
229+
value.p.Linef("}, p)")
230230
value.p.Ascend()
231231
return true
232232
}
@@ -437,11 +437,7 @@ func (conv *Converter) cgoConverter(value *ValueConverted) bool {
437437
// Set this to be freed if we have the ownership now.
438438
if value.ShouldFree() {
439439
value.header.Import("runtime")
440-
441-
// https://pkg.go.dev/github.com/diamondburned/gotk4/pkg/core/glib?utm_source=godoc#Value
442-
value.p.Linef("runtime.SetFinalizer(%s, func(v *coreglib.Value) {", value.OutName)
443-
value.p.Linef(" C.g_value_unset((*C.GValue)(unsafe.Pointer(v.Native())))")
444-
value.p.Linef("})")
440+
value.p.Linef("runtime.AddCleanup(%s, C.g_value_unset, (*C.GValue)(unsafe.Pointer(v.Native())))", value.OutName)
445441
}
446442
return true
447443

@@ -502,11 +498,11 @@ func (conv *Converter) cgoConverter(value *ValueConverted) bool {
502498
value.p.Linef("C.%s(%s)", ref, value.InNamePtr(1))
503499
}
504500

505-
value.p.Linef("runtime.SetFinalizer(%s%s, func(v %s%s) {",
501+
value.p.Linef("runtime.AddCleanup(%s%s, func(v %s%s) {",
506502
value.OutInPtr(1), value.OutName, value.OutPtr(1), value.Out.Type)
507503
value.p.Linef("C.%s((%s%s)(unsafe.Pointer(v.Native())))",
508504
unref, value.InPtr(1), value.In.Type)
509-
value.p.Linef("})")
505+
value.p.Linef("}, %s%s)", value.OutInPtr(1), value.OutName)
510506

511507
return true
512508
}
@@ -642,17 +638,12 @@ func (conv *Converter) cgoConverter(value *ValueConverted) bool {
642638
// We can take ownership if the type can be reference-counted anyway.
643639
if value.ShouldFree() || unref {
644640
value.header.Import("runtime")
645-
value.vtmpl(`
646-
runtime.SetFinalizer(
647-
gextras.StructIntern(unsafe.Pointer(<.OutInPtr 1><.OutName>)),
648-
func(intern *struct{ C unsafe.Pointer }) {
649-
`)
650-
if value.fail {
651-
value.Logln(logger.Debug, "SetFinalizer set fail")
652-
}
653-
654-
value.p.Linef(types.RecordPrintFree(value.conv.fgen, value.Resolved.Extern, "intern.C"))
655-
value.p.Linef("},")
641+
value.p.Linef("runtime.AddCleanup(")
642+
value.p.Linef(" gextras.StructIntern(unsafe.Pointer(%s%s)),", value.OutInPtr(1), value.OutName)
643+
value.p.Linef(" func(ptr unsafe.Pointer) {")
644+
value.p.Linef(" %s", types.RecordPrintFree(value.conv.fgen, value.Resolved.Extern, "ptr"))
645+
value.p.Linef(" },")
646+
value.p.Linef(" unsafe.Pointer(%s),", value.InNamePtr(1))
656647
value.p.Linef(")")
657648
}
658649

‎gir/girgen/types/typeconv/go-c.go

-12
Original file line numberDiff line numberDiff line change
@@ -653,18 +653,6 @@ func (conv *Converter) gocConverter(value *ValueConverted) bool {
653653
value.vtmpl(
654654
"<.Out.Set> = <.OutCast 1>(gextras.StructNative(unsafe.Pointer(<.InNamePtr 1>)))",
655655
)
656-
657-
// If ShouldFree is true, then ideally, we'll be freeing the C copy of
658-
// the value once we're done. However, since the C code is taking
659-
// ownership, we can't do that, since the Finalizer won't know that and
660-
// free the record. Instead, if we cannot free the data once we're done,
661-
// then we detach the finalizer so Go can't.
662-
if !value.ShouldFree() && types.RecordHasRef(v) == nil {
663-
value.header.Import("runtime")
664-
value.vtmpl(
665-
"runtime.SetFinalizer(gextras.StructIntern(unsafe.Pointer(<.InNamePtr 1>)), nil)",
666-
)
667-
}
668656
return true
669657

670658
case *gir.Callback:

‎gir/girgen/types/typeconv/valueconverted.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,7 @@ func (value *ValueConverted) OutInNamePtr(want int) string {
816816
}
817817

818818
// OutInPtr returns the left-hand side for the output name and type SPECIFICALLY
819-
// for inputting the output name elsewhere, like giving it to SetFinalizer.
819+
// for inputting the output name elsewhere, like giving it to AddCleanup.
820820
func (value *ValueConverted) OutInPtr(want int) string {
821821
has := types.CountPtr(value.Out.Type)
822822
return value._ptr(has, want)

‎pkg/core/closure/closure.go

+11
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,14 @@ func (r *Registry) Load(gclosure unsafe.Pointer) *FuncStack {
3333
func (r *Registry) Delete(gclosure unsafe.Pointer) {
3434
r.reg.Delete(uintptr(gclosure))
3535
}
36+
37+
// Len returns the number of closures in the registry.
38+
// Use this for debugging purposes only.
39+
func (r *Registry) Len() int {
40+
var i int
41+
r.reg.Range(func(_, _ interface{}) bool {
42+
i++
43+
return true
44+
})
45+
return i
46+
}

‎pkg/core/gdebug/objectinfo.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package gdebug
2+
3+
// #cgo pkg-config: gobject-2.0
4+
// #include <glib-object.h>
5+
//
6+
// const gchar *gotk4_object_type_name(gpointer obj) {
7+
// return G_OBJECT_TYPE_NAME(obj);
8+
// };
9+
import "C"
10+
import (
11+
"fmt"
12+
"log/slog"
13+
"unsafe"
14+
)
15+
16+
// ObjectInfo returns a slog.Attr with the GObject's pointer and type.
17+
func ObjectInfo(obj unsafe.Pointer) slog.Attr {
18+
return slog.Group(
19+
"gobject",
20+
slog.String("ptr", fmt.Sprintf("%p", obj)),
21+
slog.String("type", C.GoString(C.gotk4_object_type_name(C.gpointer(obj)))),
22+
slog.Int("refs", objRefCount(obj)))
23+
}
24+
25+
func objRefCount(obj unsafe.Pointer) int {
26+
return int(C.g_atomic_int_get((*C.gint)(unsafe.Pointer(&(*C.GObject)(obj).ref_count))))
27+
}

‎pkg/core/glib/glib.go

+18-46
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import "C"
1414
import (
1515
"errors"
1616
"log"
17+
"log/slog"
1718
"reflect"
1819
"runtime"
1920
"sync"
2021
"unsafe"
2122

2223
"github.com/diamondburned/gotk4/pkg/core/closure"
2324
"github.com/diamondburned/gotk4/pkg/core/gbox"
25+
"github.com/diamondburned/gotk4/pkg/core/gdebug"
2426
"github.com/diamondburned/gotk4/pkg/core/intern"
2527
)
2628

@@ -451,32 +453,6 @@ func SourceRemove(src SourceHandle) bool {
451453
return gobool(C.g_source_remove(C.guint(src)))
452454
}
453455

454-
// WipeAllClosures wipes all the Go closures associated with the given object
455-
// away. BE EXTREMELY CAREFUL WHEN USING THIS FUNCTION! If not careful, your
456-
// program WILL crash!
457-
//
458-
// There is only one specific use case for this function: if your object has
459-
// closures connected to it where these closures capture the object itself, then
460-
// it might create a cyclic dependency on the GC, preventing its finalizer from
461-
// ever running. This will cause the program to leak memory. As a temporary
462-
// hack, this function is introduced for cases where the programmer knows for
463-
// sure that the object will never be used again, and it is significant enough
464-
// of a leak that having a workaround is better than not.
465-
//
466-
// Deprecated: this function is dangerous and should not be used. Using this
467-
// function now causes a panic.
468-
func WipeAllClosures(objector Objector) {
469-
panic("WipeAllClosures is deprecated and should not be used")
470-
}
471-
472-
// Destroy destroys the Go reference to the given object. The object must not be
473-
// used ever again; it is the caller's responsibility to ensure that it will
474-
// never be used again. Resurrecting the object again is undefined behavior.
475-
func Destroy(objector Objector) {
476-
v := BaseObject(objector)
477-
v.destroy()
478-
}
479-
480456
// SignalHandle is the ID of a signal handler.
481457
type SignalHandle uint
482458

@@ -505,6 +481,7 @@ func ConnectGeneratedClosure(
505481
userData := C.gpointer(gbox.Assign(data))
506482

507483
gclosure := C.g_cclosure_new(C.GCallback(tramp), userData, (*[0]byte)(C._gotk4_removeGeneratedClosure))
484+
// C.g_closure_add_invalidate_notifier(gclosure, userData, (*[0]byte)(C._gotk4_removeGeneratedClosure))
508485

509486
// Hold the GClosure reference.
510487
data.GClosure = uintptr(unsafe.Pointer(gclosure))
@@ -531,29 +508,28 @@ func ConnectGeneratedClosure(
531508
func ConnectedGeneratedClosure(closureData uintptr) *closure.FuncStack {
532509
data, _ := gbox.Get(closureData).(*generatedClosureData)
533510
if data == nil {
534-
return nil
511+
slog.Error(
512+
"ConnectedGeneratedClosure: closure data not found in gbox",
513+
"data", closureData)
514+
panic("fatal error during callback")
535515
}
536516

537517
// Get the function value associated with this callback closure.
538518
box := intern.TryGet(unsafe.Pointer(data.GObject))
539519
if box == nil {
540-
log.Printf(
541-
"gotk4: warning: object %s %v cannot be resurrected",
542-
typeFromObject(unsafe.Pointer(data.GObject)),
543-
unsafe.Pointer(data.GObject),
544-
)
545-
return nil
520+
slog.Error(
521+
"ConnectedGeneratedClosure: object cannot be resurrected for callback",
522+
gdebug.ObjectInfo(unsafe.Pointer(data.GObject)))
523+
panic("fatal error during callback")
546524
}
547525

548526
fs := box.Closures().Load(unsafe.Pointer(data.GClosure))
549527
if fs == nil {
550-
log.Printf(
551-
"gotk4: warning: object %s %v missing closure %v",
552-
typeFromObject(unsafe.Pointer(data.GObject)),
553-
unsafe.Pointer(data.GObject),
554-
unsafe.Pointer(data.GClosure),
555-
)
556-
return nil
528+
slog.Error(
529+
"ConnectedGeneratedClosure: closure not found",
530+
"n_closures", box.Closures().Len(),
531+
gdebug.ObjectInfo(unsafe.Pointer(data.GObject)))
532+
panic("fatal error during callback")
557533
}
558534

559535
return fs
@@ -662,10 +638,6 @@ func newObject(ptr unsafe.Pointer, take bool) *Object {
662638
}
663639
}
664640

665-
func (v *Object) destroy() {
666-
intern.Free(v.box)
667-
}
668-
669641
// Cast casts v to the concrete Go type (e.g. *Object to *gtk.Entry).
670642
//
671643
//go:nosplit
@@ -1025,7 +997,7 @@ func marshalValue(p uintptr) (interface{}, error) {
1025997
}
1026998

1027999
v := &value{(*C.GValue)(unsafe.Pointer(c))}
1028-
runtime.SetFinalizer(v, (*value).unset)
1000+
runtime.AddCleanup(v, func(v *C.GValue) { C.g_value_unset(v) }, v.gvalue)
10291001

10301002
return &Value{v}, nil
10311003
}
@@ -1058,7 +1030,7 @@ func AllocateValue() *Value {
10581030
//An allocated GValue is not guaranteed to hold a value that can be unset
10591031
//We need to double check before unsetting, to prevent:
10601032
//`g_value_unset: assertion 'G_IS_VALUE (value)' failed`
1061-
runtime.SetFinalizer(v.value, (*value).unset)
1033+
runtime.AddCleanup(v.value, func(v *C.GValue) { C.g_value_unset(v) }, v.gvalue)
10621034

10631035
return v
10641036
}

‎pkg/core/glib/paramspec.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,7 @@ func ParamSpecTake(ptr unsafe.Pointer, take bool) *ParamSpec {
209209
if !take {
210210
C.g_param_spec_ref(p.intern)
211211
}
212-
runtime.SetFinalizer(p.paramSpec, func(p *paramSpec) {
213-
C.g_param_spec_unref(p.intern)
214-
})
212+
runtime.AddCleanup(p.paramSpec, func(p *C.GParamSpec) { C.g_param_spec_unref(p) }, p.intern)
215213
return p
216214
}
217215

‎pkg/core/intern/intern.c

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
#include "intern.h"
22

3-
const gchar *gotk4_object_type_name(gpointer obj) {
4-
return G_OBJECT_TYPE_NAME(obj);
5-
};
6-
73
gboolean gotk4_intern_remove_toggle_ref(gpointer obj) {
84
// First, remove the toggle reference. This forces the object to be freed,
95
// calling any necessary finalizers.
10-
g_object_remove_toggle_ref(G_OBJECT(obj), (GToggleNotify)goToggleNotify,
11-
NULL);
6+
g_object_remove_toggle_ref(G_OBJECT(obj), (GToggleNotify)goToggleNotify, NULL);
127

138
// Only once the object is freed, we can remove it from the weak reference
149
// registry, since now the finalizers will not be called anymore.

0 commit comments

Comments
 (0)