函数指针、回调函数与 GObject 闭包
本文首先复习一下基于 C 语言函数指针和回调函数的概念,进而学习 GObject 闭包的用法。这些知识都与面向对象程序设计基本上没有什么关系。
函数指针
所谓函数指针,就是可以指向函数的指针,例如:
int foo (void) { return 1; } int main (void) { int (*func) (void); func = foo; func (); return 0; }
代码中的 func 即为一个函数指针,它可以指向一个无参数且返回值为整型数的函数,还可以调用它。
只要不会将:
int (*func) (void);
与
int *func (void);
弄混(后者是返回值类型为整型数指针的函数),那么对于函数指针的理解就没什么大问题了。
由于 "int (*func) (void)" 这种声明函数指针的形式看上去有点怪异,所以很多人喜欢用 typedef 将函数值指针定义成类型以便使用,例如:
typedef int (*Func) (void); Func func = foo;
如果对于上述内容理解起来并不费劲,那么下面就可以谈谈回调函数了。
回调函数
在编写一些程序库的时候,设计者有时会认为有些功能,不应该由他自作主张,而应当由使用者来决定。这方面,比较有名的例子是 C 标准库中的 qsort 函数:
void qsort (void *base, size_t nmemb, size_t size, int (*compar) (const void *, const void *));
它的第 4 个参数即为函数指针,它可以指向形式为:
int foo (const void *param1, const void *param2);
的所有函数。
Q:这个函数指针的作用是什么?
A:它是用来指向用户提供的回调函数的。
Q:用户提供的回调函数是什么意思?
A:因为 qsort 的设计目标是对任意类型的同类数据集合进行快速排序,比如一组整型数的排序,或者一组字符串的排序。既然是排序,那么 qsort 必须知道如何判定具体类型的两个数据的大小,而 qsort 的设计者认为这一功能应当交给用户来实现,因为他并不清楚用户究竟要使用 qsort 对哪种类型的数据集合进行排序,只有用户自己清楚。
Q:用户提供的回调函数究竟是什么意思?
A:就是对于用户所要排序的数据集合而言,用户提供一个可以比较该数据集合中任意两个元素大小的函数,这个函数便是 qsort 的回调函数。
用户、被调用的函数 qsort 以及回调函数,它们之间的关系如下图所示:
下面是使用 qsort 函数进行字符串数组递增排序的示例:
#include <stdio.h> #include <stdlib.h> #include <string.h> static int str_compare (const void *s1, const void *s2) { char *str1 = *(char **)s1; char *str2 = *(char **)s2; size_t l1 = strlen (str1); size_t l2 = strlen (str2); if (l1 > l2) return 1; else if (l1 == l2) return 0; else return -1; } int main (void) { char *str_array[5] = {"a", "abcd", "abc", "ab", "abcde"}; qsort (str_array, 5, sizeof (char *), str_compare); for (int i = 0; i< 5; i++) printf ("%s ", str_array[i]); printf ("\n"); return 0; }
闭包(Closure)的概念
从上一节,我们通过函数指针向 qsort 函数传入了一个函数 str_compare,这个函数被称为回调函数,但是它还有一个比较深奥的名字——“闭包”。
所谓闭包,简而言之,就是一个函数加上它所访问的所有非局部变量,而所谓“非局部变量”,表示这些变量对于那个函数而言既非局部变量,也非全局变量。
我们向 qsort 传入函数 str_compare,它所接受排序数据集合中的 2 个元素,而且 2 个元素对于 str_compare 而言,既非是全局变量,也非其局部变量,因此 str_compare 与这 2 个参数形成了一个闭包。
在许多动态语言中,闭包通常也被昵称为“函数是第一类对象”,即函数与那些语言中基本类型具有相同的权利,例如函数可以存储在变量中,可以作为实参传递给其他函数,还可以作为其他函数的返回值。
恶魔来临
在 C 语言中,利用函数指针并配合参数的复制与传递,可模拟闭包这种结构,但是在可读性上没有那些内建支持闭包的语言优雅。
GObject 提供了 GClosure 对象与方法,实现了功能比较全面的 C 闭包模拟,我们可以在程序中直接使用它。下面,通过一个很小的示例,演示 GClosure 的使用。
先来看一个非 GClosure 的 C 闭包示例:
#include <stdio.h> #include <math.h> #include <string.h> typedef int (*Func) (void *, void *); static void compare (void *a, void *b, Func callback) { int r = callback (a, b); if (r == -1) printf ("a < b\n"); else if (r == 0) printf ("a = b\n"); else printf ("a > b\n"); } static int float_compare (void *a, void *b) { float *f1 = (float *)a; float *f2 = (float *)b; if (*f1 > *f2) return 1; else if (fabs (*f1 - *f2) <= 10E-6) return 0; else return -1; } static int str_compare (void *a, void *b) { size_t len1 = strlen ((char *)a); size_t len2 = strlen ((char *)b); if (len1 > len2) return 1; else if (len1 == len2) return 0; else return -1; } int main (void) { float a = 123.567; float b = 222.222; Func func = float_compare; compare (&a, &b, func); char *s1 = "hello world!"; char *s2 = "hello!"; func = str_compare; compare (s1, s2, func); return 0; }
上述代码主要实现了一个 compare 函数,它可以比较两个任意类型数据的大小,前提是你要向它提供特定的回调函数(闭包),例如代码中的 float_compare 与 str_compare 函数,它们分别实现了浮点数比较与字符串比较。
将上述程序改为 GClosure 实现,如下:
#include <math.h> #include <glib-object.h> void g_cclosure_user_marshal_INT__VOID_VOID (GClosure *closure, GValue *return_value G_GNUC_UNUSED, guint n_param_values, const GValue *param_values, gpointer invocation_hint G_GNUC_UNUSED, gpointer marshal_data) { typedef gint (*GMarshalFunc_INT__VOID_VOID) (gpointer data1, gpointer data2); register GMarshalFunc_INT__VOID_VOID callback; register GCClosure *cc = (GCClosure*) closure; register gpointer data1, data2; gint v_return; g_return_if_fail (return_value != NULL); g_return_if_fail (n_param_values == 1); if (G_CCLOSURE_SWAP_DATA (closure)) { data1 = closure->data; data2 = g_value_peek_pointer (param_values + 0); } else { data1 = g_value_peek_pointer (param_values + 0); data2 = closure->data; } callback = (GMarshalFunc_INT__VOID_VOID) ( marshal_data ? marshal_data : cc->callback); v_return = callback (data1, data2); g_value_set_int (return_value, v_return); } static void compare (GClosure *closure, void *b) { GValue return_value = {0}; GValue param_value = {0}; g_value_init (&return_value, G_TYPE_INT); g_value_init (¶m_value, G_TYPE_POINTER); g_value_set_pointer (¶m_value, b); g_closure_invoke (closure, &return_value, 1, ¶m_value, NULL); gint r = g_value_get_int (&return_value); if (r == -1) g_print ("a < b\n"); else if (r == 0) g_print ("a = b\n"); else g_print ("a > b\n"); g_value_unset (&return_value); g_value_unset (¶m_value); } static gint float_compare (void *a, void *b) { gfloat *f1 = a; gfloat *f2 = b; if (*f1 > *f2) return 1; else if (fabs (*f1 - *f2) <= 10E-6) return 0; else return -1; } static gint str_compare (void *a, void *b) { size_t len1 = g_utf8_strlen ((gchar *)a, -1); size_t len2 = g_utf8_strlen ((gchar *)b, -1); if (len1 > len2) return 1; else if (len1 == len2) return 0; else return -1; } int main (void) { g_type_init (); gfloat a = 123.567; gfloat b = 222.222; GClosure *closure = g_cclosure_new (G_CALLBACK (float_compare), &a, NULL); g_closure_set_marshal (closure, g_cclosure_user_marshal_INT__VOID_VOID); compare (closure, &b); g_closure_unref (closure); gchar *s1 = "Hello World!\n"; gchar *s2 = "Hello!\n"; closure = g_cclosure_new (G_CALLBACK (str_compare), s1, NULL); g_closure_set_marshal (closure, g_cclosure_user_marshal_INT__VOID_VOID); compare (closure, s2); g_closure_unref (closure); return 0; }
很恐怖,看上去 GClosure 并没有简化闭包的实现,反而将其更加复杂化了,比如多出了 GClosure、GValue 等数据类型,还多出来一个 my_cclosure_user_marshal_INT__VOID_VOID 函数,而且 compare 函数的第 1 个参数的类型也变成 GClosure 指针类型了。
理解 GClosure
对于上一节所给出的 GClosure 的应用示例,我们从 main 函数入手解读一下。
首先看下面的代码:
/* main 函数中的代码 */ gfloat a = 123.567; gfloat b = 222.222; GClosure *closure = g_cclosure_new (G_CALLBACK (float_compare), &a, NULL); g_closure_set_marshal (closure, g_cclosure_user_marshal_INT__VOID_VOID); compare (closure, &b); g_closure_unref (closure);
g_cclosure_new 函数创建了一个面向 C 语言的闭包。之所以在此强调是面向 C 语言,那是因为 GObject 的闭包是面向任意语言的,例如 pyg_closure_new 函数可以创建 python 语言的闭包。
g_cclosure_new 函数所创建的 C 语言闭包结构体是 GCClosure,其定义如下:
typedef _GCClosure GCClosure; struct _GCClosure { GClosure closure; gpointer callback; };
联想一下 GObject 的子类化,上述代码暗示着 GCClosure 是 GClosure 的子类,不过这里没有类结构体以及实例化等概念,仅仅是结构体的继承。
在阅读下面的内容之前,一定要注意分辨 GCClosure 与 GClosure,它们的名字太相似了。
GCClosure 结构体包含了一个 GClosure 结构体 closure 以及一个无类型的数据指针 callback(因为 gpointer 是 void * 的别名)。前者是 GObject 提供的闭包结构,后者是一个指向回调函数的指针。这里一定要注意,用无类型指针指向一个函数,是 GObject 设计的一个缺陷,因为它假定了数据类型的指针与函数指针的存储宽度是相等的,虽然尽管目前大部分 PC 的 CPU 支持这一假设,但是在 C99 标准里,使用无类型的数据指针指向函数,这属于未定义的行为。
GClosure 结构体主要是面向各种语言对闭包的公有功能进行了基本抽象,这样所有要与 GObject 闭包机制打交道的语言,可以先继承 GClosure 结构体,然后再根据自身需要再添加一些特定的数据成员。例如,GCClosure 添加了 callback 无类型数据指针,用于指向闭包所对应的回调函数。
GClosure 结构体中包含许多成员,其中对于使用者较为重要的三个成员是 marshal、data 与 marshal_data,其中 marshal 是一个函数指针,指向一个回调函数的调用者;data 与 marshal_data 皆为 gpointer 指针类型。对于 C 语言闭包,data 指向用户向回调函数传递的数据地址,而 marshal_data 则指向回调函数。我知道,这句话也许会引起下面的问答。
Q:那个 GCClosure 结构体中不是已经有了一个指向回调函数的 callback 指针吗?
A:这是因为callback 所指向的函数是闭包默认的回调函数,而 GObject 库允许你自由切换回调函数,可将 marshal_data 看作是一个开关,它可以暂时屏蔽 GCClosure 中的 callback 所指向的回调函数,而启用 marshal_data 所指向的回调函数。事后,可以调用 g_closure_set_meta_marshal 函数将 marshal_data 所指向的回调函数屏蔽,这样闭包的回调函数又被切换为 callback 所指向的回调函数了。下文会给出一个具体的示例来验证这一说法。
从 GObject 库使用者的角度,我们对 GClosure 结构体作用的理解在此止步即可。如果像文档 [1] 那样的深入挖掘,那么就好比是一个普通人在不具备医生心理素质的情况下解剖任何一种动物,恶心是难免的。
我们将话题转回刚才所讨论的 main 函数中的代码片段。看下面的代码:
GClosure *closure = g_cclosure_new (G_CALLBACK (float_compare), &a, NULL); g_closure_set_marshal (closure, g_cclosure_user_marshal_INT__VOID_VOID);
它们一起实现了下图所示的数据结构:
紧接着,将所建立的闭包 closure 作为第一个参数,浮点数变量 b作为第 2 个参数,代入函数 compare,即:
compare (closure, &b);
因为 closure 中即包含了浮点数变量 a,也包含了浮点数比较函数 float_compare,因此在 compare 函数中,使用 float_compare 函数可进行浮点数 a 与 b 大小比较,是显而易见的。但事实上,在 compare 函数中,我们并没有直接调用 float_compare 函数,而是将这个任务交给了 g_closure_invoke 函数。
来看 compare 函数的主干部分:
static void compare (GClosure *a, void *b) { ... ... g_closure_invoke (a, &return_value, 1, ¶m_value, NULL); gint r = g_value_get_int (&return_value); ... ... }
由于 compare 函数的第一个参数是闭包 closure,它已经包含了浮点数变量 a 和回调函数 float_compare。当我们将 closure 作为 g_closure_invoke 函数的参数时,后者会基于 closure 中的 marshal 指针调用 g_closure_user_marshal_INT__VOID_VOID 函数,而 g_closure_user_marshal_INT__VOID_VOID 函数是我们自己定义的,对于本文示例而言其主干部分如下:
void g_cclosure_user_marshal_INT__VOID_VOID ( ... ) { ... ... ... if (G_CCLOSURE_SWAP_DATA (closure)) { data1 = closure->data; data2 = g_value_peek_pointer (param_values + 0); } ... ... ... callback = (GMarshalFunc_INT__VOID_VOID) ( marshal_data ? marshal_data : cc->callback); v_return = callback (data1, data2); ... ... ... }
上述代码所做的工作无非是从 closure 闭包中先获取浮点数变量 a,即 data1,然后从 g_cclosure_user_marshal_INT__VOID_VOID 函数的参数中获取浮点数变量 b,即 data2,然后再从 closure 闭包中获得 callback 指针所指向的回调函数,即 float_compare,最终完成浮点数 a 与 b 的大小比较。
闭包调用的整个过程大致如下图所示:
GClosure 闭包的工作过程非常繁琐,它所做的主要工作就是向回调函数的调用过程中插入两个环节,即 g_closure_invoke 函数与 g_closure_user_marshal_*__** 函数。这样做的主要目的是提高闭包的灵活性。就像是链条,如果只有 2 个节点,那么它只能像一根直线段那样生存,如果将其改造成 4 个节点,它就变成了一条 3 次曲线了!
更换 marshal
前文说过,GClosure 结构体中有一个 marshal_data 指针,它也可以指向一个回调函数,并且会取代 GCClosure 结构体中 callback 指针所指向的回调函数。为了充分说明这一点,我们对前文的 GClosure 示例进行一些修改,如下:
#include <math.h> #include <glib-object.h> void g_cclosure_user_marshal_INT__VOID_VOID (GClosure *closure, GValue *return_value G_GNUC_UNUSED, guint n_param_values, const GValue *param_values, gpointer invocation_hint G_GNUC_UNUSED, gpointer marshal_data) { typedef gint (*GMarshalFunc_INT__VOID_VOID) (gpointer data1, gpointer data2); register GMarshalFunc_INT__VOID_VOID callback; register GCClosure *cc = (GCClosure*) closure; register gpointer data1, data2; gint v_return; g_return_if_fail (return_value != NULL); g_return_if_fail (n_param_values == 1); if (G_CCLOSURE_SWAP_DATA (closure)) { data1 = closure->data; data2 = g_value_peek_pointer (param_values + 0); } else { data1 = g_value_peek_pointer (param_values + 0); data2 = closure->data; } callback = (GMarshalFunc_INT__VOID_VOID) ( marshal_data ? marshal_data : cc->callback); v_return = callback (data1, data2); g_value_set_int (return_value, v_return); } static void compare (GClosure *closure, void *b) { GValue return_value = {0}; GValue param_value = {0}; g_value_init (&return_value, G_TYPE_INT); g_value_init (¶m_value, G_TYPE_POINTER); g_value_set_pointer (¶m_value, b); g_closure_invoke (closure, &return_value, 1, ¶m_value, NULL); gint r = g_value_get_int (&return_value); if (r == -1) g_print ("a < b\n"); else if (r == 0) g_print ("a = b\n"); else g_print ("a > b\n"); g_value_unset (&return_value); g_value_unset (¶m_value); } static gint str_compare (void *a, void *b) { size_t len1 = g_utf8_strlen ((gchar *)a, -1); size_t len2 = g_utf8_strlen ((gchar *)b, -1); if (len1 > len2) return 1; else if (len1 == len2) return 0; else return -1; } static gint str_compare_new (void *a, void *b) { g_print ("\nI'm a new marshaller\n"); return (str_compare (a, b)); } int main (void) { g_type_init (); gchar *s1 = "Hello World!\n"; gchar *s2 = "Hello!\n"; GClosure *closure = g_cclosure_new (G_CALLBACK (str_compare), s1, NULL); g_closure_set_marshal (closure, g_cclosure_user_marshal_INT__VOID_VOID); compare (closure, s2); g_closure_set_meta_marshal (closure, str_compare_new, g_cclosure_user_marshal_INT__VOID_VOID); compare (closure, s2); g_closure_unref (closure); return 0; }
上述代码所做的修改,主要是删除了有关浮点数比较的示例部分,然后添加了一个新函数 str_compare_new,并对 main 函数中字符串比较部分的代码做了变动,重点观察以下代码:
/* main 函数代码片段 */ GClosure *closure = g_cclosure_new (G_CALLBACK (str_compare), s1, NULL); g_closure_set_marshal (closure, g_cclosure_user_marshal_INT__VOID_VOID); compare (closure, s2); g_closure_set_meta_marshal (closure, str_compare_new, g_cclosure_user_marshal_INT__VOID_VOID); compare (closure, s2);
第一次调用 compare 函数,闭包函数调用链的终端是 str_compare 函数,而第二次调用 compare 函数时,闭包函数调用链的终端是 str_compare_new 函数,它再去调用 str_compare 实现字串比较。这是因为,在第二次调用 compare 函数之前,我们通过 g_closure_set_meta_marshal 函数设置了 closure 闭包的 marshal_data 指针,使其指向了 str_compare_new 函数。这样,在 g_cclosure_user_marshal_INT__VOID_VOID 函数中,代码:
callback = (GMarshalFunc_INT__VOID_VOID) (marshal_data ? marshal_data : cc->callback);
callback 的值会是 marshal_data,而非 cc->callback。
如何又快又好的产生 g_cclosure_user_marshal_* 函数?
GLib 库提供了一个名为 glib-genmarshal 的工具,它可以根据我们给出的函数描述信息产生有效的 marshal 代码。上文中的 g_cclosure_user_marshal_INT__VOID_VOID 函数,我便是使用这个工具产生的。
首先,准备好一份文本文档,例如 in__void_void.txt:
INT:VOID,VOID
然后,执行命令:
$ glib-genmarshal --body int__void_void.txt > int__void_void.c
即可产生 g_cclosure_user_marshal_INT__VOID_VOID 函数。
另外,GObject 也预定义了一组 marshal 函数,可参考文档 [2] 中所有 g_cclosure_marshal_ 为前缀的函数。
参考文档
转载时,希望不要链接文中图片,另外请保留本文原始出处:http://garfileo.is-programmer.com
2011年3月23日 13:49
"用无类型指针指向一个函数,是 GObject 设计的一个缺陷"
个人很不赞同这个说法,这是GCC支持的,虽然和某C标准不同
但GCC却是默认就支持void*类型和任意指针型的双向转换,还支持void*类型的运算
从这一点来说,某些万恶的教科书让我非常鄙视
如果是百分百严格按C**标准来,GTK很可能无法被编译
个人的观点是跟着GNU走比跟着C**标准走要实际
2011年3月23日 14:19
嗯,我也很希望 void * 可以指向函数,但是某些单片机的设计者不喜欢这个,例如Microchip 公司的 PIC16 芯片的程序指令是14位宽度,而数据是8位宽度。
2011年3月23日 15:16
这点不知是不是我的理解有问题,还是咱们说的针对的东西不一样
我的理解是一个函数在汇编里是类似标号的东西,并不是拿某void *去直接调用指令,而是用其实现转移,不是用void*类型去调用
印象中avr指令宽度是16为,IA32好像是变长的,
即使像某些不具备虚拟方式的单片机,其最终的全局跳转也可以用专有的指令或是编译器来实现
可能我的理解有些不对,不过我还是觉得void*跟指令宽度关系不是直接的
2011年3月23日 15:47
我对底层的理解比较浅薄,只是担心把一个函数的地址保存在 void * 指针变量中,地址会被截断。
2011年3月23日 21:53
应该不会,void*是指针就决定他要指向某个段,这个对于某个器件必须是定长的,至少我知道的都是.
而指令宽度无所谓了,最后会把代码段数据段等的内容按照一定格式装进去[由汇编器完成],所有的东西最终指令都是过程化的(即使有多核多线程乱序等技术,对于某一局部也是过程化的,因为目前的计算机都是这样设计的)
个人觉得早期的编译器是为了保证代码质量而限制了一些功能比如类型转换,不过个人觉得这对于C语言没必要,因为C本身就以开放简单低级而闻名,一个C的程序员可以不了解高级的语法和模式,但必须清楚内存的分布,必须自己保证强制转换的正确性.
而像C++等,类型转换等就是有必要了,因为编译器隐藏了太多的细节,比如一个类赋值时默认的拷贝方法,如果没有编写,用默认的可能就行不通,一些限制就是必须.
总体来说个人觉得目前的GCC无愧于C编译器的王者,取舍有度,简单易用.
2011年8月19日 10:46
文章不错,我会经常来学习的。:D
2012年3月21日 23:14
求教这图是用啥工具画的?
2012年3月29日 08:30
@jzh: vue
2012年7月31日 22:53
@Garfileo: 其實我在我的項目裏面用了個 workaround
(Func*)((intptr_t)ptr)