Type system and registration process

GObject is a base object. We don’t usually use GObject itself. Because GObject is very simple and not enough to be used by itself in most situations. Instead, we use descendant objects of GObject such as many kinds of GtkWidget. We can rather say such derivability is the most important feature of GObject.

This section describes how to define a child object of GObject.

Name convention

An example of this section is an object represents a real number. It is not so useful because we have already had double type in C language to represent real numbers. However, I think this example is not so bad to know the technique how to define a child object.

First, you need to know the naming convention. An object name consists of name space and name. For example, “GObject” consists of a name space “G” and a name “Object”. “GtkWidget” consists of a name space “Gtk” and a name “Widget”. Let the name space be “T” and the name be “Double” of the new object. In this tutorial, we use “T” as a name space for all the objects we make.

TDouble is the object name. It is a child object of GObject. It represents a real number and the type of the number is double. It has some useful functions.

Define TDoubleClass and TDouble

When we say “type”, it can be the type in the type system or C language type. For example, GObject is a type name in the type system. And char, int or double is C language types. When the meaning of the word “type” is clear in the context, we just call it “type”. But if it’s ambiguous, we call it “C type” or “type in the type system”.

TDouble object has the class and instance. The C type of the class is TDoubleClass. Its structure is like this:

typedef struct _TDoubleClass TDoubleClass;
struct _TDoubleClass {
  GObjectClass parent_class;
};

_TDoubleClass is a C structure tag name and TDoubleClass is “struct _TDoubleClass”. TDoubleClass is a newly created C type.

TDoubleClass doesn’t need its own member.

The C type of the instance of TDouble is TDouble.

typedef struct _TDouble TDouble;
struct _TDouble {
  GObject parent;
  double value;
};

This is similar to the structure of the class.

TDouble has its own member, “value”. It is the value of TDouble instance.

The coding convention above needs to be kept all the time.

Creation process of a child of GObject

The creation process of TDouble type is similar to the one of GObject.

  1. Registers TDouble type to the type system.
  2. The type system allocates memory for TDoubleClass and TDouble.
  3. Initializes TDoubleClass.
  4. Initializes TDouble.

Registration

Usually registration is done by convenient macro such as G_DECLARE_FINAL_TYPE and G_DEFINE_TYPE. You can use G_DEFINE_FINAL_TYPE for a final type class instead of G_DEFINE_TYPE since GLib version 2.70. So you don’t need to care about registration details. But, in this tutorial, it is important to understand GObject type system, so I want to show you the registration without macro, first.

There are two kinds of types, static and dynamic. Static type doesn’t destroy its class even after all the instances have been destroyed. Dynamic type destroys its class when the last instance has been destroyed. The type of GObject is static and its descendant objects’ type is also static. The function g_type_register_static registers a type of a static object. The following code is extracted from gtype.h in the Glib source files.

GType
g_type_register_static (GType           parent_type,
                        const gchar     *type_name,
                        const GTypeInfo *info,
                        GTypeFlags      flags);

The parameters above are:

Because the type system maintains the parent-child relationship of the type, g_type_register_static has a parent type parameter. And the type system also keeps the information of the type. After the registration, g_type_register_static returns the type of the new object.

GTypeInfo structure is defined as follows.

typedef struct _GTypeInfo  GTypeInfo;

struct _GTypeInfo
{
  /* interface types, classed types, instantiated types */
  guint16                class_size;

  GBaseInitFunc          base_init;
  GBaseFinalizeFunc      base_finalize;

  /* interface types, classed types, instantiated types */
  GClassInitFunc         class_init;
  GClassFinalizeFunc     class_finalize;
  gconstpointer          class_data;

  /* instantiated types */
  guint16                instance_size;
  guint16                n_preallocs;
  GInstanceInitFunc      instance_init;

  /* value handling */
  const GTypeValueTable  *value_table;
};

This structure needs to be created before the registration.

These information is kept by the type system and used when the object is created or destroyed. Class_size and instance_size are used to allocate memory for the class and instance. Class_init and instance_init functions are called when class or instance is initialized.

The C program example3.c shows how to use g_type_register_static.

#include <glib-object.h>

#define T_TYPE_DOUBLE  (t_double_get_type ())

typedef struct _TDouble TDouble;
struct _TDouble {
  GObject parent;
  double value;
};

typedef struct _TDoubleClass TDoubleClass;
struct _TDoubleClass {
  GObjectClass parent_class;
};

static void
t_double_class_init (TDoubleClass *class) {
}

static void
t_double_init (TDouble *self) {
}

GType
t_double_get_type (void) {
  static GType type = 0;
  GTypeInfo info;

  if (type == 0) {
    info.class_size = sizeof (TDoubleClass);
    info.base_init = NULL;
    info.base_finalize = NULL;
    info.class_init = (GClassInitFunc)  t_double_class_init;
    info.class_finalize = NULL;
    info.class_data = NULL;
    info.instance_size = sizeof (TDouble);
    info.n_preallocs = 0;
    info.instance_init = (GInstanceInitFunc)  t_double_init;
    info.value_table = NULL;
    type = g_type_register_static (G_TYPE_OBJECT, "TDouble", &info, 0);
  }
  return type;
}

int
main (int argc, char **argv) {
  GType dtype;
  TDouble *d;

  dtype = t_double_get_type (); /* or dtype = T_TYPE_DOUBLE */
  if (dtype)
    g_print ("Registration was a success. The type is %lx.\n", dtype);
  else
    g_print ("Registration failed.\n");

  d = g_object_new (T_TYPE_DOUBLE, NULL);
  if (d)
    g_print ("Instantiation was a success. The instance address is %p.\n", d);
  else
    g_print ("Instantiation failed.\n");
  g_object_unref (d); /* Releases the object d. */

  return 0;
}

example3.c is in the src/misc directory.

Execute it.

$ cd src/misc; _build/example3
Registration was a success. The type is 56414f164880.
Instantiation was a success. The instance address is 0x56414f167010.

G_DEFINE_TYPE macro

The registration above is always done with the same algorithm. Therefore, it can be defined as a macro such as G_DEFINE_TYPE.

G_DEFINE_TYPE does the following:

Using this macro reduces lines of the program. See the following sample example4.c which works the same as example3.c.

#include <glib-object.h>

#define T_TYPE_DOUBLE  (t_double_get_type ())

typedef struct _TDouble TDouble;
struct _TDouble {
  GObject parent;
  double value;
};

typedef struct _TDoubleClass TDoubleClass;
struct _TDoubleClass {
  GObjectClass parent_class;
};

G_DEFINE_TYPE (TDouble, t_double, G_TYPE_OBJECT)

static void
t_double_class_init (TDoubleClass *class) {
}

static void
t_double_init (TDouble *self) {
}

int
main (int argc, char **argv) {
  GType dtype;
  TDouble *d;

  dtype = t_double_get_type (); /* or dtype = T_TYPE_DOUBLE */
  if (dtype)
    g_print ("Registration was a success. The type is %lx.\n", dtype);
  else
    g_print ("Registration failed.\n");

  d = g_object_new (T_TYPE_DOUBLE, NULL);
  if (d)
    g_print ("Instantiation was a success. The instance address is %p.\n", d);
  else
    g_print ("Instantiation failed.\n");
  g_object_unref (d);

  return 0;
}

Thanks to G_DEFINE_TYPE, we are freed from writing bothersome code like GTypeInfo and g_type_register_static. One important thing to be careful is to follow the convention of the naming of init functions.

Execute it.

$ cd src/misc; _build/example4
Registration was a success. The type is 564b4ff708a0.
Instantiation was a success. The instance address is 0x564b4ff71400.

You can use G_DEFINE_FINAL_TYPE instead of G_DEFINE_TYPE for final type classes since GLib version 2.70.

G_DECLARE_FINAL_TYPE macro

Another useful macro is G_DECLARE_FINAL_TYPE macro. This macro can be used for a final type. A final type doesn’t have any children. If a type has children, it is a derivable type. If you want to define a derivable type object, use G_DECLARE_DERIVABLE_TYPE instead. However, you probably want to write final type objects in most cases.

G_DECLARE_FINAL_TYPE does the following:

You need to write the macro definition of the type of the object before G_DECLARE_FINAL_TYPE. For example, if the object is TDouble, then

#define T_TYPE_DOUBLE  (t_double_get_type ())

needs to be defined before G_DECLARE_FINAL_TYPE.

The C file example5.c uses this macro. It works like example3.c or example4.c.

#include <glib-object.h>

#define T_TYPE_DOUBLE  (t_double_get_type ())
G_DECLARE_FINAL_TYPE (TDouble, t_double, T, DOUBLE, GObject)

struct _TDouble {
  GObject parent;
  double value;
};

G_DEFINE_TYPE (TDouble, t_double, G_TYPE_OBJECT)

static void
t_double_class_init (TDoubleClass *class) {
}

static void
t_double_init (TDouble *self) {
}

int
main (int argc, char **argv) {
  GType dtype;
  TDouble *d;

  dtype = t_double_get_type (); /* or dtype = T_TYPE_DOUBLE */
  if (dtype)
    g_print ("Registration was a success. The type is %lx.\n", dtype);
  else
    g_print ("Registration failed.\n");

  d = g_object_new (T_TYPE_DOUBLE, NULL);
  if (d)
    g_print ("Instantiation was a success. The instance address is %p.\n", d);
  else
    g_print ("Instantiation failed.\n");

  if (T_IS_DOUBLE (d))
    g_print ("d is TDouble instance.\n");
  else
    g_print ("d is not TDouble instance.\n");

  if (G_IS_OBJECT (d))
    g_print ("d is GObject instance.\n");
  else
    g_print ("d is not GObject instance.\n");
  g_object_unref (d);

  return 0;
}

Execute it.

$ cd src/misc; _build/example5
Registration was a success. The type is 5560b4cf58a0.
Instantiation was a success. The instance address is 0x5560b4cf6400.
d is TDouble instance.
d is GObject instance.

Separate the file into main.c, tdouble.h and tdouble.c

Now it’s time to separate the contents into three files, main.c, tdouble.h and tdouble.c. An object is defined by two files, a header file and C source file.

tdouble.h

#pragma once

#include <glib-object.h>

#define T_TYPE_DOUBLE  (t_double_get_type ())
G_DECLARE_FINAL_TYPE (TDouble, t_double, T, DOUBLE, GObject)

gboolean
t_double_get_value (TDouble *self, double *value);

void
t_double_set_value (TDouble *self, double value);

TDouble *
t_double_new (double value);

tdouble.c

#include "tdouble.h"

struct _TDouble {
  GObject parent;
  double value;
};

G_DEFINE_TYPE (TDouble, t_double, G_TYPE_OBJECT)

static void
t_double_class_init (TDoubleClass *class) {
}

static void
t_double_init (TDouble *self) {
}

gboolean
t_double_get_value (TDouble *self, double *value) {
  g_return_val_if_fail (T_IS_DOUBLE (self), FALSE);

  *value = self->value;
  return TRUE;
}

void
t_double_set_value (TDouble *self, double value) {
  g_return_if_fail (T_IS_DOUBLE (self));

  self->value = value;
}

TDouble *
t_double_new (double value) {
  TDouble *d;

  d = g_object_new (T_TYPE_DOUBLE, NULL);
  d->value = value;
  return d;
}

main.c

#include <glib-object.h>
#include "tdouble.h"

int
main (int argc, char **argv) {
  TDouble *d;
  double value;

  d = t_double_new (10.0);
  if (t_double_get_value (d, &value))
    g_print ("t_double_get_value succesfully assigned %lf to value.\n", value);
  else
    g_print ("t_double_get_value failed.\n");

  t_double_set_value (d, -20.0);
  g_print ("Now, set d (tDouble object) with %lf.\n", -20.0);
  if (t_double_get_value (d, &value))
    g_print ("t_double_get_value succesfully assigned %lf to value.\n", value);
  else
    g_print ("t_double_get_value failed.\n");
  g_object_unref (d);

  return 0;
}

The source files are located in src/tdouble1. Change your current directory to the directory above and type the following.

$ cd src/tdouble1
$ meson setup _build
$ ninja -C _build

Then, execute the program.

$ cd src/tdouble1; _build/example6
t_double_get_value succesfully assigned 10.000000 to value.
Now, set d (tDouble object) with -20.000000.
t_double_get_value succesfully assigned -20.000000 to value.

This example is very simple. But any object has header file and C source file like this. And they follow the convention. You probably aware of the importance of the convention. For the further information refer to GObject API Reference – Conventions.

Functions

Functions of objects are open to other objects. They are like public methods in object oriented languages. They are actually called “instance method” in the GObject API Reference.

It is natural to add calculation operators to TDouble objects because they represent real numbers. For example, t_double_add adds the value of the instance and another instance. Then it creates a new TDouble instance which has a value of the sum of them.

TDouble *
t_double_add (TDouble *self, TDouble *other) {
  g_return_val_if_fail (T_IS_DOUBLE (self), NULL);
  g_return_val_if_fail (T_IS_DOUBLE (other), NULL);
  double value;

  if (! t_double_get_value (other, &value))
    return NULL;
  return t_double_new (self->value + value);
}

The first argument self is the instance the function belongs to. The second argument other is another TDouble instance.

The value of self can be accessed by self->value, but don’t use other->value to get the value of other. Use a function t_double_get_value instead. Because self is an instance out of other. Generally, the structure of an object isn’t open to other objects. When an object A access to another object B, A must use a public function provided by B.

Exercise

Write functions of TDouble object for subtraction, multiplication, division and sign changing (unary minus). Compare your program to tdouble.c in src/tdouble2 directory.