
GObject: inheritance and interfaces
In the comments to the last article, it was often suggested that the GObject system is not needed due to the presence of C ++ and other high-level languages. In addition to the purely technical points that were already discussed in the comments, I would like to touch on another aspect. Probably, most commentators see the meaning of the GLib object system in the stubborn reluctance of retrograde applicants to take advantage of civilization and come to terms with the inexorable tread of progress. This was probably the case at the dawn of the development of Glib / GTK, which originated in the world of UNIX systems, GNU, open-source, Stallman's ideas, etc. Most of that generation of hackers really preferred C, while C ++ was relatively young and underdeveloped and the benefits of its use did not seem so obvious.
Today, of course, for new projects, most of us will prefer to use more convenient, concise and safe languages, even if we are familiar with all the nuances of using GObject. However, do not lose sight of the fact that over the 20-plus years of the existence of GLib / GTK, thousands of applications and libraries have been created using them, many of which are actively developed to this day by thousands of programmers from around the world. They add new functionality, catch bugs, adapt them to modern technologies like HiDPI screens, Wayland, Vulkan, etc. In order to read (supplement, fix) the code of such projects, you need to have basic knowledge of object-oriented extensions for C, which we are talking about.
Please welcome under cat. We train, as usual, on cats :)

GObject: the basics of
GObject: inheritance and interfaces
GObject: encapsulation, instantiation, introspection
In general, GObject implies only single inheritance. For multiple inheritance, it is proposed to use Java-like interfaces, which we will discuss in the second part of the article.
Create a Tiger type that will inherit from Cat, which we described in a previous article. This time we will make it a final object and, thus, we will assume that in the future we will not inherit from it.
Create animaltiger.h:
Note that we used the macro G_DECLARE_FINAL_TYPE instead of G_DECLARE_DERIVABLE_TYPE to describe an object of type final, and also that we indicated the “parent” as the last argument to the macro - AnimalCat.
If we envisioned a further chain of inheritance with a derivable object, we would need to describe the class and create an instance of the parent class as the first field in it; in this case, we do not need this.
We declare a function that returns a new instance of our tiger:
Here it would be possible to declare other methods specific to the tiger object, but for simplicity we restrict ourselves to the one inherited from the parent - say_meow. This is a virtual function to which we will give an implementation specific to our type. After that, close the macro G_END_DECLS. In principle, there is no need to frame the header file with G_BEGIN_DECLS / G_END_DECLS macros, they are decomposed into a banal extern “C” {} and are needed for compatibility with C ++ compilers. These are just good-sounding rules adopted by the GLib / GTK + developers.
Let's get down to the file with the source code animaltiger.c:
We connect the header and standard input-output and describe the structure-basis of our object. In this case, since we are dealing with a final object, we did not create a class structure, but the main structure, the instance structure, must be described explicitly.
In the initial macro, the last parameter is a macro that returns information about the type of the parent. If you remember, we defined this macro in the header file animalcat.h.
Let's create the animal_cat_real_say_meow () function:
And two main functions:
In the animal_tiger_class_init function, which will be called when the first instance of our object is created, we get a pointer to the parent class using the ANIMAL_CAT_CLASS macro. This macro, like a number of others, is created by expanding macros in header files, such as G_DECLARE_FINAL_TYPE. Next, we simply redefine the function to the one we created a few lines above. There is nothing special in our "constructor" animal_tiger_init (), just a signal about creating our object.
In the animal_tiger_new () function, everything is arranged similarly to the parent class:
Let's check how our new type works:
We will supplement our Makefile from the previous article with new files, build and run:
As we can see, functions of the form _class_init are first worked out, then the constructors of individual _init instances. When we call the method of the parent object and pass the pointer to the instance of the descendant as a parameter, the redefined function of the descendant is processed.
In addition to direct inheritance, GObject has the concept of interface inheritance. Interfaces are non-instantiable types, similar to purely abstract C ++ classes or Java interfaces.
Let our tiger acquire the properties of a predator - it will implement the AnimalPredator interface (of course, all cats are predators, but we will not go into zoological subtleties). Create the animalpredator.h file:
As you can see, here everything looks like the headline of a normal derivable-descendant GObject. Let's pay attention to several points:
Also in the structure, we create two pointers for virtual functions that will be implemented by specific instantiated types. Finally, we will declare two wrapper functions for these pointers - animal_predator_hunt and animal_predator_eat_meat.
Now we create an auxiliary file animalpredator.c, which should not be confused with the implementation of this interface - the resulting object module only launches specific implementations.
Everything is simple here: the initial macro G_DEFINE_INTERFACE, similar to G_DEFINE_TYPE, which we used at the beginning of animalcat.c and animaltiger.c, the constructor function animal_predator_default_init, into which you can put arbitrary code that executes when creating the first instance of an object that implements our interface, and functions wrappers that launch specific implementations of virtual interface functions. The ANIMAL_PREDATOR_GET_IFACE macro returns the class structure of the interface, just as the ANIMAL_CAT_CLASS macro returns the structure of the parent class.
Let's proceed with the implementation of our interface. Supplement our animaltiger.c:
The animal_tiger_predator_interface_init function must be declared at the beginning of the file, as it will be needed for the next macro.
Replace the macro G_DEFINE_TYPE with a similar one, but with more features. As you can see, a new “argument” has been added here, which may contain a number of macros that expand into arbitrary code. In this case, we put the G_IMPLEMENT_INTERFACE macro there.
Add two functions that implement "prototypes" from the class structure of the interface:
Finally, we create a constructor function for the implementation of our interface, where we assign to the pointers from the interface structure the specific values defined above:
Let's see how it works:
Build, run:
Is it possible to implement several interfaces at once? Yes, sure. Here is the real code from GIO, where the object implements three interfaces at once:
In this case, you will need three separate _interface_init functions. Please note that the last three macros are not separated by commas, this is one “argument”.
What about interface inheritance? Here the principle is also similar to that used in Java: the mechanism of prerequisites - dependencies of types and objects among themselves - solves this problem. For example, in order to implement the AnimalPredator interface, you will also need to implement the Eater interface or be inherited from an object, say, Mammal. However, consideration of these nuances threatens to dramatically increase the volume of the article, so we leave it outside the scope of this text.
Today, of course, for new projects, most of us will prefer to use more convenient, concise and safe languages, even if we are familiar with all the nuances of using GObject. However, do not lose sight of the fact that over the 20-plus years of the existence of GLib / GTK, thousands of applications and libraries have been created using them, many of which are actively developed to this day by thousands of programmers from around the world. They add new functionality, catch bugs, adapt them to modern technologies like HiDPI screens, Wayland, Vulkan, etc. In order to read (supplement, fix) the code of such projects, you need to have basic knowledge of object-oriented extensions for C, which we are talking about.
Please welcome under cat. We train, as usual, on cats :)

The whole cycle about GObject:
GObject: the basics of
GObject: inheritance and interfaces
GObject: encapsulation, instantiation, introspection
Inheritance from descendants of GObject
In general, GObject implies only single inheritance. For multiple inheritance, it is proposed to use Java-like interfaces, which we will discuss in the second part of the article.
Create a Tiger type that will inherit from Cat, which we described in a previous article. This time we will make it a final object and, thus, we will assume that in the future we will not inherit from it.
Create animaltiger.h:
#ifndef _ANIMAL_TIGER_H_
#define _ANIMAL_TIGER_H_
#include "animalcat.h"
G_BEGIN_DECLS
#define ANIMAL_TYPE_TIGER animal_tiger_get_type()
G_DECLARE_FINAL_TYPE (AnimalTiger, animal_tiger, ANIMAL, TIGER, AnimalCat)
Note that we used the macro G_DECLARE_FINAL_TYPE instead of G_DECLARE_DERIVABLE_TYPE to describe an object of type final, and also that we indicated the “parent” as the last argument to the macro - AnimalCat.
If we envisioned a further chain of inheritance with a derivable object, we would need to describe the class and create an instance of the parent class as the first field in it; in this case, we do not need this.
/*
struct _AnimalTigerClass
{
AnimalCatClass parent_class;
};
*/
We declare a function that returns a new instance of our tiger:
AnimalTiger*
animal_tiger_new();
G_END_DECLS
#endif /* _ANIMAL_TIGER_H_ */
Here it would be possible to declare other methods specific to the tiger object, but for simplicity we restrict ourselves to the one inherited from the parent - say_meow. This is a virtual function to which we will give an implementation specific to our type. After that, close the macro G_END_DECLS. In principle, there is no need to frame the header file with G_BEGIN_DECLS / G_END_DECLS macros, they are decomposed into a banal extern “C” {} and are needed for compatibility with C ++ compilers. These are just good-sounding rules adopted by the GLib / GTK + developers.
Let's get down to the file with the source code animaltiger.c:
#include
#include "animaltiger.h"
struct _AnimalTiger
{
AnimalCat parent;
};
We connect the header and standard input-output and describe the structure-basis of our object. In this case, since we are dealing with a final object, we did not create a class structure, but the main structure, the instance structure, must be described explicitly.
G_DEFINE_TYPE (AnimalTiger, animal_tiger, ANIMAL_TYPE_CAT)
In the initial macro, the last parameter is a macro that returns information about the type of the parent. If you remember, we defined this macro in the header file animalcat.h.
Let's create the animal_cat_real_say_meow () function:
static void
animal_tiger_real_say_meow(AnimalTiger* self)
{
printf("Tiger say: ARRRRGH!!!\n");
}
And two main functions:
static void
animal_tiger_class_init(AnimalTigerClass* self)
{
printf("First instance of AnimalTiger was created.\n");
AnimalCatClass* parent_class = ANIMAL_CAT_CLASS (self);
parent_class->say_meow = animal_tiger_real_say_meow;
}
static void
animal_tiger_init(AnimalTiger* self)
{
printf("Tiger cub was born.\n");
}
In the animal_tiger_class_init function, which will be called when the first instance of our object is created, we get a pointer to the parent class using the ANIMAL_CAT_CLASS macro. This macro, like a number of others, is created by expanding macros in header files, such as G_DECLARE_FINAL_TYPE. Next, we simply redefine the function to the one we created a few lines above. There is nothing special in our "constructor" animal_tiger_init (), just a signal about creating our object.
In the animal_tiger_new () function, everything is arranged similarly to the parent class:
AnimalTiger*
animal_tiger_new()
{
return g_object_new(ANIMAL_TYPE_TIGER, NULL);
}
Let's check how our new type works:
/* main.c */
#include "animaltiger.h"
int
main(int argc, char** argv)
{
AnimalTiger* tiger = animal_tiger_new();
animal_cat_say_meow(tiger);
return 0;
}
We will supplement our Makefile from the previous article with new files, build and run:
First instance of AnimalCat was created.
First instance of AnimalTiger was created.
Little cat was born.
Tiger cub was born.
Tiger say: ARRRRGH!!!
As we can see, functions of the form _class_init are first worked out, then the constructors of individual _init instances. When we call the method of the parent object and pass the pointer to the instance of the descendant as a parameter, the redefined function of the descendant is processed.
Interfaces
In addition to direct inheritance, GObject has the concept of interface inheritance. Interfaces are non-instantiable types, similar to purely abstract C ++ classes or Java interfaces.
Let our tiger acquire the properties of a predator - it will implement the AnimalPredator interface (of course, all cats are predators, but we will not go into zoological subtleties). Create the animalpredator.h file:
#ifndef _ANIMAL_PREDATOR_H_
#define _ANIMAL_PREDATOR_H_
#include
G_BEGIN_DECLS
#define ANIMAL_TYPE_PREDATOR animal_predator_get_type()
G_DECLARE_INTERFACE (AnimalPredator, animal_predator, ANIMAL, PREDATOR, GObject)
struct _AnimalPredatorInterface
{
GTypeInterface parent;
void (*hunt)(AnimalPredator*);
void (*eat_meat)(AnimalPredator*, int);
};
void
animal_predator_hunt(AnimalPredator* self);
void
animal_predator_eat_meat(AnimalPredator* self, int quantity);
G_END_DECLS
#endif /* _ANIMAL_PREDATOR_H_ */
As you can see, here everything looks like the headline of a normal derivable-descendant GObject. Let's pay attention to several points:
- the main macro here is G_DECLARE_INTERFACE instead of G_DECLARE_DERIVABLE_TYPE (and the last "argument" in this macro is still GObject);
- the class structure is called _AnimalPredatorInterface, not _AnimalPredatorClass;
- the first field in it is reserved for the type GTypeInterface, and not for GObjectClass.
Also in the structure, we create two pointers for virtual functions that will be implemented by specific instantiated types. Finally, we will declare two wrapper functions for these pointers - animal_predator_hunt and animal_predator_eat_meat.
Now we create an auxiliary file animalpredator.c, which should not be confused with the implementation of this interface - the resulting object module only launches specific implementations.
#include
#include "animalpredator.h"
G_DEFINE_INTERFACE (AnimalPredator, animal_predator, G_TYPE_OBJECT)
static void
animal_predator_default_init(AnimalPredatorInterface* iface)
{
printf("The first instance of the object that implements AnimalPredator interface was created\n");
}
void
animal_predator_hunt(AnimalPredator* self)
{
AnimalPredatorInterface* iface = ANIMAL_PREDATOR_GET_IFACE (self);
iface->hunt(self);
}
void
animal_predator_eat_meat(AnimalPredator* self, int quantity)
{
AnimalPredatorInterface* iface = ANIMAL_PREDATOR_GET_IFACE (self);
iface->eat_meat(self, quantity);
}
Everything is simple here: the initial macro G_DEFINE_INTERFACE, similar to G_DEFINE_TYPE, which we used at the beginning of animalcat.c and animaltiger.c, the constructor function animal_predator_default_init, into which you can put arbitrary code that executes when creating the first instance of an object that implements our interface, and functions wrappers that launch specific implementations of virtual interface functions. The ANIMAL_PREDATOR_GET_IFACE macro returns the class structure of the interface, just as the ANIMAL_CAT_CLASS macro returns the structure of the parent class.
Let's proceed with the implementation of our interface. Supplement our animaltiger.c:
#include "animalpredator.h"
static void
animal_tiger_predator_interface_init(AnimalPredatorInterface *iface);
The animal_tiger_predator_interface_init function must be declared at the beginning of the file, as it will be needed for the next macro.
Replace the macro G_DEFINE_TYPE with a similar one, but with more features. As you can see, a new “argument” has been added here, which may contain a number of macros that expand into arbitrary code. In this case, we put the G_IMPLEMENT_INTERFACE macro there.
G_DEFINE_TYPE_WITH_CODE (AnimalTiger, animal_tiger, ANIMAL_TYPE_CAT,
G_IMPLEMENT_INTERFACE (ANIMAL_TYPE_PREDATOR,
animal_tiger_predator_interface_init))
Add two functions that implement "prototypes" from the class structure of the interface:
static void
animal_tiger_predator_hunt(AnimalTiger* self)
{
printf("Tiger hunts. Beware!\n");
}
static void
animal_tiger_predator_eat_meat(AnimalTiger* self, int quantity)
{
printf("Tiger eats %d kg of meat.\n", quantity);
}
Finally, we create a constructor function for the implementation of our interface, where we assign to the pointers from the interface structure the specific values defined above:
static void
animal_tiger_predator_interface_init(AnimalPredatorInterface* iface)
{
iface->hunt = animal_tiger_predator_hunt;
iface->eat_meat = animal_tiger_predator_eat_meat;
}
Let's see how it works:
#include "animaltiger.h"
#include "animalpredator.h"
int
main(int argc, char** argv)
{
AnimalTiger* tiger = animal_tiger_new();
animal_predator_hunt(tiger);
animal_predator_eat_meat(tiger, 100500);
}
Build, run:
First instance of AnimalCat was created.
The first instance of the object that implements AnimalPredator interface was created
First instance of AnimalTiger was created.
Little cat was born.
Tiger cub was born.
Tiger hunts. Beware!
Tiger eats 100500 kg of meat.
Is it possible to implement several interfaces at once? Yes, sure. Here is the real code from GIO, where the object implements three interfaces at once:
static void initable_iface_init (GInitableIface *initable_iface);
static void async_initable_iface_init (GAsyncInitableIface *async_initable_iface);
static void dbus_object_manager_interface_init (GDBusObjectManagerIface *iface);
G_DEFINE_TYPE_WITH_CODE (GDBusObjectManagerClient, g_dbus_object_manager_client, G_TYPE_OBJECT,
G_ADD_PRIVATE (GDBusObjectManagerClient)
G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init)
G_IMPLEMENT_INTERFACE (G_TYPE_DBUS_OBJECT_MANAGER, dbus_object_manager_interface_init))
In this case, you will need three separate _interface_init functions. Please note that the last three macros are not separated by commas, this is one “argument”.
What about interface inheritance? Here the principle is also similar to that used in Java: the mechanism of prerequisites - dependencies of types and objects among themselves - solves this problem. For example, in order to implement the AnimalPredator interface, you will also need to implement the Eater interface or be inherited from an object, say, Mammal. However, consideration of these nuances threatens to dramatically increase the volume of the article, so we leave it outside the scope of this text.