分类: C/C++
2011-08-30 15:46:03
上一篇文档“”只是挖了一个坑便结束了,本篇尝试填坑,不过也不敢有所保证。因为我也不确定会不会因为被 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 函数的声明如下:
1 2 3 4 5 6 7 8 9 10 | 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 内容如下:
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 | #ifndef SIGNAL_DEMO_H #define SIGNAL_DEMO_H #include #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 内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #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; } |
基于此前所写的 ,上述代码不难理解,无非就是定义了一个 SignalDemo 类,其类结构体中包含了一个函数指针 default_handler,并在类结构体初始化函数中使该指针指向函数 signal_demo_default_handler。
下面我们开始为 SignalDemo 类注册一个“hello”信号,只需修改一下 SignalDemo 类的类结构体初始化函数,即:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 函数的参数:
注意,在上述的 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 所约定的回调函数类型为:
1 | 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 个参数,它们被信号所关联的闭包分成了以下层次:
若要真正明白上述内容,我们必须去构建 SignalDemo 类的使用者,即 main.c 源文件,内容如下:
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 | #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:
1 | $ gcc signal-demo.c main.c -o test $(pkg-config --cflags --libs gobject-2.0) |
程序运行结果如下:
1 2 3 4 | $ ./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 种,也可以是多种组合。
1 2 3 4 5 6 7 8 9 10 | 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 个参数,对于它们,我现在还不知道如何为其构建实例,以后再说吧。若你读到此处并且知道它们的用法,还望不吝赐教。