Pure PerlによるPerl5 Virtual Machineの実装

Perl VMの気持ちを知るには、PurePerlで実装してみるとよい。

コード例(PerlVM.pmのコードは記事の末尾にある):

#!perl -w
use strict;
use PerlVM;
my $x = shift || 42;

PerlVM::call_sv(sub{
	print "Hello,", " world!", "\n";
	if($x){
		print $x, " is true\n";
	}
	else{
		print $x, " is false\n";
	}
});
__END__

実行結果:

$ perl hello.pl
Hello, world!
42 is true

むろん、PerlVM::call_sv()の中で引数を呼び出したりはしていない。

Hello, world!くらいならPerlVMの中身も比較的単純だ。そのメカニズムはBモジュールに依存している。Bを使えばコードリファレンスの実体であるCV構造体にアクセスできるのだが、BはさらにOPコード構造体へのインターフェイスを提供しているため、このようなことが可能となっている。
OPコードの処理自体は、Perlがコードを実行するのと同じやり方となっている。pp.h/pp.c/pp_hot.cあたりの対応するコードと比較すると、かなり単純化しているものの、メカニズム自体はほとんど同じであることがわかるだろう。何しろ、OPコード実行ループはたったのこれだけである:

    while(is_not_null( $op = &{$code{ $op->ppaddr } || _not_implemented($op)} )){
        print $op->ppaddr, "\n" if VERBOSE;
    }

そしてperlソースコードの対応する関数は以下の通り(run.c):

int
Perl_runops_standard(pTHX)
{
    dVAR;
    while ((PL_op = CALL_FPTR(PL_op->op_ppaddr)(aTHX))) {
        PERL_ASYNC_CHECK();
    }
    TAINT_NOT;
    return 0;
}

実行ループの形は同じである。あとは,pp_xxx()が実際の処理を行う。

もっとも、このPerlVM.pmで用意しているスタックは引数と戻り値のための@stackしか用意していないのだが、必要な内部スタックはそれだけではない。たとえば、スコープを扱おうとすると実装はずっと複雑になるし、ループやサブルーチン呼び出し、例外などの実装も難しい。

それでもやはり、コア部分は非常に単純である。これでVMの実行のイメージは掴めるのではないだろうか。

PerlVM.pm:

package PerlVM;

use 5.008_001;
use strict;
use warnings;

use B qw(svref_2object);
use Class::Inspector;
use Carp;

use constant VERBOSE  => $ENV{PERLVM_VERBOSE} ? 1 : 0;
use constant sv_undef => svref_2object(\do{ my $empty });


our @stack;
our $stack_ix = 0;
our $mark_ix  = 0;

our $op;
our $curcop;

our $padlist;
our $curpad;

sub PL_op() :lvalue{
    return $op;
}
sub PL_curcop() :lvalue{
    return $curcop;
}

sub PAD_SV($){
    my($targ) = @_;
    return $curpad->ARRAYelt($targ);
}

sub MARK(){
    return $stack[$mark_ix];
}
sub PUSHMARK(){
    $mark_ix = $stack_ix;
    return;
}

sub PUSH($){
    my($sv) = @_;

    $stack[$stack_ix++] = $sv;
    return;
}
sub POP(){
    $stack_ix--;
    return pop @stack;
}

sub mark_ix() :lvalue { $mark_ix  }
sub stack_ix():lvalue { $stack_ix }

sub is_not_null($){
    my($sv) = @_;

    return $$sv;
}
sub _not_implemented($){
    my($op) = @_;

    croak $op->ppaddr, ' is not yet implemented';
}

our %code;
initialize_name2code(\%code);

sub call_sv{
    my $cv       = svref_2object(shift);
    $op      = $cv->START;

    # pp_entersub
    $padlist = $cv->PADLIST;
    $curpad  = $padlist->ARRAYelt(1);

    printf "call_sv(\&%s)\n", $cv->GV->NAME if VERBOSE;

    PUSHMARK;

    # runops
    while(is_not_null( $op = &{$code{ $op->ppaddr } || _not_implemented($op)} )){
        print $op->ppaddr, "\n" if VERBOSE;
    }
}

sub initialize_name2code{
    my($ref) = @_;

    my $methods = Class::Inspector->methods(__PACKAGE__, 'public');

    foreach my $method(@{$methods}){
        if($method =~ /^pp_/){
            my $opname = $method;
            $opname =~ s/pp_/OP_/;

            no strict 'refs';
            $ref->{sprintf 'PL_ppaddr[%s]', uc $opname} = \&{__PACKAGE__ . '::' . $method};
        }
    }
    return;
}


sub pp_nextstate{
    PL_curcop = PL_op;

    return PL_op->next;
}

sub pp_pushmark{
    PUSHMARK;

    return PL_op->next;
}

sub pp_const{
    my $sv = is_not_null(PL_op->sv) ? PL_op->sv : PAD_SV(PL_op->targ);
    PUSH($sv);

    return PL_op->next;
}

sub pp_print{
    while(mark_ix < stack_ix){
        print ${ MARK->object_2svref };

        mark_ix++;
    }

    return PL_op->next;
}

sub pp_leavesub{
    return PL_op->next;
}

sub pp_cond_expr{
    my $sv = POP->object_2svref;

    if(${$sv}){
        return PL_op->other;
    }
    else{
        return PL_op->next;
    }
}

sub pp_padsv{
    PUSH( PAD_SV(PL_op->targ) );
    return PL_op->next;
}

sub pp_enter{
    # stub
    return PL_op->next;
}
sub pp_leave{
    # stub
    return PL_op->next;
}
1;
__END__

一通り実装したらAcme::Perl::VMなどとして公開するかもしれない。

(追記)
Acme::Perl::VMとして公開した。