tokuhirom's Blog

Perl5 における B optree の操作方法入門

いよいよ今週から YAPC だ。YAPC にあわせて、Perl5 の内部構造を復習できるエントリーを書いたのでご一読いただきたい。


Perl5 hack においては、XS をつかった B optree の操作が楽しい。B optree というのはいわゆる AST(抽象構文木) である。Perl5 は byte code interpreter などではなく昔ながらの AST をそのままなめるインタプリタである。であるから、この B optree を操作すれば、簡単にインタプリタの挙動を変えられるというワケ。

一方で、B optree の操作については、あまり知られていないし情報がすくない。日本語でも情報がすくないし、英語でもまた情報がすくなく、それっぽいモジュールのコードをよみながら会得するしかないのが現状だ。 すこしでも B 初心者が、ステップアップできるように、私のもっている情報をここに公開せんとするものである。

情報源

とっかかりとなる、この文書以外の情報源についてのべよう。

perlguts

http://perldoc.perl.org/perlguts.html

perlguts は Perl5 の公式文書のうち、内部の情報についてのべたものである。ひととおりよんでおくと、なにかとはかどる。XS による hack を行わない人でも、さらっとよんでおくとなにかと Perl5 の挙動の「意味」が理解できるようになるのでご一読をおすすめする。

illguts

http://cpansearch.perl.org/src/RURBAN/illguts-0.44/index.html

perlguts に書かれているような内容を図入りであらわしたもの。reini により精力的にアップデートされているので、よんでおくといい。

grep.cpan.me

http://grep.cpan.me/

CPAN のコードに grep できるというサービス。ppport.h がひっかかってきてしまうのが難点だが、マニアックな XS ハックをしたい場合に参考となるコードをさがす際にはこれが有用。

もちろん PP で書いている場合にも有用なサイトだとおもう。

B 関連モジュール

B.pm

B.pm は optree の操作と、sv 関連の操作をするための標準モジュールだ。 うまくつかえば coderef から所属するモジュールの位置をしらべたり(UNIVERSAL::whichなど)、文字列なのか数字なのかを判別する(JSON::PP など)などの使い方もできる。

Devel::Peek

XS レベルの関数である sv_dump() と同等のことができるモジュール。値の内部構造をみることができる。utf-8 flag の有無などを確認する上でも有用なので、pure perl しかつかわない人でも覚えておいた方がよいだろう。

B::Utils

B::Utils は、いろいろできて便利なモジュールだが、いまいちちゃんとうごいてない気がするので現在はオススメできない。B::Tools の方をおしておく。

(walkoptree_simple がすべての op をまわりきれてないようにみえる)

B::Tools

B::Utils がbuggyなのと、なおすのがいろいろめんどくさそうだったので筆者が新たにおこしたモジュールだ。B::Utils にくらべてシンプルな上に、バグがはいりこむ余地がないくらいみじかいコードで実現されているのがオツである。

B::Generate

Pure perl の世界から optree の構築ができるという意欲的なモジュール。XS がどうしても書きたくないという場合にはつかってみるのもよいだろう。 ただし、デバッグの効率などを考えると結局 XS をかいた方が楽なのではないか、という気がしている。

B::Deparse

OP tree からソースコードを生成するというモジュール。演算子の優先順位がどう解釈されているかの判断などにもつかえるので、pure perl しかつかわない人でも覚えておこう。

SV構造体

Perl の世界の変数はすべて、SV 構造体で表現されている。詳しくは illguts や perlguts 等で熟知すべし。

OP の構造

OP は構造体であらわされている。op_next という要素で次の OP をしめし、op_sibling という要素で兄弟 OP をしめしている。op_next だけ指定しておけば、インタプリタは動作するが、op_sibling を指定しないと B::Concise や B::Deparse などでデバッグできないので辛いから、ちゃんと設定したほうがよい。

それぞれの OP に、ppaddr という値を与えることができる。ppaddr は、実際に処理をおこなうルーチンのこと。

OP の種類

OP には以下のような種類がある。なお、主だったものだけしか紹介していない。

UNOP

1個の子をもつ演算子をあらわす OP。op_first で指定。

BINOP

2個の子をもつ演算子をあらわす OP。op_first と op_last で指定する。

LISTOP

リスト形式の子をもつ OP。op_first と op_last で指定。子は op_sibling と op_next で次のノードを指定していく。

PADOP

padlist からデータをとりだす。padlist は 5.18 で変更された。それまではただの AV だったのに。 そういうわけで、padlist 関連の操作を古いバージョンと互換性をもたせながらやるのは面倒。 PADOP の操作をさけられるならさけたほうがいいだろう。 (5.18 以後をターゲットとするのも一つの方策であるといえる)

SVOP

SV がアタッチされているOP。実際、SVOP としてつかえるものはすくない。OP_CONST などがそうだが、型の制限がきびしい。

GVOP

GV がアタッチされている OP。

OP と ithreads

OP_GV は threaded だと PADOP だが、通常は SVOPである。このように threaded かどうかにより生成される OP がちがったりするので注意が必要だ。なお、OP_GV の生成には newGVOP という専用の API が用意されていて、ithreads かどうかによる違いは吸収されているのでご安心を。

Custom op

Perl 5 では自分専用の OP を定義できる。これにより、任意の op code を定義できるから、なにかと便利。

実際、これをうまくつかえば Perl5 ではない言語の実装も容易だろう。

以下のように global 変数として XOP 構造体を定義して

static XOP my_xop_tap;

BOOT 時に登録するだけで OK。

BOOT:
    XopENTRY_set(&my_xop_tap, xop_name, "b_tap_tap");
    XopENTRY_set(&my_xop_tap, xop_desc, "b_tap_tap");
    XopENTRY_set(&my_xop_tap, xop_class, OA_BINOP);
    Perl_custom_op_register(aTHX_ XS_B_Tap_pp_tap, &my_xop_tap);

使い方も、op の type として OP_CUSTOM を指定して newBINOP() 等でつくった OP に op_ppaddr を設定するだけでいいのでよい。なおこの API は 5.14 以後でしかつかえない。

5.14 以前でも custom op はつかえるのだが、B::Concise や B::Deparse といった tool の custom op サポートがイマイチなので、実用にたえない。custom op をつかうのであれば 5.14 以後の利用をオススメする(5.14 以前は EOL だし)。

フックポイント

Perl5 には多数の hook point が用意されている。

PL_ppaddr のさしかえ

PL_ppaddr という配列に、Perl5 で実行される op がはいっている。これをさしかえることにより特定の op の実行コードをさしかえることが可能だ。 通常はあまりつかうことはないが、Devel::NYTProf ではこれをおこなっている。

これについては以下の記事がくわしい。

なお、この方法、BEGIN phase でやらないと、なぜか実行時に PL_op がとれない気がする。

PL_runops のさしかえ

PL_runops というのは、まさにインタプリタの実行部分。op をとりだして順番に実行していく部分のことだ。

これは The perl5 debugger, Scope::Upper などで利用されている。

PL_check によるハック

数年前の Shibuya.pm などでよくとりあげられていたのでご存知の方もおおいであろう。

PL_check をつかうことにより、compilation phase における OP tree の書き換えが可能となる。

autobox などが実際にこれをおこなっている。

XS における OP ハックのための tips

PL_op

XS の ppcode の中では、OP そのものは PL_op をつかうことにより参照可能。 これをつかって SVOP についた SV をとりだしたりすることが可能となる。

op_dump(OP*)

XS の世界では op_dump(OP*) でダンプが表示される。Pure perl でつかうには B::Generate にふくまれる B::OP::dump をよぶとよい。

ヘッダ

XS を内部をいじる目的で書く場合、どうしても Perl そのもののコードをよまざるをえない。 マクロの海を泳がなくてはいけないので、とっつきづらいが、だんだんと慣れてくる。

以下ではおもだったヘッダを紹介しておこう。

op.h

OP 構造体まわりの定義がある。OP の操作に関する関数もこのへん。

perl.h

TBD

XSUB.h

XSUB でつかう関数とか。XSUB というのは C でかかれたPerl 用関数のこと。

Perl5 のスタック構造

Perl5 の Argument stack に関連するマクロを以下にコピペしておく。

pp.h

 #define dMARK           SV **mark = PL_stack_base + POPMARK 
 #define dSP             SV **sp = PL_stack_sp

mark がさしてる場所をえる。

XSUB.h

 #define dAX const I32 ax = (I32)(MARK - PL_stack_base + 1) 
 #define ST(off) PL_stack_base[ax + (off)] 

FAQ

PL_stack_base のような変数はどこで定義されてるの?

それそのものは以下のような形で embedvar.h にある。

 #define PL_stack_base       (vTHX->Istack_base)

ただし、その実態は intrpvar.h の中の

 PERLVAR(I, stack_base,  SV **) 

である。

これはなんでそうなってるのかというと、ithread 有効なときと無効なときのコードを共通化しようとする涙ぐましい努力の結果である。

まとめ

OP ツリーの操作、以上の内容を把握していれば誰でも簡単に行えるとおもいます。