gtk-rs-core icon indicating copy to clipboard operation
gtk-rs-core copied to clipboard

[PROPOSAL] class! macro

Open ranfdev opened this issue 3 years ago • 31 comments

Motivation

#494 adds a way to declare properties on the private, implementation struct. In the future, we also want to add signals declaration using macros. It would be nice being able to generate the corresponding connect_{signal} and set_{property} methods on the external, wrapper struct.

It's not possible to share data between macros. Right now, (on #494) object properties and signals (still missing) are declared on a derive macro. It's not possible to list the properties and signals obtained by that macro and then reuse that list to implement the corresponding connect_{signal} and set_{property} methods.

Proposal

I propose adding a class! macro which should:

  • accept a declaration of properties and signals
  • generate the required ObjectSubclass implementation
  • generate the required ObjectImpl on the private struct
  • generate the external wrapper struct
  • generate the corresponding connect_ and set_property_ methods on the wrapper struct.

I propose the following syntax

        class! {
            const GTYPE_NAME: &'static str = "MyButton";
            class Button extends [gtk::Button], implements [gtk::ActionMap] {
                #[prop(get, set)]
                label: RefCell<gtk::Label>,
            }
            impl Button {
                #[signal]
                pub fn clicked(&self) {};
            }
        }

ranfdev avatar Feb 09 '22 16:02 ranfdev

Sounds like a good idea generally :)

Are you aware of @federicomenaquintero's previous attempt at something like this from many years ago? You can find it here: https://gitlab.gnome.org/federico/gnome-class

            const GTYPE_NAME: &'static str = "MyButton";

Why not as an attribute on the class?

                #[prop(get, set)]
                label: gtk::Label,

How would that handle interior mutability? There are many options and that probably should be configurable somehow.

            class Button extends [gtk::Button], implements [gtk::ActionMap] {

How would the macro figure out all the parent classes of gtk::Button?

sdroege avatar Feb 09 '22 16:02 sdroege

How would that handle interior mutability? There are many options and that probably should be configurable somehow.

To add to this, how would the implementation access the field values?

sdroege avatar Feb 09 '22 16:02 sdroege

How would that handle interior mutability?

Whoops, that line should have said

label: RefCell<gtk::Label>,

And that works as it does on the current #494 PR

ranfdev avatar Feb 09 '22 16:02 ranfdev

how would the implementation access the field values?

Using the classic self.imp(), as it is now.

ranfdev avatar Feb 09 '22 16:02 ranfdev

How would the macro figure out all the parent classes of gtk::Button?

It's manual, the user has to specify every parent, as it is now when implementing ObjectSubclass

ranfdev avatar Feb 09 '22 16:02 ranfdev

And yes, I've looked at the work done in https://gitlab.gnome.org/federico/gnome-class. It's a nice work and a great resource.

ranfdev avatar Feb 09 '22 16:02 ranfdev

Where would the impl blocks for the private and public struct be put here, and the impl blocks for the parent class' Impl traits?

Ideally you would have as little code as possible inside the macro to make things easier for tooling.

sdroege avatar Feb 09 '22 16:02 sdroege

This sounds like a good idea, but overall I believe before trying to build something that would be super complex, having the building blocks handled separately would simplify writing/reviewing/testing such macro. Currently, the most verbose part of creating gobjects is

  • the object properties
  • the object signals
  • virtual functions. this will probably need separate handling between the two uses cases: applications and libraries.

The class! macro could then be built on top of all of those. There will also be a need to figure out how we could use a different class_init as it is quiet useful, at least for widgets to call all the various gtk_widget_class functions. Maybe the class! macro could be inspired a little bit with vala and have either a constructor block or a construct method that gives you the glib::Class in parameter.

bilelmoussaoui avatar Feb 09 '22 17:02 bilelmoussaoui

It's not possible to share data between macros. Right now, (on https://github.com/gtk-rs/gtk-rs-core/pull/494) object properties and signals (still missing) are declared on a derive macro. It's not possible to list the properties and signals obtained by that macro and then reuse that list to implement the corresponding connect_{signal} and set_{property} methods.

Can't you generate those methods as part of a Ext trait and auto implement it for Self::Type ?

bilelmoussaoui avatar Feb 09 '22 17:02 bilelmoussaoui

Can't you generate those methods as part of a Ext trait and auto implement it for Self::Type ?

Yes, we talked about this in the Props PR. I just thought it was nicer to have the methods defined directly on the wrapper, without having to introduce another trait (which must be imported every time the wrapper is used).

The class! macro could then be built on top of all of those

It makes sense. I'll leave this here and focus on the single macros.

ranfdev avatar Feb 09 '22 22:02 ranfdev

It's not possible to share data between macros.

This is one of the key problems with implementing an ergonomic API for properties and signals. In https://github.com/gtk-rs/gtk-rs-core/issues/214, I suggested adding syntax for signals to glib::wrapper! to define signals, which would generate the necessary wrapper functions, though the user would have to write a ObjectImpl::signals() implementation calling into a function generated by this.

I generally like how that would work for signals, but properties are harder that way. They could be defined similarly, but generally more complicated (may have custom getter/setter, etc.), and in particular if they are specified in glib::wrapper! it's not clear how it could add data members to store their state.

I'd really like to see progress on this, but I don't know if it's good to merge something like https://github.com/gtk-rs/gtk-rs-core/pull/494 without having a plan to handle both signals and properties, including generation of connect_* methods and such. And it needs to avoid too much redundancy.

ids1024 avatar Feb 10 '22 16:02 ids1024

I have a very WIP macro that is getting pretty close to this: https://github.com/jf2048/gobject-impl Currently it looks like this:

glib::wrapper! {
    pub struct MyObject(ObjectSubclass<imp::MyObject>);
}
mod imp {
    use glib::subclass::prelude::*;
    #[glib::object_subclass]
    impl ObjectSubclass for MyObject {
        const NAME: &'static str = "MyObject";
        type Type = super::MyObject;
    }
    #[gobject_impl::object_impl(final, type = super::MyObject)]
    impl ObjectImpl for MyObject {
        properties! {
            #[derive(Default)]
            pub struct MyObject {
                #[property(get, set)]
                my_prop: std::cell::Cell<u64>,
            }
        }
        #[signal]
        fn abc(&self) {}
    }
}

See the tests for more examples. With not too much effort it could have the ObjectSubclass impl combined with everything else.

Another area that I think could use some support from this type of macro is virtual methods, currently there is quite a lot of boilerplate to make those.

jf2048 avatar Feb 18 '22 16:02 jf2048

I just noticed the way this is done by cxx-qt, by creating a new module and putting an attribute macro on it: https://github.com/KDAB/cxx-qt/blob/6ca215311584189e451633e755a0dd7d061b6b6e/examples/qml_features/src/lib.rs

#[make_qobject]
mod my_object {
    #[derive(Default)]
    pub struct Data {
        // data members...
    }

    #[derive(Default)]
    struct RustObj;

    impl RustObj {
        #[invokable]
        fn myfunc(&self) {
            // ...
        }
    }
}

IMO this should seriously be considered, it's much cleaner than using a function-like macro and doesn't break rustfmt.

jf2048 avatar Mar 02 '22 15:03 jf2048

Ok, here is another draft of a class macro and interface macro: https://github.com/jf2048/gobject

There is more examples ported into the tests, a lot of the boilerplate is heavily reduced. This is all that is needed to define a simple object with a property and a signal:

#[gobject::class(final)]
mod obj {
    #[properties]
    #[derive(Default)]
    pub struct MyObj {
        #[property(get, set)]
        my_prop: std::cell::Cell<u64>,
    }
    #[methods]
    impl MyObj {
        #[signal]
        fn abc(&self) {}
    }
}

jf2048 avatar Mar 31 '22 05:03 jf2048

That looks like an interesting idea.

#[properties] #[methods]

Are these needed? Maybe we could just say "don't put any extra stuff into mod obj"?

YaLTeR avatar Mar 31 '22 06:03 YaLTeR

Are these needed?

I would say yes. It is quite typical to have member variables in GObjects that are not exposed as properties.

Hofer-Julian avatar Mar 31 '22 07:03 Hofer-Julian

It is quite typical to have member variables in GObjects that are not exposed as properties.

The annotations I'm referring to are on the whole struct at once, not on the individual members.

YaLTeR avatar Mar 31 '22 07:03 YaLTeR

It is quite typical to have member variables in GObjects that are not exposed as properties.

The annotations I'm referring to are on the whole struct at once, not on the individual members.

Ah yes, fair enough.

Hofer-Julian avatar Mar 31 '22 07:03 Hofer-Julian

I was thinking of making them optional if there is only one struct and impl

jf2048 avatar Mar 31 '22 13:03 jf2048

The package I've been working on is pretty stable now with built-in code generation for:

  • Properties
  • Signals
  • Virtual methods
  • Constructors
  • Final/abstract/derived classes
  • Interfaces
  • GIO actions
  • Initable/AsyncInitable
  • GTK4 templates
  • GTK4 widget actions
  • ToVariant/FromVariant traits on objects/interfaces
  • Serde Serialize/Deserialize traits on objects/interfaces
  • Everything under the macro automatically supports the clone_block style I described here

The only part I consider missing here is a scanner that can parse the whole package and generate C API and gobject-introspection data.

jf2048 avatar May 02 '22 17:05 jf2048

That sounds nice. Where is that code and does it come with examples? :)

sdroege avatar May 02 '22 17:05 sdroege

It all ended up in this repository: https://github.com/jf2048/gobject

All the examples are in the tests folder now. It could use some documentation, but I probably need to come up with something outside of rustdoc since that's not great for documenting complex macros.

jf2048 avatar May 02 '22 17:05 jf2048

If you have some time, can you try porting something like https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/tree/main/tutorial/src/sinesrc ? :) In my experience GStreamer is usually a good stress-test for any such things.

sdroege avatar May 02 '22 17:05 sdroege

I didn't get around to it yet but I wanted to add a few more things for gstreamer before that:

  • A gst_element macro that defaults to enforcing Sync on signal handlers, and lets you specify the metadata on an attribute
  • A better macro for pad templates that's closer to GST_STATIC_PAD_TEMPLATE
  • Variant support for GstCaps and GstStructure

Is there anything else that would be good to have?

jf2048 avatar May 02 '22 22:05 jf2048

My main concern at this point is the properties: usually there's a single struct with all property values, which during processing is cloned to get a snapshot of all properties at that time.

Another thing that might be tricky is the handling of the associated constants in various FooImpl traits.

sdroege avatar May 03 '22 07:05 sdroege

Since this is a big API change, what do you think about merging the changes (when ready) behind a cargo feature flag? We won't get everything right the first time... Using a cargo flag will enable people to experiment earlier and send some feedback

ranfdev avatar May 11 '22 18:05 ranfdev

Using a cargo flag will enable people to experiment earlier and send some feedback

What would be the advantage over mentioning that this is experimental API in the docs?

Hofer-Julian avatar May 11 '22 18:05 Hofer-Julian

There are many advantages:

  • if the flag is called "unstable_class_macro", you can't use it accidentally.
  • each function is tracked consistently and a global search for "unstable_class_macro" will bring up everything related.
  • there may be multiple unstable features, letting the user free to choose (and track) how much risk he wants to take.

This practice is used in every big project I know: rust itself (even on nightly, most features are hidden behind a feature flag), Haskell, react...

ranfdev avatar May 11 '22 19:05 ranfdev

I think that makes sense. We did the same for subclassing and futures support in the beginning.

sdroege avatar May 12 '22 06:05 sdroege

If you have some time, can you try porting something like https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/tree/main/tutorial/src/sinesrc ? :) In my experience GStreamer is usually a good stress-test for any such things.

@sdroege I implemented a webrtcsrc (in the webrtcsink repo for now but it will eventually all be moved to -plugins-rs) where I implemented a GInterface for the Signaller, this way I can expose a clean interface and with the new gobject crate it is all very simple. The element itself is obviously implemented using that same crate and the result is pretty good. It still misses documentation but there are plenty of examples about how to do things in the tests. I found a few little bugs on the way and opened MRs for them, but all in all it is all very usable to me.

As it was suggested we could have dedicated helpers to implement GstElements, add support for GStreamer fundamental types (and Pspecs as props) etc... but it is not blocking.

@jf2048 What is you plan with that work? I have to say I like the approach you took there and the result is really enjoyable to use!

thiblahute avatar Jul 17 '22 02:07 thiblahute