http://code.google.com/p/fcitx/issues/detail?id=528
这是起因,当然铺垫还有很多,我就懒得说了。
Glib 使用 C 实现 signal 的一个缺陷。具体来讲就是这么回事,在glib里面,很多时候处理signal,为了通用,以及能够使用同一个函数作为signal_connect,于是所有的函数指针都会转化为 (void*)(G_CALLBACK)
g_signal_connect(context->slave, "commit", G_CALLBACK(_slave_commit_cb), context);
那么问题在于,缺少对 callback 函数的合法性验证,可能会出现各种潜在的问题。在不是有意为之的前提下,程序员仍然会更容易犯错误。
拿pidgin里面的一个问题举例,pidgin的状态输入框,不知道有没有人注意到,在里面用backspace和输入法是很不正常的,会同时删掉输入法这边和pidgin里面两个字符。
为什么?明显是一个按键事件被搞了两次。
我的一个朋友花了一些时间去debug这个问题:
Finally I traced it down to the problem. 我本来是看见这么一行: g_signal_connect(G_OBJECT(gtkconv->entry), "key-release-event", G_CALLBACK(gtk_conv_key_press_cb), NULL); 去掉就没事了。 这个函数如下: static void gtk_conv_key_press_cb(GtkWidget *tv, GdkEventKey *event, gpointer data) { gboolean togglemenus = purple_prefs_get_bool(PREF_TOGGLE_MENUS); gboolean enabled = purple_prefs_get_bool(PREF_ENABLED); if(enabled && togglemenus){ if(event->state & GDK_CONTROL_MASK){ if(event->keyval == GDK_F11){ ToggleConvMenu(); } } } }
好吧,看起来很正常吗?编译器都会很happy的连warning都没有就run过去了。
但是实际上key-release-event的返回值是gboolean。
于是人民群众喜闻乐见的输入法bug来了。这个函数本来应当提供返回值,但是没有提供,在c里面这个指针在注册的时候是没有经过类型检查的,于是出现了一个本不应当注册的函数。但是在调用的时候,其实是预留了返回值的位置(由编译器),所以不会出现导致crash的错误,但是依然会导致这个返回值出于未定义状态。
好吧,那么当年scim和ibus为了从源头干掉这些中低水平程序员问题,于是用了gtk_key_snooper,使得所有gtk程序的按键事件先从im module这边走一圈,然后才会丢回给程序。于是今天我也给fcitx加了key snooper(要不然真没法活了),我本来对于程序和输入法之间的观点是,程序应该保持行为正常,输入法尽量不干预,即使我在最初实现fcitx的gtk的im module的时候可以实现它,我还是没去用,不过gtk程序们抱歉了,如果你们当中连gedit这种货色都敢来惹我,我没理由不相信类似情况是在gtk程序中普遍存在的,是你们先来惹我的。
但输入法本来提供的接口只有imcontext那些,这也就是感谢上苍还肯给输入法个活路。但是一个bug即使workaround掉了,还是一个bug。如同药片裹了糖衣,它还是药片,只不过……嗯,没那么难吃罢了。
其实是C的问题啦老K
@shellex 要么学习我和postgres的搞法,所有变量类型都需要手工读取,强迫去认真读文档,要么就直接用带类型的函数指针,或者我想到宏也可以担当此大任,如果要做类型检查这种事的话。
@csslayer 我们这边工程上也会大量地使用void*来简化接口。不过我自己实现类似的东西总是用固定的结构传递消息
@shellex 嘛,自己折腾爱用啥用啥啰。问题在于如果拿个库出来用也许可以提供更友好的方式啰?
我是觉得有个signature在头文件里不会好得多吗?
比如说最最简单的
typedef gboolean (*a_callback)(a* var1, b* var2)
#define signal_connect(a, sig, callback, type, arg) do {
type compile_test = callback;
signal_real_connect(a,, sig, (void*) callback, arg)
} while(0)
搞一些类似cxx模板型编译检查的trick。同时也允许直接用内部的不受检查的方式。
这里后半段我也有个处理返回值的trick嘛。
https://www.csslayer.info/wordpress/fcitx-dev/about-coding-style/
trick可以是可以,但是这样就多了一个参数了。人总是懒的,如果一种方法能少敲几个字母他们一定会信心满满地使用这种方法
@shellex 你可以参考我链接中后半段的方法,不用多参数,比如说
typedef gboolean (*callback_name_callback_type)(a* var1, b* var2)
#define signal_connect(a, sig, callback, arg) do {
sig##_callback_type compile_test = callback;
signal_real_connect(a, "##sig##", (void*) callback, arg)
} while(0)
gtk-doc 都写了一大票,我想不会在乎多写个typedef吧。
我记得Qt的文档里说过这个问题……说Qt用Signal/Slot机制的一个主要目的就是更安全,信号和槽发出和接受的参数都是明确的。