XS入门指南

可扩展子例程(XS)是用C语言编写的子例程,可以从Perl代码中调用。您可能希望使用XS的两个常见原因:您想使用与Perl一起使用的C库,或者您想通过在C中而不是在Perl中处理来加快子例程的速度。

本教程将带您了解使用基本XS示例启动所需的所有组件。这里会有很多新术语和概念

如果您想编写XS,就必须学习它。学习XS非常困难

Steven W. McDougall

如果一开始事情没有顺利点击,请不要气馁:我保证您学习XS会非常有回报:您将能够编写闪电般快速的课程;更好地理解Perl的内部工作原理,并能够集成您选择的任何C库,并从Perl中使用它。

组件

编写xsub需要一些基本组件。第一个是提供任何XS函数命名空间的Perl模块。这就是所需的一切

package XS::Tutorial::One;
require XSLoader;

XSLoader::load();
1;

该文件应保存为lib/XS/Tutorial/One.pm。默认情况下,XSLoader::load在从其调用的包名中搜索匹配的XS代码。

让我们也创建一个主分布模块

package XS::Tutorial;
BEGIN { our $VERSION = 0.01 }
1;

=encoding utf8

=head1 NAME

XS::Tutorial - documentation with examples for learning Perl XS

=cut

该文件应保存为lib/XS/Tutorial.pm

接下来,我们需要一个.xs文件,该文件定义了将被XS::Tutorial::One加载的xsubs

#define PERL_NO_GET_CONTEXT // we'll define thread context if necessary (faster)
#include "EXTERN.h"         // globals/constant import locations
#include "perl.h"           // Perl symbols, structures and constants definition
#include "XSUB.h"           // xsubpp functions and macros
#include <stdlib.h>         // rand()

// additional c code goes here

MODULE = XS::Tutorial::One  PACKAGE = XS::Tutorial::One
PROTOTYPES: ENABLE

 # XS code goes here

 # XS comments begin with " #" to avoid them being interpreted as pre-processor
 # directives

unsigned int
rand()

该文件应保存为lib/XS/Tutorial/One.xs。文件上半部分是纯C代码。以MODULE = XS::Tutorial::One开始的行指示XS代码的开始。此部分将由xsubpp解析和编译为C代码。

MODULEPACKAGE指令定义了将加载我们定义的任何xsubs的Perl模块和包。PROTOTYPES: ENABLE行告诉xsubpp为任何我们创建的xsubs定义子程序原型。这通常是您想要的:原型可以帮助Perl捕获编译时错误。

文件的最后两行是一个xsub

unsigned int
rand()

第一行定义了返回类型。第二行做两件事:它指示要调用的C函数的名称,并定义了xsub的签名。

在这种情况下,我们调用rand而不接受任何参数。这不是Perl的内置rand函数,这个rand来自stdlib.h。

最后,我们需要一个Makefile.PL脚本——因为XS代码是编译的,我们需要一个在我们可以使用它之前构建它的工具

use 5.008005;
use ExtUtils::MakeMaker 7.12; # for XSMULTI option

WriteMakefile(
  NAME           => 'XS::Tutorial',
  VERSION_FROM   => 'lib/XS/Tutorial.pm',
  PREREQ_PM      => { 'ExtUtils::MakeMaker' => '7.12' },
  ABSTRACT_FROM  => 'lib/XS/Tutorial.pm',
  AUTHOR         => 'David Farrell',
  CCFLAGS        => '-Wall -std=c99',
  OPTIMIZE       => '-O3',
  LICENSE        => 'freebsd',
  XSMULTI        => 1,
);

ExtUtils::MakeMaker 文档解释了这些选项。

但让我们谈谈XSMULTI。这是一个相对较新的特性,它允许您为模块拥有单独的.xs文件。默认情况下,EUMM假定xs文件与分发名称匹配。在这种情况下,这意味着有一个单一的Tutorial.xs文件,其中包含多个xs MODULEPACKAGE声明。通过使用XSMULTI,我们可以有多个XS文件,每个文件对应分发中的一个模块。

实际上它搜索编译的C代码,但效果是一样的。

构建

现在我们应该有四个文件

lib/XS/Tutorial.pm
lib/XS/Tutorial/One.pm
lib/XS/Tutorial/One.xs
Makefile.PL

以下命令将构建分布

$ perl Makefile.PL
$ make

一篇关于理解xsubpp生成的C代码的简要文章

make创建了一大批文件,但看看lib/XS/Tutorial/One.c。这是xsubpp的输出。如果您仔细观察,您可以在其中找到来自lib/XS/Tutorial/One.xs的C代码行。但看看我们的rand xsub发生了什么

XS_EUPXS(XS_XS__Tutorial__One_rand); /* prototype to pass -Wmissing-prototypes */
XS_EUPXS(XS_XS__Tutorial__One_rand)
{
    dVAR; dXSARGS;
    if (items != 0)
       croak_xs_usage(cv,  "");
    {
  unsigned int        RETVAL;
  dXSTARG;

  RETVAL = rand();
  XSprePUSH; PUSHu((UV)RETVAL);
    }
    XSRETURN(1);
}

xsubpp 已经用一些相当丑陋的 C 宏替换了我们的 XS 代码!这些宏是 Perl 解释器 C API 的一部分。许多宏在 perlapi 中有文档说明,通常在 Perl 源代码中的 XSUB.hperl.h 中定义。

那么这些宏在做什么呢?从高层次来看,dVARdXSARGS 设置全局指针栈和一些局部变量。 items 是传递给 xsub 的参数的数量。由于 rand 是一个 void 函数,如果这个值不为零,它将崩溃。 croak_xs_usage 接受一个 coderef 和一个 args 字符串。在这个上下文中,cv 是 xsub,没有参数,所以字符串是空的。

接下来,代码声明了 RETVAL,这是 xsub 的返回值。 dXTARG 初始化 TARG 指针。接下来调用 rand() 并将返回值赋给 RETVALXSprePUSH 将栈指针回退一个位置,PUSHuRETVAL 复制到 TARG 并将其推入全局栈指针。 XSRETURN 从 xsub 返回,表示它向栈中添加了多少个参数,在这个例子中是 1。

编写 XS 时,通常不需要研究生成的 C 代码,但了解这个过程是有帮助的。

安装

现在代码已编译,使用以下命令安装:

$ make install

如果你使用的是系统 Perl,你可能需要使用 sudo 来安装。现在我们可以使用单行命令测试该模块

$ perl -MXS::Tutorial::One -E 'say XS::Tutorial::One::rand()'
1804289383

它工作了!但你试过运行两次吗?

$ perl -MXS::Tutorial::One -E 'say XS::Tutorial::One::rand()'
1804289383

每次我们都得到相同的伪随机序列……我们需要调用 srand 来生成序列。该函数已经由 stdlib.h 提供,所以我们只需要将以下文本追加到 lib/XS/Tutorial/One.xs

void
srand(seed)
  unsigned int seed

这个 xsub 与第一个不同:它的返回类型是 void,这意味着它不返回任何内容。它还包括一个名为 seed 的参数,最后一行将其定义为无符号整数。

重新构建并安装发行版

$ make && make install

现在我们可以在调用 rand 之前调用 srand 来生成伪随机序列。

$ perl -MXS::Tutorial::One -E 'XS::Tutorial::One::srand(777);\
say XS::Tutorial::One::rand()'
947371799

我们使用了幸运的(777)种子数字,并且 rand 输出了一个不同的数字,太好了!

我们打败了 Perl 吗?

正如你所知,xsubs 通常比纯 Perl 代码快。我们为 randsrand 构建了两个 xsubs,它们也可以作为 Perl 的内置函数使用。你认为 xsubs 更快吗?这是我机器上的基准测试

              Rate xs_rand bi_rand
xs_rand 15691577/s      --    -64%
bi_rand 43095739/s    175%      --

哦,不!尽管我们的 rand xsub 直接调用 C 的 stdlib 函数,但它的速度比 Perl 的内置 rand 慢得多。这并不是因为 xsubs 慢,而是因为 Perl 的内置函数非常快。调用 xsubs 有一些开销,这是内置函数不需要支付的。

测试

我们可以编写单元测试来代替运行单行命令来检查我们的代码是否正常工作。这是一个基本脚本

#!/usr/bin/perl
use Test::More;

BEGIN { use_ok 'XS::Tutorial::One' }

ok my $rand = XS::Tutorial::One::rand(), 'rand()';
like $rand, qr/^\d+$/, 'rand() returns a number';

ok !defined XS::Tutorial::One::srand(5), 'srand()';
ok $rand ne XS::Tutorial::One::rand(), 'after srand, rand returns different number';
done_testing;

将此文件保存为 t/one.t。假设你已经构建并安装了发行版,你可以这样做

$ perl t/one.t
ok 1 - use XS::Tutorial::One;
ok 2 - rand()
ok 3 - rand() returns a number
ok 4 - srand()
ok 5 - after srand, rand returns different number
1..5

现在在将来构建发行版时,你应该这样做

$ perl Makefile.PL && make && make test

这将重新构建并测试发行版。由于 XS 代码是编译的,编写测试和使用单行命令,你可以快速在编码和测试之间切换。

不要忘记将 Test::More 添加到 Makefile.PL 中的 PREREQ_PM 条目。当你没有特定最低版本时,只需使用 0

PREREQ_PM => { 'Test::More' => 0, 'ExtUtils::MakeMaker' => '7.12' },

清理

构建发行版会生成大量的临时文件。ExtUtils::MakeMaker 提供了一个 realclean 例程

$ make realclean

这将删除所有构建文件并将工作目录重置为正常。

参考

  • 这篇文章和代码在 CPAN 上作为 XS::Tutorial::One 提供
  • 由 Steven W. McDougall 编写的 XS Mechanics 是我最喜欢的第二本书 :) XS 教程
  • perlxs 定义了由 xsubpp 识别的关键词
  • perlapi : 用于与 Perl 数据结构(和解释器)交互的 C 宏
  • stdlib.h》手册页定义了 C 标准库函数和类型
  • 编写 Makefile.PL 文件时:ExtUtils::MakeMaker 文档 非常宝贵
  • Perl 的内置 randsrand 函数


本文最初发布在 PerlTricks.com 上。

标签

David Farrell

David 是一名专业程序员,他经常在 推特博客 上分享关于代码和编程艺术的见解。

浏览他们的文章

反馈

这篇文章有什么问题吗?请在 GitHub 上打开一个问题或拉取请求来帮助我们。