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.
g_connect_signal
or its family functions.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.
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(const gchar *signal_name,
g_signal_new ,
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 ("div-by-zero",
g_signal_new (class),
G_TYPE_FROM_CLASS | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
G_SIGNAL_RUN_LAST 0 /* class offset.Subclass cannot override the class handler (default handler). */,
/* accumulator */,
NULL /* accumulator data */,
NULL /* C marshaller. g_cclosure_marshal_generic() will be used */,
NULL /* return_type */,
G_TYPE_NONE 0 /* n_params */
);
t_double_signal
is a static guint variable. The type
guint is the same as unsigned int. It is set to the signal id returned
by the function g_signal_new
.G_TYPE_FROM_CLASS (class)
returns the type
corresponds to the class (class
is a pointer to the class
of the object).n_params
is a number of parameters. This signal doesn’t
have parameters, so it is zero.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 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
(TDouble *self, gpointer user_data) {
div_by_zero_cb ("\nError: division by zero.\n\n");
g_print }
A signal and a handler are connected with the function g_signal_connect
.
(self, "div-by-zero", G_CALLBACK (div_by_zero_cb), NULL); g_signal_connect
self
is an instance the signal belongs to.G_CALLBACK
.Signals are emitted on the object. The following is a part of
tdouble.c
.
*
TDouble (TDouble *self, TDouble *other) {
t_double_div ... ... ...
if ((! t_double_get_value (other, &value)))
return NULL;
else if (value == 0) {
(self, t_double_signal, 0);
g_signal_emit return NULL;
}
return t_double_new (self->value / value);
}
If the divisor is zero, the signal is emitted. g_signal_emit
has three parameters.
g_signal_new
.If a signal has parameters, they are fourth and subsequent arguments.
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
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(const gchar *signal_name,
g_signal_new_class_handler ,
GType itype,
GSignalFlags signal_flags, /*default signal handler */
GCallback class_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
(TDouble *self, gpointer user_data) {
div_by_zero_cb ("\nError happens in main.c.\n");
g_print }
int
(int argc, char **argv) {
main ... ... ...
(d1, "div-by-zero", G_CALLBACK (div_by_zero_cb), NULL);
g_signal_connect ... ... ...
}
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
(TDouble *self, gpointer user_data) {
div_by_zero_after_cb ("\nError has happened in main.c and an error message has been displayed.\n");
g_print }
int
(int argc, char **argv) {
main ... ... ...
(d1, "div-by-zero", G_CALLBACK (div_by_zero_after_cb), NULL);
g_signal_connect_after ... ... ...
}
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.
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_FIRST
: the default handler is called
before any user provided handler.G_SIGNAL_RUN_LAST
: the default handler is called after
the normal user provided handler (not connected with
g_signal_connect_after
).G_SIGNAL_RUN_CLEANUP
: the default handler is called
after any user provided handler.G_SIGNAL_RUN_LAST
is the most appropriate in many
cases.
Other signal flags are described in GObject API Reference.