Perlテックブログ

ITエンジニアの成長意欲を刺激する技術考察、モジュール開発の日記。Perlイベントや国内や海外のPerlの記事の紹介。

SPVM 0.0326リリース - メソッド定義、無名パッケージ、インターフェース、JIT

SPVMの開発を長いこと続けている。ここ最近にやっていたことを紹介する。

SPVMの実装はどんどん変わっている。まだリリース前なので、使いにくいなぁと思ったらどんどん変える。利用者が少ないうちは、とにかく品質を上げたい。

メソッド呼び出しがPerlと完全に一致した

最近のSPVMの一番大きな変更は、サブルーチン定義がデフォルトで、クラスメソッドになったことと、「self」を指定することで、インスタンスメソッドにできるということだ。

Perlとできる限り互換性を持たせたいのだけれど、細かい部分で、どうにもならないという部分も存在する。その一つはクラスメソッド定義だ。

package A;

# Perlのクラスメソッド
sub foo  {
  my $class = shift;
}

# Perlのインスタンスメソッド
sub foo {
  my $self = shift;
}

Perlの場合は、第一引数にクラス名を受け取るのだけれど、これは、単なる文字列であって、識別子やオブジェクトではない。

これを静的言語のSPVMの引数として受け取るのは、非常に不自然だ。だから、SPVMでは、メソッド定義の方に、Perlとは異なる部分を集めることにして、メソッド呼び出しの部分で、一致させるようにしてみた。

SPVMのクラスメソッド定義とインスタンスメソッド定義は以下のようになる。SPVMでは、パッケージブロック構文でクラス定義を書く。

package A {

  # SPVMのクラスメソッド定義
  sub foo : int () {
  
  }

  # SPVMのインスタンスメソッド定義
  sub foo : int ($self : self) {
  
  }
}

selfというのは、インスタンスを表現する特別な型で、これによって、インターフェースを利用したポリモーフィズムが可能になる。

呼び出し方法はPerlでもSPVMでもどちらも同じ。

# Perlのクラスメソッド呼び出し
A->foo;

# Perlのインスタンスメソッド呼び出し
$obj->foo;

# SPVMのクラスメソッド呼び出し
A->foo;

# SPVMのインスタンスメソッド呼び出し
$obj->foo;

Perlと互換性がない部分については、サブルーチン定義のほうに、非互換を寄せて、サブルーチンの内部では、ほぼ同じ記述ができるようにと心がけている。

無名パッケージの定義ができるようになった

SPVMに無名サブルーチンを追加しようと当初は考えていたのだけれど、よくよく考えると、SPVMには、クロージャーの仕組みがない。

すべての変数は、サブルーチンの中で閉じている。これはJavaと同じだ。そのため、Javaクロージャが作れない。Javaにはラムダがあるが、ラムダの実際は、匿名クラスを使って実装されていて、匿名クラスのシンタックスシュガーになっている。

関数環境のすべてが保存されるのではなく、ラムダが呼び出される時点で存在している変数の値が、オブジェクトに保存される。

ラムダは一つのインスタンスメソッドと、複数のフィールドを持つ。このフィールドに、参照している変数の値が保存される。

SPVMでは、できる限りシンプルな実装にしようということも心がけているので、まず最初として、Javaの匿名クラスに当たる機能を実装してみた。

匿名クラスに当たる機能があれば、コールバックを書けるし、sortの実装もできる。

無名パッケージを使うとオブジェクトのsortを実装できる。

  # 無名パッケージとインターフェースを使ったsortの実装
  sub sort_obj : void ($values : Object[], $comparator : TestCase::Comparator) {

    my $change_cnt = @$values - 1;
    while( $change_cnt > 0){
      for (my $i = 0; $i < $change_cnt; $i++) {
        my $ret = $comparator->compare($values->[$i], $values->[$i + 1]);
        
        if ($comparator->compare($values->[$i], $values->[$i + 1]) == 1) {
          my $tmp_value = $values->[$i];
          $values->[$i] = $values->[$i + 1];
          $values->[$i + 1] = $tmp_value;
        }
      }
      $change_cnt--;
    }
  }

  sub test : void () {
    my $comparator = new package {
      sub compare : int ($self : self, $object1 : Object, $object2 : Object) {
        my $minimal1 = (TestCase::Minimal)$object1;
        my $minimal2 = (TestCase::Minimal)$object2;
        
        my $x1 = $minimal1->{x};
        my $x2 = $minimal2->{x};
        
        if ($x1 > $x2) {
          return 1;
        }
        elsif ($x1 < $x2) {
          return -1;
        }
        else {
          return 0;
        }
      }
    };
    
    my $minimals = new TestCase::Minimal[3];
    $minimals->[0] = new TestCase::Minimal;
    $minimals->[0]{x} = 3;
    $minimals->[1] = new TestCase::Minimal;
    $minimals->[1]{x} = 1;
    $minimals->[2] = new TestCase::Minimal;
    $minimals->[2]{x} = 2;
    
    sort_obj($minimals, $comparator);
  }

JITの実装のために、ビルドディレクトリが必要になった

SPVMのJITの実装は、現状では、メモリ上では行われない。理想は、メモリ上で機械語に変換できることだけれど、これは、ほんとうに大変な作業だ。

一からやろうとすると、地獄の実装である。それぞれのCPUの命令を読み解いて、対応する命令に置き換えないといけない。ポータビリティを維持するにも、とても大変だ。

現状のSPVMのJITの実装は以下のようになっている。

  1. SPVMのソースコードyaccを使って、抽象構文木に変換
  2. 抽象構文木を、バイトコードに変換
  3. バイトコードを、C言語ソースコードに変換
  4. C言語ソースコードgccを使って、共有ライブラリ(.so, .dllなど)に変換
  5. 共有ライブラリを読み込んで、関数のアドレスを取得して保存
  6. 関数のアドレスを使って機械語の命令を実行

いったいこの中で、何が一番の問題だと思いますか?


この中で一番の問題は、C言語ソースコードgccに読み込ませるところだ。

C言語ソースコードを読み込ませるということは、一時的にディレクトリに保存する必要があって、メモリ上で完結できないということだ。

これはポータビリティの観点から見ると非常に辛い。

でも将来的には解決できる日がくると考えて、前に進む。

最初は一時ファイルにしようと考えていた

最初は一時ファイルにしようと考えていた。「/tmp」に保存するということだね。

でも、いろいろと調べてみると、/tmpに保存することは、セキュリティ上の問題がありますと書かれていた。

ガーン。

というわけで、却下だ。

JITの時だけ、ビルドディレクトリを準備してもらう

SPVMはバイトコードなので、JITを使わない限りは、ポータビリティ上の問題はない。

だから、デフォルトは、バイトコード実行にして、JITの時だけ、JITを有効にしてもらうこととと、ビルドディレクトリを作成してもらうということにした。

use SPVM::EnableJIT;
use FindBin;
use SPVM::BuildDir "$FindBin::Bin/spvm_build";
プロセスセーフな実装

さて、これでもまだ問題が残る。

もしディレクトリの中で、JITコンパイルをするのであれば、ファイル名が同じだった場合は、forkなどで子プロセスを生成した場合に、子プロセスでコンパイルの衝突が起こることになる。

メモリはプロセスの空間なので、よいのだけれど、ディレクトリはプロセス空間ではない。というわけで、競合が起こってしまう。

そのために、ビルドディレクトリの中に、プロセスIDを付けたディレクトリを作成して、そこでJITのビルドを行うことにした。

spvm_build/1234
          /8576
          /3789

上のように。でも、考えてみてほしい。もう一度同じプロセスIDで、プログラムが実行されてしまうと、これもまた競合が起きてしまう。

ファイルが残りっぱなしになるので、過去に実行されたプロセスと同じプロセスIDを持つ現在のプロセスが競合してしまう。

時刻をディレクトリ名に入れることにした

これを解決するために、時刻をディレクトリに入れることにした。

spvm_build/1234.1847348576
          /8576.1847348592
          /3789.1847348513

こうしておくことで、過去のプロセスと競合が起こらないようにした。

ディレクトリがどんどん増えていく。

でも、ちょっと待ってほしい。こんなことをしたら、ディレクトリがどんどんどんどん増えていってしまうじゃないか。

最初に考えたことはENDブロックを使うことだ。

でもENDブロックの最大の問題点は、プロセスをkillしたときに、実行されないということだ。

ENDブロックに頼ると、killしたりセグメンテーションフォールトしたときに、そのままディレクトリが残ってしまう。

どうしようか。

セーフティなクリーンアップ

最善の解決策ではないかもしれないけれど、次のような方法を思いついた。それは以下のアルゴリズムだ。

1. スクリプトを実行する最初に、クリーンアップを実行する
2. 最初にプロセスの起動時刻を保存しておいて、それよりも過去のディレクトリだけを削除する
3. 「kill 0, プロセスID」の構文を使って、プロセスが存在しないディレクトリだけを削除する

こうすると、次回実行時に、必要のないディレクトリだけが、きれいにクリーンアップされ続ける。

今日実装したばかりなので、不具合はある可能性は高いが、これで前に進む。