Skip to content
This repository has been archived by the owner on Jun 8, 2021. It is now read-only.

Missing documentation/examples for subclassing & few questions #279

Closed
haecker-felix opened this issue Dec 24, 2019 · 9 comments
Closed

Comments

@haecker-felix
Copy link

I started to play with subclassing gtk-rs widgets. It took me a while, to understand few basic things. In my opinion we need more examples, or at least a more basic one with more explanations.

As a starter I had following questions:

  • Why we need a ExampleObjectPrivate and a ExampleObject struct
  • What does the glib_wrapper! macro
  • How I can access from the ExampleObject struct stuff from the ExampleObjectPrivate struct

Now I managed to subclass my first widget. It resulted into this:
https://gitlab.gnome.org/World/Shortwave/commit/4a4e2442f9c15f92b5a2ea32c5eb0d874496793f

Now I wonder...

I'm refering to this example:
https://github.com/gtk-rs/examples/blob/master/src/bin/basic_subclass.rs

And BTW, there's an issue with it:
grafik
Shouldn't be it SimpleWindow instead of the SimpleWindowPrivate in the inspector?

To be fair, I should admit that I have no knowledge with handling gobject boilerplate manually (Thanks Vala 😄 ), so I might missing few bits.

As soon as I understand everything correctly, I want to submit one more basic example for subclassing (like just subclassing a simple gtk::Box with more explanations)

@sdroege
Copy link
Member

sdroege commented Jan 10, 2020

Sorry for the late reply, I missed the github notification for this or didn't get one :)

I started to play with subclassing gtk-rs widgets. It took me a while, to understand few basic things. In my opinion we need more examples, or at least a more basic one with more explanations.

I have that on my list, going through an example step by step. But my todo list is overflowing :)

I have some tutorial-style stuff here for GStreamer: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/tree/master/gst-plugin-tutorial . The GObject parts of it also apply to GTK and anything else. Maybe that helps a bit for the time being.

Note that all these don't really need the glib_wrapper! part due to how things are a bit different with GStreamer :) But more on that a bit below

* I know, the `glib_wrapper!` macro turns it into a gtk-rs like usable object. But what's the object, _before_ I wrap it? Is it a FFI-like object?

There are 4 parts to the GObject here from a Rust point of view

  1. The struct that you implement ObjectSubclass etc on, which contains all the state of your object. This is basically like private instance fields in C++/Java/etc and on the C side corresponds to the instance private struct. This is not a GObject from a GObject point of view but just some struct, from a Rust point of view it's your implementation part of the object.
  2. The two structs that you set the InstanceStruct and ClassStruct associated types to. These are irrelevant (mostly) from a Rust point of view unless you want to make a C/GObject-Introspection API for your subclass. From a C/GObject point of view these are the instance and class struct. The former contains the public/protected instance fields (and the pointers to these are the actual instances of the objects in C), the latter contains virtual methods (in C++ terms this is the vtable) and "virtual member variables" (Vala can represent those, I forgot how it's called there... static virtual?)
  3. The struct declared by glib_wrapper!. This has no correspondence in C at all, it only exists in Rust. This part is equivalent to the safe Rust API exposed by the gtk crate and others for types defined in C. It's the safe Rust API for your object, it's the public API for your object. From a Rust point of view this is a proper GObject and you can use all the public API from glib::Object and any of your base classes and interfaces you implement on it. This is the thing you are going to pass around in normal Rust code outside the internal implementation of it.
* Is there a more elegant way as using `OnceCell`s? Since I cannot initialize my `SongRow` directly anymore, so I have to use a Cell or something as placeholder. The annoying thing is now, that I have to use `.get().unwrap()` everytime, if I want to access one of the struct variables. I wonder if something like `Option` exists, but without having to `unwrap()` it every time.

I saw a crate for such an Option wrapper somewhere but that's also not very nice. Maybe a once_cell::Lazy might work for this here, which the initializes itself on first use?

But I agree that we should find a nicer pattern here. The problem is mostly that in C you would always use NULL pointers for these things until that point, in Rust you have to use Options for representing these safely.

And we can't unfortunately create most fields during creation of the private struct (1. above) because you most likely need access too the actual GObject (i.e. the gtk::Widget or whatever) but this does not fully exist yet. You need to create your private data for it to actually fully exist and it can't be safely used before.

* Is it possible to use composite templates (`<template class="ExampleBox" parent="GtkBox">`) with subclassing (like as it is possible in Vala)? #275

Not supported yet but from what I saw it's not difficult to add support for that. Someone who needs it and actually uses GTK will have to take a look at that at some point :) If you have any questions for the GObject-side of things there I'd be happy to help.

* Is there any elegant way to pass values with `new()`, so I can initialize my struct directly? Like I have done it before: https://gitlab.gnome.org/World/Shortwave/blob/bf8e4a7d2bad3ec1412f0b2c4968f291bfe4a536/src/ui/song_row.rs#L30

You can declare properties on your GObject and then pass values for these properties to glib::Object::new(). See https://github.com/gtk-rs/examples/blob/pending/src/bin/listbox_model.rs for an example of that. That's how you can do "constructor arguments" with GObject. If they should only be possible to be set during construction you can declare the properties as CONSTRUCT_ONLY.

During object construction then your ObjectImpl::set_property() will be called with the corresponding values. This of course also means that during creation of your struct you need to initialize all of them with some default value or use Options.

* What does the `SongRowClass` field? It seems that I can enter here whatever I want.
  https://gitlab.gnome.org/World/Shortwave/blob/master/src/ui/song_row.rs#L50

Not much, it's only needed because of macro hygiene. glib_wrapper! is declaring a SongRawClass struct that represents the class struct (2.b at the top of this comment). The class struct can be used to access class specific API, like class methods. You can also use it for example to list all available properties and things like that. In the context of GTK you will rarely need this, maybe for templates.

Shouldn't be it SimpleWindow instead of the SimpleWindowPrivate in the inspector?

Yes, can you create an issue for that? :)

@sdroege
Copy link
Member

sdroege commented Jan 10, 2020

How I can access from the ExampleObject struct stuff from the ExampleObjectPrivate struct

Oh I missed this part of your questions (did I miss anything else?). ObjectSubclass::from_instance(&some_example_object) gives you the private one, your implementation of the object. ObjectSubclass::get_instance() goes the other way around.

@haecker-felix
Copy link
Author

Thanks for the very helpful answer @sdroege !

I'll continue to implement subclassing in Shortwave, and will submit a more basic subclassing example to this repository soon. IMO a common use case is just subclassing a simple Gtk.Box 😃

@YaLTeR
Copy link

YaLTeR commented Apr 24, 2020

I was asking about the subclassing mechanism in the Matrix channel, and this issue was linked. The answer by @sdroege was very helpful, however I still had a few questions. I was asked to put them here so this issue could be used as a source for new documentation.

  • Examples of when one would want to use a custom InstanceStruct or ClassStruct would be helpful. It's still unclear to me what they do or what they are used for. Examples of what would be visible from the C side, or from the GIR side from other languages.

  • What is get_type in glib_wrapper!, why is it needed?

  • Why are we creating and downcasting a glib::Object in public struct's new instead of creating it directly?

  • What's the order of initialization, why do I have to use OnceCells and initialize them via properties instead of passing parameters into new directly?

  • Why is the private/public struct distinction necessary if I can call private overriden methods by calling the class methods anyway? Why can't I just make the "private" struct a glib object and have private fields and methods by simply not making them public, without needing a separate public struct?

  • Why do I need to specify the ParentType on the private struct as well as extends in the glib_wrapper!? Why is ParentType only the direct parent, while extends needs to list multiple types of the hierarchy? Why do I need to specify Instance and Class both on the private type and in glib_wrapper!?

@sdroege
Copy link
Member

sdroege commented Apr 24, 2020

* Examples of when one would want to use a custom `InstanceStruct` or `ClassStruct` would be helpful. It's still unclear to me what they do or what they are used for. Examples of what would be visible from the C side, or from the GIR side from other languages.

I have an example here https://github.com/sdroege/gobject-example-rs . You almost never want a custom InstanceStruct, but you would use one for exposing some instance variable directly to other languages (not hidden behind a function). It's generally an anti-pattern unless there's no other way. For the ClassStruct, the main reason why you would want a custom one is if you want to expose additional virtual methods for your type that you want subclasses of your type to be able to override/implement. For example if you were re-implementing gio::Application in Rust, you would have the activate virtual method in there.

* What is `get_type` in `glib_wrapper!`, why is it needed?

It's mapping (in this case) directly to the get_type function of your new type. The function returns the glib::Type, the type identifier, for your new type. On the very first call it registers your type with the GObject type system to get such an identifier, on all future calls it is just returning it. With the type identifier you can then create new instances (glib::Object::new()) or for example check if some object instance is of your type.

The glib_wrapper macro specifically is using this to ensure at runtime that any C pointers used as your type are actually instances of your type, and for storing/retrieving instances of your type in glib::Values.

* Why are we creating and downcasting a `glib::Object` in public struct's `new` instead of creating it directly?

How would you create it directly? The only way to create a new GObject instance is via glib::Object::new() by passing the type id. As that can create anything, the return value is a plain glib::Object and you need to downcast it to the concrete type you want. (That last part could be improved a bit though, to not require a cast anymore)

* What's the order of initialization, why do I have to use `OnceCell`s and initialize them via properties instead of passing parameters into `new` directly?

Initialization order: ObjectSubclass::class_init (the very first time only), ObjectSubclass::new (for every instance), ObjectImpl::set_property (for each property set via the glib::Object::new call), ObjectImpl::constructed and then glib::Object::new() returns. There are some other virtual methods that would be called on the way (specifically ObjectImpl::constructor) but we don't wrap those yet.

You have to use OnceCell or similar because in ObjectSubclass::new you have no context at all. If you can create everything you need in your struct out of thin air then that's fine, but if you need things from somewhere else you need to use OnceCell or RefCell<Option<_>> or similar. See also https://github.com/gtk-rs/glib/issues/630

You can call ObjectSubclass::new directly but that only gives you your private struct, it does not create an actual GObject.

* Why is the private/public struct distinction necessary if I can call private overriden methods by calling the class methods anyway?

You probably don't want the fields in your private struct to be public but if anything only some functions. Also not every virtual method directly maps to public functions on the type so there are quite a few that couldn't be directly called like this (or would misbehave at least).

The difference between the two is that the private struct is the private state of your type's implementation, while the public one (the one generated by glib_wrapper!) is just a Rust wrapper object around the C GObject instance (which is a pointer to the InstanceStruct btw). The public one does not even exist if your type is instantiated from other languages, it's something that only exists in Rust (see also the C/JS/Python bindings to some Rust objects in my repository above). It is something that exists only in Rust to have safe public API available to other parts of the code.

Why can't I just make the "private" struct a glib object and have private fields and methods by simply not making them public, without needing a separate public struct?

I think that's answered above already. But conceptually the main reason would be encapsulation and protection of your state and its invariants.

* Why do I need to specify the `ParentType` on the private struct as well as `extends` in the `glib_wrapper!`?

Because glib_wrapper! also works for types that are defined in C libraries, etc. It is completely separate from the ObjectSubclass trait and does not know anything about it.

Also from macros you can't look into trait implementations (macros are expanded before anything related to types is happening in the compiler), so even then the macro couldn't get the ParentType.

Why is ParentType only the direct parent, while extends needs to list multiple types of the hierarchy?

For the same reason as above, but especially because of the type limitations of macros. The macro has to generate trait impls for each of the types in the class hierarchy but there's no way for a macro to know them as the compiler would only do anything related to types after the macros are expanded. Macros work on the syntax level, not the semantic level.

Why do I need to specify Instance and Class both on the private type and in glib_wrapper!?

Same as above really. But for types declared e.g. by C libraries you would list in glib_wrapper! the actual instance/class structs declared there.

Most of this could be hidden away by having a complex procedural macro that would allow you to define new GObjects in some kind of DSL, but that requires someone to do that work first :)

You could imagine some kind of gobject_define! {... } macro where you describe all these parts and it then expands into what you currently have to write manually.

@YaLTeR
Copy link

YaLTeR commented Apr 24, 2020

Thanks, the explanations are very helpful for understanding how all of this works!

I have an example here https://github.com/sdroege/gobject-example-rs .

Something like this would be very helpful as a tutorial, especially with some kind of index (what example demonstrates what) and more thorough comments.

It's mapping (in this case) directly to the get_type function of your new type. The function returns the glib::Type, the type identifier, for your new type.

Is this some kind of override of a virtual get_type function or not? If it is, then why is it done like this and not via an impl SomethingImpl like other overrides? If not, perhaps it could be wrapped to be done via an impl anyway to make it less weird?

How would you create it directly? The only way to create a new GObject instance is via glib::Object::new() by passing the type id.

Oh, so it's a particularity of how GObject object creation works.

Initialization order: ObjectSubclass::class_init (the very first time only), ObjectSubclass::new (for every instance), ObjectImpl::set_property (for each property set via the glib::Object::new call), ObjectImpl::constructed and then glib::Object::new() returns. There are some other virtual methods that would be called on the way (specifically ObjectImpl::constructor) but we don't wrap those yet.

So in theory you could implement a more-Rusty "new with arguments" construction by passing the arguments via CONSTRUCT_ONLY properties and then calling the user-provided new(arguments) with them from constructed?

The difference between the two is that the private struct is the private state of your type's implementation, while the public one (the one generated by glib_wrapper!) is just a Rust wrapper object around the C GObject instance (which is a pointer to the InstanceStruct btw). The public one does not even exist if your type is instantiated from other languages, it's something that only exists in Rust (see also the C/JS/Python bindings to some Rust objects in my repository above). It is something that exists only in Rust to have safe public API available to other parts of the code.

I still don't quite understand. Couldn't I have just the private struct, with the same safe public methods as generated by glib_wrapper! on the public struct? If all fields on the private struct are not marked as pub and there are no public functions in an impl, wouldn't that be the same behavior but with 1 struct instead of two?

Most of this could be hidden away by having a complex procedural macro that would allow you to define new GObjects in some kind of DSL, but that requires someone to do that work first :)

You could imagine some kind of gobject_define! {... } macro where you describe all these parts and it then expands into what you currently have to write manually.

Perhaps this is close to what I'm imagining about how this should work. 😀

@sdroege
Copy link
Member

sdroege commented Apr 24, 2020

Is this some kind of override of a virtual get_type function or not? If it is, then why is it done like this and not via an impl SomethingImpl like other overrides? If not, perhaps it could be wrapped to be done via an impl anyway to make it less weird?

It's done by the glib_object_subclass! macro and the reason for doing it like this is because it's nothing you would want to implement yourself. It's always the same and involves unsafe code. See https://github.com/gtk-rs/glib/blob/b5b4df5dc7ca52af170aafd18203bfdd42ca5608/src/subclass/types.rs#L203-L217

Or do you mean something else?

So in theory you could implement a more-Rusty "new with arguments" construction by passing the arguments via CONSTRUCT_ONLY properties and then calling the user-provided new(arguments) with them from constructed?

No, because in constructed your struct already has to exist. We have a little chicken/egg problem here: we need an instance of the private struct before anything else (constructed, set_property) but the values we want to put in there might only be known in those virtual methods.

If you initialize your struct fields with None or similar, you can already have something like the above by passing the properties to glib::Object::new(). You will then get set_property() called for each of them. I think the listbox_model.rs in the gtk-rs examples does that.

I still don't quite understand. Couldn't I have just the private struct, with the same safe public methods as generated by glib_wrapper! on the public struct? If all fields on the private struct are not marked as pub and there are no public functions in an impl, wouldn't that be the same behavior but with 1 struct instead of two?

The struct defined by glib_wrapper! is just a struct Foo(*mut InstanceStruct). If you wanted a single struct you could use the InstanceStruct for that, but it's discouraged from the GObject point of view to put anything in there unless you can't avoid it.

Also note that the instance struct is always public, even if you mark your fields not pub in Rust. It's corresponding to a C struct.

Another reason for having two structs is to force a clean separation between implementation and public interface and not accidentally leak implementation details.

@YaLTeR
Copy link

YaLTeR commented Apr 24, 2020

It's done by the glib_object_subclass! macro and the reason for doing it like this is because it's nothing you would want to implement yourself. It's always the same and involves unsafe code. See https://github.com/gtk-rs/glib/blob/b5b4df5dc7ca52af170aafd18203bfdd42ca5608/src/subclass/types.rs#L203-L217

Or do you mean something else?

I mean instead of writing

match fn {
    get_type => || SimpleWindowPrivate::get_type().to_glib(),
}

you would write some kind of

impl RequiredGLibWrapperFunctions for SimpleWindow {
    fn get_type() -> glib::types::Type {
        SimpleWindowPrivate::get_type()
    }
}

No, because in constructed your struct already has to exist. We have a little chicken/egg problem here: we need an instance of the private struct before anything else (constructed, set_property) but the values we want to put in there might only be known in those virtual methods.

If you initialize your struct fields with None or similar, you can already have something like the above by passing the properties to glib::Object::new(). You will then get set_property() called for each of them. I think the listbox_model.rs in the gtk-rs examples does that.

My idea there was that this OnceCell/None step would be done by glib macros over the whole user-provided private struct so the user doesn't have to do any of it manually.

@sdroege
Copy link
Member

sdroege commented Apr 24, 2020

I mean instead of writing [...]

That doesn't work because the glib_wrapper! macro is generic and not just used for Rust subclasses, and also not just for GObjects.

My idea there was that this OnceCell/None step would be done by glib macros over the whole user-provided private struct so the user doesn't have to do any of it manually.

That's certainly possible, yes. It would need some more higher-level API on top of what we have right now.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants