NAME
Acme::Perl::VM::JA - Pure PerlによるPerl5仮想マシンの実装(AVPM)
SYNOPSIS
use Acme::Perl::VM;
run_block{
print "Hello, APVM world!\n";
};
DESCRIPTION
Amce::Perl::VM
(APVM)はPure Perlで実装されたPerl5の仮想マシンです。
Perlディストリビューションにはコンパイルされた構文木にアクセスするためのモジュールが用意されており,B - The Perl Compilerと呼ばれています。 APVMはこのBモジュールを利用して構文木を解釈・実行するモジュールです。
この文書では,Perl5の仮想マシンについて概説しつつ,APVMとPerl5実装との対応について解説します。
The Perl5 Virtual Machine
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
これが構文木を解釈する基本的な流れです。プログラムの分岐やサブルーチンの呼び出しなどがあると更に複雑になりますが,一連の流れは同じです。
以下のセクションでは,仮想マシンの実装の中で特に重要なコンポーネントについて説明します。
The Perl Stack (PL_stack_base)
Perlプログラムの手続きと戻り値のために使われるスタックです。 このスタックは組み込み関数とサブルーチンの両方で使われます。
現在のperl5の実装では,このスタックはCの配列で表現され,必要に応じてrealloc()
で拡張されます。スタックの先頭はスタックポインタ(PL_stack_sp
)として参照できるのですが,ppcode内ではこのグローバルなスタックポインタを一旦ローカルにコピーします(dSP
)。PUSHs
/POPs
/TOPs
などのマクロはこのローカルコピーを参照します。そしてスタックポインタを使った操作が終わったところでPUTBACK
マクロによりローカルスタックポインタをグローバルスタックポインタ変数に戻します。なお,SPAGAIN
マクロはローカルスタックポインタ変数をグローバルスタックポインタで再初期化するマクロで,スタックを操作する可能性のあるPerl API(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.
The Perl Stack Marker (PL_markstack)
可変長引数を扱うためのスタックのマーカーです。
二項演算子などは引数の数が固定ですが,print
のように引数の数が可変長である組み込み関数もあります。可変長引数を扱うためには,引数スタック中で引数が始まる位置を保存する必要があります。また,このマーカーは入れ子になる可能性があるので,このマーカーそれ自体もスタックに保存します。
可変長引数の開始を宣言するためには,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
See also pp.h.
The Opcode Family
Perlプログラムの最小単位である,手続きとデータを持ったオブジェクトです。 opcodeクラス群はCの構造体の先頭メンバをいくつか共有する構造体群として表現されます。
opcodeオブジェクトの持つ手続きは対応するppcodeであり,op_ppaddr
メンバで参照します。データはPerlの値やCの値,または他のopcodeへのリンクです。
各opcodeオブジェクトは名前と外部出力用の説明を持ち,それぞれOP_NAME(op)
,OP_DESC(op)
マクロで得ることができます。
See also op.h, cop.h, opcode.h and opcode.pl.
The PPcodes
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;
}
一つひとつを順に追うと以下のようになります。
dSP
-
ローカルスタックポインタ変数(SP)を宣言し,グローバルスタックポインタの値で初期化します。
EXTEND(SP, 1)
-
スタックポインタに値を1つプッシュすることを宣言します。このとき,必要に応じてスタックは拡張されます。
cSVOPx_sv(PL_op)
-
現在実行中のopcodeのsvフィールドを参照します。
PUSH(sv)
-
スタックポインタを通じて引数スタックに値を1つプッシュします。
PUTBACK
-
ローカルスタックポインタをグローバルスタックポインタ変数に戻します。
return PL_op->op_next
-
次に実行するopcodeを返します。
OP_CONSTであれば可能性のあるプログラムの経路は常に一つですが,OP_COND_EXPRのような制御を担うopcodeであれば
PL_op->next
以外のopcodeを返すことがあります。
ppcodeはopcodeオブジェクトのop_ppaddr
メンバを通じて取得・変更することができます。このop_ppaddr
をPL_check
というコンパイル時フックテーブルを通じて変更し,プログラムの挙動を変える手法がPL_check hackとして知られています。たとえば,autobox
はこのPL_check
ハックを用いてプリミティブ値に対するメソッド呼び出しを実現しています。
See also pp.c, pp_hot.c, pp_ctl.c, pp_sys.c, pp_sort.c and pp_pack.c.
The interpreter loop (PL_runops)
構文木を解釈・実行するループの実装です。
デフォルトでは,run.cにあるPerl_runops_standard()
が用いられます。 これは,perl 5.8.8では以下のようになっています:
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を返すことになっています。
PERL_ASYNC_CHECK()
は単にセーフシグナルの処理なので実行には関係ありません。したがって,インタプリタループの実体は一行しかありません。
ところで,このようなopcodeの多態性を利用した実行ループと対極にあるのが,switch文やifの連鎖による分岐を利用した実行ループです。Perl4の実行ループはそのような実装でしたし,Perl5の実装の中にもswitchによる実行ループも存在します。たとえば,scope.cにあるleave_scope()
はまさに巨大なswitch文を利用した実行ループでスコープの後処理を行っています。
インタプリタループについては,APVMのrunops_standard()
でも実装はほぼ同じです。
See also run.c.
Other components
この他にもいくつか重要なコンポーネントがあります。
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]を参照してください。
DEBUGGING
Opcode Tracing
perlを-DDEBUGGING
コンパイルオプションを指定してビルドすると,プログラムの実行をopcodeレベルでトレースできるようになります。perl
コマンドに-Dt
または-Dts
を渡して実行してみてください。
APVMにもopcodeトレース機能があります。環境変数APVM_DEBUG
にtrace
を指定すると,opcodeトレースを行います。また,stack
を指定すると,opcodeトレースと同時に引数スタックの中身も報告します。
Perlの標準モジュールB::Concise
でも構文木を出力することができます。このとき-exec
オプションを渡すと,実行順にopcodeを並べて出力します。ただし,B::Concise
では静的な解析しかできません。
CPANにあるDevel::Optrace
はAPVMのopcodeトレース機能を標準Perl VMで使えるようにしたモジュールです。このモジュールは-DDEBUGGING
が指定されていないperlでも利用できます。
NOTES
このモジュールは2009年4月22日に東京で開催されたShibuya.pm テクニカルトーク#11で発表されました。
AUTHOR
Goro Fuji (gfx) <gfuji(at)cpan.org>.
SEE ALSO
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.
LICENSE AND COPYRIGHT
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.