GObject 信号机制——信号注册
上一篇文档“GObject 的信号机制”只是挖了一个坑便结束了,本篇尝试填坑,不过也不敢有所保证。因为我也不确定会不会因为被 GObject 的信号内幕再次搞晕。
我们先老老实实的阅读 GObject 参考手册的“Concepts / Signal”部分,尽量多获得一些面上的认识。手册中最关键的一句话是:每一个信号在注册的时候都要与某种数据类型(包括 GObject 库中的内建类型或 GObject 子类类型)存在关联,这种数据类型的使用者需要实现信号与闭包的连接,这样在信号被发射时,闭包会被调用。这句话,意味着我们要使用 GObject 信号机制,那么就必须要完成两个步骤:第一个步骤是信号注册,主要解决信号与数据类型的关联问题;第二个步骤是信号连接,主要处理信号与闭包的连接问题。本文主要考察信号注册的大致过程。
信号可以与 GObject 库的类型管理机制中“可实例化”的数据类型进行关联,但是 GObject 参考手册建议我们最好是只在 GObject 子类类型中使用信号,因为信号跟类/对象在逻辑上比较相符。
有 三个函数可以实现信号注册,即 g_signal_newv、g_signal_new_valist 以及 g_signal_new,其中 g_signal_new_valist 与 g_signal_new 函数皆基于 g_signal_newv 函数实现,但是 g_signal_new 函数的名字看上去最平易近人。我们不理睬它们内部是如何实现的,只需要理解它们所接受的参数的含义即可。所以,我们可以从分析 g_signal_new 函数的参数来理解有关信号注册的一些概念。
g_signal_new 函数的声明如下:
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, ...);
g_signal_new 函数的参数较多,其中每个参数多少都有点深不可测的背景,所以直接理解是非常困难的。我们需要构建实例,从而获得最直观的理解。
首先,我们定义一个 GObject 的子类——SignalDemo 类,其头文件 signal-demo.h 内容如下:
#ifndef SIGNAL_DEMO_H #define SIGNAL_DEMO_H #include <glib-object.h> #define SIGNAL_TYPE_DEMO (signal_demo_get_type ()) #define SIGNAL_DEMO(object) \ G_TYPE_CHECK_INSTANCE_CAST ((object), SIGNAL_TYPE_DEMO, SignalDemo) #define SIGNAL_IS_DEMO(object) \ G_TYPE_CHECK_INSTANCE_TYPE ((object), SIGNAL_TYPE_DEMO)) #define SIGNAL_DEMO_CLASS(klass) \ (G_TYPE_CHECK_CLASS_CAST ((klass), SIGNAL_TYPE_DEMO, SignalDemoClass)) #define SIGNAL_IS_DEMO_CLASS(klass) \ (G_TYPE_CHECK_CLASS_TYPE ((klass), SIGNAL_TYPE_DEMO)) #define SIGNAL_DEMO_GET_CLASS(object) (\ G_TYPE_INSTANCE_GET_CLASS ((object), SIGNAL_TYPE_DEMO, SignalDemoClass)) typedef struct _SignalDemo SignalDemo; struct _SignalDemo { GObject parent; }; typedef struct _SignalDemoClass SignalDemoClass; struct _SignalDemoClass { GObjectClass parent_class; void (*default_handler) (gpointer instance, const gchar *buffer, gpointer userdata); }; GType signal_demo_get_type (void); #endif
SignalDemo 类的源文件 signal-demo.c 内容如下:
#include "signal-demo.h" G_DEFINE_TYPE (SignalDemo, signal_demo, G_TYPE_OBJECT); static void signal_demo_default_handler (gpointer instance, const gchar *buffer, gpointer userdata) { g_printf ("Default handler said: %s\n", buffer); } void signal_demo_init (SignalDemo *self) { } void signal_demo_class_init (SignalDemoClass *klass) { klass->default_handler = signal_demo_default_handler; }
基于此前所写的 GObject 学习笔记系列,上述代码不难理解,无非就是定义了一个 SignalDemo 类,其类结构体中包含了一个函数指针 default_handler,并在类结构体初始化函数中使该指针指向函数 signal_demo_default_handler。
下面我们开始为 SignalDemo 类注册一个“hello”信号,只需修改一下 SignalDemo 类的类结构体初始化函数,即:
void signal_demo_class_init (SignalDemoClass *klass) { klass->default_handler = signal_demo_default_handler; g_signal_new ("hello", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (SignalDemoClass, default_handler), NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); }
此时,观察一下 g_signal_new 函数的参数:
- 第 1 个参数是字符串“hello”,它表示信号。
- 第 2 个参数是 SignalDemo 类的类型 ID,可以使用 G_TYPE_FROM_CLASS 宏从 SignalDemoClass 结构体中获取,也可直接使用 signal-demo.h 中定义的宏 SIGNAL_TYPE_DEMO。
- 第 3 个参数可暂时略过。
- 第 4 个参数比较关键,它是一个内存偏移量,主要用于从 SignalDemoClass 结构体中找到 default_handler 指针的位置,可以使用 G_STRUCT_OFFSET 宏来获取,也可以直接根据 signal-demo.h 中的 SignalDemoClass 结构体的定义,使用 sizeof (GObjectClass) 来得到内存偏移量,因为 default_handler 指针之前只有一个 GObjectClass 结构体成员。
- 第 5 个和第 6 个参数暂时略过。
- 第 7 个参数设定闭包的 marshal。在文档“函数指针、回调函数与 GObject 闭包” 中,描述了 GObject 的闭包的概念与结构,我们可以将它视为回调函数 + 上下文环境而构成的一种数据结构,或者再简单一点,将其视为回调函数。另外,在那篇文档中,我们也对 marshal 的概念进行了一些粗浅的解释。事实上 marshal 主要是用来“翻译”闭包的参数和返回值类型的,它将翻译的结果传递给闭包。之所以不直接调用闭包,而是在其外加了一层 marshal 的包装,主要是方便 GObject 库与其他语言的绑定。例如,我们可以写一个 pyg_closure_marshal_VOID__STRING 函数,其中可以调用 python 语言编写的“闭包”并将其计算结果传递给 GValue 容器,然后再从 GValue 容器中提取计算结果。
- 第 8 个参数指定 marshal 函数的返回值类型。由于本例的第 7 个参数所指定的 marshal 是 g_cclosure_marshal_VOID__STRING 函数的返回值是 void,而 void 类型在 GObject 库的类型管理系统是 G_TYPE_NONE 类型。
- 第 9 个参数指定 g_signal_new 函数向 marshal 函数传递的参数个数,由于本例使用的 marshal 函数是 g_cclosure_marshal_VOID__STRING 函数,g_signal_new 函数只向其传递 1 个参数。
- 第 10 个参数是可变参数,其数量由第 8 个参数决定,用于指定 g_signal_new 函数向 marshal 函数传递的参数类型。由于本例使用的 marshal 函数是 g_cclosure_marshal_VOID__STRING 函数,并且 g_signal_new 函数只向其传递一个参数,所以传入的参数类型为 G_TYPE_STRING(GObject 库类型管理系统中的字符串类型)。
注意,在上述的 g_signal_new 函数的第 7 个参数的解释中,我提到了闭包。事实上,g_signal_new 函数并没有闭包类型的参数,但是它在内部的确是构建了一个闭包,而且是通过它的第 4 个参数实现的。因为 g_signal_new 函数在其内部调用了 g_signal_type_cclosure_new 函数,后者所做的工作就是从一个给定的类结构体中通过内存偏移地址获得回调函数指针,然后构建闭包返于 g_signal_new 函数。既然 g_signal_new 函数的内部是需要闭包的,那么它的第 7~10 个参数自然都是为那个闭包做准备的。
需要注意,g_cclosure_marshal_VOID__STRING 所约定的回调函数类型为:
void (*callback) (gpointer instance, const gchar *arg1, gpointer user_data)
这表明 g_cclosure_marshal_VOID__STRING 需要使用者向其回调函数传入 3 个参数,其中前两个参数是回调函数的必要参数,而第 3 个参数,即 userdata,是为使用者留的“后门”,使用者可以通过这个参数传入自己所需要的任意数据。由于 GObject 闭包约定了回调函数的第 1 个参数必须是对象本身,所以 g_cclosure_marshal_VOID__STRING 函数实际上要求使用者向其传入 2 个参数,但是在本例中 g_signal_new 只向其传递了 1 个类型为 G_TYPE_STRING 类型的参数,这有些蹊跷。
这 是因为 g_signal_new 函数所构建闭包只是让信号所关联的数据类型能够有一次可以自我表现的机会,即可以在信号被触发的时候,能够自动调用该数据类型的某个方法,例如 SignalDemo 类结构体的 default_handler 指针所指向的函数。也就是说,SignalDemo 类自身是没有必要向闭包传递那个“userdata”参数的,只是信号的使用者有这种需求。这就是 g_signal_new 的参数中只表明它向闭包传递了 1 个 G_TYPE_STRING 类型参数的缘故。
上面讲的有些凌乱。现在总结一下:g_signal_new 函数内部所构建的闭包,它在被调用的时候,肯定是被传入了 3 个参数,它们被信号所关联的闭包分成了以下层次:
- 第 1 个参数是信号的默认闭包(信号注册阶段出现)和信号使用者提供的闭包(信号连接阶段出现)所必需的,但是这个参数是隐式存在的,由 g_signal_new 暗自向闭包传递。
- 第 2 个参数是显式的,同时也是信号的默认闭包和信号使用者提供的闭包所必须的,这个参数由信号的发射函数(例如 g_signal_emit_by_name)向闭包传递。
- 第 3 个参数也是显式的,且只被信号使用者提供的闭包所关注,这个参数由信号的连接函数(例如 g_signal_connect)向闭包传递。
若要真正明白上述内容,我们必须去构建 SignalDemo 类的使用者,即 main.c 源文件,内容如下:
#include "signal-demo.h" static void my_signal_handler (gpointer *instance, gchar *buffer, gpointer userdata) { g_print ("my_signal_handler said: %s\n", buffer); g_print ("my_signal_handler said: %s\n", (gchar *)userdata); } int main (void) { g_type_init (); gchar *userdata = "This is userdata"; SignalDemo *sd_obj = g_object_new (SIGNAL_TYPE_DEMO, NULL); /* 信号连接 */ g_signal_connect (sd_obj, "hello", G_CALLBACK (my_signal_handler), userdata); /* 发射信号 */ g_signal_emit_by_name (sd_obj, "hello", "This is the second param", G_TYPE_NONE); return 0; }
编译 signal-demo.c 与 main.c:
$ gcc signal-demo.c main.c -o test $(pkg-config --cflags --libs gobject-2.0)
程序运行结果如下:
$ ./test Default handler said: This is the second param my_signal_handler said: This is the second param my_signal_handler said: This is userdata
结合程序的运行结果,再回顾一下第 1 个实例中的那些乱七八糟的内容,现在应该清晰了许多。
现在,我们再来看一下在第 1 个实例中被我们忽略的 g_signal_new 函数的第 3 个参数,我们将其设为 G_SIGNAL_RUN_FIRST。实际上,这个参数是枚举类型,是信号默认闭包的调用阶段的标识,可以是下面 7 种形式中 1 种,也可以是多种组合。
typedef enum { G_SIGNAL_RUN_FIRST = 1 << 0, G_SIGNAL_RUN_LAST = 1 << 1, G_SIGNAL_RUN_CLEANUP = 1 << 2, G_SIGNAL_NO_RECURSE = 1 << 3, G_SIGNAL_DETAILED = 1 << 4, G_SIGNAL_ACTION = 1 << 5, G_SIGNAL_NO_HOOKS = 1 << 6 } GSignalFlags;
这个参数被设为 G_SIGNAL_RUN_FIRST,表示信号的默认闭包要先于信号使用者的闭包被调用,这个观察一下上面的 test 程序的输出结果便可知悉。如果我们将这个参数设为 G_SIGNAL_RUN_LAST,则表示信号的默认闭包要迟于信号使用者的闭包而被调用。对于这个参数的理解暂且到此为止,后面在讲述信号连接的时候 还会再次谈到它。
小结
现在,对信号注册的主要过程已有所了解,但是依 g_signal_new 函数的第 5 个与第 6 个参数,对于它们,我现在还不知道如何为其构建实例,以后再说吧。若你读到此处并且知道它们的用法,还望不吝赐教。
转载时,希望不要链接文中图片,另外请保留本文原始出处:http://garfileo.is-programmer.com
2011年3月25日 21:59
你真的好有耐心啊,不管怎样赞一个,
如果翻译的话我会把SignalAccumulator翻译成"信号聚合器"
虽然也没有具体的应用过,但是按照源码中的说法是用来管理
信号响应(绑定的回调函数指针)的返回管理,它本身也是个函数指针,初步判断应该是再封装了下回调或是在signal循环中紧挨着对应的回调函数来调用,其返回true(1)信号继续响应,返回false(0)则终止,其应该是配合SignalFlag来工作的,因为这个函数指针的第一个参数类型时InvocationHint的(这个字面意义好理解),其结构体中包含了一个SignalFlag.....
我是不愿再往下看下去了,虽然有河蟹版的SE这样的利器也不愿继续看了,因为要到那个恶心的信号循环了,还有SignalNode等结构.......
GObject还有一个极不好的地方就是当你好长时间不用的时候,很难回忆起来细节,有时不得不重新学习一遍,虽然
重学会比早先的顺,但还是要费很大劲.....
2011年3月25日 23:01
@pingf: 因为打算在手里的一个项目中使用 gobj,所以比较上心一些。
那个 accumulator,我是将它翻译成信号累加器。它的用法,通过文档大致上可以看明白,不过昨天晚上写的测试程序出错,是 g_signal_newv 的断言,说是 accumulator == NULL 失败。当时被整懵了。
现在才注意到,我那个测试示例所使用的 marshal 是没有返回值的,而那个 accumulator 则是专门用于处理信号返回值的。
2011年3月25日 23:46
我觉得累加的翻译不好,因为acc词根随表累加的意思,但也有聚积器的意思,比如说"蓄电池"这个意思,有"蓄"无"加"
而从GObject中的作用而言,它是负责信号返回的管理的,有一点收拢的感觉,你说翻译成累加感觉就没这层意思了
而一般词典这个词的意思是
聚财者; 积聚者, 蓄电池; 记录, 电脑 CPU 记忆用来暂时储存最近计算结果的部分 (计算机用语)
虽然好多书上计算机上的书都译成累加器
"現今的 CPU 通常有很多暫存器,所有或多數都可以被用來當作累加器。因為這個原因,"累加器" 這名詞就顯得有些老舊。" --wiki
从计算机体系结构来说原先译成累加器是合适的,现在也不太合适了
而从GObject的信号而言,很难找到累加的意思,所以我个人支持聚合器或累加器的译法.
2011年3月25日 23:50
@pingf:
还有就是在计算机寄存器中累加是由其多用于累加的功能得来的
GObject这个也做累加么?
如果一个信号挂接了多个回调,返回多个值GObject中是以一个整形,累加起来来区分的,那么译成累加器还有点意思
不过我没机会测试这个了
2012年10月28日 22:28
我觉的accumulator这个东西,应该是处理回调函数返回值的,看是否继续发射信号。 我
由pringf的评论去查了一下clutter的源码。
actor_signals[BUTTON_RELEASE_EVENT] = g_signal_new (I_("button-release-event"), G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (ClutterActorClass, button_release_event), _clutter_boolean_handled_accumulator, NULL, _clutter_marshal_BOOLEAN__BOXED, G_TYPE_BOOLEAN, 1, CLUTTER_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE);
其中_clutter_boolean_handled_accumulator是
多谢楼主写这么好的文章。真是好人。
2012年10月29日 08:03
@queminye: 恩,是处理信号处理函数的返回值的,后来在 http://garfileo.is-programmer.com/2011/3/27/gobject-signal-extra-2.25621.html 解释了一下。