perldoc perlxstutを読んだときのメモ

正月番組をみながら[http://perldoc.perl.org/perlxstut.html#EXAMPLE-3:title=perldoc perlxstut]を読んだ.メモをとったのでせっかくだからはっつけておくよ.

ちょっとメモがはしょり気味なのと,うちの理解があやしいところがあってこころもとないですが,perlxstut読むときの参考にでもなればばば.

Example1

$ h2xs -A -n Mytest

で雛形がつくれる.生成されたMytest.xsにXSのコードを書く

値を返さないHello, WorldだとCODE:以下にprintfを書くだけでOK

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

CODE: みたいな部分はXS特有の書き方.あとで展開されてCのコードになる.

$ perl Makefile.PL
$ make

するとビルドされる.このとき作られるMytest.cをみるとXSから生成されたCのコードを見れる.

t/Mytest.tにコードを書いてみて実行結果を確認できる.

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

Mytest::hello();

Example2

引数をとって値を返す関数をXSで定義してみる.さっき作ったMytest.xsに以下を追加.

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

inputで整数値を受け取り,RETVALに結果を代入する.RETVALはXSで容易される返り値のための変数.

OUTPUT: のところに変数を書くと返り値として使われる.

Example3

引数に渡ってきた変数自体を変更することで値を返す破壊的な関数を定義してみる.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

このように仮引数argに代入してそのままOUTPUT:で返却すると,argで渡ってきた変数に変更が加えられる.

my $i;                                                                     

$i = -1.5;                                                                 
Mytest::round($i);                                                         
is $i, -2.0; # ok

ということ.

ちなみに,XS => C変換はXSUBPPというのがやってるらしい.

TYPEMAPについて

TYPEMAPはCの型とPerlの値を自動的にマッピングする仕組み.

typemapファイルに型の対応や変換する方法が書いてある.(macportsのPerl5.8.8だと /opt/local/lib/perl5/5.8.8/ExtUtils/typemap にある)

Example4

Cライブラリのヘッダの情報にモジュールテンプレートを作る.ヘッダを準備してから.

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

のようにヘッダを引数に指定して実行.

くわえて,Cのライブラリも一緒にビルドするためのMakefile.PLの書き方の解説など.詳細は割愛.

XSのなかでデフォルトで用意されているもの以外のTYPEMAPが必要な場合は,モジュールディレクトリ直下のtypemapファイルに書く.

const char * T_PV
.xsファイルの構成
MODULE = Mytest2                PACKAGE = Mytest2

xsファイルのこの行より上は普通のCのコードが書ける.この行より下はXSのコードになる.XSのコードはxsubppコマンドでCのコードに変換される.

通常のXSでは,実際に呼び出されるCの関数はこの行より上の部分か,外部のライブラリで定義される.XSではPerlとそのCの関数のGlueだけが定義される.XSにちょくせつ関数の機能を書くのは例外的だ.

XSUBもうちょい詳細

XSはかしこいので,

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

本来このように書かないといけないところが,

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

のように省略して書ける.

XSUBの引数についてもうちょっと
int
foo(a, b)
    char    &a
    char *   b

XSで&aとかくと,Cの関数にaではなくaのアドレスがわたるようになる.ポインタをわたすときは*とbの間にスペースが必要なので注意.

引数スタック

生成されたCのコードからはST(n)マクロで引数を取得できる.

XSのコードで引数をOUTPUTでそのままかえすと引数自体が変更される.例えば以下のコードは,

void                                                                       
round(arg)                                                                 
    double arg                                                             
    CODE:
        # do something                                                          
    OUTPUT:                                                                
        arg 

次のようなCのコードに変換される.ST(0)に記録されていたargを処理したあと,ST(0)戻している.

    double  arg = (double)SvNV(ST(0));
    # do something
    sv_setnv(ST(0), (double)arg);

Example 5

Arrayを返す関数の例.次のように書くことで,関数の返り値としてArrayを返せる.

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)));
        }

MacOSXだとstatfs構造体がどのヘッダファイルに定義されてるかわからなくてコンパイルできなかった.

この例で新しくでてきたこと
  • INITディレクティブは引数のデコード処理の直後に実行されるので変数の宣言に丁度良い
  • PPCODEディレクティブは返り値の処理を自分で行うときに使う.
  • 返り値をArrayで返すために値をスタックにつむにはXPUSHマクロを使う.
  • 返り値スタックのSVはmortalになってる.mortalってなに?
  • XPUSHマクロは呼び出されるたびにスタックを拡張するので無駄がある.EXTENDマクロで予めスタックを拡張してからPUSHsマクロを使うと効率が良い

Example6

引数にArrayのリファレンスをとって,HashのArrayのリファレンスを返す関数の例.Perlのデータ構造を操作するちょっと複雑な例.(コードは長いので割愛)

この例で新しくでてきたこと
  • typemapを使わずにSV *を受けとってSV *を返してる.返してるのはArrayリファレンス一つなのでPPCODEを使う必要がない.
  • 引数で受け取った値のValidationをINITでやっている.
	    if ((!SvROK(paths)) # リファレンスか?
		|| (SvTYPE(SvRV(paths)) != SVt_PVAV) # Arrayのリファレンスか?
		|| ((numpaths = av_len((AV *)SvRV(paths))) < 0)) # 値がはいってるか?
	    {
		XSRETURN_UNDEF; # undefを返すマクロ
	    }
  • Arrayは内部ではAV *で表現される.PerlのArray操作関数と似たマクロが用意されてる.(av_len, av_fetch, av_push など).スタックと同様に事前にArrayを拡張しておくと効率がよくなる.
av_extend(results, numpaths);
  • Hashは内部ではHV *で表現される.ArrayといっしょでPerlと似た操作用マクロが用意されている.
  • リファレンスはnewRV関数で作る.引数のSV *はAV *やHV *をキャストして渡せる.逆にSvRVでリファレンスからSV *を得た時は自分で適切な型にキャストする必要がある.

Example7 (Coming Soon)

Example8 (Coming Soon)

Example9

ひらいているファイルをXSに渡す方法.

# PerlIOがちゃんと理解できていないのでいろいろあやしい.

stdio.hで定義されてるfputsをPerlで使うには,以下のように書く.

	#define PERLIO_NOT_STDIO 0
	#include "EXTERN.h"
	#include "perl.h"
	#include "XSUB.h"

	#include <stdio.h>

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

typemapがうまくやってくれるおかげでPerlIOレイヤをうまくとおるようになってる?

じぶんでPerlIOつかう場合は以下のPerlIO_putsのようにPerlIO用の関数を使う.

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

PerlIOから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

まとめ

  • XSまわりの仕組みがどうなっているのか,だいたいわかった
  • XSはいろいろと空気を読んでやってくれるので,Cのコードはそんなにかかなくて良い.typemapすごい
  • Perl内部系の話(SV, AV, HV とかのはなし)は最低限だけだったので勉強したい