XS残酷物語

笑畜

XSで高速なモジュールが書きたい!

perldoc perlxs

- XS言語リファレンスマニュアル

perldoc perlxstut

- XSUBを書くためのチュートリアル

perldoc perlguts

- Perl APIイントロダクション

perldoc perlapi

- PerlパブリックAPI

……

やった、XSできたよ!

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

MODULE = Foo    PACKAGE = Foo

SV* hello(SV *sv)
    CODE:
        SV *message = newSVpv("Hello, ", 0);
        sv_catsv(message, sv);
        RETVAL = message;
    OUTPUT:
        RETVAL

$perl -Mblib -MFoo -e"use 5.010; say Foo::hello( shift )"  Bob

$perl -Mblib -MFoo -e"use 5.010; say Foo::hello( shift )"  Bob
Hello, Bob

いぃやぁあぁtたぁ!

貴方が本当に必要だったもの

say 'Hello, ', shift;

XSから
  PPへ

BY Makamaka @ YAPC::Asia Tokyo 2010 Oct 16

自己紹介

まかまか般若波羅蜜

アカウント類

http://www.donzoko.net/cgi-bin/tdiary/

twitter: @maka2_donzoko

hatena: makamaka_at_donzoko

github: makamaka

バラバラでわかりにくいです。

本編

XSからPP

XS

Perlと一緒に使うCのコードやCライブラリと、Perlとをつなぐインターフェース記述用言語

- perlxs(in Perl5.12.2)最初のパラグラフの超適当なまとめ

XSで書かれたPerlモジュール
→XSモジュール

XSモジュールを書くには

Perl内部のデータ構造やAPIについて知らないといけない

下手に手を出すとクラッシュするはメモリリークするはたいして効率よくなかったり……

結局メンテもおざなりになりそう……

だから無理にXSモジュールを書く必要はない

Don't write XS!

by miyagawa in Tsukuba.xs #1

だけど、Perlに慣れてくると、もっとPerlのことを知りたくなるのが人情

Perl APIの勉強したい、XSモジュール書いてみたい

でも、とりあえずXSモジュールは

lestrrat氏やgfx氏に任せて

今日はXSをPP化するお話

PPって?

Passion Panty?

(ArcadeGamer FUBUKI)

Poor Perl?

Pure Perl!

What defines "Pure Perl"?

http://www.perlmonks.org/?node_id=709757

Pure Perl

XSモジュールをPP化すると……

ポータビリティの向上

Perl API、XSの知識が得られる

暇つぶし勉強になる

デメリット

色々なXS互換PP

Data::MessagePack::PP (Data::MessagePack)
JSON::PP (JSON::XS)
Math::Random::ISAAC::PP (Math::Random::ISAAC)
Mouse::PurePerl (Mouse::の色々な部分)
Plack::HTTPParser::PP (HTTP::Parser::XS)
Scalar::Random::PP (Scalar::Random)
Set::IntSpan::Fast::PP (Set::IntSpan::Fast)
Text::CSV_PP (Text::CSV_XS)
Text::VisualWidth::PP (Text::VisualWidth)
Text::Xslate::PP (Text::Xslate)
……

PP化
してみる

Step 0

PP化したいXSモジュールを選ぶ

何がPP化に適しているか

Perlで書けるもの

なぜそのモジュールをPP化するのか

Step 1

ファイルを確認

t/*ファイル

*.xsファイル、*.cファイルを確認

typemapファイル

// JSON::XSのtypemap
JSON *		T_JSON

INPUT

T_JSON
	if (!(
           SvROK ($arg) && SvOBJECT (SvRV ($arg))
           && (SvSTASH (SvRV ($arg)) == JSON_STASH || sv_derived_from ($arg, \"JSON::XS\"))
        ))
          croak (\"object is not of type JSON::XS\");
        $var = (JSON *)SvPVX (SvRV ($arg));

あんまり確認してない。

Step 2

テストが通るようにガシガシ書き始める

PP化の過程で色々わかるようになる

SV *value → $value
AV *array → @array
HV *hash  → %hash
CV *code  → &code
RV *ref   → $ref = \$value

まあ、だいたい見たまんま。

なんか似たようなのが多いことに辟易する

SvTYPE(SV* sv) → ref $sv;
SvOK(SV* sv) → defined $value
SvIOKp(SV* sv) → ああ、整数なんだな
SvPOKp(SV* sv)→ ああ、文字列なんだな
SvOBJECT(SV* sv) → blessed $obj

どう違うのかと辟易する

HV*	gv_stashpv(const char* name, I32 flags)
HV*	gv_stashpvn(const char* name, U32 namelen, I32 flags)
HV*	gv_stashpvs(const char* name, I32 create)
HV*	gv_stashsv(SV* sv, I32 flags)

perlのcall大変ね、と辟易する

 // XS.xs(JSON::XS)、encode_rvより
GV *to_json = gv_fetchmethod_autoload (SvSTASH (sv), "TO_JSON", 0);

if (to_json) {
 dSP;

 ENTER; SAVETMPS; PUSHMARK (SP);
 XPUSHs (sv_bless (sv_2mortal (newRV_inc (sv)), SvSTASH (sv)));

 // calling with G_SCALAR ensures that we always get a 1 return value
 PUTBACK;
 call_sv ((SV *)GvCV (to_json), G_SCALAR);
 SPAGAIN;

 // catch this surprisingly common error
 if (SvROK (TOPs) && SvRV (TOPs) == sv)
   croak ("%s::TO_JSON method returned same object as was passed instead of a new one", HvNAME (SvSTASH (sv)));

     sv = POPs;
     PUTBACK;

     encode_sv (enc, sv);

     FREETMPS; LEAVE;

お気楽

 // JSON::PP
 if ( $convert_blessed and $obj->can('TO_JSON') ) {
     my $result = $obj->TO_JSON();
     if ( defined $result and overload::Overloaded( $obj ) ) {
         if ( overload::StrVal( $obj ) eq $result ) {
             encode_error( sprintf(
                 "%s::TO_JSON method returned same object as was passed instead of a new one",
                 ref $obj
             ) );
         }
     }

     return $self->object_to_json( $result );

まあ、こうやって得られる知識は

『モダンPerl入門』(牧大輔著)の8章読めば書いてありますよ!(涙目)

PP残酷物語

PP化時によく使うかもしれないモノ

B

    use B qw( svref_2object );
    my $ref   = [];
    my $value = 100;
    my $b_obj = svref_2object($ref);

    print "arrayref\n" if $b_obj->isa('B::AV');

    $b_obj = svref_2object(\$value);

    my $flags = $b_obj->FLAGS;

    if ( $flags & B::SVf_IOK or $flags & B::SVp_IOK ) {
        # 整数として
    }
    elsif ( $flags & B::SVf_NOK or $flags & B::SVp_NOK ) {
        # 浮動小数として
    }
    # 注:文字列のフラグたってるかも

    SVp_IOK
      ^
	
    if ( $flags & B::SVp_IOK ) {
        # 整数
    }
    elsif ( $flags & B::SVp_NOK ) {
        # 浮動小数
    }

プライベートのp。tieされた変数ではSVf_*OKの代わりにSVp_*OKが立つ場合がある。

通常の変数でもpは立つので、結局pの方をチェックすればよい。

SV関連クラス(Perl 5.11.1以降:Bドキュメントより)

                           B::SV
                             |
                +------------+------------+
                |            |            |
              B::PV        B::IV        B::NV
                  \         /           /
                   \       /           /
                    B::PVIV           /
                         \           /
                          \         /
                           \       /
                            B::PVNV
                               |
                               |
                            B::PVMG
                               |
           +-------+-------+---+---+-------+-------+
           |       |       |       |       |       |
         B::AV   B::GV   B::HV   B::CV   B::IO B::REGEXP

SV関連クラスのメソッド

他にもSV関連クラス毎に便利なメソッドがある

    $b_sv->REFCNT;    # 参照カウントを返す
    
    $b_iv->IV;        # 符号付き整数として
    $b_iv->int_value; # 符号の有り無しを考慮
    # などなど

pack/unpack

ちょっと(どうでも)いい話

Perl 5.9.2からエンディアンを指定できる"<"と">"が加わった。

# ビッグエンディアンのdouble型としてpack
$packed_double = pack( 'd>', $double );

# それ以前の場合、使ってる環境がリトルエンディアンの場合はこんな感じ?
$packed_double = pack( 'N2', reverse unpack( 'V2', pack( 'd', $double ) ) );

Configモジュール使う

実行してるPerlのコンフィグ

use Config;
my $byteorder_is_LE = $Config{byteorder} =~ /^1234/;

Math::BigInt/Math::BigFloat

Step 3

ブラッシュアップ

とりあえず動くようになったら最適化など

例:文字列のパース

// Text::CSV_XSのCSV_XS.xsより

while ((c = CSV_GET) != EOF) { // ←一文字ずつ取り出して色々判定
....
}

馬鹿正直にPerlで実装すると果てしなく遅い

状況に応じて正規表現などより早いものを検討

$str =~ /koreda/;

ベンチマークとってより良いものを目指したい

エラー時の結果もなるべく同じに(好みの問題?)

Step 4

パッケージ、テスト

バックエンドの統合

XSモジュールが使えればXSを、ダメならPPをバックエンドにして透過的に利用できるようにすると、便利。

use Foo; # Foo::XSかFoo::PPが呼ばれる

バックエンド指定の環境変数

デバッグの為に強制的にXSかPPかを指定できるように環境変数 'PERL_FOO_BACKEND'とか'PERL_ONLY'などを利用

BEGIN { $ENV{ PERL_FOO_BACKEND } = 'pp' }
use Foo; # 強制的にFoo::PPが呼ばれる

環境変数の名前

msgpackやjsonのような他言語でも使われるようなものは環境変数'PERL_'で始めることで、他の言語とのバッティングを避ける

ってMarc Lehmannが言ってました

テスト

XSモジュールのテストが全て通るようなPPモジュールがディストリビューションに同梱されているような場合

Module::Install::ExtendsMakeTest

xaicron氏のModule::Install::ExtendsMakeTest(Dev version)が便利かも。

  use inc::Module::Install;
  tests 't/*t';
  extends_make_test(
      before_run_script => 'tool/force-pp.pl',
      target            => 'test_pp',
  );
  
  # force-pp.pl には
  # $ENV{ PERL_FOO_BACKEND } = 'pp'; などが書かれている
  
  >make test_pp
  # バックエンドをPPにしてテスト

まとめ

XSモジュールをPP化して

Enjoy PP!!

ご静聴
ありがとうございます