前言 在上一节中我们留下一个待优化的项点:对于除运算,没有对除数为0的特殊情况做处理。
这个问题我们将在本章节使用信号机制解决。
信号 信号提供了一种类型之间通信的方式。信号在某些事件发生或完成时会被发射。
编写信号的步骤如下:
①编写信号处理器:信号处理器是函数,在信号被发射时被调用。
②注册信号:信号属于一个类型,因此信号的注册是在该类型的类初始化函数中完成的。
③连接信号和处理函数:信号通过g_signal_connect
函数族与处理函数连接。
④发射信号。
不必严格按照上述步骤的顺序来编写信号,比如①和②有时会交换顺序。步骤②和④是在信号所属的类型上完成的。步骤③是可选的,通常是在类型外部完成的。
信号机制是灵活且复杂的,解释所有功能需要很长时间。本节内容仅限于编写一个简单信号所需的最基本的内容,可能并不完全准确。如果需要更精确的信息,请参考GObject API文档。其中描述信号的部分为:
信号处理器 信号处理器是函数,它在信号被发射时被调用。它的参数列表如下
实例参数:指向信号所属实例的指针,此参数不可省略且总是位于第一个;
特定参数:用户指定的特定于信号的参数,若不需要可以省略;
用户数据:在信号连接中给出的指向用户数据的指针,此参数通常是最后一个,若不需要可以省略。
因此信号处理器函数的声明看起来像这样:
1 2 3 4 return_type function_callback (gpointer instance, ..., gpointer user_data) ;
本文中我们使用一个简单的函数,无需特定参数以及用户数据,唯一的作用就是输出错误信息:
1 2 3 4 static void div_by_zero_default_cb (PawNumber *self) { g_printerr("\nError: division by zero.\n\n" ); }
这里再补充一点,信号处理器函数一般被分为两种:
对象方法处理器:也称为默认处理器,在信号定义时指定,通常在类的初始化阶段设置。
用户提供处理器:通常在类型实例化之后连接,可以覆盖默认处理器或在默认处理器之前或之后被调用
本文中,我们主要使用的是对象方法处理器。但是在文中,我们也会展示用户提供处理器的用法。
信号注册 本节中的一个例子是在除零行为发生时发出信号。
有多个函数可以用来注册信号。本文我们将使用g_signal_new
,并在后面展示使用g_signal_new_class_handler
代替g_signal_new
注册信号的方案。
首先先介绍一下g_signal_new
函数:
g_signal_new g_signal_new
是GObject中用于创建新信号的一般函数。它非常灵活,允许使用者指定广泛的信号属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 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, ... )
其中:
signal_name
:信号名称,唯一标识一个信号。由ASCII字母和数字的字段组成,用-
或_
字符分隔(不可混合使用)。信号名称的第一个字符必须是字母。建议使用-
分隔符,因为它的效率更高。违反命名规则会导致未定义的行为。
itype
:信号所属类型的GType,通常是GObject或其子类的类型。
signal_flags
:信号的标志,决定信号的行为,是GSignalFlags 的组合。调用者至少应该指定G_SIGNAL_RUN_FIRST
或G_SIGNAL_RUN_LAST
。
class_offset
:指定信号处理器在类结构中的位置,即为信号指定一个类成员函数为默认信号处理器。通常使用G_STRUCT_OFFSET()
获取偏移量。如果不需要设置默认信号处理器,则此值设为0,在这种情况下,子类不能通过在自己类结构初始化函数中修改父类的函数指针来达到重写信号处理器的效果。此时需使用g_signal_override_class_handler
。
accumulator
:累加器函数指针,详见GSignalAccumulator 。用于处理多个信号处理器的返回值。可以为NULL。
accu_data
:传递给累加器的用户上下文数据,可以为NULL。
c_marshaller
:信号封送器函数指针,详见GSignalCMarshaller ,用于将信号参数封装成适合C语言调用的形式。如果此参数为NULL,则默认使用 g_cclosure_marshal_generic()
。
return_type
:信号的返回类型的GType,对于没有返回值的信号此值为G_TYPE_NONE
。
n_params
:信号后续参数的数量。
...
:信号后续参数的GType。
使用g_signal_new注册除0信号 根据信号的命名规则,我们将自定义的除0信号命名为div-by-zero
。
然后使用g_signal_new
创建此信号:
1 2 3 4 5 6 7 8 9 10 11 12 13 static guint paw_number_signal;paw_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, G_STRUCT_OFFSET(PawNumberClass, div_by_zero), NULL , NULL , NULL , G_TYPE_NONE , 0 );
paw_number_signal
是一个静态的guint
变量。guint
类型与unsigned int
相同。它被设置为由g_signal_new
函数返回的信号ID。
第一个参数是唯一标识信号的名称
第二个参数是信号所属类型的GType。
第三个参数是信号标志。详见GSignalFlags 。
第四个参数是信号处理器在类结构中的偏移量。
第八个参数是返回类型,G_TYPE_NONE
意味着信号处理函数不会返回任何值。
第九个参数是参数的数量。由于这个信号没有其他参数,所以它的值是零。
除此以外,我们还需要在PawNumber类型的类结构体中增加一个函数指针(div_by_zero
)指向信号处理器函数:
1 2 3 4 5 6 7 8 9 10 11 struct _PawNumberClass { GObjectClass parent_class; PawNumber* (*mul)(PawNumber* self, PawNumber* other); PawNumber* (*div)(PawNumber* self, PawNumber* other); PawNumber* (*uminus)(PawNumber* self); char * (*to_s)(PawNumber* self); void (*div_by_zero) (PawNumber *self); };
函数指针(div_by_zero
)指向信号处理器函数(div_by_zero_default_cb
)的操作和信号注册的操作都是在类结构体初始化函数(paw_number_class_init
)中完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static guint paw_number_signal;static void paw_number_class_init (PawNumberClass* class) { class -> mul = NULL ; class -> div = NULL ; class -> uminus = NULL ; class -> to_s = NULL ; class -> div_by_zero = div_by_zero_default_cb; paw_number_signal = g_signal_new("div-by-zero" , G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, G_STRUCT_OFFSET(PawNumberClass, div_by_zero), NULL , NULL , NULL , G_TYPE_NONE, 0 ); }
上述步骤即完成了除0信号的注册,下面将展示使用g_signal_new_class_handler
代替g_signal_new
的信号注册方案:
g_signal_new_class_handler g_signal_new_class_handler
是g_signal_new
的一个变种,它们参数唯一的区别就是第四个参数。g_signal_new_class_handler
接受C回调函数而不是信号的类处理程序在类结构中的偏移量。这意味着使用者无需在类型的类结构中公开一个函数指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 guint g_signal_new_class_handler ( const gchar* signal_name, GType itype, GSignalFlags signal_flags, GCallback class_handler, GSignalAccumulator accumulator, gpointer accu_data, GSignalCMarshaller c_marshaller, GType return_type, guint n_params, ... )
使用g_signal_new_class_handler注册除0信号 信号处理器函数依旧定义如下:
1 2 3 4 static void div_by_zero_default_cb (PawNumber *self) { g_printerr("\nError: division by zero.\n\n" ); }
只不过我们无需在_PawNumberClass
中定义函数指针(div_by_zero
),因此类结构体初始化函数就可以写为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static guint paw_number_signal;static void paw_number_class_init (PawNumberClass* class) { class -> mul = NULL ; class -> div = NULL ; class -> uminus = NULL ; class -> to_s = NULL ; paw_number_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 , NULL , NULL , G_TYPE_NONE, 0 ); }
g_signal_new_class_handler专用场景 可能会有一个疑问,既然g_signal_new_class_handler
和g_signal_new
如此相似,为什么还有存在的必要?
这是因为,对于最派生类型,它们是没有自己的类区域的(即其类结构体中唯一成员是其父类的类结构体),因此无法使用g_signal_new
设置默认处理器,在这种情况下,我们就会使用g_signal_new_class_handler
来注册信号。
信号连接 由于示例中使用的是对象方法处理器,在信号的注册过程中就已经指定好默认的处理函数了,因此可以不用额外进行信号连接操作。但是为了演示信号连接,本文将在类型外部定义一个函数,即作为用户提供处理器,然后使用g_signal_connect
将其连接到div-by-zero
。
用户提供处理器 1 2 3 4 static void div_by_zero_cb_user (PawNumber* self, gpointer user_data) { g_printerr("\nError: division by zero. user \n\n" ); }
g_signal_connect 1 2 3 4 5 6 #define g_signal_connect ( instance, detailed_signal, c_handler, data )
一个函数宏,用于将信号处理器连接到一个特定对象的某个信号上。
其中:
instance
:指向实例的指针。
detailed_signal
:字符串,指定要连接的信号的名称。它可能包括信号名称及其详细信息,这些详细信息用于区分不同的信号重载或者为相同事件提供不同的上下文。格式为:signal-name::detail
c_handler
:函数指针,指向当信号被发出时应该调用的回调函数。回调函数类型为GCallback。
data
:指向用户数据的指针,当回调函数被调用时,这个数据将被传递给它。
使用g_signal_connect连接信号与处理器 1 2 3 4 5 6 7 8 9 10 11 12 static void div_by_zero_cb_user (PawNumber* self, gpointer user_data) { g_printerr("\nError: division by zero. user \n\n" ); } int main (int argc, char **argv) { PawNumber* i; i = ...; g_signal_connect(i, "div_by_zero" , G_CALLBACK(div_by_zero_cb_user), NULL ); }
信号发射 信号发射是通过使用g_signal_emit
函数族来完成的,在本文中,我们使用g_signal_emit_by_name
。
g_signal_emit_by_name g_signal_emit_by_name
用于同步发射信号:
1 2 3 4 5 6 void g_signal_emit_by_name ( GObject* instance, const gchar* detailed_signal, ... )
其中
instance
:发出信号的实例。
detailed_signal
:signal-name::detail
形式的字符串。
...
: 要传递给信号的参数,后跟返回值的位置。如果信号的返回类型是G_TYPE_NONE
,返回值的位置可以省略。传递给该函数的参数数量是在创建信号时定义的。
使用g_signal_emit_by_name发射信号 我们要在除法函数中,当传入的除数为0时,发射除0信号div_by_zero
。因此对于PawNumber的派生类PawInt和PawDouble,我们需要修改它们的除法函数。下面以PawInt类型为例展示代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static PawNumber* paw_int_div (PawNumber* self, PawNumber* other) { g_return_val_if_fail(PAW_IS_INT(self), NULL ); int i; double d; if (PAW_IS_INT(other)) { i = PAW_INT(other)->value; if (0 == i) { g_signal_emit_by_name(self, "div_by_zero" ); return NULL ; } else { return PAW_NUMBER(paw_int_new(PAW_INT(self)->value / i)); } } else { d = PAW_DOUBLE(other)->value; if (0 == d) { g_signal_emit_by_name(self, "div_by_zero" ); return NULL ; } else { return PAW_NUMBER(paw_int_new(PAW_INT(self)->value / (int )d)); } } }
代码汇总 阅读完上述的四个步骤:信号处理器创建,信号注册,信号连接,信号发射。我们将其中的代码进行汇总。
代码还是基于上一节提供的PawNumber,PawInt,PawDouble类型的实现,并在这个基础上引入了信号机制,为除0的情况进行特殊处理:
PawNumber 对于PawNumber中的除0信号div_by_zero
,示例中使用g_signal_new_class_handler
来注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #ifndef PAW_NUMBER_H #define PAW_NUMBER_H #include <glib-object.h> G_BEGIN_DECLS #define PAW_TYPE_NUMBER (paw_number_get_type()) G_DECLARE_DERIVABLE_TYPE(PawNumber, paw_number, PAW, NUMBER, GObject) struct _PawNumberClass { GObjectClass parent_class; PawNumber* (*mul)(PawNumber* self, PawNumber* other); PawNumber* (*div)(PawNumber* self, PawNumber* other); PawNumber* (*uminus)(PawNumber* self); char * (*to_s)(PawNumber* self); }; PawNumber* paw_number_mul (PawNumber* self, PawNumber* other) ; PawNumber* paw_number_div (PawNumber* self, PawNumber* other) ; PawNumber* paw_number_uminus (PawNumber* self) ; char * paw_number_to_s (PawNumber* self) ;G_END_DECLS #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include "PawNumber.h" static guint paw_number_signal;G_DEFINE_ABSTRACT_TYPE(PawNumber, paw_number, G_TYPE_OBJECT) PawNumber* paw_number_mul (PawNumber* self, PawNumber* other) { g_return_val_if_fail(PAW_IS_NUMBER(self), NULL ); g_return_val_if_fail(PAW_IS_NUMBER(other), NULL ); PawNumberClass* class = PAW_NUMBER_GET_CLASS(self); return class->mul ? class->mul(self, other) : NULL ; } PawNumber* paw_number_div (PawNumber* self, PawNumber* other) { g_return_val_if_fail(PAW_IS_NUMBER(self), NULL ); g_return_val_if_fail(PAW_IS_NUMBER(other), NULL ); PawNumberClass* class = PAW_NUMBER_GET_CLASS(self); return class->div ? class->div(self, other) : NULL ; } PawNumber* paw_number_uminus (PawNumber* self) { g_return_val_if_fail(PAW_IS_NUMBER(self), NULL ); PawNumberClass* class = PAW_NUMBER_GET_CLASS(self); return class->uminus ? class->uminus(self) : NULL ; } char * paw_number_to_s (PawNumber* self) { g_return_val_if_fail(PAW_IS_NUMBER(self), NULL ); PawNumberClass* class = PAW_NUMBER_GET_CLASS(self); return class->to_s ? class->to_s(self) : NULL ; } static void div_by_zero_default_cb (PawNumber *self) { g_printerr("\nError: division by zero. Use g_signal_new_class_handler \n\n" ); } static void paw_number_class_init (PawNumberClass* class) { class -> mul = NULL ; class -> div = NULL ; class -> uminus = NULL ; class -> to_s = NULL ; paw_number_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 , NULL , NULL , G_TYPE_NONE, 0 ); } static void paw_number_init (PawNumber* self) { }
PawInt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #ifndef PAW_INT_H #define PAW_INT_H #include <glib-object.h> G_BEGIN_DECLS #define PAW_TYPE_INT (paw_int_get_type()) G_DECLARE_FINAL_TYPE(PawInt, paw_int, PAW, INT, PawNumber) struct _PawInt { PawNumber parent; int value; }; PawInt* paw_int_new (int value) ; G_END_DECLS #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 #include "PawNumber.h" #include "PawInt.h" #include "PawDouble.h" G_DEFINE_TYPE(PawInt, paw_int, PAW_TYPE_NUMBER) #define paw_int_binary_op(op)\ int i;\ double d;\ if (PAW_IS_INT(other))\ {\ i = PAW_INT(other)->value;\ return PAW_NUMBER(paw_int_new(PAW_INT(self)->value op i));\ }\ else \ {\ d = PAW_DOUBLE(other)->value;\ return PAW_NUMBER(paw_int_new(PAW_INT(self)->value op (int)d));\ } static PawNumber* paw_int_mul (PawNumber* self, PawNumber* other) { g_return_val_if_fail(PAW_IS_INT(self), NULL ); paw_int_binary_op(*); } static PawNumber* paw_int_div (PawNumber* self, PawNumber* other) { g_return_val_if_fail(PAW_IS_INT(self), NULL ); int i; double d; if (PAW_IS_INT(other)) { i = PAW_INT(other)->value; if (0 == i) { g_signal_emit_by_name(self, "div_by_zero" ); return NULL ; } else { return PAW_NUMBER(paw_int_new(PAW_INT(self)->value / i)); } } else { d = PAW_DOUBLE(other)->value; if (0 == d) { g_signal_emit_by_name(self, "div_by_zero" ); return NULL ; } else { return PAW_NUMBER(paw_int_new(PAW_INT(self)->value / (int )d)); } } } static PawNumber* paw_int_uminus (PawNumber* self) { g_return_val_if_fail(PAW_IS_INT(self), NULL ); return PAW_NUMBER(paw_int_new(-PAW_INT(self)->value)); } static char * paw_int_to_s (PawNumber* self) { g_return_val_if_fail(PAW_IS_INT(self), NULL ); PawInt* int_self = PAW_INT(self); return g_strdup_printf("%d" , int_self->value); } PawInt* paw_int_new (int value) { PawInt* d; d = g_object_new(PAW_TYPE_INT, NULL ); d->value = value; return d; } static void paw_int_class_init (PawIntClass* class) { PawNumberClass* number_class = PAW_NUMBER_CLASS(class); number_class->mul = paw_int_mul; number_class->div = paw_int_div; number_class->uminus = paw_int_uminus; number_class->to_s = paw_int_to_s; } static void paw_int_init (PawInt* self) { }
PawDouble 与PawInt的定义类似,此处略
main 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <glib-object.h> #include "PawNumber.h" #include "PawInt.h" #include "PawDouble.h" static void div_by_zero_cb_user (PawNumber* self, gpointer user_data) { g_printerr("\nError: division by zero. user \n\n" ); } int main (int argc, char **argv) { PawNumber *i, *d, *num; i = PAW_NUMBER(paw_int_new(23 )); d = PAW_NUMBER(paw_double_new(0.0 )); g_signal_connect(i, "div_by_zero" , G_CALLBACK(div_by_zero_cb_user), NULL ); num = paw_number_div(i, d); g_object_unref(d); g_object_unref(i); return 0 ; }
输出 1 2 3 4 Error: division by zero. user Error: division by zero. Use g_signal_new_class_handler
补充 信号发射阶段 我们观察一下上述输出,会发现用户提供处理器先于默认处理器被调用。那么它们的调用顺序是由什么决定的?
在GObject官方文档中提到,信号发射可以分解为以下六个阶段:
RUN_FIRST
:如果在信号注册时使用了G_SIGNAL_RUN_FIRST
标志,并且存在针对此信号的类闭包(class closure
),则调用此类闭包。
EMISSION_HOOK
:如果为该信号添加了任何发射钩子(emission hook
),它们将按照添加顺序从先到后被调用。累积返回值。
HANDLER_RUN_FIRST
:如果任何闭包通过g_signal_connect()
系列函数连接,并且它们未被阻塞(通过g_signal_handler_block()
系列函数),则闭包按照连接顺序在此阶段运行。
RUN_LAST
:如果在注册时设置了G_SIGNAL_RUN_LAST
标志,并且设置了类闭包,那么在此阶段调用。
HANDLER_RUN_LAST
:如果任何闭包通过g_signal_connect_after()
系列函数连接,如果它们在HANDLER_RUN_FIRST
中未被调用并且未被阻塞,那么闭包按照连接顺序在此阶段运行。
RUN_CLEANUP
:如果在注册时设置了G_SIGNAL_RUN_CLEANUP
标志,并且设置了类闭包,那么在此阶段调用类闭包。信号发射在此阶段完成。
在示例中:
对于对象方法处理器,我们在注册时使用了G_SIGNAL_RUN_LAST
标志,因此它会在RUN_LAST
阶段被调用;
对于用户提供处理器,我们使用g_signal_connect
将其连接到信号上,因此它会在HANDLER_RUN_FIRST
阶段被调用。
HANDLER_RUN_FIRST
阶段早于RUN_LAST
阶段,所以先调用的是用户提供处理器。
user_data 在示例中,对象方法处理器函数没有user_data
参数,这是因为对象方法处理器通常是类方法,已经被绑定到类结构中,通常无需额外的用户数据来维持状态或上下文。而用户提供处理器通常需要user_data
参数。当通过g_signal_connect()
或相关函数连接一个信号处理器时,user_data
参数用来传递额外的上下文信息给处理器。这个参数在连接信号时被指定,并且在信号发射时传递给处理器。
参考文章 1.GObject Tutorial for beginners