为什么Gtk程序总爱给我找麻烦

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。如同药片裹了糖衣,它还是药片,只不过……嗯,没那么难吃罢了。

This entry was posted in fcitx development. Bookmark the permalink.

7 Responses to 为什么Gtk程序总爱给我找麻烦

  1. shellex says:
    Google Chrome 19.0.1041.0 GNU/Linux x64

    其实是C的问题啦老K

  2. csslayer says:
    rekonq GNU/Linux x64

    @shellex 要么学习我和postgres的搞法,所有变量类型都需要手工读取,强迫去认真读文档,要么就直接用带类型的函数指针,或者我想到宏也可以担当此大任,如果要做类型检查这种事的话。

  3. shellex says:
    Google Chrome 19.0.1041.0 GNU/Linux x64

    @csslayer 我们这边工程上也会大量地使用void*来简化接口。不过我自己实现类似的东西总是用固定的结构传递消息

  4. csslayer says:
    Firefox 12.0a2 Windows 7

    @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/

  5. shellex says:
    Google Chrome 19.0.1041.0 GNU/Linux x64

    trick可以是可以,但是这样就多了一个参数了。人总是懒的,如果一种方法能少敲几个字母他们一定会信心满满地使用这种方法

  6. csslayer says:
    Firefox 12.0a2 Windows 7

    @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吧。

  7. 右京样一 says:
    Google Chrome 17.0.963.78 GNU/Linux

    我记得Qt的文档里说过这个问题……说Qt用Signal/Slot机制的一个主要目的就是更安全,信号和槽发出和接受的参数都是明确的。

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.