在 Perl 中使用内联
嵌入非 Perl 代码时不必再有使用 XS 的烦恼
简介: 新的 Perl 内联模块允许您使用其他语言编写代码(如C、Python、Tcl 或Java),并将其随意地放进 Perl 脚本中。不像以前将C 语言代码与 Perl进行连接的方法那样,内联的使用将非常简便,尤其是在与Perl体系保持一致方面更加出色。内联的一个非常实用的地方是可以依据 C语言的库编写出快速包装代码并将在Perl上使用它,这样(就我而言)就可以将 Perl变成世界上 最好的测试平台。
开始之前
您是否遇到过这样的应用程序或系统,它们需要访问脚本环境或组件?这个需求的一个明显的原因是,为了使用由脚本语言提供的、用 C 难以实现或实现起来比较复杂的功能。对于文本处理、解析和重新格式化这样的任务,Perl 语言是一个理想的选择。Perl 是一种实用且易于使用的脚本语言,其优势是具有丰富且设计良好的应用程序接口(application program interface,API),这些 API 支持 Perl 与宿主应用程序集成。
本教程是为这样的开发人员编写的,他们想要知道如何将 Perl 解释器合并到应用程序中,以及如何暴露这些应用程序以允许 Perl控制和管理组件。要使用本教程中的技术,您必须精通 C ,并且至少具有关于 Perl、Perl 的结构以及如何用 Perl编写基本的应用程序的基本知识。
本教程的内容:
- Perl 的工作原理。
- Perl 数据类型。
- 嵌入解释器。
- 在应用程序中运行 Perl 脚本。
- 将应用程序暴露给 Perl 脚本。
嵌入 Perl 脚本编程的简介
在应用程序中嵌入 Perl 脚本编程的关键原因是,为了使用在 C 语言中没有大量代码就难以实现的功能。经典的例子包括 Perl 提供的文本操作和处理 —— 具体来说就是,解析文本和提取特定元素或者只是使用正则表达式引擎的能力。
过去我已经对各种各样的任务使用过嵌入式 Perl —— 甚至代替真正的 C 库在网络上通信 —— 因为 Perl 对于某些操作比较容易使用。我也在一些更加广泛的安装中使用过 Perl,在这些安装中,Perl 代码是应用程序的一个集成部分,用于执行某些代码段。也许并不奇怪,Perl 本身是用 C 编写的,并且 Perl 解释器包含一个广泛的 API。甚至 perl
可执行文件也使用 Perl API 来创建 Perl 解释器并执行脚本。
理解如何在应用程序中嵌入 Perl 的第一步是理解 Perl 的内部工作原理。我们来看一下 Perl 是如何解释 Perl 脚本的。
了解 Perl 的工作原理,可以向您展示,典型嵌入过程中所需的不同组件如何一起用于为应用程序支持一个内部的脚本编程语言。图 1展示了 Perl 语言的基本结构图。
图 1. Perl 的内部体系结构
Perl 是一个脚本语言,但是作为集成过程的一部分,您提供的 Perl 脚本被转换成一系列操作码(opcode)。 操作码 是小段的脚本,组成应用程序中的一个或多个序列。操作码在技术上类似于典型 CPU 上执行的单个指令。例如,Perl 使用“add”操作码将下面的表达式:
$a = $b+$c |
转换成操作码结构。基本的 Perl 指令集包含大约 350 个操作码,用于处理各种事情,从基本的数学函数到更复杂的 Perl 内部函数(比如 grep
)。其他操作码通过 Perl 扩展接口与外部函数及 C库连接。所有的操作码实质上都是 C 函数:一些已经在 C 中进行了优化,以产生最佳的性能,而其他的则只是到 C 库中相应函数的接口。
当您执行一个 Perl 脚本时,应用程序的结构就被转换成一个操作码序列。操作码(或者更加明确地说是支持单个操作码的函数)然后将由 Perl 虚拟机执行。这些操作码处理特定的数据结构,使 Perl 可以具有标量(scalar)、数组和散列等内置数据类型。
因为这些操作码和用于支持它们的结构及函数都是用 C 编写的,所以您可以通过创建必要的数据结构,然后执行使用 Perl 虚拟机处理信息的 Perl脚本,来与 Perl 解释器交互。该交互基本上是单向的:您可以填充变量并编写脚本。为了完整的集成,您需要通过编写一个在 C 中使用的数据类型和Perl 中使用的数据结构之间进行转换的接口,在应用程序中将这些函数暴露给 Perl。我们首先来看每个主要的数据类型,以及如何在 C中创建、更新和操纵这些值。
Perl 数据类型
要使执行单元能够工作,必须要设置处理的值和数据结构。操作码是系统的可执行组件,并且它们处理几种不同的值。存在以下值的值类型:
- 标量值:Perl 在内部标量值类型中存储整数、浮点数、字符串和引用(与其他类型一起)。在到 Perl 内部标量值类型的 C 接口中,存在单独的函数,用于基于 C 采用的相应数据类型来创建不同的标量类型。
- 数组值:数组值实质上就是内部标量值类型的数组。与标准 C 数组类型不同,Perl 可以被自动扩展;Perl 内部标量值类型会带来内存分配和其他问题。
- 散列值:C 中没有散列的直接等价物,但是提供的函数可以让您创建和控制您所创建的散列的内容。
- Glob 值:Glob 值用于几乎所有其他类型的信息。它们负责持有到文件句柄(filehandle)和格式的指针,并为访问其他数据类型的引用提供一个“万能(catch-all)”解决方案。
- 代码值:代码值不怎么使用;它们组成可执行代码块、函数和组件。与其他值类型不同,代码值通常通过按名称直接执行代码块或函数,而不是通过直接引用代码值来访问。
要通过 C 接口使用 Perl,必须使用、创建和操纵存储在这些不同值类型中的信息。在接下来的几屏中,我们将更加仔细地了解如何创建和使用这些不同类型。
关于操纵 Perl 值的所有函数存在一些公共的主题,主要因为这些值是基于一个公共的主题。对于数组和散列值,其子类型是标量值;当操纵数组内容时,您将使用操纵标量值的大多数函数。
因为 Perl 类型不匹配 C 的内部类型,所以必须使用函数来创建、更新和存储 Perl 结构中的信息。当您创建了 Perl 结构中的变量之后,嵌入在应用程序中的 Perl 脚本就可以使用甚至访问这些变量。
要创建新的标量值,需要使用下面的一个函数。每个函数返回到一个值的指针(SV *
):
newSViv(long i)
从所提供的整数类型i
创建一个新的标量值。该函数创建一个 IV(整数值,Integer Value)类型 SV。newSVnv(double f)
从所提供的浮点数或双精度数f
创建一个新的标量值。该函数创建一个 NV(数字值,Numerical Value)类型 SV。newSVpv(char str, int len)
从所提供的长度为len
的字符串str
创建一个新的标量值。如果len
为 0,Perl 会计算字符串的长度。该函数创建一个 PV(字符串值,string value)类型 SV。
注意,不能分配任何值以创建新的标量值。相反,必须告诉 Perl 接口,您创建的是哪种类型的标量值,这违背了 Perl 所宣称的标量值可以包含任何东西的性质。在构建到内部标量值类型的接口时,需要注意该问题,因为必须及时使用正确的函数。
在脚本中,Perl 将标量值当作“万能”结构;但是在内部,信息是单独存储的。例如,在 Perl脚本中,通常将一个数值赋给标量值,然后将该数值合并到字符串中以打印出来。反之也有可能:将字符串赋给 Perl 作为数值。最初,Perl将该值存储为字符串,如果您在数值表达式中使用该值,Perl 就会将它转换为数值。在幕后,就像您在 Perl脚本中创建一个标量值,并给它分配一个整数(值在相应的 SV 中就是这样存储的)。只有当 Perl脚本需要时,该值才会被转换成浮点数或字符串,并且 Perl 会自动执行转换。 C 中没有这样的好处。如果想要在 C接口中执行类似的任务,必须手动去完成。
有三组函数用于操纵标量值。第一组返回存储在标量值中的值的类型。第二组将现有的标量值更改为一个特定类型的新值。第三组转换标量值,以便存储的值被识别为特定的类型。首先,我们来看类型函数:
SvIOK(SV *)
在标量值是整数值时返回 true。SvNOK(SV *)
在标量值是双精度值时返回 true。SvPOK(SV *)
在标量值是字符串值时返回 true。
还存在附加的函数,用于确定标量值是不是引用、未定义或“true”值。
您也可以使用 SVTYPE(SV *)
函数,它返回一个用于指定类型的值。针对各个类型存在一些宏: SVt_IV
(整数)、SVt_NV
(双精度值)和 SVt_PV
(字符串)。针对引用和对特定类型(数组、散列、代码、glob)的引用存在其他的宏。
要更改现有的标量值,可以使用一个 sv_set*
函数。例如,下面的代码段将初始化一个整数 SV,然后给它分配一个双精度值:
SV * myscalar; myscalar = newSViv(3); /* myscalar is type SVt_IV */ sv_setnv(myscalar,3.14159265); /* myscalar is now SVt_NV */ |
要更改标量的“默认”类型,使用 SvIV(SV *)
转换成整数,用 SvNV(SV *)
转换成双精度值,或者用 SvPV(SV *, int len)
转换成字符串。当将双精度值转换成整数时,小数部分被丢弃。当将字符串转换成 IV 或 NV 时,Perl 使用 sscanf()
的等价物来转换成相应的值。
还有许多函数可以帮助您在 Perl 中设置、使用和处理标量值。例如,存在一整套的指令,用于处理对其他结构的引用和处理是其他包的对象或部分的标量值。这些组件不适用于本教程,如果您想要了解更多信息的话,请访问 perlapi
主页。
两个有用的其他函数是 perl_get_sv(char *varname, int create)
和 sv_dump(SV *)
。perl_get_sv(char *varname, int create)
函数将 Perl 脚本中指定的标量值转换成 C SV 结构。例如,如果运行一个这样的脚本:
$returnstring = my_perl_function() |
那么通过调用下面的函数,可以获得一个到该标量值的指针。
SV *returnstring = perl_get_sv('main::returnstring',0) |
如果 create
参数不为 0,那么函数将创建一个该名称的标量变量。
另一个有用的函数 sv_dump(SV *)
,“以优美格式打印”标量值,而不管其内置类型是什么样的。该函数可以作为开发过程中的调试工具,允许您检查不同组件的值。
有了我们到目前为止介绍的函数,您就可以释放和删除您创建的任何标量值。该任务比较费时,尤其当您创建并使用大量标量值时更是如此,并且需要非常仔细地编写代码并确保在使用完之后释放了标量。另外,您也可以通过将标量值标记为 mortal 而让 Perl 来处理该任务。这样做会在变量超出作用域(例如,函数内)时自动释放内存。可以通过调用 sv_2mortal(SV *)
将现有标量值标记为 mortal,或者通过使用 sv_newmortal(SV *)
创建一个“空的” mortal 标量值。
既然理解了标量值的基础知识,那么操纵数组就比较直观了。因为数组值是一个特定的类型,所以不能像 C 中那样来使用数组:所有访问 ——从向数组添加元素到提取值 —— 都是通过函数来完成的。一直要记住,数组值存储指向标量值的指针,所以更新和提取过程在每个阶段都使用标量值。
创建数组的方式有两种:可以创建一个以后更新的空数组值,或者通过提供一个指向一组标量值的指针而创建一个已填充的数组。使用 AV *newAV()
函数创建空数组值。要创建已填充的数组值,可使用 AV *av_make(int num, SV **ptr)
函数。
通过 av_len(AV *)
函数确定数组值的长度;使用下面的函数获取标量值:
av_fetch(AV *, I32 index, I32 lval) |
该函数返回数组中 index
处的 SV。lval
应该保持为 0(如果您只想读取信息)或非 0(如果您想要值设置为 undef
)。要在数组中保存值,可以使用:
av_store(AV *, I32 index, SV *val) |
注意,这是将值直接保存到数组指定位置的惟一方式。
最后,数组的大多数常见操作 —— 在 Perl 脚本内或外 —— 都是通过添加或删除条目来完成的。在 Perl 中,该操作是通过 push
、pop
、shift
和 unshift
函数来处理的。这些函数在到 Perl 的 C API 中与在 Perl 本身中工作起来相同,只有 av_unshift(AV *, I32 num)
函数例外。与 Perl 函数不同,该函数只将 num
元素添加到数组,因而您需要使用 av_store
函数真正地用值来更新元素。
如果可以处理数组,那么处理散列就没什么难的了。与数组一样,散列存储标量值。键可以指定为字符串或标量值,但是需要相应使用不同的函数。散列比较容易处理,因为您不用 C 处理字符串。
可以使用 newHV()
函数创建新的散列值。要存储一个值,以字符字符串作为键,使用下面的函数:
hv_store(HV *hash, char *key, U32 klen, SV *val, U32 hashval) |
要取得一个标量值,使用下面的函数:
SV **hv_fetch(HV *hash, char *key, U32 klen, I32 lval) |
也可以使用两个函数的组合,迭代通过散列中的键/值对。hv_iterinit(HV *hash)
函数初始化迭代,返回所处理的散列中的元素个数。然后使用下面的函数迭代通过每个键/值对:
SV *hv_iternextsv(HV *hash, char **key, I32 *pkeylen) |
键用 key
更新了,返回值是散列中的相应值。
最后,如果将 create
参数设置为 0
,使用下面的函数可以获得现有散列的散列值:
HV *perl_get_hv(char *varname, int create) |
(如果将 create
参数设置为 1
,将会创建一个具有指定名称的散列。)注意,名称在典型 Perl 解释器的作用域中必须是全限定的(也就是说,在一个特定的软件包中,比如“main”)。
既然您了解了变量和值之间从 C 接口中进行交互的基本知识,现在我们就来看如何嵌入解释器,然后使用这些函数来操纵其间生成的信息和值。
基本嵌入
简单地嵌入 Perl 解释器是一个容易的任务。使用下面的代码创建一个新的围绕着 Perl 解释器的包装器(即一个简单的基于 C 的应用程序):
#include <EXTERN.h> #include <perl.h> static PerlInterpreter *my_perl; int main(int argc, char **argv, char **env) { my_perl = perl_alloc(); perl_construct(my_perl); perl_parse(my_perl, NULL, argc, argv, env); perl_run(my_perl); perl_destruct(my_perl); perl_free(my_perl); } |
两个头部文件是代码的其余部分所需要的。EXTERN.h
包含用于集成与 Perl 一起使用的外部组件所需的定义。PerlInterpreter
类型是一种结构,其中包含 Perl 解释器的单个实例的所有信息。一次具有多个活动的 Perl 解释器是可能的,虽然实际中很少在一个应用程序中具有多个活动解释器。
在 main
函数中,perl_alloc()
函数为 Perl 解释器的实例分配资源,而 perl_construct()
创建实例。perl_parse()
函数解析 Perl 代码。注意,您从主机程序为 Perl解释器提供参数和环境。(您应该为解释器创建了自己的特定环境。)第二个参数可以包含您可能需要的任何初始化例程;在很多情况下,您使用第二个参数来初始化到基于 C 的 Perl 扩展模块的 XS 接口,尽管它也可以用于任何特定于 Perl 的初始化。
当 Perl 表准备好并初始化了,您就调用 perl_run()
函数来执行解释器的实例。因为您没有为解释器提供任何要执行的实际代码,所以当应用程序执行时,解释器就会从 stdin要求代码。在本例中,清除并不十分必要,因为当程序退出时,作为正常过程销毁的一部分,实例将会被关闭,内存将会被释放。但是,主动进行清除是一个好习惯。
Perl 有很多版本,并且可以安装在任何位置。如果不赞成 ExtUtils::Embed
模块提供的信息,那么提供一个编译上面这个应用程序并且适用于所有平台和环境的命令行是不可能的。该模块可以生成必要的 C 编译器和链接器选项,不管 Perl 是如何安装和配置的。
要编译前一屏中的应用程序,使用这样一个命令:
$ cc -o myperl myperl.c `perl -MExtUtils::Embed -e ccopts -e ldopts` |
当然,生成的确切输出依赖于系统和环境。
如果运行新编译的程序,您会发现产生了自己版本的围绕着 Perl 解释器的包装器:
$ myperl print join(', ',keys %ENV),"/n"; EOF PWD, LOGNAME, SHLVL, BASH_ENV, XSHELL, SHELL, DYLD_LIBRARY_PATH, USER, SSH_TTY, CC, VISUAL, TMPDIR, DISTCC_HOSTS, HISTSIZE, LS_COLORS, USERNAME, SSH_CLIENT, LESSOPEN, EDITOR, ASAVE, TERM, SSH_CONNECTION, CCACHE_PREFIX, CCACHE_LOGFILE, CVSROOT, MAIL, LANG, CLASSPATH, PATH, HOSTNAME, LD_LIBRARY_PATH, SSH_ASKPASS, MANPATH, MAILCHECK, HOME, ROGUEOPTS, PS1, cw, JAVA_HOME, QTDIR, _, LANGTYPE, CCACHE_DIR, TEXINPUTS, INPUTRC, G_BROKEN_FILENAMES |
您现在已经执行了 C 程序中的第一部分 Perl 代码。但是必须键入您想要运行的 Perl 还不太理想,更不用说 C 包装器与您生成的 Perl 程序之间没有很多的通信了。
要在嵌入式 Perl 解释器中使用扩展模块(即任何依赖于到外部库的 C 接口的模块),必须改变初始化 Perl 实例的方式,否则,Perl 将不能加载外部组件。该初始化然后被提供给 perl_parse
调用:
perl_parse(my_perl, xs_init, argc, argv, env); |
xs_init
函数相对来说比较直观,但是也不用您自己去创建。相反,使用 ExtUtils::Embed
接口来生成它:
$ perl -MExtUtils::Embed -e xsinit - -o xsinit.c $ cc -c xsinit.c `perl -MExtUtils::Embed -e ccopts` |
然后,当构建和链接嵌入式应用程序时,提供已编译的 xsinit.o
作为附加的源对象:
$ cc -c myperl.c `perl -MExtUtils::Embed -e ccopts` $ cc -o myperl myperl.o xsinit.o `perl -MExtUtils::Embed -e ccopts -e ldopts` |
如果想要简化构建过程,可以将所有这些代码放到一个 Makefile 中。
与脚本集成
此时,所有您所做的都是重新生成 Perl 解释器。但是在您运行应用程序并为它提供 Perl 代码之前,解释器不会做任何事情。真正的工具一开始就可以运行任意的 Perl 代码段,并可以在这些脚本中使用包含它们的 C 应用程序中的信息。
要调用特定的 Perl 函数,可使用 call_argv
函数。该函数调用一个指定的函数,包括给该 Perl 函数提供任何参数的能力。下面是调用 print 函数的应用程序的一个例子:
#include <EXTERN.h> #include <perl.h> static PerlInterpreter *my_perl; int main(int argc, char **argv, char **env) { char *print_args[] = {"This","is","a","list","of","printable","items","/n",NULL}; my_perl = perl_alloc(); perl_construct(my_perl); perl_parse(my_perl, NULL, argc, argv, NULL); PL_exit_flags |= PERL_EXIT_DESTRUCT_END; call_argv("printwrap", G_DISCARD, print_args); perl_destruct(my_perl); perl_free(my_perl); } |
该应用程序的关键部分是创建参数列表和调用 call_argv
本身。您将该参数列表作为一个指针来提供,该指针指向一个以单个 NULL 元素终止的字符字符串列表。注意,不能直接调用 print
,而是调用一个包装器函数,如下所示:
sub printwrap { my (@words) = @_; print @words; } |
将代码编译为 myperlfunc
,然后将 Perl 脚本保存为 printwrap.pl
。使用下面的命令来执行程序:
$ myperlfunc printwrap.pl Thisisalistofprintableitems |
call_argv()
函数可用于这样的情形,即当您想要执行应用程序中的任意 Perl 代码并产生一个结果时 —— 所有事情都完全是在 Perl 中完成。必要时,您可以向函数传递参数(正如您在上面的脚本中所见的)。但是要从调用获得返回值,需要使用不同的方法。
到目前为止,您还不能使用 Perl 来执行函数并获得返回值,以用于应用程序中。这是在嵌入式情形下理解和使用 Perl 的下一步。要这样使用 Perl,必须使用堆栈;当提供值给函数时,call_argv
函数会为您自动做这件事。但是为了获得信息,需要直接操纵堆栈。
Perl 中的堆栈用于在函数之间交换数据。当调用函数时,您放入函数调用中的任何参数实际上都被压入堆栈。Perl 中的函数然后从堆栈取出参数以进行处理。堆栈 实质上就是一个大数组,因而堆栈的入栈和出栈方式都与数组相同,通过 Perl 的 push
和 pop
函数进行控制。
用实际的术语来讲,实际上有两个堆栈:参数堆栈(argument stack) 和标志堆栈(markstack)。前者包含实际的参数,后者包含指向参数堆栈的指针。这两个堆栈的组合使您不要在函数中对堆栈使用太多的变量。@_
变量只用参数堆栈上的变量直到标志堆栈的最后一个指针来填充。在图 2 中您可以更加看清关系,该图展示了在 Perl 中调用一个简单的两参数 add(a,b)
函数时两个堆栈的状态。
图 2. Perl 堆栈
图 2 展示了函数调用时两个堆栈的状态。堆栈中有两个参数,并且 markstack-sp
(标志堆栈的堆栈指针)指向放在 add()
函数调用的参数堆栈上的元素的开头。同时,stack-sp
(参数堆栈的堆栈指针)指向整个参数堆栈的末尾。
堆栈操纵基于另外一套函数。下面是一个序列的例子,每个阶段使用的函数放在括号中。
- 初始化环境(
dSP
)。 - 开始作用域(
ENTER
)。 - 设置环境,以便临时变量在作用域的末尾会被自动删除(
SAVETMPS
)。 - 记住堆栈的当前栈顶(
PUSHMARK
)。 - 将参数压入堆栈(
XPUSH*
)。 - 表示参数的末尾(
PUTBACK
)。 - 调用 Perl 函数。
- 表示返回值回收的开始(
SPAGAIN
)。 - 从堆栈获得值(
POP*
)。 - 表示返回值回收的末尾(
PUTBACK
)。 - 释放临时变量。
- 结束作用域(
LEAVE
)。
现在,我们来将该序列付诸实现。
要理解该序列,我将向您展示如何构建一个围绕着以下 Perl 代码的包装器。该代码以一个字符串和一个分隔符作参数,并以反向返回一个分开的单词的列表。仅供参考,我添加了两条语句,用于输出我所处理的信息。
sub reverse { my ($string,$separator) = @_; my @words = split /$separator/,$string; print "Words in source are: ",join(", ", @words),"/n"; my @sorted = sort { lc($b) cmp lc($a) } @words; print "Words in return are: ",join(", ", @sorted),"/n"; return @sorted; } |
要在 C 中使用该代码,需要构建一个包装器函数,该函数将进行压栈、调用函数,然后再读取返回值。然后您就可以从 C 应用程序中调用该包装器函数,以运行 Perl 代码。因为您已经构建了包装器,所以它对于您的 C 应用程序就像任何其他函数调用一样。
对于包装器函数,Perl 代码段紧跟在您刚才看到的基本序列后面:初始化堆栈的连接和操纵,将 Perl 函数的参数压入堆栈,调用 Perl 函数,然后再取回返回值。代码如下:
#include <EXTERN.h> #include <perl.h> static PerlInterpreter *my_perl; void perl_reverse(char *string, char *separator) { int retval; STRLEN n_a; dSP; ENTER; SAVETMPS; PUSHMARK(sp); XPUSHs(sv_2mortal(newSVpv(string,0))); XPUSHs(sv_2mortal(newSVpv(separator,0))); PUTBACK; retval = perl_call_pv("reverse", G_ARRAY); SPAGAIN; if (retval > 0) while(retval-- != 0) printf("Returned: %s/n",POPpx); PUTBACK; FREETMPS; LEAVE; } int main(int argc, char **argv, char **env) { char *my_argv[] = { "", "reverse.pl" }; my_perl = perl_alloc(); perl_construct(my_perl); perl_parse(my_perl, NULL, argc, my_argv, NULL); perl_reverse("Come grow old along with me"," "); perl_destruct(my_perl); perl_free(my_perl); } |
首先来看 perl_reverse
函数。基本的序列如前所述。重要的部分是调用 XPUSHs
,它将一个字符串压入堆栈。XPUSH*
函数增加堆栈的大小,然后将值压入堆栈。注意,您前面使用了 SAVETMPS
函数:当调用 FREETMPS
时,该函数将释放这其间创建的任何临时 mortal 标量值。因此,您基于 SV 字符串类型创建一个 mortal 标量值。perl_reverse
函数的调用嵌入在 call_pv
的调用中,当前者返回时,后者返回堆栈中的函数个数。
最后的阶段包括从堆栈读取每个参数,以及如本例中的输出返回的单词。POPpx
函数返回一个字符串(通过定义 STRLEN n_a
,使 Perl不必关心字符串的长度)。您只是减量返回值,直到读取完堆栈中的内容。但是请注意,您已经故意从堆栈读取了一个字符串(实际上是指向字符串的指针)。一般最好编写一个返回已知值类型的包装器,以便您每次可以明确地从堆栈删除一个该类型的值。还要注意,您从堆栈弹出了一个 C 类型,而不是 SV。
main
部分只有两处变化。您仍然初始化并释放了 Perl 解释器,但是对 Perl 解释器的调用是由包装器函数来处理的。另一处变化是,与以前的例子不同,您压入自己的 argv
,它告诉 Perl 用函数定义自动加载脚本,而不用您在命令行指定脚本。这样做将应用程序的执行简化成:
$ ./reverse Words in source are: Come, grow, old, along, with, me Words in return are: with, old, me, grow, Come, along Returned: along Returned: Come Returned: grow Returned: me Returned: old Returned: with |
您将注意到,输出并不像您预期的一样是反向的。这是因为,当 Perl 将值压入堆栈时,值没有以所提供的顺序入栈,但是 POP*
函数却以反向顺序从堆栈取值。要以原来的顺序将值放回堆栈,需要更改代码以考虑该顺序:
if (retval > 0) { counter = retval; while(counter-- != 0) strings[counter] = POPpx; while(counter++ < (retval-1)) printf("Returned: %s/n",strings[counter]); } |
如果处理的不是字符串值,需要考虑系统的反转特性,以确保将适当的返回值放入适当的变量中。
没有必要使用 XPUSH*
函数,您依靠 Perl 来为您管理堆栈,包括扩展和管理堆栈,以确保有足够的空间压入值。尽管这对相对比较少的参数和返回值是很好的,但是对于大的堆栈就会出问题。在操纵 10 个条目时,我就遇到过问题,但是您设想一下一些基于数组的函数,问题会变得特别麻烦。
这样的问题与 C API 无关:在 Perl 中也一样,尽管该问题不太常见一些,因为 Perl 对函数执行所在的环境具有完全的控制。但是在两个系统中,性能都是一个问题。每次 PUSH
堆栈都意味着增加内存分配和添加元素本身。对于数百甚至数千个元素,这就成了一个较大的问题。在 Perl 中,明显的解决方案是使用对数组的引用,而不是交换数组本身。通过使用下一节中的技术,您可以从 C API 做同样的工作。
使用引用来简化堆栈交互
提供的参数与返回值之间的不一致性问题的一个解决方案是,使用散列或数组的引用。使用该方法,不用从某个方向将条目压入堆栈,就可以提供参数并获得返回值。该策略在 Perl 中相当常见,您也可以在这里使用它。
首先,创建一个 Perl 脚本,它定义您将使用的函数。本质上,该脚本与最后一个脚本相同,只是现在您从散列提取要处理的信息,并提供单词列表的一个正向存储版本和一个反向存储版本作为返回值的一个散列引用:
sub reorder { my ($args) = @_; my $string = $args->{string}; my $separator = $args->{separator}; my @words = split /$separator/,$string; print "Words in source are: ",join(", ", @words),"/n"; my $forward = join (', ',sort { lc($b) cmp lc($a) } @words); my $reverse = join (', ',sort { lc($a) cmp lc($b) } @words); my $retvalues = {'forward' => $forward, 'reverse' => $reverse,}; return $retvalues; } |
使用引用需要函数中多一点预先处理和后处理,但是本质上内容没有什么不同。但是,C 包装器函数要复杂一些。
该脚本的 C代码更加复杂。此时,必须将您想要提供的参数填入一个散列中,然后创建一个对该散列的引用,并将该引用压入堆栈。当信息恢复之后,您就进行反向:必须将对返回的散列的引用从堆栈弹出,对散列解除引用,迭代通过散列的键/值对,并输出结果。下面是您将看到的完整代码:
#include <EXTERN.h> #include <perl.h> static PerlInterpreter *my_perl; void perl_reorder(char *string, char *separator) { int retval; HV * arghash; HV * rethash; SV * hashref; char *key; SV *value; int keycount; I32 *keylen; STRLEN n_a; arghash = newHV(); rethash = newHV(); hv_store(arghash, "string", 6, sv_2mortal(newSVpv(string,0)),0); hv_store(arghash, "separator", 9, sv_2mortal(newSVpv(separator,0)),0); hashref = newRV_inc((SV *)arghash); dSP; ENTER; SAVETMPS; PUSHMARK(sp); XPUSHs(sv_2mortal(hashref)); PUTBACK; retval = perl_call_pv("reorder", G_ARRAY); SPAGAIN; if (retval == 1) { rethash = (HV *)SvRV(POPs); keycount = hv_iterinit(rethash); printf("%d items in return/n",keycount); while(keycount-- != 0) { value = hv_iternextsv(rethash, &key, &keylen); printf("Return (%s) -> (%s)/n",key,SvPV_nolen(value)); } } PUTBACK; FREETMPS; LEAVE; } int main(int argc, char **argv, char **env) { char *my_argv[] = { "", "reversehash.pl" }; my_perl = perl_alloc(); perl_construct(my_perl); perl_parse(my_perl, NULL, argc, my_argv, NULL); perl_reorder("Come grow old along with me"," "); perl_destruct(my_perl); perl_free(my_perl); } |
我们来仔细看看包装器的关键组件。
重要的代码段是:
hv_store(arghash, "string", 6, sv_2mortal(newSVpv(string,0)),0); hv_store(arghash, "separator", 9, sv_2mortal(newSVpv(separator,0)),0); hashref = newRV_inc((SV *)arghash); |
该代码段用提供给该函数的参数填充散列,其间还创建了 mortal 标量值。注意,您使用单纯的字符串作为键。最后的过程是创建对散列的引用,以便您可以将它压入堆栈。
同样重要的是用于输出结果的代码段:
rethash = (HV *)SvRV(POPs); keycount = hv_iterinit(rethash); printf("%d items in return/n",keycount); while(keycount-- != 0) { value = hv_iternextsv(rethash, &key, &keylen); printf("Return (%s) -> (%s)/n",key,SvPV_nolen(value)); } |
为了取回信息,您将对散列的引用弹出堆栈(使用 POPs
函数),然后将它解除引用,使用 SvRV
将它强制类型转换成一个散列值。然后就是简单的迭代通过键/值对并输出信息。
现在您应该具备将基本 Perl 脚本嵌入应用程序的足够知识了。很明显,在更大的环境中,需要指定一个脚本,用于在主机应用程序中集成几个函数。每当需要使用 Perl 时,只要调用一个调用了相应函数的包装器函数就可以了。
结束语和参考资料
本教程介绍了将 Perl 函数和表达式嵌入 C程序中的基本知识。执行这样的任务最容易的方式是,直接调用您想要使用的自定义函数。但是如果想要处理返回值,就需要使用 Perl 变量和 Perl堆栈,以填充函数参数并提取函数返回的值。对于更加复杂的双向交互,甚至可以使用散列和引用来向所调用的函数提供信息和返回信息。
这里展示的基本方法可以应用于其他解决方案和接口。例如,有一套附加的函数,用于在 Perl 脚本中与对象交互和在对象上调用方法。这些方法可以与Perl 扩展结合起来使用,以提供对外部组件和系统的访问 —— 甚至那些具有其自己的 C API 的外部组件和系统,比如 MySQL 和Web 服务。
尽管这里没有介绍,您也可以其他方式在嵌入式应用程序中使用扩展。例如,设想您创建了一个对应用程序中的组件的扩展。使用嵌入式脚本,您可以使用 Perl来控制应用程序的元素。游戏开发人员使用这样的组合来提供脚本组件,比如玩家以及您与之交互的环境。通过使用嵌入式脚本语言,这些开发人员可以修改不同组件的行为,而不用重新构建组件。
本文来自:http://www.ibm.com/developerworks/cn/education/linux/l-perlscript/index.html