gotk4
                                
                                 gotk4 copied to clipboard
                                
                                    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.