diamondburned/gotk4

Lazy toggle reference

diamondburned opened this issue · 1 comments

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.