GObject学习笔记(四)信号
前言
在上一节中,PawInt和PawDouble已经通过重写PawNumber中的虚函数实现了乘法、除法等操作,但除法函数还没有处理除数为0的情况。
本节将在上一节代码的基础上引入GObject信号机制:当PawNumber的具体子类在除法运算中发现除数为0时,发射一个div-by-zero信号,由默认处理器或用户连接的处理器负责输出错误信息。
信号概念
GObject信号是一种基于GObject类型系统的通信机制。一个信号先注册到某个GObject类型上;当该GObject类型或其子类型的某个GObject实例发生特定事件时,这个GObject实例可以发射该信号。外部代码可以连接信号处理器,在信号发射时执行自己的响应逻辑。
换句话说,GObject实例只需要在事件发生时发射信号,不需要知道是否有外部代码正在监听,也不需要知道外部代码会如何处理。是否连接处理器、处理器中执行什么逻辑,由使用该GObject实例的代码决定。
对于GObject类型定义者,使用信号通常包括:
- 定义默认信号处理器:默认信号处理器是GObject类型提供的处理函数,用于给信号提供默认响应逻辑;
- 注册信号:信号归属于GObject类型,因此信号注册通常在类型的类初始化函数中完成;
- 发射信号:在运行过程中,当特定事件发生时,由GObject实例发射信号。
对于信号使用者,可以在GObject实例上:
- 连接用户信号处理器:使用
g_signal_connect*(),将用户提供的回调函数连接到某个实例的某个信号上。
补充:
信号处理器函数一般分为两种:
- per-class callbacks,在信号创建时指定(通常在
class_init阶段设置)。官方文档称之为class signal handler或default signal handler,本文中我们称之为默认信号处理器; - user provided callbacks,通常由使用者通过
g_signal_connect*()连接到某个GObject实例的某个信号上,它们可以在默认信号处理器之前或之后被调用;在更复杂的场景中,也可能通过停止发射等机制影响后续处理。官方文档称之为signal handler,本文中我们称之为用户信号处理器。
信号也有自己的生命周期概念。信号注册到某个GObject类型之后,其注册信息属于该GObject类型,不需要为每个GObject实例重复注册。用户连接的处理器则依附于具体的GObject实例,必要时可以手动断开;如果该实例被销毁,相关连接也会随之失效。本文的例子不需要手动管理信号生命周期,只需要知道“注册属于GObject类型,默认处理器通常关联GObject类结构,连接和发射面向GObject实例”即可。
信号机制是灵活且复杂的,解释所有功能需要很长时间。本节内容仅涉及编写一个简单信号所需的最基本的内容。如果需要了解更高级的机制,请参考GObject API文档。其中描述信号的部分为:
- Type System Concepts – signals
- Signals
- Functions (g_signal_XXX series)
- Function Macros (g_signal_XXX series)
- GObject Tutorial – How to create and use signals
示例
示例目标
本节继续使用上一节中的PawNumber、PawInt和PawDouble类型,并在它们的除法运算中加入除0处理。
我们希望实现的效果是:
- 在
PawNumber类型上注册一个名为div-by-zero的信号; - 为
div-by-zero信号提供一个默认信号处理器,用于输出除0错误信息; - 当
PawInt实例或PawDouble实例执行除法且发现除数为0时,发射div-by-zero信号(PawInt和PawDouble是PawNumber的子类型,因此PawInt实例和PawDouble实例也可以发射这个信号); - 在需要时,使用者可以通过
g_signal_connect*()为具体的PawInt实例或PawDouble实例连接用户信号处理器。
也就是说,PawInt和PawDouble的除法函数只负责判断除数是否为0,并在除0发生时发射信号;具体如何响应这个事件,则交给默认信号处理器或用户信号处理器完成。
本文会先实现一个最简单的div-by-zero信号:它没有额外参数,也没有返回值。这样可以先把信号注册、默认信号处理器、信号发射和用户信号处理器连接这几个基本步骤讲清楚。
信号处理器
信号处理器是一个函数,它会在信号发射过程中被调用。
处理器函数签名
不同信号可以有不同的参数和返回值,因此信号处理器的函数签名需要和信号定义保持一致。
一般来说,信号处理器的参数列表包括:
- 实例参数:为指向发射信号的GObject实例的指针,此参数不可省略且总是位于第一个;
- 信号参数:信号定义时声明的额外参数,,若不需要可以省略;
- 用户数据:为指向用户数据的指针。此参数通常位于最后,若不需要可以省略。
因此,一个信号处理器的形式通常类似于:
1 | static return_type |
默认信号处理器
默认信号处理器是GObject类型提供的默认响应逻辑。它不是通过g_signal_connect*()连接到某个GObject实例上的,而是在信号注册时(作为GObject类型层面的默认处理逻辑设置)。
本文中div-by-zero信号只表示“除数为0这一事件发生了”,不需要信号参数以及用户数据,也不需要返回值。因此它的默认信号处理器可以定义为:
1 | static void div_by_zero_default_cb(PawNumber *self) |
这个函数只做一件事:输出除0错误信息。
对于默认信号处理器,后文会分别介绍两种设置方式:
- 在GObject类结构体中保存默认信号处理器函数指针,并通过
g_signal_new()注册信号; - 通过
g_signal_new_class_handler()注册信号并直接指定默认信号处理器。
用户信号处理器
信号处理器
本文中我们使用一个简单的函数,无需特定参数以及用户数据,唯一的作用就是输出错误信息:
本文中,我们主要使用的是对象方法处理器。但是在文中,我们也会展示用户提供处理器的用法。
信号注册
本节中的一个例子是在除零行为发生时发出信号。
有多个函数可以用来注册信号。本文我们将使用g_signal_new,并在后面展示使用g_signal_new_class_handler代替g_signal_new注册信号的方案。
首先先介绍一下g_signal_new函数:
g_signal_new
g_signal_new是GObject中用于创建新信号的一般函数。它非常灵活,允许使用者指定广泛的信号属性:
1 | guint |
其中:
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 | static guint paw_number_signal; |
paw_number_signal是一个静态的guint变量。guint类型与unsigned int相同。它被设置为由g_signal_new函数返回的信号ID。- 第一个参数是唯一标识信号的名称
- 第二个参数是信号所属类型的GType。
- 第三个参数是信号标志。详见GSignalFlags 。
- 第四个参数是信号处理器在类结构中的偏移量。
- 第八个参数是返回类型,
G_TYPE_NONE意味着信号处理函数不会返回任何值。 - 第九个参数是参数的数量。由于这个信号没有其他参数,所以它的值是零。
除此以外,我们还需要在PawNumber类型的类结构体中增加一个函数指针(div_by_zero)指向信号处理器函数:
1 | struct _PawNumberClass |
函数指针(div_by_zero)指向信号处理器函数(div_by_zero_default_cb)的操作和信号注册的操作都是在类结构体初始化函数(paw_number_class_init)中完成:
1 | static guint paw_number_signal; |
上述步骤即完成了除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 | guint |
使用g_signal_new_class_handler注册除0信号
信号处理器函数依旧定义如下:
1 | static void div_by_zero_default_cb(PawNumber *self) |
只不过我们无需在_PawNumberClass中定义函数指针(div_by_zero),因此类结构体初始化函数就可以写为
1 | static guint paw_number_signal; |
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 | static void div_by_zero_cb_user(PawNumber* self, gpointer user_data) |
g_signal_connect
1 |
|
一个函数宏,用于将信号处理器连接到一个特定对象的某个信号上。
其中:
instance:指向实例的指针。detailed_signal:字符串,指定要连接的信号的名称。它可能包括信号名称及其详细信息,这些详细信息用于区分不同的信号重载或者为相同事件提供不同的上下文。格式为:signal-name::detailc_handler:函数指针,指向当信号被发出时应该调用的回调函数。回调函数类型为GCallback。data:指向用户数据的指针,当回调函数被调用时,这个数据将被传递给它。
使用g_signal_connect连接信号与处理器
1 | static void div_by_zero_cb_user(PawNumber* self, gpointer user_data) |
信号发射
信号发射是通过使用g_signal_emit函数族来完成的,在本文中,我们使用g_signal_emit_by_name。
g_signal_emit_by_name
g_signal_emit_by_name用于同步发射信号:
1 | void |
其中
instance:发出信号的实例。detailed_signal:signal-name::detail形式的字符串。...: 要传递给信号的参数,后跟返回值的位置。如果信号的返回类型是G_TYPE_NONE,返回值的位置可以省略。传递给该函数的参数数量是在创建信号时定义的。
使用g_signal_emit_by_name发射信号
我们要在除法函数中,当传入的除数为0时,发射除0信号div_by_zero。因此对于PawNumber的派生类PawInt和PawDouble,我们需要修改它们的除法函数。下面以PawInt类型为例展示代码:
1 | static PawNumber* paw_int_div(PawNumber* self, PawNumber* other) |
代码汇总
阅读完上述的四个步骤:信号处理器创建,信号注册,信号连接,信号发射。我们将其中的代码进行汇总。
代码还是基于上一节提供的PawNumber,PawInt,PawDouble类型的实现,并在这个基础上引入了信号机制,为除0的情况进行特殊处理:
PawNumber
对于PawNumber中的除0信号div_by_zero,示例中使用g_signal_new_class_handler来注册:
1 | // file: PawNumber.h |
1 | // file: PawNumber.c |
PawInt
1 | // file: PawInt.h |
1 | // file: PawInt.c |
PawDouble
与PawInt的定义类似,此处略
main
1 | // file: main.c |
输出
1 | Error: division by zero. user |
补充
信号发射阶段
我们观察一下上述输出,会发现用户提供处理器先于默认处理器被调用。那么它们的调用顺序是由什么决定的?
在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参数用来传递额外的上下文信息给处理器。这个参数在连接信号时被指定,并且在信号发射时传递给处理器。
参考文章
- 标题: GObject学习笔记(四)信号
- 作者: paw5zx
- 创建于 : 2024-11-28 10:27:55
- 更新于 : 2026-05-07 10:27:25
- 链接: https://paw5zx.github.io/GObject-tutorial-beginner-04/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。