zhumao

新手上路

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
在 Perl 中使用内联
嵌入非 Perl 代码时不必再有使用 XS 的烦恼

Michael Roberts
Owner, Vivtek
2001 年 6 月

新的 Perl 内联模块允许您使用其他语言编写代码(如C、Python、Tcl 或 Java),并将其随意地放进 Perl 脚本中。不像以前将C 语言代码与 Perl进行连接的方法那样,内联的使用将非常简便,尤其是在与 Perl体系保持一致方面更加出色。内联的一个非常实用的地方是可以依据 C语言的库编写出快速包装代码并将在 Perl上使用它,这样(就我而言)就可以将 Perl变成世界上 最好的测试平台。

Perl 一向是比较折衷的,这样显得它很可怜,但是直到今天,要让 Perl 使用其它语言或不是特别为它构造的库并非很容易。您必须用 XS 语言来编写接口代码(或让 SWIG 来帮您编写),接着构造一个有序的模块,随后通常还有很多细节需要处理。

但是现在情况就不一样了。由 Brian Ingerson 编写并积极( 非常积极)维护的内联模块提供了将其它语言绑定到 Perl 上的工具。此外,如果在 Perl 文件中有需要出现、创建和以一种完全透明的方式动态装载其它语言的地方,Perl 的子模块(Inline::C、Inline::Python、Inline::Tcl、Inline::Java、Inline::Foo 等)允许您 直接在 Perl 文件中嵌入那些语言。使用您脚本的用户不会看出其中的不同,只不过第一次调用使用内联的代码时,程序将多花一点时间来完成对嵌入代码的编译。

世界上最简单的 Inline::C 程序
为了向您说明我的用意,让我们来看看最简单可行的内联程序;这个程序使用了内嵌的 C 语言函数,不过您完全可以用其它支持内联的语言来实现同样的功能。

清单 1. 内联 "Hello, world"
use Inline C => <<'END_C';
    
void greet() {
  printf("Hello, world!\n");
}
END_C

greet;

当然,这段代码的 功能很明显。它定义了一个 C 语言的函数来完成预期的任务,然后又将它作为一个 Perl 函数来处理。换句话来说, 内联所实现的恰好就是一个扩展模块所应该实现的内容。也许您心中最想问的问题就是,“它是怎样 实现的?”其实答案和您预计的差不多:内联得到您的 C 语言代码,然后在它基础上构建一个 XS 文件,就像一个人工的扩展模块编写器所执行的一样,接着创建那个模块,然后装载它。接下来的代码调用就会很容易找到以前构建过的那个模块,然后直接装载它。

您甚至可以使用 Inline->bind bind 函数,从而在程序运行时调用内联。在此除了告诉您这个事实,我并不想再做任何其它事,因为它并没有什么特别的地方,只不过如果您想实现它,您就可以做到。

与 XS 和 SWIG 相比较
与不使用内联的同样接口相对比。如果您要从 Perl 中调用 C 语言代码,您必须使用 h2xs 创建一个模块。然后再编写 XS 代码( xsubpp 用此代码来编写可以兼容所有 Perl 变量转换和诸如此类事情的 C 语言代码)。 用户必须取得您的模块,然后运行 Makefile.PL 来创建一个 Makefile,再运行 make 来对它进行编译、测试和安装。只有这样,使用这个模块的脚本才能得以正确运行。

在这整个的过程中,SWIG 是一个非常有用的工具,它可以被用来为您生成大量的 XS 代码。但是,SWIG 分析 C 语言头文件的能力并不是很强,所以它能做的也很有限。对于更复杂的项目来说,您必须手工编码至少一部分函数,以便让 SWIG 知道该做什么。然后,如果您希望完成任何的特殊处理(也许您希望有一个简单的 Perl 函数,它可以根据您传递的参数数目或种类来调用不同的 C 语言函数),您就可以最终更改它的输出。

但这样更糟,是真的。XS 只能生成 C 语言代码,而 SWIG 只能工作在 C(或 C++)的包含文件上,以生成它的输出。这就意味着,如果要在其它语言(如 Python)中连接代码,您必须首先 用 C 语言编写包装代码来调用 Python 代码,然后用 XS 对它进行再一次的包装,最后 XS 会用 Perl 来包装它。这样的话,您必须要非常熟悉 Perl 和 C,以及它们之间是如何互相作用的, 还要非常熟悉您的目标语言,以及它与 C 语言之间是如何相互作用的。

在另一个方面,情况更糟糕:您的潜在用户(如果您想分发您的代码)必须觉得,在 Perl 中取得您的模块并安装它不是件很麻烦的事。如果要在 UNIX 下安装,您的用户还得有根权限或者至少一部分的管理权限,因为模块是要被安装到 Perl 自身的库文件目录中去的。换句话说,您的目标用户不能只是普通用户。那样的话,要问的事情就太多了。

如果有了内联,情况就大不相同了。每一种语言模块都知道如何读取它们各自的语言从而找到相应的函数定义,如何编写 XS 和 C 包装代码从而调用它们(或是在遇到如 Python 的解释型语言时知道如何去调用解释器),以及如何去安装这一整套东西。最好的是,您的 Perl 脚本的最终用户将不会看到任何有关这些的显示。 makecompiler 的输出都不会显示出来。而且,因为内联可以将其创建文件和动态链接对象保存至任何目录(默认目录是您正在运行的脚本位置下的一个 _Inline 或 .Inline 子目录),这样,您的用户就不再需要任何特殊权限去使用基于内联的模块了。

唯一的真正要求是,所需的语言确实被完全安装了。在 C 语言下,这意味着用来创建 Perl 安装的编译器还能够使用。在 UNIX 下这几乎不是问题;在 Windows 下也许存在问题,因为几乎所有 Windows 的 Perl 用户都使用 ActiveState Perl,它是用 Microsoft Visual C++ 6.0 来编译的(直到撰写本文时)。如果编译器是 gcc 的话,一种解决方法是使用 Cygwin 兼容层和为解决这个问题而构建的 Perl 版本。另一种方法(就是我现在使用的)就是使用 Mingw32 工具包(Minimum Win32 GNU),但是在 Windows 的某些特性作用下,对其的支持还有点不稳定。(好吧,所以我有时也使用 Windows ME。我确实也会按时洗澡。)Brian Ingerson 正在努力制作一个 ActiveState 的内联安装程序,以便用户能够使用除了 Visual C++ 之外的编译器。所以,当你阅读本文时,这也许已经不再是一个问题了。

那么我可以用它来做什么呢?
内联有两个显而易见的应用。第一个是将常用代码从 Perl 转换到 C;因为 C 语言是编译的,所以它的执行速度要快得多。而且因为程序的其它部分还是用 Perl 来编写的,所以您并不会丧失 开发的速度、灵活性和其它 Perl 的优秀功能。您甚至可以在 C 语言代码中调用 Perl 的函数(谁需要正规表达式?)。我们有可能把用于 GUI 工作的内联、Perl 和 wxWindows 与用于加快速度的任意 C 语言代码组合起来,一想到这些我就激动不已。

内联的另一个显而易见的应用是可以为外部库建立包装程序(例如 wxWindows)。实际上,内联很可能将成为 Perl 6 中的默认接口方式。那样就好了。内联在它的最新版本(撰写本文时是版本 0.40)中还提供了系统级的内联模块的安装。如果模块是被安装在系统级(对应的是 on-the-fly 式的编译),您就可以将它们作为以预编译过的二进制代码发布出去。如果用户还是再次编译了,内联可以被告知不自动进行重新编译(如果不想让您的核心 Perl 被意外地重新编译的话)。

至少对我来说,内联还有一个有点令人意外的优点。那就是您可以使用内联来测试您正在开发的 C 语言库。因为 Perl 拥有一个十分出色的测试(和测试文档)工具,您甚至可以经常使用 Perl 的测试功能来维护您的 C 语言库。当然,因为内联的包装程序非常容易创建和维护,所以您很 可能坚持长期做这样的仔细的测试。

要连接到一个库,您所要做的就是将连接器的适当标记包含在您的内联调用中。这就是为什么我还要举一个在大小上比上面那个更合理的示例,这样您就可以不仅仅看到库是怎样被连接起来的,还可以看到更加有序的文件布局,而它利用了能将 C 语言代码从 Perl 代码中分离出来的 __DATA__ 记号的优势。我非常乐意向您展示我自己编写的一个测试包装程序,但其中要讲的东西太冗长繁琐,不便包含在本文中,所以我将用一个有价值的 C 语言库的示例来代替它。

一个更大的示例
我们假定有一个使用会话对象的库。当我们开始与库交流时,我们调用一个名为 newsession 的函数,它会返回一个空指针(这是组织事情的惯用方法)。在接下来对库的调用中,我们把这个会话放进去来建立一个上下文。因为这个会话是一个 malloc 过的对象,所以我们在使用完它时还必须将其释放。说的相对简单一些,让我们假定有四个函数: newsession 用来分配会话, freesession 用来释放会话, setattribute 用来设置某种属性, getresult 用来获得某种结果。我们将整个过程称作 "mylib",而且假定将其保存在目录 "/usr/local/mylib" 中。要注意的是,如果在 windows 中,您可以把目录改为 "c:\projects\mylib",而且只需使用被讨论到的 DLL 文件的文件名,而 不需要加上 .dll 扩展名。

当然,Perl 相比直接的 C 语言的优势之一是在于它确实可以在对象上进行堆积管理,这样当一个对象超出范围时,就会被销毁。所以,我们就当这个模块是一个面向 Perl 对象的模块来获得这一性能。就我所知,这是所有当中最好的 -- 我喜欢面向对象的好处,因为它可以忽略清除。但是我并不喜欢要从头到尾写面向对象的代码的规定。我更加喜欢编写 C 语言中低级的例程,并且时刻注意其中的细节。Perl 的构造正好让我可以这么做,所以我想,从现在开始起我会变得更加危险。

清单 2. 一个有价值的内联示例
package MyWrapper;

use Inline => Config => LIBS => '-L/usr/local/mylib -lmylib';
use Inline => Config => INC  => '-I/usr/local/mylib';
use Inline C;

sub version {
   return "MyWrapper 2.0";
}

__DATA__
__C__
#include "mylib.h"
    
SV* new() {
    void * session = newsession();
    SV*    obj_ref = newSViv(0);
    SV*    obj = newSVrv(obj_ref, class);

    sv_setiv(obj, (IV)session);
    SvREADONLY_on(obj);
    return obj_ref;
}
    
void set (SV* obj, char *attribute, char *value) {
    setattribute ( ((void*)SvIV(SvRV(obj))), attribute, value);
}
    
char* get (SV* obj) {
    return getresult ( ((void *)SvIV(SvRV(obj))) );
}
    
void DESTROY(SV* obj) {
    return freesession ( ((void *)SvIV(SvRV(obj))) );
}

这个小小的示例向您示范了要建立任何库的包装程序所需要的基本条件;因为包装程序是在 Perl 中,而且每次改变它时它都会被重新编译,所以它用来测试一个还在开发中的库是再理想不过了。让我们一步一步地来看。最先要注意的是 use Inline 语句,它示范了如何去设置内联的配置参数。在这里我们感兴趣的只有两个参数,那就是 LIBSINC ,它们提供了连接而且包括了编译器的参数。如果不是在用 gcc 作为您的编译器,您或许要做一些改动。如果您没有为代码指定字符串, Inline C 语句就会在您脚本的 __DATA__ 区域内寻找它的代码。这为混合语言提供了一种方式,因为每条 use Inline <lang> 语句都会使用其自身的部分,这样您就可以将它们堆叠到您的核心内容之中。

紧接着上面,我使用了一个很小的 Perl 函数来返回一个没有意义的字符串。这样做其实完全是为了证实要建立一个将 C 和 Perl 混合起来的模块是多么容易。实际上,模块中的 Perl 函数甚至可以调用 C 语言函数本身,而且如果有需要的话,模块中的 C 语言函数甚至可以算出 Perl 函数的值。(例如,根据参数类型,您可能会调用不同的函数,或者需要某些预处理或后处理,以使得用 Perl 编写代码会更有意义。)

version 函数后,我们来专心研究一些很不错的 C 语言代码。

任何一个 Perl 对象都需要被定义好的 newDESTROY 方法。在一个内联的对象中,这些函数可以存在于任何语言中,只要它们被正确命名。因为我们正在使用的是一个正常的对象, new 函数必须返回一个 Perl 对象( SV* ,代表 "scalar variable"),而且所有其它函数都必须接受与它们的第一个参数相同的参数。其它的所有参数可以是一个完全正常的 C 类型,不管怎样,内联都会为您自动给出转换代码。当您的确需要知道如何使用 Perl 变量时,您可以发现 "perlguts" 说明文档有很大价值。它包含在您的 Perl 发行包中,也可以访问 Perl 网站来得到它(请参阅 参考资料)。

这样做的方法是,Perl 对象包含一个只读的整数,此整数与函数 newsession() 返回的空指针相对应。 new 函数除了构建一个包括它的对象引用以外,基本上没什么作用。而且您可以看到,其它的函数在执行前必须先将那个空指针解开。其它的参数(要设置的 attributename 不需要做任何 Perl 转化就可以像处理 C 字符串一样处理其它字符串)。

基本上,我要说的就这么多。您可以简单地剪切和粘贴从而包装您要的任何库;在内联软件包中包含的 C cookbook 中还有更多精彩的例子,但是要想弄明白这个模块并不是太难。所以,赶快去下载这个模块,然后开始吧 -- 您还在等什么?

参考资料

关于作者
Michael Roberts 自成年以来一直都把时间花在编码上(小孩,青年时代也是如此),但开始撰写有关这方面的文章只有几个月的时间。他在编程之余,喜欢计划新的项目。可以通过 michael@vivtek.com 联系他,或者访问他的网站 Vivtek.com

posted on 2005-08-09 21:59  zhumao-2  阅读(601)  评论(0编辑  收藏  举报