Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support subclassing and interfaces #22

Open
diamondburned opened this issue Jul 24, 2021 · 7 comments
Open

Support subclassing and interfaces #22

diamondburned opened this issue Jul 24, 2021 · 7 comments
Assignees
Labels
enhancement New feature or request tracking Progress tracking issues

Comments

@diamondburned
Copy link
Owner

diamondburned commented Jul 24, 2021

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:

@diamondburned diamondburned added the enhancement New feature or request label Jul 24, 2021
@diamondburned
Copy link
Owner Author

diamondburned commented Oct 6, 2021

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.

@diamondburned
Copy link
Owner Author

Created the subclassing branch to keep track of this.

@diamondburned diamondburned changed the title Subclassing and interfaces Support subclassing and interfaces Oct 6, 2021
@diamondburned diamondburned pinned this issue Oct 24, 2021
@diamondburned
Copy link
Owner Author

diamondburned commented Oct 24, 2021

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,
})

@diamondburned
Copy link
Owner Author

See ./gtype.c:4115 g_type_check_instance_cast and ``G_TYPE_CIC`.

@diamondburned
Copy link
Owner Author

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,
	})
}

@diamondburned
Copy link
Owner Author

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.

@diamondburned
Copy link
Owner Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request tracking Progress tracking issues
Projects
None yet
Development

No branches or pull requests

1 participant