The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

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プログラムの最小単位となります。オペコードはさらにデータと手続きからなるオブジェクトとして表現され,その手続きは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オブジェクトが入っているスレッドグローバルな変数です。op_ppaddrはopcodeに対応したppcodeが入っており,ppcodeは次に実行するopcodeを返すことになっています。

PERL_ASYNC_CHECK()は単にセーフシグナルの処理なので実行には関係ありません。したがって,インタプリタループの実体は一行しかありません。

ところで,このようなopcodeの多態性を利用した実行ループと対極にあるのが,switch文やifの連鎖による分岐を利用した実行ループです。perlの実装の中にはそのような実行ループも存在します。たとえば,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 Infomation (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では静的な解析しかできません。

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.

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.