函数指针、回调函数与 GObject 闭包
GObject 子类对象的析构过程

GObject 的信号机制——概览

Garfileo posted @ 2011年3月21日 20:16 in GObject 笔记 with tags glib GObject gsignal , 18005 阅读

手册所述,GObject 信号(Gignal)主要用于特定事件与响应者之间的连接,它与操作系统级中的信号没有什么关系。例如,当我向一个文件中写入数据的时候,我期望能够有一个或多个函数响应这个“向文件写入数据”的事件,这一期望便可基于 GObject 信号予以实现。

为了更好的理解 GObject 信号机制的内幕,我们需要从回调函数开始。

基于回调函数与可变参数的事件响应

首先,写出事件的制造者,它是一个向文件写入数据的函数 file_write:

#include <stdio.h>

void
file_write (FILE *fp, const char *buffer)
{
        fprintf (fp, "%s\n", buffer);
}

向文件写入数据完毕之后,我们希望有一个函数能够将文件全部的内容在终端打印出来,所以我们又增加了一个函数 file_print,并对 file_write 函数进行一点修改:

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 或者别的甚么格式打印出来呢?

为了应对更多的使用者的需求,我们需要使用回调函数来隔离变化,例如:

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 函数定义为:

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 函数的时候,可传递多个函数供其调用,例如:

file_write (fp, "Hello world!", 
            file_print, 
            file_print_xml, 
            file_print_tex, 
            NULL);

基于回调函数与可变参数实现特定“事件”的多个“响应”,这种方案是最有效的,但不是最好的。例如,受到函数栈空间的大小限制,可变参数用尽之时。此外,这种方式使用起来也不够直观。

基于 GObject 信号的事件响应

对于上一节的示例所解决的问题,基于 GObjet 信号的解决方案大致像下面这样:

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 函数中,文件数据写入操作完毕后,就这一事件向外发射一个“CHANGED”信号,告诉所有响应者,文件内容改变了。至于哪些函数是这一信号的响应者,file_write 函数不必知道。
  • file_write 函数的使用者,如果希望哪些函数用于响应 file_write 函数修改文件内容这一事件,那么就使用 g_signal_connect 函数(实际上它是一个宏)将响应函数与信号挂接到一起。这样,一旦事件的对应信号被 g_signal_emit 所发射,这些响应函数便会被自动调用。

为了实现上述的“信号/响应”模拟,那么 file_write 函数的参数便不可能再是 FILE 类型的文件指针了,而是我们自定义的 File 类型的对象,其中封装了“信号/响应”功能。事实上,GObject 类的内部便封装了这些功能,所有经由 GObject 子类化而产生的对象,便可拥有这些功能。

GObject 子类对象的信号处理

首先,我们定义 GObject 子类 MyFile。这个过程,我们应当已经不再陌生,参考文档 [1]。

my-file.h 头文件内容如下:

#ifndef MY_FILE_H
#define MY_FILE_H

#include <glib-object.h>

#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 源文件内容如下:

#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 文件内容如下:

#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 函数的参数有很多,很复杂,推荐阅读文档 [2]。

其次,是 MyFile 对象的析构函数。在 my-file.c 源文件中,函数 my_file_dispose 与 my_file_finalize 构成了 MyFile 对象的析构函数,前者用于解除 MyFile 对象对其它对象(是指那些具有引用计数且被 GObject 库的类型系统所管理的对象)的引用,后者用于 MyFile 对象属性的内存释放。至于分何要分为两个阶段进行 GObject 子类对象析构以及相关细节知识,还是另外开一篇文章来讨论吧,否则问题会被越搞越复杂。或者,也可阅读文档 [3]。

小结

当我刚开始写这篇文章的时候,我期望能够理清 GObject 信号与闭包的关系,但是现在不得不宣布很失败。还是冷静几天再卷土重来吧。

这篇文章,写了一整天。现在我不得不告诉你,其实 GObject 真的很复杂。不过,从我向自己抛出了第一个谎言之后,一直坚持到现在。尽管复杂,但是我们正在一点一点克服它。但是,最大的敌人不是 GObject,而是我自己。因为在这个过程中,我经常无法抗拒一种解剖 GObject 的欲望。它导致我经常陷入一个又一个的技术细节,而忘记了当初的目标。这种欲望之所以出现,是因为 GObject 是开源的,它赋予了我们每个人可以窥视它内部实现的权力。

我需要再次纠正一下认识。对于 GObject 牌的汽车,我现在只需要学习如何驾驶它,根本不需要去了解它的发动机是如何工作的。

参考文档

[1] 温故而知新

[2] 对 g_signal_new () 参数的解释

[3] Objec memory management 的 Reference counts and cycles 部分

转载时,希望不要链接文中图片,另外请保留本文原始出处:http://garfileo.is-programmer.com

Avatar_small
doyle 说:
2011年3月22日 09:19

文档2中描述与本文代码实现不符.求解释.
另:有搭建gobject开发环境的文档么?

Avatar_small
Garfileo 说:
2011年3月22日 10:19

是相符的。本文的 g_signal_new 函数的最后一个参数为 0,表示没有额外的参数传递给回调函数,所以后面的可变参数便不需要再写了。

gobject 开发环境是指什么?

Avatar_small
doyle 说:
2011年3月22日 16:40

不相符是指这个:分隔符可以是"-"或"_"——事实上,系统会先调用g_strdelimit把"_"转化为"-"再存储signal_name。因此,在调用g_singal_emit_by_name时,detailed_signal参数中的分隔符必须是"-"。
算了,这个我自己去试试就知道了。

开发环境是指:
虽然知道在linux上用gcc可以编译,但是作为在win上成长起来的程序员表示对gcc那么多的选项有压力。
那么,当我要写GObject程序时,我在编译时的命令到底看上去是什么样子的,哪些库文件是需要被引用的。

但是在我自己清晰的描述了自己的问题后,我觉得这个问题根本和这个系列无关……我还是自觉的去看man吧。呵呵

Avatar_small
Garfileo 说:
2011年3月22日 16:50

编译的问题,在前面的文章提到过,库文件和头文件的位置,用 pkg-config 工具来查找。对于使用 gobject 库的程序,可以:

gcc $(pkg-config --cflags --libs gobject-2.0) your-code.c -o your-app

Avatar_small
Garfileo 说:
2011年3月22日 17:06

至于那个 detailed_signal 参数,是不能基于分隔符来理解。最近还得加一篇文档来解释这个,因为信号与闭包的关系还没有涉及呢。

Avatar_small
pingf 说:
2011年3月23日 13:13

win下可以用MinGW套件[依旧是gcc,注意要把msys和binutils装全了,这样相当一部分类库都可以编译,包括Gtk,Clutter这样的库都可以在win下用MinGW来编译,如果你用Qt的话,官方SDK好像有MinGW的官方支持]
到GTK官网有针对win32的编译好的包,不过gtk版本没有linux的新.
编译方式上和linux下完全一致,无论用makefile或是pkg-config,MinGW都有的

Avatar_small
pingf 说:
2011年3月23日 13:17

另外Gtk在win32默认界面比较"复古",一般win下的程序都选择gtk-whimp风格的主题
该风格至少在win下看着还说的过去

Avatar_small
pingf 说:
2011年3月23日 13:26

个人认为GObject 信号,闭包,编组器是GObject最复杂的地方
而最最复杂的个人认为是其信号传递机制的实现(信号的声明和使用还勉强可以接受),看源码会让人吐血......

Avatar_small
Garfileo 说:
2011年3月23日 13:39

所以,源码还是留给 gobject 开发/维护者去看好了。

signal 机制的内幕,我觉得对生活影响不大,只需要知道信号如何定义与使用就差不多了,通常是在 GTK+ 自定义 widget 的时候用一下。

我觉得对于一般用户而言,GType 的类型识别、GObject 子类化、接口、closuer 和 marshal 这几个是可以考虑在自己的 C 程序中使用的。一方面可以让程序框架更干净,另一方面不至于换一种 OO 语言。

黑暗诗人 说:
2011年10月13日 15:29

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;
中的(问题1)set_property、get_property、dispose、finalize是属于Gobject类的成员吗,是不是像事件expose_event一样定义好的?比如下面的代码,
GtkWidgetClass *widget_class = (GtkWidgetClass*)klass;
widget_class->expose_event = gtk_ns3_stage_expose;
(问题2)要是是内部定义的话在哪里去找其定义呢
(问题3)还有GtkObject也是继承于gobject吧
您前面的文章我都看完了,但是我还是没能理解这里

Avatar_small
Garfileo 说:
2011年10月13日 15:47

@黑暗诗人:
(1)和(2):set_property, get_property 函数在『GObject 子类私有属性的外部访问』那篇文章里说过了,至于 dispose 与 finalize 函数见『GObject 子类对象的析构过程』。

(3)是阿。

casinee 说:
2015年1月09日 10:36

“对于 GObject 牌的汽车,我现在只需要学习如何驾驶它,根本不需要去了解它的发动机是如何工作的”,哈哈哈


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter