gtk-rs-core
gtk-rs-core copied to clipboard
[PROPOSAL] class! macro
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_
andset_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) {};
}
}
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
?
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?
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
how would the implementation access the field values?
Using the classic self.imp()
, as it is now.
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
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.
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.
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.
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
?
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.
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.
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.
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.
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) {}
}
}
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
"?
Are these needed?
I would say yes. It is quite typical to have member variables in GObjects that are not exposed as properties.
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.
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.
I was thinking of making them optional if there is only one struct and impl
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.
That sounds nice. Where is that code and does it come with examples? :)
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.
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.
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 enforcingSync
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
andGstStructure
Is there anything else that would be good to have?
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.
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
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?
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...
I think that makes sense. We did the same for subclassing and futures support in the beginning.
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!