diamondburned/gotk4

Safer toggle referencing

diamondburned opened this issue · 1 comments

Preamble

Right now, signal callbacks in gotk4 are implemented by using uintptrs to
trick the Go garbage collector (GC) into thinking that an object, in our case a
per-object callback registry, is no longer used. The finalizer will confirm
that, or it will delay the GC of that registry until later.

This method works, because a global registry of registries is required for the
Cgo callback to know what to access the actual callback pointer from. There
isn't a safe way to directly hand the callback that pointer.

This is needed, because any strong global reference to the object will keep the
object alive, and therefore none of the signal callback finalizers will be
called, forever keeping the object on the global registry. The uintptr borrows
the GC to periodically check when an object can be thrown off the global
registry.

Why change?

This method is very ugly, in that it relies on the assumption that
heap-allocated pointers will not be moved by the GC. The import
assume-no-moving-gc helps guarantee this.

However, relying on such an assumption is relying on an implementation detail of
the Go runtime, meaning that it's an extremely bad idea.

Proposal

There is a safer way of achieving the aforementioned use case without using
uintptr.

Give each GObject 2 finalizers:

  • 1 for handling a regular reference using g_object_ref and g_object_unref,
    and
  • 1 for handling a toggle reference for the callback registry.

The regular reference handler shall be specific to each *glib.object instance,
meaning that for each *glib.object allocated on Go's heap, a regular reference
must be taken.

The callback registry's reference is that of a toggle reference, so all
*glib.object instances of the same C object should all share that one toggle
reference.

The use of the toggle reference lets us know when the toggle reference is the
only reference left on the object. By requiring all Go instances of *object
take a regular reference every time it's used, the toggle reference callback
will let us know when the global registry reference is the only reference
remaining.

If that were the case, it implies that neither C nor Go currently has a
reference to the object except for the internal callback registry in the Go
heap, which makes it impossible for the user to "revive" the object. We can then
safely delete it off the registry.

Note that no uintptr trick is needed here: we're not relying on Go's GC to do
the dirty work anymore. All we're doing is using GLib's toggle reference
callback to wipe itself off the global registry, and Go's GC will do the rest
for us the same way it handles everything that isn't global.

Prototype

type Object struct {
	*object // setFinalizer
}

type object struct {
	reg *registry // setFinalizer
	obj *C.GObject
}

This doesn't work. When a signal callback captures the outer variable, both the
toggle reference and the regular reference is kept alive. This creates a cycle,
and neither gets freed.