gotk4
gotk4 copied to clipboard
Support subclassing and interfaces
This issue tracks gotk4's subclass and interface support. The goal is to make Go structs accessible and callable from C's side through the use of interfaces, as well as overriding and/or extending certain methods, similar to subclassing.
Relevant resources:
- Forked GLib with subclassing support (repository)
- Exporting GObject from Go (article)
- Exporting a GObject C API from Rust code and using it from C, ... (article)
Generate these:
// OverrideWidget creates a new Widget from the given overrider.
func OverrideWidget(overrider WidgetOverrider) Widget {
obj := externglib.Register(externglib.Type(C.gtk_widget_get_type()), overrider)
return wrapWidget(obj)
}
To implement a new widget, the user can do this to subclass a widget:
type Widget struct {
gtk.Widget // ours
label *gtk.Label // theirs
}
// Note that we're making methods that implement Overrider. The actual
// Widget instance MIGHT NOT implement this.
var _ gtk.WidgetOverrider = (*Widget)(nil)
func NewWidget(label string) *Widget {
w := Widget{
label: gtk.NewLabel(label),
}
w.Widget = gtk.OverrideWidget(w)
return &w
}
func (w *Widget) Snapshot(s *gtk.Snapshot) {}
And they can use it like so:
w := NewWidget("Hello, world!")
b := gtk.NewBox(gtk.OrientationVertical, 0)
b.Append(w)
Or they can do this to implement an abstract class or interface:
type HTTPMedia struct {
gtk.MediaStream
}
func NewHTTPMedia() gtk.MediaStreamer {
h := HTTPMedia{}
h.MediaStream = gtk.OverrideMediaStreamer(&h)
return &h
}
func (h *HTTPMedia) Pause() {}
func (h *HTTPMedia) Play() {}
It might be worth it to make glib.Object work for both C and Go objects, though. The user will no longer need to embed glib.GoObject if so.
If this is the case (and the examples are already applied as such), then OverrideX functions will rely on glib.Object's specific methods to determine if it's a C or Go object.
It is important to note that the Object's implementation for Go subclassing/implementing will have to check that the Object instance isn't already a valid C instance. Once the check passes, it can then create a new Object and swap that in place.
It might be quite troublesome for a Go GObject class to extend another Go class, because at that point, it wouldn't be dealing with just abstract classes anymore, rather actual classes. This is basically dealing with inheritance, which might not be a good idea.
Since GTK4 is shifting from the inheritance paradigm to a more composition-like
one, it might not be worth the effort to consider this in the implementation.
The user is expected to create a struct that implements the overrider for each
layer, anyway.
For example, if someone makes a Window struct that implements
WidgetOverrider, and someone else wants to "extend" it, then that user is
expected to rewrite all the methods. Of course, Go's struct embedding will help
save some of that work, however, so it should just work normally. It's worth
noting that by going with this approach, each layer will have to create a new
GObject on the C side, each with its own GType.
The code will look roughly like this:
type OtherWidget struct {
Widget
box *gtk.Box
}
func NewOtherWidget() *OtherWidget {
w := OtherWidget{
Widget: NewWidget("Hello, world!"),
box: gtk.NewBox(),
}
w.Widget.Widget = gtk.OverrideWidget(&w)
return &w
}
The only quirk with this method is that it requires the caller to manually dive
down the embedding tree to override the inner widget instance while throwing
away the old Widget's internal object. This isn't very ideal. It could also be
the reason that doing code in Init() would be a better idea.
Maybe externglib.Register could recognize the previous Object allocated and
reuse that instead of creating a new one. It might not always be possible,
though.
There's also the issue of registering for signals. As for properties, it can probably be done like this:
// NewOtherWidget...
w.AddProperties(map[string]interface{}{
"fold": false,
})
Old Draft:
Note that for all types that the user can override/implement, the methods and functions will always take in an interface type, e.g. Widgetter. This means that the functions will always check if the type is an Object or a GoObject. In both cases, the type is expected to implement the entire interface.
Also note that there's no need for the user to implement Init, Dispose and Finalize. Those methods are only there so the user can use it, but there's not really a good reason to do so. Objects that are inside the struct are all GC'd in undefined order, which doesn't really matter, since by the time they need to be finalized by the GC, everything wouldn't have been referenced anymore.
Also note that NewClass will basically register a new GType, and that's it. It should be a fairly cheap call, since sync.Map can prove to be very cheap after a while, and we're only ever growing that map.
The map will probably be a reflect.Type to a GType map. It'll basically provide type interning similar to GLib while abstracting the Type method away from the user nicely.
The Register function will definitely need a valid GoObject instance, though note that the user didn't have to construct the type. As long as we have a valid zero-value of it, we can easily do that ourselves in Register. This means the user will not have to do any heavy-lifting.
Created the subclassing branch to keep track of this.
As for properties, it can probably be done like this:
This API doesn't work. The properties have to be set during construction of the object as well.
Perhaps this might:
func OverrideWidget(w gtk.WidgetOverrider, opts ...glib.ObjectOptions)
gtk.OverrideWidget(w, glib.WithProperties{
"fold": false,
})
See ./gtype.c:4115 g_type_check_instance_cast and ``G_TYPE_CIC`.
New proposal:
type Gadget struct {
gtk.Widget
Child gtk.Widgetter `glib:"child,Construct,ConstructOnly"`
}
var gadgetType = glib.RegisterSubclass[*Gadget](
glib.WithParamSpecs([]glib.ParamSpec{
// TODO
}),
)
func NewGadget(child gtk.Widgetter) *Gadget {
return gadgetType.NewWithProperties(map[string]any{
"child": child,
})
}
Update: subclassing is now on the main branch. Things might be very unstable; please report bugs and crashes to issues.
A release will be made that points to the last working pre-subclassing commit, just to mitigate this potential instability.
Crashing with
2022/12/20 05:18:46 Critical: GLib-GObject: validate_and_install_class_property: assertion 'class->set_property != NULL' failed
2022/12/20 05:18:46 Critical: GLib-GObject: g_object_new_is_valid_property: object class 'AdaptiveFold' has no property named 'position'
This could be gotk4 not calling parent class initializers.