Acme::Perl::VM::JA - Pure PerlによるPerl5仮想マシンの実装(AVPM)
use Acme::Perl::VM; run_block{ print "Hello, APVM world!\n"; };
Amce::Perl::VM(APVM)はPure Perlで実装されたPerl5の仮想マシンです。
Amce::Perl::VM
Perlディストリビューションにはコンパイルされた構文木にアクセスするためのモジュールが用意されており,B - The Perl Compilerと呼ばれています。 APVMはこのBモジュールを利用して構文木を解釈・実行するモジュールです。
この文書では,Perl5の仮想マシンについて概説しつつ,APVMとPerl5実装との対応について解説します。
Perl5の仮想マシンはスタックマシンであり,組み込み演算子やサブルーチンなどの手続きの引数と戻り値を,スタックを通じてやり取りします。
この仮想マシンのマシンコードはopcodeと呼ばれ,これがコンパイルされたPerlプログラムの最小単位となります。opcodeはさらにデータと手続きからなるオブジェクトとして表現され,その手続きはppcode(PUSH/POP code)と呼ばれる関数として実現されます。
opcodeオブジェクトは他のopcodeオブジェクトへのリンクを持つ木構造を成しており,このopcodeの木を構文木と呼びます。したがって,Perlプログラムを実行するというのは,opcodeを実行しつつ,この木構造をたどって行く過程ということになります。
ここでは以下のPerlコードを例にとり,プログラムの実行を追っていきます:
print(10 + 20);
まず,このコードをコンパイルすると,以下のような構文木が生成されます:
nextstate # ステートメントの始まり print # 引数リストを印字 pushmark # 可変長引数のためのマーク add # 加算演算 const(10) # 定数[10] const(20) # 定数[20]
構文木は子ノードから実行されるので,この構文木を解釈すると以下の順になります。
nextstate pushmark const(10) const(20) add print
それぞれのopcodeは必要に応じてスタックから引数をポップし,戻り値をプッシュします。opcodeの実行とスタックの中身を同時に表すと以下のようになります。
nextstate () pushmark () # mark = -1 (MARKについては後のセクションで解説) const(10) (10) # スタックに値をPUSH const(20) (10, 20) # スタックに値をPUSH add (30) # 値を2つPOPし,演算結果をPUSH print (1) # mark+1からTOPまでを印字し,結果(真)をPUSH
これが構文木を解釈する基本的な流れです。プログラムの分岐やサブルーチンの呼び出しなどがあると更に複雑になりますが,一連の流れは同じです。
以下のセクションでは,仮想マシンの実装の中で特に重要なコンポーネントについて説明します。
Perlプログラムの手続きと戻り値のために使われるスタックです。 このスタックは組み込み関数とサブルーチンの両方で使われます。
現在のperl5の実装では,このスタックはCの配列で表現され,必要に応じてrealloc()で拡張されます。スタックの先頭はスタックポインタ(PL_stack_sp)として参照できるのですが,ppcode内ではこのグローバルなスタックポインタを一旦ローカルにコピーします(dSP)。PUSHs/POPs/TOPsなどのマクロはこのローカルコピーを参照します。そしてスタックポインタを使った操作が終わったところでPUTBACKマクロによりローカルスタックポインタをグローバルスタックポインタ変数に戻します。なお,SPAGAINマクロはローカルスタックポインタ変数をグローバルスタックポインタで再初期化するマクロで,スタックを操作する可能性のあるPerl API(call_sv()など)を呼び出した後に使用します。
realloc()
PL_stack_sp
dSP
PUSHs
POPs
TOPs
PUTBACK
SPAGAIN
call_sv()
APVMではこれはPerlの配列で表現され,スタックポインタは配列の最後の添え字です。 ローカルコピーは作りません。
AVPMとの対応: perl APVM
PL_stack_base @PL_stack PL_stack_sp $#PL_stack dSP (nothing) SP $#PL_stack TOPs TOP PUSHs(sv) PUSH($sv) POPs POP SPAGAIN (nothing) PUTBACK (nothing) EXTEND(SP, n) (nothing)
See also pp.h.
可変長引数を扱うためのスタックのマーカーです。
二項演算子などは引数の数が固定ですが,printのように引数の数が可変長である組み込み関数もあります。可変長引数を扱うためには,引数スタック中で引数が始まる位置を保存する必要があります。また,このマーカーは入れ子になる可能性があるので,このマーカーそれ自体もスタックに保存します。
print
可変長引数の開始を宣言するためには,PUSHMARK(SP)マクロを使います。 また,dMARKマクロによりスタックから値をポップし,MARKマクロを使えるようにします。
PUSHMARK(SP)
dMARK
MARK
APVMとの対応: perl APVM
PUSHMARK(SP) PUSHMARK($#PL_stack) TOPMARK TOPMARK POPMARK POPMARK dMARK my $mark = POPMARK MARK++ $mark++ *MARK $PL_stack[$mark] dORIGMARK my $origmark = $mark SP = ORIGMARK $#PL_stack = $origmark
Perlプログラムの最小単位である,手続きとデータを持ったオブジェクトです。 opcodeクラス群はCの構造体の先頭メンバをいくつか共有する構造体群として表現されます。
opcodeオブジェクトの持つ手続きは対応するppcodeであり,op_ppaddrメンバで参照します。データはPerlの値やCの値,または他のopcodeへのリンクです。
op_ppaddr
各opcodeオブジェクトは名前と外部出力用の説明を持ち,それぞれOP_NAME(op),OP_DESC(op)マクロで得ることができます。
OP_NAME(op)
OP_DESC(op)
See also op.h, cop.h, opcode.h and opcode.pl.
opcodeが持つ手続きで,実際に行う処理を実装した関数です。
たとえば,OP_CONSTに対応するppcodeは以下のようになっています:
/* in pp_hot.c (5.8.8) */ PP(pp_const) { dSP; XPUSHs(cSVOP_sv); RETURN; }
マクロをいくつか展開すると以下のようになります。
PP(pp_const) { dSP; EXTEND(SP, 1); PUSHs(cSVOPx_sv(PL_op)); PUTBACK; return PL_op->next; }
一つひとつを順に追うと以下のようになります。
ローカルスタックポインタ変数(SP)を宣言し,グローバルスタックポインタの値で初期化します。
EXTEND(SP, 1)
スタックポインタに値を1つプッシュすることを宣言します。このとき,必要に応じてスタックは拡張されます。
cSVOPx_sv(PL_op)
現在実行中のopcodeのsvフィールドを参照します。
PUSH(sv)
スタックポインタを通じて引数スタックに値を1つプッシュします。
ローカルスタックポインタをグローバルスタックポインタ変数に戻します。
return PL_op->op_next
次に実行するopcodeを返します。
OP_CONSTであれば可能性のあるプログラムの経路は常に一つですが,OP_COND_EXPRのような制御を担うopcodeであればPL_op->next以外のopcodeを返すことがあります。
PL_op->next
ppcodeはopcodeオブジェクトのop_ppaddrメンバを通じて取得・変更することができます。このop_ppaddrをPL_checkというコンパイル時フックテーブルを通じて変更し,プログラムの挙動を変える手法がPL_check hackとして知られています。たとえば,autoboxはこのPL_checkハックを用いてプリミティブ値に対するメソッド呼び出しを実現しています。
PL_check
autobox
See also pp.c, pp_hot.c, pp_ctl.c, pp_sys.c, pp_sort.c and pp_pack.c.
構文木を解釈・実行するループの実装です。
デフォルトでは,run.cにあるPerl_runops_standard()が用いられます。 これは,perl 5.8.8では以下のようになっています:
Perl_runops_standard()
int Perl_runops_standard(pTHX) { while ((PL_op = CALL_FPTR(PL_op->op_ppaddr)(aTHX))) { PERL_ASYNC_CHECK(); } TAINT_NOT; return 0; }
PL_opは現在実行中のopcodeオブジェクトが入っているグローバル変数(またはTLS変数)です。op_ppaddrはopcodeに対応したppcodeが入っており,ppcodeは次に実行するopcodeを返すことになっています。
PL_op
PERL_ASYNC_CHECK()は単にセーフシグナルの処理なので実行には関係ありません。したがって,インタプリタループの実体は一行しかありません。
PERL_ASYNC_CHECK()
ところで,このようなopcodeの多態性を利用した実行ループと対極にあるのが,switch文やifの連鎖による分岐を利用した実行ループです。Perl4の実行ループはそのような実装でしたし,Perl5の実装の中にもswitchによる実行ループも存在します。たとえば,scope.cにあるleave_scope()はまさに巨大なswitch文を利用した実行ループでスコープの後処理を行っています。
leave_scope()
インタプリタループについては,APVMのrunops_standard()でも実装はほぼ同じです。
runops_standard()
See also run.c.
この他にもいくつか重要なコンポーネントがあります。
The Scratchpads (PL_comppad and PL_curpad)
The Save Stack (PL_savestack)
The Temporary Value Stack (PL_tmps)
The Context and Block Stack (PL_cxstack)
The Stack Information (PL_curstackinfo)
なお,この文書ではPerlの値の実装であるSV構造体群については解説しません。 SV構造体群のAPIについてはperlapiを,その実装についてはsv.[hc], av.[hc], hv.[hc], gv.[hc]を参照してください。
perlを-DDEBUGGINGコンパイルオプションを指定してビルドすると,プログラムの実行をopcodeレベルでトレースできるようになります。perlコマンドに-Dtまたは-Dtsを渡して実行してみてください。
-DDEBUGGING
perl
-Dt
-Dts
APVMにもopcodeトレース機能があります。環境変数APVM_DEBUGにtraceを指定すると,opcodeトレースを行います。また,stackを指定すると,opcodeトレースと同時に引数スタックの中身も報告します。
APVM_DEBUG
trace
stack
Perlの標準モジュールB::Conciseでも構文木を出力することができます。このとき-execオプションを渡すと,実行順にopcodeを並べて出力します。ただし,B::Conciseでは静的な解析しかできません。
B::Concise
-exec
CPANにあるDevel::OptraceはAPVMのopcodeトレース機能を標準Perl VMで使えるようにしたモジュールです。このモジュールは-DDEBUGGINGが指定されていないperlでも利用できます。
Devel::Optrace
このモジュールは2009年4月22日に東京で開催されたShibuya.pm テクニカルトーク#11で発表されました。
Goro Fuji (gfx) <gfuji(at)cpan.org>.
perlapi.
perlhack.
pp.h for PUSH/POP macros.
pp.c, pp_ctl.c, and pp_hot.c for ppcodes.
op.h for opcodes.
cop.h for COP and context blocks.
scope.h and scope.c for scope stacks.
pad.h and pad.c for pad variables.
run.c for runops.
B::Concise.
Devel::Optrace.
Copyright (c) 2009, Goro Fuji (gfx). Some rights reserved.
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
To install Acme::Perl::VM, copy and paste the appropriate command in to your terminal.
cpanm
cpanm Acme::Perl::VM
CPAN shell
perl -MCPAN -e shell install Acme::Perl::VM
For more information on module installation, please visit the detailed CPAN module installation guide.