Wrapping GObjects in C++
GObject is the foundational dynamic type system implemented on top of the C language that is used by many other libraries like GLib, GTK and many other components, most of them part of the GNOME desktop environment stack.
I’ve been lately wrapping a C library that uses GObject for C++ and I learned about some of the challenges.
GObject
Any general programming language can be used under the Object Oriented Programming (OOP) paradigm, and the difference between them is whether the language offers built-in support for that or not. So, when we say that Java is OOP we basically mean that the language has concepts which are meant to support this paradigm out of the box.
C is not one of those languages.
For reasons lost in the mist of time, related to the origins of the GNU Image Manipulation Program, the GTK toolkit, a GUI toolkit, was written in C. And its foundations are built on top of a library called GLib. GLib provides GObject: a library based OOP type system built on top of C. GTK and other libraries, part of the GNOME Desktop software stack, are built on top of GObject.
Now, GObject is powerful (just read about it but it also acknowledges the fact that there are more programming languages than just C, even if C serves as the common denominator here.
This is also the current reality: C these days can be seen as an interoperable layer between programming languages. Most foreign-function interfaces (foreign as in “written in another programming language”) target C as the interoperable layer. There are technical reasons for that fact, which are out of scope of this blog.
C++ is not, strictly, a superset of C but it can interoperate with C very, very easily (the C heritage in C++ enables this and also fuels many pain points of C++ itself). And C++, even if it has been dubbed as “multi paradigm”, has reasonable support for OOP.
So it makes sense to provide a C++ interface to GObject.
Wrapping on top of glibmm
GLib is the library that contains GObject and there already exists a C++ version of it called glibmm.
glibmm, along with another component called mm-common, allows systematically
wrapping GObject-based C libraries in a consistent and coherent way. This is
achieved using a tool called gmmproc
. I used this approach for my wrap of
libadwaitamm.
There are some design decisions made by glibmm that permeate and impact the wrappers.
Classes and objects
Because GObject is actually a library and implements an OOP type system, all the concepts of such system must exist as entities of the program. When working on a typical OOP language like C++ or Java, the concept of “class” is a concept provided and supported by the language itself.
This is not the case in GObject. Classes are entities represented in the memory of the program like regular data.
In fact when reading the GObject tutorial you will identify lots of steps required to register (or bring up) a class in GObject. GObject programmers identify that some of those steps are annoying and feel like boilerplate. To ease the pain they use C macros so the GObject classes can be declared and defined in a more convenient way.
Toshio Sekiya made this excellent GObject tutorial in C that is worth checking.
Once a class has been registered in GObject, we can instantiate it.
glibmm tries to make the use of GObject instances as convenient as regular C++ objects so it combines the class registration in GObject with the instantiation of a GObject class.
This works most of the time but complicates the process because classes themselves do not have a “constructor” method in C++ (only instances do). These “class constructors” are used to register class-level attributes like signals and properties.
glibmm solves this problem by using a secondary class, which is automatically generated by the wrapping machinery, that represents the class itself. This class object is used as a singleton of the application and it is initialised upon the creation of the first instance of a GObject class. This initialisation can then invoke a function that can register properties, signals and interfaces implementations.
Signals
Signals in GObject are close to what in other programming languages (like C# or Java) are called delegates or listeners. It is possible to connect to a signal so a piece of code, as a callback, is executed when something happens. Signals can be arbitrarily defined by a GObject class so the GObject instance can emit those signals as needed.
glibmm was written in a pre-C++11 world and back then it used the libsigc++ library to ensure type-safety in the callbacks (something that C can’t do and it is sometimes [ab]used by the C libraries). This library is still very useful these days, but in a post-C++11 world some of the heavy lifting can be delegated to the C++ standard library itself.
libsigc++ provides two concepts: signals (something that can be emitted) and
slots (something that can be connected to a signal and will be invoked when the
signal is emitted). Because libsigc++ is generic and not tied to glibmm (even
if it is, maybe, one of its biggest users), the glibmm wrapping machinery has
to translate a signal callback (a C callback) into a proper libsigc++ slot.
Luckily, almost all callbacks in GObject are closures that receive a void*
argument where anything related to the context can be passed to the callback.
This way, when wrapping a GObject implemented in C, the wrapping machinery
connects the existing (GObject’s) signals to a callback (a free function,
typically generated) that unwraps the context pointer into libsigc++’s slots
for that libsigc++ signal.
Properties
Many OOP programming languages (like C# or Object Pascal) have the concept of “properties”. They look like object attributes (fields) but can invoke a function when reading or writing the attribute.
GObject properties follow this philosophy and introduce a couple of extra features: properties have a (GObject) signal associated to them that can be used to signal updates to the property and can be generically read and written using GObject generic mechanisms. These two features allow properties to be bound to other properties and build expressive GUIs with reasonable effort.
For instance, if we have a hypothetical list widget with a property
number-of-elements
, we can bind this property to the sensitive
property of
a Gtk.Button intended to clear
that list widget. This way, we can enable or disable the button based on
whether the list widget contains items. More complex scenarios are possible
using Gtk.Expression.
Properties are implemented in GObject with two callbacks that are invoked when a property is read or written, respectively.
The challenge of subclassing
Now, if our goal was to only wrap existing GObjects, a scenario that all the machinery of glibmm supports very well, we would be done.
Although the GObject type system allows to introduce new fundamental
types
(which are mostly meant to represent built-in language types such as int
or
double
), most of the new types defined by a library or application are
created by means of subclassing (if indirectly) the
GObject.Object class type
itself.
Now, subclassing a class in GObject means registering a class and letting the registration procedure know the parent class (GObject, like Java or C# but in contrast to C++, allows only single base class). This process would be burdensome given that the additional class that represents the class is a bit of a pain to write. The glibmm mechanism of a separate class that represents the class entity in GObject is not super convenient to write manually.
So in that line glibmm devised a convenient mechanism in which by using the regular C++ inheritance one could create a new class almost transparently.
Subclassing is magic
Consider that you want to subclass Gtk::Button
.
You can just do
class MyButton : public Gtk::Button {
public:
MyButton(const Glib::ustring &label) : Gtk::Button(label) {}
// ...
};
And that’s it. No need for a separate MyButton_Class
or the likes that
represents the GObject class itself. Cool, but how does this work?
gmmproc
-wrapped classes always register a derived class that just clones the
original wrapped class. In the case of Gtk::Button, the original C class is
GtkButton
. The wrapped code registers (just once) a gtkmm__GtkButton
class
in the GObject typesystem and makes it a subclass of GtkButton
. The reason
why this is done is in order to allow implementing a virtual method mechanism,
explained below.
Note, however, that no class is registered in GObject for MyButton
. At
the eyes of GObject any instance of MyButton
is just a
gtkmm__GtkButton
.
Virtual methods
GObject would not be a complete OOP mechanism if it did not support polymorphism via virtual table classes. In the C implementation, virtual methods are implemented as pointers to functions and those are overriden explicitly by subclasses in the “class constructor” by setting them to point to specific functions.
Virtual methods are exposed as a convenience in gmmproc
-wrapped classes as
regular C++ virtual methods. To make this work, however, the class must have
had to overriden the GObject virtual method so it ultimately calls the C++
virtual method. This can only happen in the “class constructor”. By subclassing
with a wrapper that introduces no extra data, gmmproc-wrapped classes can
override GObject virtual methods at will.
This is exactly what happens with Gtk.Button.clicked
virtual method. When
initialising the class gtkmm__GtkButton
this virtual method is made to invoke
a C++ virtual method (generated by gmmproc
) called on_clicked
. If the method
is not actually overridden in the subclass, gmmproc calls the current virtual
method implementation (if any).
class MyButton : public Gtk::Button {
public:
MyButton(const Glib::ustring &label) : Gtk::Button(label) {}
virtual on_clicked() override {
// ...
}
};
Properties
But if we did not create a new GObject class to represent MyButton
and
we’re just using C++ owns mechanism for virtual methods, what about new signals
or properties we might want to add?
This is where this convenient scheme of inheriting, one that does not require a description of the class, starts showing its limits.
First we need to make sure the new class is actually a new one. This can be
achieved using a different constructor of Glib::ObjectBase
. While the root
of the hierarchy is Glib::Object
(it wraps GObject.Object
), Glib::ObjectBase
is a virtual base of Glib::Object
that is used to change some of the behaviour
when creating Glib::Object
. Glib::ObjectBase
has a constructor where you can
specify a class name.
class MyButton : public Gtk::Button
{
public:
MyButton(const Glib::ustring &label) :
Glib::ObjectBase("MyButton"),
Gtk::Button(label) {}
// ...
};
When using this constructor, glibmm will register a new class
gtkmm__CustomObject_MyButton
. And this allow us to define properties.
class MyButton : public Gtk::Button {
public:
MyButton(const Glib::ustring &label)
: Glib::ObjectBase("MyButton"), Gtk::Button(str) {}
Glib::Property<int> my_value{*this, "my-value", 0};
};
Now, properties are class-level attributes so ideally those should be
registered (installed) in the class constructor, which we cannot access.
However, GObject allows installing properties later and this is what happens
when executing the constructor of the property my_value
that is run as part
of the constructor of MyButton
.
Signals
What about signals? Unfortunately, as far as I can tell, there is no straightforward way to install new custom GObject signals.
Note that libsigc++ can be used in some signalling scenarios as an alternative to GObject signals. This is because, in contrast to properties, GObject signals do not seem to be composable between them. So we may only need a thing that acts like a wrapped signal even if it is not a proper GObject signal.
If we do want a GObject signal, one thing we can do is using
Glib::ExtraClassInit
which allows us to define our own class initialisation function. But note that
this will be executed the first time we instantiate our class. This fragile (at
least to me) behaviour is again part the price we pay for not decoupling the
C++ class that represents instances from the C++ class that represents the
GObject class itself.
Why would we want to use C++ to write a GObject?
If we look at the wrapper libraries as a mean to write C++, one might think that we only need the minimal wrapping surface and then be able to use C++, outside of GObject, to develop the rest of the functionality.
While I do not think is super essential to be able to write a GObject in C++ so
it can be called from outside C++ (this would force us to provide a C interface
anyways), I think it is useful to be able to bring up a GObject in C++ so it
can be used in some of the convenient machinery that GTK provides: mainly
.ui
files and Gtk.Builder.
Now, .ui
files are very powerful and can do lots of things for us in a
convenient way. But this can only happen if the GTK library sees a full-fledged
GObject. The class type must have been registered in GObject and its
properties, signals and interfaces must have been registered during class
initialisationn (not later, like glibmm allows us to do).
And I would like to use C++ to do that, as much as possible. So in a next post I will explore some approaches I have been using in my projects.