分类: C/C++
2011-08-29 17:30:28
手册所述,GObject 信号(Gignal)主要用于特定事件与响应者之间的连接,它与操作系统级中的信号没有什么关系。例如,当我向一个文件中写入数据的时候,我期望能够有一 个或多个函数响应这个“向文件写入数据”的事件,这一期望便可基于 GObject 信号予以实现。
为了更好的理解 GObject 信号机制的内幕,我们需要从回调函数开始。
基于回调函数与可变参数的事件响应首先,写出事件的制造者,它是一个向文件写入数据的函数 file_write:
1 2 3 4 5 6 7 | #include void file_write (FILE *fp, const char *buffer) { fprintf (fp, "%s\n", buffer); } |
向文件写入数据完毕之后,我们希望有一个函数能够将文件全部的内容在终端打印出来,所以我们又增加了一个函数 file_print,并对 file_write 函数进行一点修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | void file_print (FILE *fp) { char *line = NULL; size_t len = 0; ssize_t read; while ((read = getline(&line, &len, fp)) != -1){ printf("%s", line); } free (line); } void file_write (FILE *fp, const char *buffer) { fprintf (fp, "%s\n", buffer); file_print (fp); } |
但是,作为设计者应当尽可能的考虑更多更复杂的变化。单纯增加一个 file_print 函数,并在 file_write 函数中调用,固然可以实现“文件变化时便通知 file_print 函数去执行打印任务”,但是这只是我们的一厢情愿的想法,也许 file_write 函数的其他使用者希望在向文件写入数据后能够将文件内容以 XML、TeX 或者别的甚么格式打印出来呢?
为了应对更多的使用者的需求,我们需要使用回调函数来隔离变化,例如:
1 2 3 4 5 6 7 8 | typedef void (*ChangedCallback) (FILE *fp); void file_write (FILE *fp, const char *buffer, ChangedCallback callback) { fprintf (fp, "%s\n", buffer); callback (fp); } |
这样,如果 file_write 的使用者仅需要在文件内容发生变动后打印文件的原始数据,那么就可以将前文中的 file_print 函数作为参数传递于 file_write 函数。如果 file_write 的使用者希望在文件内容发生变动后以 XML 格式打印文件,那么他可以写一个 file_print_xml 函数并将其传递于 file_write 函数。
如果进一步考虑更多的变化,例如在 file_write 向文件写入数据后,我们希望能够一举“通知”文件原始数据打印、XML 格式打印、TeX 格式打印等函数,这应当如何处理?如果使用 C 语言的可变参数功能,这个问题很好解决。例如,可以将 file_write 函数定义为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void file_write (FILE *fp, const char *buffer, ...) { fprintf (fp, "%s\n", buffer); va_list args; ChangedCallback callback; va_start (args, buffer); while (1) { callback = va_arg (args, ChangedCallback); if (!callback) break; callback (fp); } va_end(args); } |
这样,在使用 file_write 函数的时候,可传递多个函数供其调用,例如:
1 2 3 4 5 | file_write (fp, "Hello world!", file_print, file_print_xml, file_print_tex, NULL); |
基于回调函数与可变参数实现特定“事件”的多个“响应”,这种方案是最有效的,但不是最好的。例如,受到函数栈空间的大小限制,可变参数用尽之时。此外,这种方式使用起来也不够直观。
基于 GObject 信号的事件响应对于上一节的示例所解决的问题,基于 GObjet 信号的解决方案大致像下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | void file_write (File *self, const char *buffer) { /* 向文件写入数据 */ ... ... ... /* 发射“文件改变了”这一信号 */ g_signal_emit (self, CHANGED, 0); } int main (void) { File *file = file_new ("test.txt"); g_signal_connect (file, "changed", file_print, NULL); g_signal_connect (file, "changed", file_print_xml, NULL); g_signal_connect (file, "changed", file_print_tex, NULL); ... ... ... } |
上述代码的含义如下:
为了实现上述的“信号/响应”模拟,那么 file_write 函数的参数便不可能再是 FILE 类型的文件指针了,而是我们自定义的 File 类型的对象,其中封装了“信号/响应”功能。事实上,GObject 类的内部便封装了这些功能,所有经由 GObject 子类化而产生的对象,便可拥有这些功能。
GObject 子类对象的信号处理首先,我们定义 GObject 子类 MyFile。这个过程,我们应当已经不再陌生,参考文档 [1]。
my-file.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 | #ifndef MY_FILE_H #define MY_FILE_H #include #define MY_TYPE_FILE (my_file_get_type ()) #define MY_FILE(object) G_TYPE_CHECK_INSTANCE_CAST ((object), MY_TYPE_FILE, MyFile) #define MY_IS_FILE(object) G_TYPE_CHECK_INSTANCE_TYPE ((object), MY_TYPE_FILE)) #define MY_FILE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MY_TYPE_FILE, MyFileClass)) #define MY_IS_FILE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MY_TYPE_FILE)) #define MY_FILE_GET_CLASS(object) (\ G_TYPE_INSTANCE_GET_CLASS ((object), MY_TYPE_FILE, MyFileClass)) typedef struct _MyFile MyFile; struct _MyFile { GObject parent; }; typedef struct _MyFileClass MyFileClass; struct _MyFileClass { GObjectClass parent_class; }; GType my_file_get_type (void); #endif |
my-file.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 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 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | #include "my-file.h" G_DEFINE_TYPE (MyFile, my_file, G_TYPE_OBJECT); #define MY_FILE_GET_PRIVATE(object) (\ G_TYPE_INSTANCE_GET_PRIVATE ((object), MY_TYPE_FILE, MyFilePrivate)) typedef struct _MyFilePrivate MyFilePrivate; struct _MyFilePrivate { GString *name; GIOChannel *file; }; enum PropertyDList { PROPERTY_FILE_0, PROPERTY_FILE_NAME }; static void my_file_dispose (GObject *gobject) { MyFile *self = MY_FILE (gobject); MyFilePrivate *priv = MY_FILE_GET_PRIVATE (self); if (priv->file){ g_io_channel_unref (priv->file); priv->file = NULL; } G_OBJECT_CLASS (my_file_parent_class)->dispose (gobject); } static void my_file_finalize (GObject *gobject) { MyFile *self = MY_FILE (gobject); MyFilePrivate *priv = MY_FILE_GET_PRIVATE (self); g_string_free (priv->name, TRUE); G_OBJECT_CLASS (my_file_parent_class)->finalize (gobject); } static void my_file_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { MyFile *self = MY_FILE (object); MyFilePrivate *priv = MY_FILE_GET_PRIVATE (self); switch (property_id){ case PROPERTY_FILE_NAME: if (priv->name) g_string_free (priv->name, TRUE); priv->name = g_string_new (g_value_get_string (value)); priv->file = g_io_channel_new_file (priv->name->str, "a+", NULL); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void my_file_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { MyFile *self = MY_FILE (object); MyFilePrivate *priv = MY_FILE_GET_PRIVATE (self); switch (property_id){ case PROPERTY_FILE_NAME: g_value_set_string (value, priv->name->str); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void my_file_init (MyFile *self) { } static void my_file_class_init (MyFileClass *klass) { g_type_class_add_private (klass, sizeof (MyFilePrivate)); GObjectClass *base_class = G_OBJECT_CLASS (klass); base_class->set_property = my_file_set_property; base_class->get_property = my_file_get_property; base_class->dispose = my_file_dispose; base_class->finalize = my_file_finalize; GParamSpec *pspec; pspec = g_param_spec_string ("name", "Name", "File name", NULL, G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT); g_object_class_install_property (base_class, PROPERTY_FILE_NAME, pspec); g_signal_new ("file_changed", MY_TYPE_FILE, G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } void my_file_write (MyFile *self, gchar *buffer) { MyFilePrivate *priv = MY_FILE_GET_PRIVATE (self); g_io_channel_write_chars (priv->file, buffer, -1, NULL, NULL); g_io_channel_flush (priv->file, NULL); g_signal_emit_by_name(self, "file_changed"); } |
MyFile 类的使用者——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 31 32 33 34 35 36 37 | #include "my-file.h" static void file_print (gpointer gobject, gpointer user_data) { g_printf ("invoking file_print!\n"); } static void file_print_xml (gpointer gobject, gpointer user_data) { g_printf ("invoking file_print_xml!\n"); } static void file_print_tex (gpointer gobject, gpointer user_data) { g_printf ("invoking file_print_tex!\n"); } int main (void) { g_type_init (); MyFile *file = g_object_new (MY_TYPE_FILE, "name", "test.txt", NULL); g_signal_connect (file, "file_changed", G_CALLBACK (file_print), NULL); g_signal_connect (file, "file_changed", G_CALLBACK (file_print_xml), NULL); g_signal_connect (file, "file_changed", G_CALLBACK (file_print_tex), NULL); my_file_write (file, "hello world!\n"); g_object_unref (file); return 0; } |
虽然 GObject 子类化以及对象私有属性等知识均已有所介绍,但是上述的 MyFile 类的实现依然有许多细节需要加以解释。
首先,是在 MyFile 类的类结构题初始化函数 my_file_class_init 中,除了设置类属性之外,我们调用了 g_signal_new 函数用于建立 MyFile 类型与 "file_changed" 信号的关联。至于究竟是何种关联,那不是我们所关心的!还有,g_signal_new 函数的参数有很多,很复杂,推荐阅读文档 []。
其次,是 MyFile 对象的析构函数。在 my-file.c 源文件中,函数 my_file_dispose 与 my_file_finalize 构成了 MyFile 对象的析构函数,前者用于解除 MyFile 对象对其它对象(是指那些具有引用计数且被 GObject 库的类型系统所管理的对象)的引用,后者用于 MyFile 对象属性的内存释放。至于分何要分为两个阶段进行 GObject 子类对象析构以及相关细节知识,还是另外开一篇文章来讨论吧,否则问题会被越搞越复杂。或者,也可阅读文档 []。
小结当我刚开始写这篇文章的时候,我期望能够理清 GObject 信号与闭包的关系,但是现在不得不宣布很失败。还是冷静几天再卷土重来吧。
这篇文章,写了一整天。现在我不得不告诉你,其实 GObject 真的很复杂。不过,从我向自己抛出了第一个谎言之后,一直坚持到现在。尽管复杂,但是我们正在一点一点克服它。但是,最大的敌人不是 GObject,而是我自己。因为在这个过程中,我经常无法抗拒一种解剖 GObject 的欲望。它导致我经常陷入一个又一个的技术细节,而忘记了当初的目标。这种欲望之所以出现,是因为 GObject 是开源的,它赋予了我们每个人可以窥视它内部实现的权力。
我需要再次纠正一下认识。对于 GObject 牌的汽车,我现在只需要学习如何驾驶它,根本不需要去了解它的发动机是如何工作的。
参考文档[1]
[2]
[3] Objec memory management 的 Reference counts and cycles 部分