gotk4
gotk4 copied to clipboard
Lazy toggle reference
Right now, every object creation involves acquiring the global mutex to register itself, as well as hitting several extra C functions to register a toggle reference and its callback. Not all objects created has signals attached, though, so all of that work is not needed most of the time.
Object creation should, instead, use a regular reference by default with a finalizer attached to that instance. The reference will be converted to a toggle reference only once a signal has been attached.
The object wrapper function (newObject
) must always assume a regular
reference, unless a toggle reference is already taken.
The idea can be implemented as such:
package glib
// object keeps a possibly nil box until the caller calls Need(), then it
// returns the existing box or queries the registry. It is thread-safe.
type object struct {
o unsafe.Pointer
v atomic.Value
}
// newObject creates a new object instance.
func newObject(gobject unsafe.Pointer, take bool) *object {
object := &object{o: gobject}
// Get should acquire a RLock, which is alright.
if box := intern.Get(gobject); box != nil {
// Store the box locally if it's known.
object.value.Store(box)
// If we're not taking, then we're being given an existing regular
// reference, so we unreference it since we already have our toggle
// reference.
if !take {
C.g_object_unref((*C.GObject)(object.o))
}
} else {
// We have no box for this object, so take a regular reference (if
// needed).
if take {
C.g_object_ref((*C.GObject)(object.v))
}
// Set the regular finalizer, which is undone by Box when needed.
runtime.SetFinalizer(object, func(object *object) {
C.g_object_unref((*C.GObject)(object.o))
})
}
return object
}
// HasBox returns true if the object already has a known box, which implies that
// it already has a toggle reference.
func (o *object) HasBox() bool {
_, ok := o.v.Load().(*Box)
return ok
}
// Box grabs the interned Box from the instance. It is thread-safe.
func (o *object) Box() *intern.Box {
box, _ := o.v.Load().(*intern.Box)
if box != nil {
return box
}
// Remove the object's finalizer, since we'll be using the finalizer from
// the returned Box instead.
runtime.SetFinalizer(o, nil)
// No box, so ask the global registry. New() will handle synchronizing and
// interning for us, so it's fine if we call Store() multiple times.
box = intern.New(o.o)
o.v.Store(box)
return box
}
Changes would have to be done to intern.New
: it should always assume that we
already own a reference to the given object, and that by taking a toggle
reference, it must always undo the regular reference. The take
parameter is
therefore redundant.
The Take
and AssumeOwnership
can wrap newObject
, like so:
type Object struct {
*object
}
func Take(ptr unsafe.Pointer) *Object {
return &Object{newObject(ptr, true)}
}
func AssumeOwnership(ptr unsafe.Pointer) *Object {
return &Object{newObject(ptr, false)}
}
Side note that this doesn't work if the object is resurrected twice, since we can't unref
all of them, so there needs to be a way to either keep track of all existing references or only take a regular reference once, and both ways require us to keep a global registry of objects anyway.