Signals

Signals

Signals provide a means of communication between objects. Signals are emitted when something happens or completes.

The steps to program a signal is shown below.

  1. Register a signal. A signal belongs to an object, so the registration is done in the class initialization function of the object.
  2. Write a signal handler. A signal handler is a function that is invoked when the signal is emitted.
  3. Connect the signal and handler. Signals are connected to handlers with g_connect_signal or its family functions.
  4. Emit the signal.

Step one and Four are done on the object to which the signal belongs. Step three is usually done outside the object.

The process of signals is complicated and it takes long to explain all the features. The contents of this section is limited to the minimum things to write a simple signal and not necessarily accurate. If you need an accurate information, refer to GObject API reference. There are four parts which describe signals.

Signal registration

An example in this section is a signal emitted when division-by-zero happens. First, we need to determine the name of the signal. Signal name consists of letters, digits, dash (-) and underscore (_). The first character of the name must be a letter. So, a string “div-by-zero” is appropriate for the signal name.

There are four functions to register a signal. We will use g_signal_new for “div-by-zero” signal.

guint
g_signal_new (const gchar *signal_name,
              GType itype,
              GSignalFlags signal_flags,
              guint class_offset,
              GSignalAccumulator accumulator,
              gpointer accu_data,
              GSignalCMarshaller c_marshaller,
              GType return_type,
              guint n_params,
              ...);

It needs a lot to explain each parameter. At present I just show you g_signal_new function call extracted from tdouble.c.

t_double_signal =
g_signal_new ("div-by-zero",
              G_TYPE_FROM_CLASS (class),
              G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
              0 /* class offset.Subclass cannot override the class handler (default handler). */,
              NULL /* accumulator */,
              NULL /* accumulator data */,
              NULL /* C marshaller. g_cclosure_marshal_generic() will be used */,
              G_TYPE_NONE /* return_type */,
              0     /* n_params */
              );

This function is located in the class initialization function (t_double_class_init).

You can use other functions such as g_signal_newv. See GObject API Reference for details.

Signal handler

Signal handler is a function that is called when the signal is emitted. The handler has two parameters.

The “div-by-zero” signal doesn’t need user data.

void div_by_zero_cb (TDouble *self, gpointer user_data) { ... ... ...}

The first argument self is the instance on which the signal is emitted. You can leave out the second parameter.

void div_by_zero_cb (TDouble *self) { ... ... ...}

If a signal has parameters, the parameters are between the instance and the user data. For example, the handler of “window-added” signal on GtkApplication is:

void window_added (GtkApplication* self, GtkWindow* window, gpointer user_data);

The second argument window is the parameter of the signal. The “window-added” signal is emitted when a new window is added to the application. The parameter window points a newly added window. See GTK API reference for further information.

The handler of “div-by-zero” signal just shows an error message.

static void
div_by_zero_cb (TDouble *self, gpointer user_data) {
  g_print ("\nError: division by zero.\n\n");
}

Signal connection

A signal and a handler are connected with the function g_signal_connect.

g_signal_connect (self, "div-by-zero", G_CALLBACK (div_by_zero_cb), NULL);

Signal emission

Signals are emitted on the object. The following is a part of tdouble.c.

TDouble *
t_double_div (TDouble *self, TDouble *other) {
... ... ...
  if ((! t_double_get_value (other, &value)))
    return NULL;
  else if (value == 0) {
    g_signal_emit (self, t_double_signal, 0);
    return NULL;
  }
  return t_double_new (self->value / value);
}

If the divisor is zero, the signal is emitted. g_signal_emit has three parameters.

If a signal has parameters, they are fourth and subsequent arguments.

Example

A sample program is in src/tdouble3.

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)

/* getter and setter */
gboolean
t_double_get_value (TDouble *self, double *value);

void
t_double_set_value (TDouble *self, double value);

/* arithmetic operator */
/* These operators create a new instance and return a pointer to it. */
TDouble *
t_double_add (TDouble *self, TDouble *other);

TDouble *
t_double_sub (TDouble *self, TDouble *other);

TDouble *
t_double_mul (TDouble *self, TDouble *other);

TDouble *
t_double_div (TDouble *self, TDouble *other);

TDouble *
t_double_uminus (TDouble *self);

/* create a new TDouble instance */
TDouble *
t_double_new (double value);

tdouble.c

#include "tdouble.h"

static guint t_double_signal;

struct _TDouble {
  GObject parent;
  double value;
};

G_DEFINE_TYPE (TDouble, t_double, G_TYPE_OBJECT)

static void
t_double_class_init (TDoubleClass *class) {
  t_double_signal = g_signal_new ("div-by-zero",
                                 G_TYPE_FROM_CLASS (class),
                                 G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                                 0 /* class offset.Subclass cannot override the class handler (default handler). */,
                                 NULL /* accumulator */,
                                 NULL /* accumulator data */,
                                 NULL /* C marshaller. g_cclosure_marshal_generic() will be used */,
                                 G_TYPE_NONE /* return_type */,
                                 0     /* n_params */
                                 );
}

static void
t_double_init (TDouble *self) {
}

/* getter and setter */
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;
}

/* arithmetic operator */
/* These operators create a new instance and return a pointer to it. */
#define t_double_binary_op(op) \
  if (! t_double_get_value (other, &value)) \
    return NULL; \
  return t_double_new (self->value op value);

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;

  t_double_binary_op (+)
}

TDouble *
t_double_sub (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;

  t_double_binary_op (-)
}

TDouble *
t_double_mul (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;

  t_double_binary_op (*)
}

TDouble *
t_double_div (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;
  else if (value == 0) {
    g_signal_emit (self, t_double_signal, 0);
    return NULL;
  }
  return t_double_new (self->value / value);
}
TDouble *
t_double_uminus (TDouble *self) {
  g_return_val_if_fail (T_IS_DOUBLE (self), NULL);

  return t_double_new (-self->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"

static void
div_by_zero_cb (TDouble *self, gpointer user_data) {
  g_printerr ("\nError: division by zero.\n\n");
}

static void
t_print (char *op, TDouble *d1, TDouble *d2, TDouble *d3) {
  double v1, v2, v3;

  if (! t_double_get_value (d1, &v1))
    return;
  if (! t_double_get_value (d2, &v2))
    return;
  if (! t_double_get_value (d3, &v3))
    return;

  g_print ("%lf %s %lf = %lf\n", v1, op, v2, v3);
}

int
main (int argc, char **argv) {
  TDouble *d1, *d2, *d3;
  double v1, v3;

  d1 = t_double_new (10.0);
  d2 = t_double_new (20.0);
  if ((d3 = t_double_add (d1, d2)) != NULL) {
    t_print ("+", d1, d2, d3);
    g_object_unref (d3);
  }

  if ((d3 = t_double_sub (d1, d2)) != NULL) {
    t_print ("-", d1, d2, d3);
    g_object_unref (d3);
  }

  if ((d3 = t_double_mul (d1, d2)) != NULL) {
    t_print ("*", d1, d2, d3);
    g_object_unref (d3);
  }

  if ((d3 = t_double_div (d1, d2)) != NULL) {
    t_print ("/", d1, d2, d3);
    g_object_unref (d3);
  }

  g_signal_connect (d1, "div-by-zero", G_CALLBACK (div_by_zero_cb), NULL);
  t_double_set_value (d2, 0.0);
  if ((d3 = t_double_div (d1, d2)) != NULL) {
    t_print ("/", d1, d2, d3);
    g_object_unref (d3);
  }

  if ((d3 = t_double_uminus (d1)) != NULL && (t_double_get_value (d1, &v1)) && (t_double_get_value (d3, &v3))) {
    g_print ("-%lf = %lf\n", v1, v3);
    g_object_unref (d3);
  }

  g_object_unref (d1);
  g_object_unref (d2);

  return 0;
}

Change your current directory to src/tdouble3 and type as follows.

$ meson setup _build
$ ninja -C _build

Then, Executable file tdouble is created in the _build directory. Execute it.

$ _build/tdouble
10.000000 + 20.000000 = 30.000000
10.000000 - 20.000000 = -10.000000
10.000000 * 20.000000 = 200.000000
10.000000 / 20.000000 = 0.500000

Error: division by zero.

-10.000000 = -10.000000

Default signal handler

You may have thought that it was strange that the error message was set in main.c. Indeed, the error happens in tdouble.c so the message should been managed by tdouble.c itself. GObject system has a default signal handler that is set in the object itself. A default signal handler is also called “default handler” or “object method handler”.

You can set a default handler with g_signal_new_class_handler.

guint
g_signal_new_class_handler (const gchar *signal_name,
                            GType itype,
                            GSignalFlags signal_flags,
                            GCallback class_handler, /*default signal handler */
                            GSignalAccumulator accumulator,
                            gpointer accu_data,
                            GSignalCMarshaller c_marshaller,
                            GType return_type,
                            guint n_params,
                            ...);

The difference from g_signal_new is the fourth parameter. g_signal_new sets a default handler with the offset of the function pointer in the class structure. If an object is derivable, it has its own class area, so you can set a default handler with g_signal_new. But a final type object doesn’t have its own class area, so it’s impossible to set a default handler with g_signal_new. That’s the reason why we use g_signal_new_class_handler.

The C file tdouble.c is changed like this. The function div_by_zero_default_cb is added and g_signal_new_class_handler replaces g_signal_new. Default signal handler doesn’t have user_data parameter. A user_data parameter is set in the g_signal_connect family functions when a user connects their own signal handler to the signal. Default signal handler is managed by the instance, not a user. So no user data is given as an argument.

static void
div_by_zero_default_cb (TDouble *self) {
  g_printerr ("\nError: division by zero.\n\n");
}

static void
t_double_class_init (TDoubleClass *class) {
  t_double_signal =
  g_signal_new_class_handler ("div-by-zero",
                              G_TYPE_FROM_CLASS (class),
                              G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                              G_CALLBACK (div_by_zero_default_cb),
                              NULL /* accumulator */,
                              NULL /* accumulator data */,
                              NULL /* C marshaller */,
                              G_TYPE_NONE /* return_type */,
                              0     /* n_params */
                              );
}

g_signal_connect and div_by_zero_cb are removed from main.c.

Compile and execute it.

$ cd src/tdouble4; _build/tdouble
10.000000 + 20.000000 = 30.000000
10.000000 - 20.000000 = -10.000000
10.000000 * 20.000000 = 200.000000
10.000000 / 20.000000 = 0.500000

Error: division by zero.

-10.000000 = -10.000000

The source file is in the directory src/tdouble4.

If you want to connect your handler (user-provided handler) to the signal, you can still use g_signal_connect. Add the following in main.c.

static void
div_by_zero_cb (TDouble *self, gpointer user_data) {
  g_print ("\nError happens in main.c.\n");
}

int
main (int argc, char **argv) {
... ... ...
  g_signal_connect (d1, "div-by-zero", G_CALLBACK (div_by_zero_cb), NULL);
... ... ...
}

Then, both the user-provided handler and default handler are called when the signal is emitted. Compile and execute it, then the following is shown on your display.

10.000000 + 20.000000 = 30.000000
10.000000 - 20.000000 = -10.000000
10.000000 * 20.000000 = 200.000000
10.000000 / 20.000000 = 0.500000

Error happens in main.c.

Error: division by zero.

-10.000000 = -10.000000

This tells us that the user-provided handler is called first, then the default handler is called. If you want your handler called after the default handler, then you can use g_signal_connect_after. Add the lines below to main.c again.

static void
div_by_zero_after_cb (TDouble *self, gpointer user_data) {
  g_print ("\nError has happened in main.c and an error message has been displayed.\n");
}

int
main (int argc, char **argv) {
... ... ...
  g_signal_connect_after (d1, "div-by-zero", G_CALLBACK (div_by_zero_after_cb), NULL);
... ... ...
}

Compile and execute it, then:

10.000000 + 20.000000 = 30.000000
10.000000 - 20.000000 = -10.000000
10.000000 * 20.000000 = 200.000000
10.000000 / 20.000000 = 0.500000

Error happens in main.c.

Error: division by zero.

Error has happened in main.c and an error message has been displayed.

-10.000000 = -10.000000

The source files are in src/tdouble5.

Signal flag

The order that handlers are called is described in GObject API Reference – Sigmal emission.

The order depends on the signal flag which is set in g_signal_new or g_signal_new_class_handler. There are three flags which relate to the order of handlers’ invocation.

G_SIGNAL_RUN_LAST is the most appropriate in many cases.

Other signal flags are described in GObject API Reference.