目录

名称

perlxstut - XSUB 编写教程

说明

本教程将指导读者了解创建 Perl 扩展涉及的步骤。假定读者可以访问 perlgutsperlapiperlxs

本教程从非常简单的示例开始,变得越来越复杂,每个新示例都添加了新功能。为了让读者逐步了解扩展的构建,某些概念可能直到本教程的后面才会得到完全解释。

本教程是从 Unix 的角度编写的。如果我知道其他平台(例如 Win32)与此不同,我会列出它们。如果您发现遗漏的内容,请告诉我。

特别说明

make

本教程假定 Perl 配置为使用名为 make 的 make 程序。在以下示例中,您可能需要用 Perl 配置为使用的任何 make 程序替换“make”。运行 perl -V:make 应会告诉您它是什么。

版本注意事项

在编写供一般使用目的的 Perl 扩展时,应该预期该扩展将与不同于您机器上可用版本的 Perl 版本一起使用。由于您正在阅读本文档,因此您机器上的 Perl 版本可能是 5.005 或更高版本,但您的扩展的用户可能拥有更早的版本。

要了解可能遇到的不兼容性类型,并且在极少数情况下,您机器上的 Perl 版本早于本文档,请参阅“对这些示例进行故障排除”部分以获取更多信息。

如果您的扩展使用 Perl 的某些功能,而这些功能在较早版本的 Perl 中不可用,那么您的用户将很乐意收到一个有意义的早期警告。您可能会将此信息放入 README 文件中,但如今扩展的安装可能会自动执行,由 CPAN.pm 模块或其他工具指导。

在基于 MakeMaker 的安装中,Makefile.PL 提供了执行版本检查的最早机会。为此,可以将类似内容放入 Makefile.PL

eval { require 5.007 }
    or die <<EOD;
############
### This module uses frobnication framework which is not available
### before version 5.007 of Perl.  Upgrade your Perl before
### installing Kara::Mba.
############
EOD

动态加载与静态加载

通常认为,如果系统没有动态加载库的能力,则无法构建 XSUB。这是不正确的。可以构建它们,但必须将 XSUB 子例程与 Perl 的其余部分链接,从而创建一个新的可执行文件。这种情况类似于 Perl 4。

本教程仍然可以在此类系统上使用。XSUB 构建机制将检查系统并在可能的情况下构建可动态加载的库,否则构建静态库,然后可以选择构建一个新的静态链接的可执行文件,其中链接了该静态库。

如果您希望在可以动态加载库的系统上构建静态链接的可执行文件,则在所有以下示例中,在执行不带参数的命令“make”时,可以改为运行命令“make perl”。

如果您已选择生成此类静态链接的可执行文件,则应说“make test_static”,而不是说“make test”。在根本无法构建可动态加载库的系统上,只需说“make test”就足够了。

线程和 PERL_NO_GET_CONTEXT

对于线程构建,perl 需要当前线程的上下文指针,如果没有 PERL_NO_GET_CONTEXT,perl 将调用函数来检索上下文。

为了提高性能,请包括

#define PERL_NO_GET_CONTEXT

如下所示。

有关更多详细信息,请参阅 perlguts

教程

现在让我们继续表演!

示例 1

我们的第一个扩展将非常简单。当我们在扩展中调用例程时,它将打印一条众所周知的消息并返回。

运行“h2xs -A -n Mytest”。这将创建一个名为 Mytest 的目录,如果当前工作目录中存在 ext/ 目录,则可能创建在 ext/ 下。Mytest 目录下将创建多个文件,包括 MANIFEST、Makefile.PL、lib/Mytest.pm、Mytest.xs、t/Mytest.t 和 Changes。

MANIFEST 文件包含刚在 Mytest 目录中创建的所有文件的文件名。

Makefile.PL 文件应类似于以下内容

use ExtUtils::MakeMaker;

# See lib/ExtUtils/MakeMaker.pm for details of how to influence
# the contents of the Makefile that is written.
WriteMakefile(
    NAME         => 'Mytest',
    VERSION_FROM => 'Mytest.pm', # finds $VERSION
    LIBS         => [''],        # e.g., '-lm'
    DEFINE       => '',          # e.g., '-DHAVE_SOMETHING'
    INC          => '-I',        # e.g., '-I. -I/usr/include/other'
);

Mytest.pm 文件应以类似以下内容开头

package Mytest;

use 5.008008;
use strict;
use warnings;

require Exporter;

our @ISA = qw(Exporter);
our %EXPORT_TAGS = ( 'all' => [ qw(

) ] );

our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );

our @EXPORT = qw(

);

our $VERSION = '0.01';

require XSLoader;
XSLoader::load('Mytest', $VERSION);

# Preloaded methods go here.

1;
__END__
# Below is the stub of documentation for your module. You better
# edit it!

.pm 文件的其余部分包含用于为扩展提供文档的示例代码。

最后,Mytest.xs 文件应类似于以下内容

#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"

MODULE = Mytest		PACKAGE = Mytest

让我们通过将以下内容添加到文件末尾来编辑 .xs 文件

    void
    hello()
	CODE:
	    printf("Hello, world!\n");

从“CODE:”行开始的行没有缩进是可以的。但是,出于可读性目的,建议将 CODE: 缩进一级,并将后续行再缩进一级。

现在,我们将运行“perl Makefile.PL”。这将创建一个真正的 Makefile,它需要 make。其输出类似于

% perl Makefile.PL
Checking if your kit is complete...
Looks good
Writing Makefile for Mytest
%

现在,运行 make 将生成类似以下内容的输出(一些长行已缩短以提高清晰度,一些无关行已删除)

% make
cp lib/Mytest.pm blib/lib/Mytest.pm
perl xsubpp  -typemap typemap  Mytest.xs > Mytest.xsc && \
mv Mytest.xsc Mytest.c
Please specify prototyping behavior for Mytest.xs (see perlxs manual)
cc -c     Mytest.c
Running Mkbootstrap for Mytest ()
chmod 644 Mytest.bs
rm -f blib/arch/auto/Mytest/Mytest.so
cc -shared -L/usr/local/lib Mytest.o -o blib/arch/auto/Mytest/Mytest.so

chmod 755 blib/arch/auto/Mytest/Mytest.so
cp Mytest.bs blib/arch/auto/Mytest/Mytest.bs
chmod 644 blib/arch/auto/Mytest/Mytest.bs
Manifying blib/man3/Mytest.3pm
%

您可以放心地忽略有关“原型行为”的行 - 它在 perlxs 中的“PROTOTYPES: 关键字” 中进行了说明。

Perl 有自己编写测试脚本的特殊方法,但仅针对此示例,我们将创建我们自己的测试脚本。创建一个名为 hello 的文件,如下所示

#! /opt/perl5/bin/perl

use ExtUtils::testlib;

use Mytest;

Mytest::hello();

现在,我们使脚本可执行(chmod +x hello),运行脚本,我们应该看到以下输出

% ./hello
Hello, world!
%

示例 2

现在,让我们为我们的扩展添加一个子例程,该子例程将接受一个数字参数作为输入,如果该数字是偶数,则返回 1,如果该数字是奇数,则返回 0。

将以下内容添加到 Mytest.xs 的末尾

    int
    is_even(input)
	    int input
	CODE:
	    RETVAL = (input % 2 == 0);
	OUTPUT:
	    RETVAL

int input”行的开头不需要有空格,但它有助于提高可读性。在该行的末尾放置分号也是可选的。可以在“int”和“input”之间放置任意数量和类型的空格。

现在重新运行 make 以重建我们的新共享库。

现在执行与之前相同的步骤,从 Makefile.PL 文件生成 Makefile,并运行 make。

为了测试我们的扩展是否有效,我们现在需要查看文件 Mytest.t。此文件被设置为模仿 Perl 自身具有的相同类型的测试结构。在测试脚本中,您执行多项测试以确认扩展的行为,在测试正确时打印“ok”,在测试不正确时打印“not ok”。

use Test::More tests => 4;
BEGIN { use_ok('Mytest') };

#########################

# Insert your test code below, the Test::More module is use()ed here
# so read its man page ( perldoc Test::More ) for help writing this
# test script.

is( Mytest::is_even(0), 1 );
is( Mytest::is_even(1), 0 );
is( Mytest::is_even(2), 1 );

我们将通过“make test”命令调用测试脚本。您应该看到类似以下内容的输出

%make test
PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" "-e"
"test_harness(0, 'blib/lib', 'blib/arch')" t/*.t
t/Mytest....ok
All tests successful.
Files=1, Tests=4, 0 wallclock secs ( 0.03 cusr + 0.00 csys = 0.03 CPU)
%

发生了什么?

h2xs 程序是创建扩展的起点。在后面的示例中,我们将看到如何使用 h2xs 读取头文件并生成模板以连接到 C 例程。

h2xs 在扩展目录中创建了许多文件。Makefile.PL 文件是一个 perl 脚本,它将生成一个真正的 Makefile 来构建扩展。我们稍后会仔细看看它。

.pm 和 .xs 文件包含扩展的主要内容。.xs 文件保存构成扩展的 C 例程。.pm 文件包含告诉 Perl 如何加载扩展的例程。

生成 Makefile 并运行 make 在当前工作目录中创建了一个名为 blib(代表“构建库”)的目录。此目录将包含我们将构建的共享库。一旦我们对其进行测试,我们就可以将其安装到其最终位置。

通过“make test”调用测试脚本做了一些非常重要的事情。它使用所有这些 -I 参数调用 perl,以便它可以找到扩展中包含的各种文件。在您仍在测试扩展时使用“make test非常重要。如果您尝试单独运行测试脚本,您将收到一个致命错误。使用“make test”运行测试脚本的另一个重要原因是,如果您正在测试对现有版本的升级,使用“make test”可确保您将测试您的新扩展,而不是现有版本。

当 Perl 看到 use extension; 时,它会搜索一个与 use'd 扩展同名的文件,该文件具有 .pm 后缀。如果找不到该文件,Perl 会出现致命错误。默认搜索路径包含在 @INC 数组中。

在我们的例子中,Mytest.pm 告诉 perl 它将需要 Exporter 和 Dynamic Loader 扩展。然后它设置 @ISA@EXPORT 数组以及 $VERSION 标量;最后它告诉 perl 引导模块。Perl 将调用其动态加载程序例程(如果存在)并加载共享库。

两个数组 @ISA@EXPORT 非常重要。@ISA 数组包含一个其他包列表,用于搜索当前包中不存在的方法(或子例程)。这通常只对面向对象扩展(我们将在后面讨论)很重要,因此通常不需要修改。

@EXPORT 数组告诉 Perl 扩展的哪些变量和子例程应放入调用包的命名空间中。因为您不知道用户是否已经使用了您的变量和子例程名称,所以仔细选择要导出什么至关重要。不要在没有充分理由的情况下默认导出方法或变量名称。

作为一般规则,如果模块尝试面向对象,则不要导出任何内容。如果它只是函数和变量的集合,则可以通过另一个名为 @EXPORT_OK 的数组导出它们。除非用户明确要求执行此操作,否则此数组不会自动将子例程和变量名称放入名称空间中。

有关更多信息,请参阅 perlmod

$VERSION 变量用于确保 .pm 文件和共享库彼此“同步”。每当对 .pm 或 .xs 文件进行更改时,都应增加此变量的值。

编写良好的测试脚本

编写良好的测试脚本的重要性怎么强调都不为过。您应紧密遵循 Perl 本身使用的“ok/not ok”样式,以便非常容易且明确地确定每个测试用例的结果。当您发现并修复错误时,请确保为其添加一个测试用例。

通过运行“make test”,您可以确保 Mytest.t 脚本运行并使用扩展的正确版本。如果您有许多测试用例,请将测试文件保存在“t”目录中并使用后缀“.t”。当您运行“make test”时,所有这些测试文件都将被执行。

示例 3

我们的第三个扩展将采用一个参数作为其输入,对该值进行舍入,并将参数设置为舍入值。

将以下内容添加到 Mytest.xs 的末尾

void
round(arg)
	double  arg
    CODE:
	if (arg > 0.0) {
		arg = floor(arg + 0.5);
	} else if (arg < 0.0) {
		arg = ceil(arg - 0.5);
	} else {
		arg = 0.0;
	}
    OUTPUT:
	arg

编辑 Makefile.PL 文件,以便相应的行看起来像这样

LIBS      => ['-lm'],   # e.g., '-lm'

生成 Makefile 并运行 make。将 Mytest.t 中的测试编号更改为“9”,并添加以下测试

my $i;

$i = -1.5;
Mytest::round($i);
is( $i, -2.0, 'Rounding -1.5 to -2.0' );

$i = -1.1;
Mytest::round($i);
is( $i, -1.0, 'Rounding -1.1 to -1.0' );

$i = 0.0;
Mytest::round($i);
is( $i, 0.0, 'Rounding 0.0 to 0.0' );

$i = 0.5;
Mytest::round($i);
is( $i, 1.0, 'Rounding 0.5 to 1.0' );

$i = 1.2;
Mytest::round($i);
is( $i, 1.0, 'Rounding 1.2 to 1.0' );

现在运行“make test”应打印出所有九个测试都正常。

请注意,在这些新的测试用例中,传递给 round 的参数是一个标量变量。您可能想知道是否可以舍入常量或文字。要查看发生的情况,请暂时将以下行添加到 Mytest.t

Mytest::round(3);

运行“make test”,并注意 Perl 会出现致命错误而终止。Perl 不会允许您更改常量值!

这里有什么新内容?

输入和输出参数

您在声明函数的返回值和名称之后的行中指定将传递到 XSUB 中的参数。每个输入参数行以可选的空格开头,并且可以具有可选的终止分号。

输出参数列表出现在函数的最后,紧跟在 OUTPUT: 指令之后。使用 RETVAL 告诉 Perl 您希望将此值作为 XSUB 函数的返回值发送回去。在示例 3 中,我们希望将“返回值”放置在传递进去的原始变量中,因此我们在 OUTPUT: 部分中列出了它(而不是 RETVAL)。

XSUBPP 程序

xsubpp 程序采用 .xs 文件中的 XS 代码,并将其转换为 C 代码,将其放置在后缀为 .c 的文件中。创建的 C 代码大量使用了 Perl 中的 C 函数。

TYPEMAP 文件

xsubpp 程序使用规则将 Perl 的数据类型(标量、数组等)转换为 C 的数据类型(int、char 等)。这些规则存储在 typemap 文件($PERLLIB/ExtUtils/typemap)中。下面有简要讨论,但所有细枝末节的详细信息都可以在 perlxstypemap 中找到。如果您有足够新版本的 perl(5.16 及更高版本)或升级的 XS 编译器(ExtUtils::ParseXS 3.13_01 或更高版本),则可以在 XS 中内联 typemap,而不是编写单独的文件。无论哪种方式,此 typemap 内容都分为三个部分

第一部分将各种 C 数据类型映射到一个名称,它与各种 Perl 类型大致对应。第二部分包含 C 代码,xsubpp 使用它来处理输入参数。第三部分包含 C 代码,xsubpp 使用它来处理输出参数。

让我们看看为我们的扩展创建的 .c 文件的一部分。文件名是 Mytest.c

XS(XS_Mytest_round)
{
    dXSARGS;
    if (items != 1)
	Perl_croak(aTHX_ "Usage: Mytest::round(arg)");
    PERL_UNUSED_VAR(cv); /* -W */
    {
	double  arg = (double)SvNV(ST(0));	/* XXXXX */
	if (arg > 0.0) {
		arg = floor(arg + 0.5);
	} else if (arg < 0.0) {
		arg = ceil(arg - 0.5);
	} else {
		arg = 0.0;
	}
	sv_setnv(ST(0), (double)arg);	/* XXXXX */
	SvSETMAGIC(ST(0));
    }
    XSRETURN_EMPTY;
}

注意用“XXXXX”注释的两行。如果您检查 typemap 文件(或部分)的第一部分,您会看到 double 的类型是 T_DOUBLE。在 typemap 的 INPUT 部分,通过对某些内容调用例程 SvNV,然后将其强制转换为 double,再将其分配给变量 arg,来将 T_DOUBLE 的参数分配给变量 arg。类似地,在 OUTPUT 部分,一旦 arg 具有其最终值,它就会传递给 sv_setnv 函数,以传递回调用子例程。这两个函数在 perlguts 中进行了说明;我们稍后将在关于参数堆栈的部分中讨论“ST(0)”的含义。

关于输出参数的警告

通常,编写修改其输入参数的扩展并不是一个好主意,如示例 3 所示。相反,您可能应该在数组中返回多个值,并让调用者处理它们(我们将在后面的示例中执行此操作)。但是,为了更好地适应调用预先存在的 C 例程(它们通常会修改其输入参数),这种行为是可以容忍的。

示例 4

在此示例中,我们现在将开始编写将与预定义 C 库交互的 XSUB。首先,我们将构建我们自己的一个小库,然后让 h2xs 为我们编写 .pm 和 .xs 文件。

在与 Mytest 目录同级的位置创建一个名为 Mytest2 的新目录。在 Mytest2 目录中,创建另一个名为 mylib 的目录,并 cd 进入该目录。

在这里,我们将创建一些文件,这些文件将生成一个测试库。这些文件将包括一个 C 源文件和一个头文件。我们还将在该目录中创建一个 Makefile.PL。然后,我们将确保在 Mytest2 级别运行 make 将自动运行此 Makefile.PL 文件和生成的 Makefile。

在 mylib 目录中,创建一个类似以下内容的文件 mylib.h

#define TESTVAL	4

extern double	foo(int, long, const char*);

同样创建一个类似以下内容的文件 mylib.c

#include <stdlib.h>
#include "mylib.h"

double
foo(int a, long b, const char *c)
{
	return (a + b + atof(c) + TESTVAL);
}

最后创建一个类似以下内容的文件 Makefile.PL

use ExtUtils::MakeMaker;
$Verbose = 1;
WriteMakefile(
    NAME  => 'Mytest2::mylib',
    SKIP  => [qw(all static static_lib dynamic dynamic_lib)],
    clean => {'FILES' => 'libmylib$(LIB_EXT)'},
);


sub MY::top_targets {
	'
all :: static

pure_all :: static

static ::       libmylib$(LIB_EXT)

libmylib$(LIB_EXT): $(O_FILES)
	$(AR) cr libmylib$(LIB_EXT) $(O_FILES)
	$(RANLIB) libmylib$(LIB_EXT)

';
}

确保在以“$(AR)”和“$(RANLIB)”开头的行中使用制表符而不是空格。如果您使用空格,Make 将无法正常运行。据报道,在 Win32 系统上 $(AR) 的“cr”参数是不必要的。

现在我们将创建主顶级 Mytest2 文件。切换到 Mytest2 上方的目录并运行以下命令

% h2xs -O -n Mytest2 Mytest2/mylib/mylib.h

这将打印出关于覆盖 Mytest2 的警告,但这没关系。我们的文件存储在 Mytest2/mylib 中,并且不会被修改。

h2xs 生成的常规 Makefile.PL 不了解 mylib 目录。我们需要告诉它有一个子目录,并且我们将在其中生成一个库。让我们将 MYEXTLIB 参数添加到 WriteMakefile 调用中,使其看起来像这样

WriteMakefile(
    NAME         => 'Mytest2',
    VERSION_FROM => 'Mytest2.pm', # finds $VERSION
    LIBS         => [''],   # e.g., '-lm'
    DEFINE       => '',     # e.g., '-DHAVE_SOMETHING'
    INC          => '',     # e.g., '-I/usr/include/other'
    MYEXTLIB     => 'mylib/libmylib$(LIB_EXT)',
);

然后在末尾添加一个子例程(它将覆盖预先存在的子例程)。记住使用制表符缩进以“cd”开头的行!

sub MY::postamble {
'
$(MYEXTLIB): mylib/Makefile
	cd mylib && $(MAKE) $(PASSTHRU)
';
}

我们还可以通过附加以下三行来修复 MANIFEST 文件

mylib/Makefile.PL
mylib/mylib.c
mylib/mylib.h

为了保持我们的命名空间简洁且不受污染,编辑 .pm 文件并将变量 @EXPORT 更改为 @EXPORT_OK。最后,在 .xs 文件中,编辑 #include 行以读取

#include "mylib/mylib.h"

并在 .xs 文件的末尾添加以下函数定义

double
foo(a,b,c)
	int             a
	long            b
	const char *    c
    OUTPUT:
	RETVAL

现在,我们还需要创建一个 typemap,因为默认 Perl 目前不支持 const char * 类型。在上述函数之前在 XS 代码中包含一个新的 TYPEMAP 部分

        TYPEMAP: <<END
	const char *	T_PV
        END

现在在顶级 Makefile.PL 上运行 perl。请注意,它还在 mylib 目录中创建了一个 Makefile。运行 make 并观察它是否 cd 进入 mylib 目录并在其中运行 make。

现在编辑 Mytest2.t 脚本并将测试数量更改为“5”,并在脚本末尾添加以下行

is( Mytest2::foo( 1, 2, "Hello, world!" ), 7 );
is( Mytest2::foo( 1, 2, "0.0" ),           7 );
ok( abs( Mytest2::foo( 0, 0, "-3.4" ) - 0.6 ) <= 0.01 );

(在处理浮点比较时,最好不要检查相等性,而是检查预期结果和实际结果之间的差异是否低于一定量(称为 epsilon),在本例中为 0.01)

运行“make test”,一切都应该很好。对于 Mytest2::mylib 扩展有一些关于缺少测试的警告,但您可以忽略它们。

这里发生了什么?

与之前的示例不同,我们现在在真正的包含文件中运行了 h2xs。这导致在 .pm 和 .xs 文件中都出现了一些额外的优点。

.xs 文件的解剖

“示例 4” 的 .xs 文件包含一些新元素。要理解这些元素的含义,请注意读取

MODULE = Mytest2		PACKAGE = Mytest2

此行之前的任何内容都是描述要包含哪些头文件并定义一些便利函数的纯 C 代码。此部分不执行任何翻译,除了跳过嵌入的 POD 文档(请参阅 perlpod),它按原样进入生成的输出 C 文件。

此行之后的任何内容都是 XSUB 函数的描述。这些描述由 xsubpp 翻译成使用 Perl 调用约定实现这些函数的 C 代码,并使这些函数对 Perl 解释器可见。

特别注意函数 constant。此名称在生成的 .xs 文件中出现两次:一次在第一部分中,作为静态 C 函数,另一次在第二部分中,当定义此静态 C 函数的 XSUB 接口时。

这对于 .xs 文件来说非常典型:通常,.xs 文件提供对现有 C 函数的接口。然后,此 C 函数在某个位置(外部库或 .xs 文件的第一部分)中定义,并且对该函数的 Perl 接口(即“Perl 粘合剂”)在 .xs 文件的第二部分中描述。“示例 1”“示例 2”“示例 3” 中的情况是,所有工作都在“Perl 粘合剂”中完成,这在某种程度上是例外,而不是规则。

从 XSUB 中去除多余部分

“示例 4” 中,.xs 文件的第二部分包含以下 XSUB 描述

double
foo(a,b,c)
	int             a
	long            b
	const char *    c
    OUTPUT:
	RETVAL

请注意,与 “示例 1”“示例 2”“示例 3” 相比,此描述不包含在调用 Perl 函数 foo() 期间实际执行的操作的代码。要了解此处发生的情况,可以向此 XSUB 添加 CODE 部分

double
foo(a,b,c)
	int             a
	long            b
	const char *    c
    CODE:
	RETVAL = foo(a,b,c);
    OUTPUT:
	RETVAL

不过,这两个 XSUB 提供几乎相同的生成 C 代码:xsubpp 编译器足够智能,可以从 XSUB 描述的前两行找出 CODE: 部分。OUTPUT: 部分呢?事实上,它们完全相同!OUTPUT: 部分也可以移除,只要未指定 CODE: 部分或 PPCODE: 部分xsubpp 可以看出它需要生成函数调用部分,并且也会自动生成 OUTPUT 部分。因此,可以将 XSUB 简化为

double
foo(a,b,c)
	int             a
	long            b
	const char *    c

我们是否可以用 XSUB 执行相同的操作

int
is_even(input)
	int	input
    CODE:
	RETVAL = (input % 2 == 0);
    OUTPUT:
	RETVAL

"EXAMPLE 2"?要执行此操作,需要定义 C 函数 int is_even(int input)。正如我们在 "Anatomy of .xs file" 中看到的,此定义的合适位置是 .xs 文件的第一部分。事实上,C 函数

int
is_even(int arg)
{
	return (arg % 2 == 0);
}

对于此操作来说可能有些过剩。像 #define 一样简单的东西也可以

#define is_even(arg)	((arg) % 2 == 0)

在 .xs 文件的第一部分中添加此内容后,“Perl 胶水”部分变得像

int
is_even(input)
	int	input

将胶水部分与主力部分分开的这种技术有明显的权衡:如果您想更改 Perl 接口,则需要更改代码中的两个位置。但是,它消除了很多混乱,并且使主力部分独立于 Perl 调用约定的特殊性。(事实上,在上面的描述中没有特定于 Perl 的内容,不同版本的 xsubpp 可能也已将其转换为 TCL 胶水或 Python 胶水。)

有关 XSUB 参数的更多信息

完成示例 4 后,我们现在可以轻松模拟一些实际库,其接口可能不是世界上最干净的。现在,我们将继续讨论传递给 xsubpp 编译器的参数。

当您在 .xs 文件中为例程指定参数时,实际上是为列出的每个参数传递三条信息。第一条是该参数相对于其他参数的顺序(第一个、第二个等)。第二条是参数的类型,包括参数的类型声明(例如,int、char* 等)。第三条是调用库函数时参数的调用约定。

虽然 Perl 通过引用将参数传递给函数,但 C 通过值传递参数;要实现修改其中一个“参数”的数据的 C 函数,此 C 函数的实际参数将是指向该数据的指针。因此,具有声明的两个 C 函数

int string_length(char *s);
int upper_case_char(char *cp);

可能具有完全不同的语义:第一个函数可能检查 s 指向的 char 数组,而第二个函数可能立即取消引用 cp 并仅操作 *cp(使用返回值作为成功指示符)。从 Perl 中,将以完全不同的方式使用这些函数。

可以通过用 & 替换参数前的 * 来将此信息传达给 xsubpp& 表示应通过其地址将参数传递给库函数。上述两个函数可以 XSUB 化为

int
string_length(s)
	char *	s

int
upper_case_char(cp)
	char	&cp

例如,考虑

int
foo(a,b)
	char	&a
	char *	b

此函数的第一个 Perl 参数将被视为一个字符并分配给变量 a,并且它的地址将被传递到函数 foo 中。第二个 Perl 参数将被视为一个字符串指针并分配给变量 b。b 的将被传递到函数 foo 中。xsubpp 生成的对函数 foo 的实际调用看起来像这样

foo(&a, b);

xsubpp 将以相同的方式解析以下函数参数列表

char	&a
char&a
char	& a

但是,为了便于理解,建议您将“&”放在变量名旁边,远离变量类型),并将“*”放在变量类型附近,但远离变量名(如上面的对 foo 的调用)。通过这样做,很容易理解将传递给 C 函数的内容;它将是“最后一列”中的内容。

在可能的情况下,您应该尽力传递函数所需的变量类型。从长远来看,这将为您节省很多麻烦。

参数堆栈

如果我们查看除示例 1 之外的任何示例生成的任何 C 代码,您会注意到很多对 ST(n) 的引用,其中 n 通常为 0。“ST”实际上是一个宏,它指向参数堆栈上的第 n 个参数。因此,ST(0) 是堆栈上的第一个参数,因此也是传递给 XSUB 的第一个参数,ST(1) 是第二个参数,依此类推。

当您在 .xs 文件中列出传递给 XSUB 的参数时,会告诉 xsubpp 哪个参数对应于哪个参数堆栈(即,列出的第一个参数是第一个参数,依此类推)。如果您没有按照函数期望的顺序列出它们,您会招致灾难。

参数堆栈上的实际值是传递的值的指针。当一个参数被列为 OUTPUT 值时,它在堆栈上的对应值(即,如果它是第一个参数,则为 ST(0))会发生改变。您可以通过查看为示例 3 生成的 C 代码来验证这一点。round() XSUB 例程的代码包含如下所示的行

double  arg = (double)SvNV(ST(0));
/* Round the contents of the variable arg */
sv_setnv(ST(0), (double)arg);

arg 变量最初通过获取 ST(0) 中的值来设置,然后在例程的末尾存储回 ST(0)。

XSUB 也允许返回列表,而不仅仅是标量。这必须通过以微妙的不同方式操作堆栈值 ST(0)、ST(1) 等来完成。有关详细信息,请参阅 perlxs

XSUB 还允许避免将 Perl 函数参数自动转换为 C 函数参数。有关详细信息,请参阅 perlxs。有些人更喜欢通过检查 ST(i) 来手动转换,即使在自动转换可以完成的情况下也是如此,他们认为这使 XSUB 调用的逻辑更加清晰。与 "Getting the fat out of XSUBs" 进行比较,以获得类似的权衡,即 XSUB 的“Perl 粘合剂”和“主力”部分的完全分离。

虽然专家们可能会争论这些惯用语,但 Perl 核心新手可能更喜欢一种尽可能不特定于 Perl 核心的方法,即自动转换和自动调用生成,如 "Getting the fat out of XSUBs" 中所述。这种方法还有额外的优点,即保护 XSUB 编写者免受未来 Perl API 更改的影响。

扩展您的扩展

有时您可能希望提供一些额外的方法或子例程,以帮助简化或更容易理解 Perl 与您的扩展之间的接口。这些例程应位于 .pm 文件中。它们在扩展本身加载时自动加载,还是仅在调用时加载,取决于子例程定义在 .pm 文件中的放置位置。您还可以查阅 AutoLoader 以了解存储和加载额外子例程的备用方法。

记录您的扩展

绝对没有借口不记录您的扩展。文档应位于 .pm 文件中。此文件将馈送到 pod2man,嵌入式文档将转换为手册页格式,然后放置在 blib 目录中。在安装扩展时,它将被复制到 Perl 的手册页目录中。

您可以在 .pm 文件中插入文档和 Perl 代码。事实上,如果您想使用自动加载方法,您必须这样做,如 .pm 文件中的注释所述。

有关 pod 格式的更多信息,请参阅 perlpod

安装您的扩展

一旦您的扩展完成并通过所有测试,安装它非常简单:您只需运行“make install”。您要么需要对 Perl 安装所在的目录具有写入权限,要么要求您的系统管理员为您运行 make。

或者,您可以通过在 make install 后(或在 make 和 install 之间,如果您有脑死的 make 版本)放置“PREFIX=/destination/directory”来指定放置扩展文件的确切目录。如果您正在构建最终将分发到多个系统的扩展,这将非常有用。然后,您只需将文件存档到目标目录并将其分发到目标系统即可。

示例 5

在此示例中,我们将对参数堆栈进行更多操作。前面的示例都只返回了一个值。现在,我们将创建一个返回数组的扩展。

此扩展非常面向 Unix(struct statfs 和 statfs 系统调用)。如果您不在 Unix 系统上运行,则可以将 statfs 替换为返回多个值的任何其他函数,您可以对要返回给调用者的值进行硬编码(尽管这将使测试错误情况变得更加困难),或者您也可以不执行此示例。如果您更改 XSUB,请务必修复测试用例以匹配更改。

返回 Mytest 目录,并将以下代码添加到 Mytest.xs 的末尾

void
statfs(path)
	char *  path
    INIT:
	int i;
	struct statfs buf;

    PPCODE:
	i = statfs(path, &buf);
	if (i == 0) {
		XPUSHs(sv_2mortal(newSVnv(buf.f_bavail)));
		XPUSHs(sv_2mortal(newSVnv(buf.f_bfree)));
		XPUSHs(sv_2mortal(newSVnv(buf.f_blocks)));
		XPUSHs(sv_2mortal(newSVnv(buf.f_bsize)));
		XPUSHs(sv_2mortal(newSVnv(buf.f_ffree)));
		XPUSHs(sv_2mortal(newSVnv(buf.f_files)));
		XPUSHs(sv_2mortal(newSVnv(buf.f_type)));
	} else {
		XPUSHs(sv_2mortal(newSVnv(errno)));
	}

您还需要在 .xs 文件的顶部添加以下代码,紧跟在包含“XSUB.h”之后

#include <sys/vfs.h>

同时将以下代码段添加到 Mytest.t,同时将“9”个测试增加到“11”

    my @a;

	@a = Mytest::statfs("/blech");
	ok( scalar(@a) == 1 && $a[0] == 2 );

	@a = Mytest::statfs("/");
	is( scalar(@a), 7 );

此示例中的新内容

此示例添加了许多新概念。我们将一次处理它们。

示例 6

在此示例中,我们将接受对数组的引用作为输入参数,并返回对哈希数组的引用。这将演示如何从 XSUB 操纵复杂的 Perl 数据类型。

此扩展有点牵强。它基于前一个示例中的代码。它多次调用 statfs 函数,接受对文件名数组的引用作为输入,并返回对哈希数组的引用,其中包含每个文件系统的数据。

返回 Mytest 目录,并将以下代码添加到 Mytest.xs 的末尾

    SV *
    multi_statfs(paths)
	    SV * paths
	INIT:
	    AV * results;
	    SSize_t numpaths = 0, n;
	    int i;
	    struct statfs buf;

	    SvGETMAGIC(paths);
	    if ((!SvROK(paths))
		|| (SvTYPE(SvRV(paths)) != SVt_PVAV)
		|| ((numpaths = av_top_index((AV *)SvRV(paths))) < 0))
	    {
		XSRETURN_UNDEF;
	    }
	    results = (AV *)sv_2mortal((SV *)newAV());
	CODE:
	    for (n = 0; n <= numpaths; n++) {
		HV * rh;
		STRLEN l;
		SV * path = *av_fetch((AV *)SvRV(paths), n, 0);
		char * fn = SvPVbyte(path, l);

		i = statfs(fn, &buf);
		if (i != 0) {
		    av_push(results, newSVnv(errno));
		    continue;
		}

		rh = (HV *)sv_2mortal((SV *)newHV());

		hv_store(rh, "f_bavail", 8, newSVnv(buf.f_bavail), 0);
		hv_store(rh, "f_bfree",  7, newSVnv(buf.f_bfree),  0);
		hv_store(rh, "f_blocks", 8, newSVnv(buf.f_blocks), 0);
		hv_store(rh, "f_bsize",  7, newSVnv(buf.f_bsize),  0);
		hv_store(rh, "f_ffree",  7, newSVnv(buf.f_ffree),  0);
		hv_store(rh, "f_files",  7, newSVnv(buf.f_files),  0);
		hv_store(rh, "f_type",   6, newSVnv(buf.f_type),   0);

		av_push(results, newRV_inc((SV *)rh));
	    }
	    RETVAL = newRV_inc((SV *)results);
	OUTPUT:
	    RETVAL

并将以下代码添加到 Mytest.t,同时将“11”个测试增加到“13”

my $results = Mytest::multi_statfs([ '/', '/blech' ]);
ok( ref $results->[0] );
ok( ! ref $results->[1] );

此示例中的新内容

此处介绍了许多新概念,如下所述

示例 7(即将推出)

XPUSH 参数并设置 RETVAL 并将返回值分配给数组

示例 8(即将推出)

设置 $!

示例 9 将打开的文件传递给 XSes

你可能会认为,使用所有 typeglob 和其他内容将文件传递给 XS 很困难。好吧,事实并非如此。

假设由于某种奇怪的原因,我们需要一个标准 C 库函数 fputs() 的包装器。我们只需要

#define PERLIO_NOT_STDIO 0  /* For co-existence with stdio only */
#define PERL_NO_GET_CONTEXT /* This is more efficient */
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include <stdio.h>

int
fputs(s, stream)
  char *          s
  FILE *          stream

实际工作在标准类型图中完成。

有关更多详细信息,请参阅 perlapio 中的“与 stdio 并存”

但是您会失去 perlio 层完成的所有精细工作。这会调用 stdio 函数 fputs(),它对这些精细工作一无所知。

标准类型图提供 PerlIO * 的三个变体:InputStream (T_IN)、InOutStream (T_INOUT) 和 OutputStream (T_OUT)。一个裸的 PerlIO * 被视为 T_INOUT。如果在您的代码中很重要(请参阅以下内容了解其重要性),请 #define 或 typedef 一个特定名称,并将其用作 XS 文件中的参数或结果类型。

标准类型图在 perl 5.7 之前不包含 PerlIO *,但它有三个流变体。直接使用 PerlIO * 向后不兼容,除非您提供自己的类型图。

对于来自 perl 的流,主要区别在于 OutputStream 将获取输出 PerlIO * - 这可能会在套接字上产生差异。就像在我们的示例中...

对于传递 perl 的流,将创建一个新的文件句柄(即对新 glob 的引用),并将其与提供的 PerlIO * 关联。如果 PerlIO * 的读/写状态不正确,那么当使用文件句柄时,您可能会收到错误或警告。因此,如果您以“w”打开 PerlIO *,则它实际上应该是 OutputStream,如果以“r”打开,则它应该是 InputStream

现在,假设您想在 XS 中使用 perlio 层。我们将使用 perlio PerlIO_puts() 函数作为示例。

在 XS 文件的 C 部分(在第一个 MODULE 行上方),您有

	#define OutputStream	PerlIO *
    or
	typedef PerlIO *	OutputStream;

这是 XS 代码

int
perlioputs(s, stream)
	char *          s
	OutputStream	stream
CODE:
	RETVAL = PerlIO_puts(stream, s);
OUTPUT:
	RETVAL

我们必须使用 CODE 部分,因为 PerlIO_puts() 的参数与 fputs() 相反,而我们希望保持参数相同。

想要彻底探索这一点,我们希望在 PerlIO * 上使用 stdio fputs()。这意味着我们必须向 perlio 系统请求 stdio FILE *

int
perliofputs(s, stream)
	char *          s
	OutputStream	stream
PREINIT:
	FILE *fp = PerlIO_findFILE(stream);
CODE:
	if (fp != (FILE*) 0) {
		RETVAL = fputs(s, fp);
	} else {
		RETVAL = -1;
	}
OUTPUT:
	RETVAL

注意:PerlIO_findFILE() 将在层中搜索 stdio 层。如果找不到,它将调用 PerlIO_exportFILE() 以生成新的 stdio FILE。请仅在您想要一个 FILE 时调用 PerlIO_exportFILE()。它将在每次调用时生成一个,并推送一个新的 stdio 层。因此,不要在同一文件中重复调用它。PerlIO_findFILE() 将在 PerlIO_exportFILE() 生成 stdio 层后检索该层。

这仅适用于 perlio 系统。对于 5.7 之前的版本,PerlIO_exportFILE() 等同于 PerlIO_findFILE()

对这些示例进行故障排除

如本文档开头所述,如果您在使用这些示例扩展时遇到问题,您可以查看以下内容是否有帮助。

另请参见

有关更多信息,请参阅 perlgutsperlapiperlxsperlmodperlapioperlpod

作者

Jeff Okamoto <[email protected]>

Dean Roehrich、Ilya Zakharevich、Andreas Koenig 和 Tim Bunce 审阅并提供协助。

Lupe Christoph 撰写了 PerlIO 材料,Nick Ing-Simmons 进行了部分澄清。

Renee Baecker 对 Perl 5.8.x 的 h2xs 进行了更改

此文档现在作为 Perl 本身的一部分进行维护。

上次更改

2020-10-05