GIMP插件结构

GIMP是基于插件的结构,插件结构的特点是核心部分的内容并不多,更多的功能是通过插件完成的。GIMP是一个图象处理程序,支持很多图象处理的方法,而这些方法都是以插件的形式出现的。其架构如下图所示:

GIMP在它的开发者网站上提供了一套插件编写教程,引导插件新手入门:How to write a GIMP plug-in,第一部分,第二部分,第三部分。

我们一起来看一下,GIMP是如何实现插件结构的。上面的教程对插件结构有个概要的介绍。

先来看一下GIMP插件的存在形式。为了辅助构建GIMP插件,GIMP提供了一个工具——gimptool,实际上它就是对构建插件过程的封装,省去了自己寻找相应软件包的烦恼。运行完gimptool,我们会在gimp插件的目录(通常是~/.gimp-2.0/plug-ins)下发现我们的插件。实际上,它就是一个标准应用程序,而不是一个库。它甚至可以单独运行,当然,在一无所知的情况下运行它,它会告诉你,我要运行在GIMP下。随便查看一个GIMP插件,甚至是上面提到的那个教程,你会发现这样一个宏调用:MAIN()。翻看源码,在$GIMP/libgimp/gimp.h中,我们找到了它,简化之:#  define MAIN()        int          main (int argc, char *argv[])      {            return gimp_main (&PLUG_IN_INFO, argc, argv);    }

前面说过,插件是一个应用程序,这里提供了最好的证据。GIMP插件的运行逻辑都隐藏在gimp_main($GIMP/libgimp/gimp.c)中。

接下来,再来看看GIMP的主程序是如何与插件打交道的。

既然插件以应用程序的形式存在,那么主程序调用插件功能也就是调用另外一个程序完成工作。完成打开二者之间通道的工作在plug_in_open($GIMP/app/plug-in/plug-in.c)中完成。可以看到,这段代码的主要工作就是为调用插件程序设置相关参数,之后以g_spawn_async开启通往插件的通道。g_spawn_async是glib提供的一个函数,用以创建一个新的进程。

通道打开了,那接下来的工作就是二者之间如何通信。通信自然要有通信的协议,对于调用程序来说,最简单的通信协议就是调用程序的参数。GIMP插件编写通常要指定四个函数,分别是用作初始化、退出、查询和运行用,其中运行部分是整个插件中最核心的部分。回到gimp_main的实现中,我们便不难发现,初始化和查询用到的调用方法就是程序参数的形式。这是最为简单的方式,当然,不足以解决所有的问题,比如查询的结果如何送回。

其实这是一个进程间如何通信的问题。在*nix下,进程间通信的方式有很多,GIMP选择的是管道(pipe)。回到plug_in_open中,在设置参数的时候,打开了两个管道,分别用于读写。当然,随后它又用glib的io_channel对管道进行封装。随后,再把两个管道的文件描述符(实际上就是个整数)作为调用参数传递给插件程序。插件程序得到描述符之后,在自己这端将管道打开,于是两端便建立起连接。注意,管道是个半双工的,也就是一个管道只能读或是写,所以,通常创建管道时,得到便是两个描述符,一个用于读一个用于写。为了实现主程序和插件之间的双向通信,同时打开了两个管道,也就是得到了四个描述符,两个作为参数传递给插件程序,另外两个留给了主程序自己。同上面的开启进程相比,管道才是二者之间真正的通信渠道。

一样的讨论,通道打开了,接下来就是通信协议。二者之间的通信协议选择了二进制的方式,这一点在开启管道时设置了二进制的访问方式可见一斑。文本化还是二进制是一个设计上的抉择,这里选择了二进制的方式,也许运行时效率是一个重要的考虑因素,网络通信的瓶颈——传输带宽在这里已经不是问题了。有一个没有想清楚的问题是这种进程间通信的方式是否还需要如网络通信一样查看中间流过的数据,如果需要,又如何做呢?

主程序和插件之间通信协议被抽象为消息模型,所以,两端的处理看上去是一个典型的消息处理过程:在一个大循环中,取消息,处理消息。插件端的处理参见gimp_loop($GIMP/libgimp/gimp.c),主程序端的处理参见plug_in_handle_message($GIMP/app/plug-in/plug-in-message.c)。在取消息的程序(wire_read_msg,$GIMP/libgimpbase/gimpwire.c)中,我们看到也是一个类似于消息处理的流程,先取出一个消息类型,然后,根据消息类型找到相应的处理函数,把该消息的其它部分读出来。对比写入消息的过程(wire_write_msg,$GIMP/libgimpbase/gimpwire.c),整个消息发送和接收便一目了然了。由此,我们可以看出,如果需要对通信协议进行扩展,要做的就是添加一个消息类型,在增加对应的处理函数。

仅仅是消息级的通信显得还是有些低级,所以,在此基础上,GIMP还提供了函数级通信。我们知道GIMP为编写插件提供了不少API,而有些API需要和主程序交互方能体现其作用,比如,如果需要某个插件处理一副图像,插件本身并没有图像,从何而来,它要调用一个函数得到,而这个函数实际上是通过与主程序交互得到。这些函数借助就是已有的消息级通信完成,而对于插件的编写者来说,一切都是透明的。如同网络协议的七层模型,这里也是通过一层层的构建起一个完整的体系,一个清楚的设计。有了函数调用的方法,通信的可扩展性便大大增强。

为了相应插件的请求,主程序也应该有所表示。主程序专门为此建立了一个库,这就是架构图上的那个PDB(过程数据库),每当发来运行过程的请求,主程序便会在PDB中查找相应的函数进行调用(plug_in_handle_proc_run,$GIMP/app/plug-in/plug-in-message.c)。这大名鼎鼎的PDB实际上就是一个hash表($GIMP/app/pdb/procedural_db)。

上面简要介绍了GIMP插件结构的实现,这便是最近学习GIMP的一些心得,不见得完全准确,源代码是最好的答案。跨越时间的程序都有许多值得学习的东西,十多岁的GIMP也在其中。顺便说一下,GIMP的实现很清楚,虽然是用C实现,但模块划分得非常清楚。语言限制的只是表达能力而已,而非思想。

Category

Archives