The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

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_ppaddrPL_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_DEBUGtraceを指定すると,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

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.

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.