编写自己的XS函数

第一部分中,我们学习了XS的基本组件,并将两个C函数集成到Perl中。本章将向您展示如何定义接受多个参数的xsubs,并定义自己的逻辑,而不是使用XS作为访问C库的外部函数接口。

要执行本文中的代码,您需要第一部分中的文件。

模块代码

与之前一样,我们将定义模块代码以加载我们的XS。这需要以下内容

package XS::Tutorial::Two;
require XSLoader;

XSLoader::load();
1;

应该保存为lib/XS/Tutorial/Two.pm

XS代码

XS文件的开头将与上一章类似

#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 "stdint.h"         // portable integer types

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

请记住,在PROTOTYPES行之后追加任何XS代码。应该保存为lib/XS/Tutorial/Two.xs

添加数字

这是一个简单的声明,声明了一个添加两个整数的xsub

int
add_ints (addend1, addend2)
  int addend1
  int addend2
  CODE:
    RETVAL = addend1 + addend2;
  OUTPUT:
    RETVAL

这声明了一个名为add_ints的xsub,它接受两个整数,返回类型为int。注意函数定义的K&R风格。这也可以写成

add_ints (int addend1, int addend2)

但在现实中很少这样做。我不知道这是否是货船崇拜的事情,或者是否有我未知的xsub编译器的边缘案例。为了安全起见,我会继续像其他人一样做(崇拜持续存在!)。

而之前在之前,我们本质上是在将C函数如srand映射到Perl,而在这里,我们在声明自己的逻辑:add_ints没有从任何地方导入,我们将其声明为一个新函数。

由于add_ints是一个新函数,我们需要定义它的逻辑,这就是CODE部分的作用所在。在这里,我们可以编写C代码,它构成了函数的主体。在这个例子中,我将两个子程序参数相加,并将结果赋给RETVAL

RETVAL(“返回值”)是一个由xsub处理器(xsubpp)声明的特殊变量。输出部分接受xsub的返回变量,将其放置在堆栈上,以便调用代码可以接收它。

添加超过两个数字

添加两个数字很好,但是列表是Perl的语言。让我们更新add_ints xsub以接受n个值

int32_t
add_ints (...)
  CODE:
    uint32_t i;
    for (i = 0; i < items; i++) {
      if (!SvOK(ST(i)) || !SvIOK(ST(i)))
        croak("requires a list of integers");

      RETVAL += SvIVX(ST(i));
    }
  OUTPUT:
    RETVAL

首先,请注意我已更新了返回值。使用C中的int的一个问题是它可能在不同的机器体系结构中具有不同的大小。int32_t来自stdint.h库,并保证为32位有符号整数。

我用...替换了函数参数,这表示该函数接受可变数量的参数,就像在C中一样。在CODE部分,我声明了一个名为iuint32_t整数(uint32_t是32位无符号整数)。

循环使用特殊变量items(传递给函数的参数数量)来迭代参数。如果测试失败,则代码调用croak以抛出一个致命异常。

否则,将整数值从标量中提取出来(SvIVX)并添加到RETVAL中。如果你觉得这些C宏看起来很奇怪,不用担心,它们很奇怪!它们是Perl C API的一部分,在perlapi中有文档说明。

边缘情况

现在可能是一个为这个函数编写一些测试的好时机,下面是一个开始

use Test::More;

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

cmp_ok XS::Tutorial::Two::add_ints(7,3), '==', 10;
cmp_ok XS::Tutorial::Two::add_ints(1500, 21000, -1000), '==', 21500;

done_testing;

我将该文件保存为t/two.t,并通过使用make构建发行版来运行它

perl Makefile.PL && make && make test

你知道如果用无参数调用add_ints,返回值会是什么吗?可能是undef,因为没有参数时,for循环将不会进行任何迭代。这里有一个测试这个条件的示例

ok !defined XS::Tutorial::Two::add_ints(), 'empty list returns undef';

使用以下命令重新构建和运行测试

make clean && perl Makefile.PL &&  make && make test

这个测试失败了,因为返回值是零!这是C的一个怪癖:未初始化的整数可以是零。让我们修复xsub,使其在未接收任何参数时返回undef

SV *
add_ints (...)
  PPCODE:
    uint32_t i;
    int32_t total = 0;
    if (items > 0) {
      for (i = 0; i < items; i++) {
        if (!SvOK(ST(i)) || !SvIOK(ST(i)))
          croak("requires a list of integers");

        total += SvIVX(ST(i));
      }
      PUSHs(sv_2mortal(newSViv(total)));
    }
    else {
      PUSHs(sv_newmortal());
    }

哇,有很多改动!首先,我将返回类型从int32_t更改为SV *。原因将在下面变得清晰。现在,CODE部分被称为PPCODE,这告诉xsubpp我们将自己管理xsub的返回值,因此去掉了OUTPUT部分。

我声明了一个新变量total来捕获随着参数的添加而运行的累计总和。如果我们至少接收到了一个参数,total就会复制到一个新的标量整数值(newSViv),它的引用计数被修正(sv_2mortal),然后被推入栈指针(PUSHs)。

否则,使用sv_newmortal声明一个新的undef标量,并将其推入栈指针。所以在这两种情况下,我们都在返回一个SV。由于我们返回的是Perl类型而不是C类型(int32_t),因此不需要xsubpp将我们的返回值强制转换为Perl标量,我们已经做了。

总结

本教程涵盖了一些编写xsubs的关键技能:如何接受多个参数,如何编写自己的逻辑,以及如何管理栈指针。如果你掌握了所有这些,并且已经理解了第一部分的内容,你就有了编写自己的XS代码的基础。

参考文献

  • 此文档和代码位于CPAN(XS::Tutorial
  • perlxs定义了xsubpp所识别的关键字
  • perlapi列出了用于与Perl数据结构(和解释器)交互的C宏
  • stdint.h C库提供了一系列可移植的整数类型

标签

David Farrell

David是一位专业的程序员,他经常推文博客关于代码和编程艺术。

查看他们的文章

反馈

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