書いた人:ささだ
YARV: Yet Another RubyVM を解説するこの連載。ずっと命令列についてばかり解説していたので、今号ではちょっと脱線して、命令列のシリアライズ機能について解説します。
すでに YARV は命令列のシリアライズ機能、デシリアライズ機能を持っています。今回はその使い方の紹介と、実際に応用してみた話をしてみます。
いやー、日本 Ruby カンファレンス 2006、終わりましたねぇ。運営側の人間としてはまだ残務が残っているんですが、無事に終わって本当に一安心といったところです。お世話になった方々、関係者の方々、ご参加頂いた方々、本当にありがとうございました。この場を借りて御礼申し上げます。
さて、RubyKaigi2006 の初日には Ruby 2.0 というパネルを行ったのですが、そこでまつもとさんから「Ruby 1.9.1 を来年のクリスマスに出す」「Ruby 2.0 は 2010 年くらいに出す」「1.9.1 に YARV が入っているといいねぇ」という話がありました。うーん、マージされるといいですねぇ。YARV のバグだしって何年くらいかかるか想像すると、今年中にはマージしないと 1.9.1 に間に合わないと考えるべきですかねぇ、うーん。
そもそも、RubyKaigi 2006 の準備なり残務なりで、YARV の開発が全然進んでいない。Ruby の開発を阻害する RubyKaigi。うーむ。
さて、そのパネルで、Java の VM が利用するようなクラスファイルとか、コード隠蔽みたいなことは出来ないのか、という質問を青木さんから受けたんですが (事前の打ち合わせどおりです。感謝)、「それを行うための機能は実装している」と答えました(詳細はるびま RubyKaigi2006 特別号を見てね)。その機能が今回紹介する命令列のシリアライズ機能です。
ちなみに、RubyKaigi では「コード隠蔽みたいなことはやる気がないので、プライオリティは低い」と言ったんですが、この連載のことを思い出して、ちょうどいい、ということで簡単に実装することにしました。
ところで、この命令列のシリアライズは、順番が逆で、デシリアライズ、というか Ruby スクリプト以外の他の表現から命令列を生成する方法が欲しくて作りました。今年 (2006 年) の 3 月に YAPC::Asia というイベントが開催されましたが、Pugs の作者である Audrey Tang さんに Pugs のバックエンドとして YARV も使ってみたい、という申し出を受けて作り始めました。作ってみて、作ったよー、と報告はしたんですが、実際に使ってるのかどうかは謎です。
えーと、前回までで説明していた命令の名前、色々と変えてしまいました。前回も後悔しているって書いたんですが、まぁ影響範囲が皆無であるうちに、えいや、ということで変えてしまいました。今度は従来の Ruby プログラムなどのキーワード、メソッド名などと被らないようにしてあります。
変更結果は YARV: Instruction Table を見てください。具体的には、前回後悔していた if 命令などが branchif 命令などに変更されています。
今回の記事を書くために色々と修正したので、その辺をまとめて 0.4.1 としてリリースしました。記事中のサンプルは YARV 0.4.1 を利用して実行してみてください。
そもそも YARV 命令列とは何か、というのは以前にも書いたような気がするのですがもう一度おさらいしておきます。
YARV は VM、仮想マシンですので、命令を順番に実行していきます。その命令を表現しているのが命令列です。簡単ですね。
Ruby プログラムを読み込むと、まず YARV は従来のパーサを使って構文木にします。私はパーサには (とくに Ruby のようなマジカルなパーサには) 手を出さないように幼少のころから教育されていますので、現在の Ruby 処理系そのまんまのものを使っています。で、パーサは Ruby プログラムを抽象構文木にして返します。ちょっとかっこつけて言うと Abstract Syntax Tree といって、頭文字をとって AST といいます。その AST を YARV のコンパイラが YARV 命令列に変換する、というわけです。簡単ですね。
YARV では、命令列もオブジェクトとして取り扱っています。YARVCore::InstructionSequence という長ーいクラス名のオブジェクトになっています。ところで、YARV は YARV じゃなくなるので、YARVCore という名前もなくなると思います。YARVCore じゃなくて RubyCore になるのかなぁ。それとも、ただの VM クラスになるのかなぁ。ご意見募集中です。あ、ところで YARVCore クラスは YARV を実行すると勝手に定義されています。
InstructionSequence は、そのまんま命令列の意味ですね。違ったら、誰かこっそり教えてください。YARV のソース中では、これをそのまま書くのはめんどくさいので iseq と略しています。YARV のソースコード中には大量に iseq が出没しますので、ソースを読むときは覚えておいてください。
オブジェクトなのでメソッドを持ちます。たとえば、InstructionSequence#disasm メソッドは逆アセンブルした結果を文字列で返します。文字列で返すのはカッコわりーなー、もうちょっとマシな設計無いのかよ、と自分でも思うのですが、誰も使う人がいないのでとりあえずこのまんまです。将来的にはこの辺も整理されることでしょう。されるといいなぁ。
命令列オブジェクトは、実際にはいくつもの命令列オブジェクトがツリー上になって構成されています。
いったいどういうことかというと、いくつかの種類の命令列オブジェクトが、変数のスコープが独立しているという単位 (など) で、異なる命令列オブジェクトとなり、それらがリンクしています。具体的な種類は次のとおりです。
たとえば、
なんていうやる気のないプログラムは
というような木構造になっています。
ちなみに、命令列オブジェクトも Ruby の普通のオブジェクトですので GC されます。もちろん実行中には GC されることはありませんが、たとえばトップレベルの命令列は別のトップレベルへ実行が遷移したとき、GC されるかもしれません1。
ちなみに、命令列同士の親子関係で参照を持っているものがあります。たとえば、ブロックの命令列はその親への参照を持っています。そのため、子どもを実行中に親が GC されるということは多分ありません。
ところで、このオブジェクトはどうやって生成するんでしょうか。前述したように、YARV は命令列にそって実行します。つまり、Ruby プログラムを読み込んで実行している時点で、しっかり存在するわけですね。
でも、YARV を使っていても 命令列オブジェクトなんて見た事ねーよ、という方がほとんどではないかと思います2。私もほとんどありません。
今回の話題は、この命令列オブジェクトに対してなんかする、ということなので、Ruby レベルのオブジェクトとして取り出したいわけですが、どうしましょう。
実は、実行するために命令列にしてしまうと、それを取り出す手段は (今は) ありません3。require なども同様です。
そのため、YARV 命令列オブジェクトのファクトリーメソッドを用意してあります。これは、Ruby プログラムである文字列を YARV 命令列オブジェクトに変換して返すという機能を持っています。
ファクトリーメソッドは以下の二つです。
compile メソッドは文字列を、compile_file メソッドはファイルを対象にパースとコンパイルを行います。
compile メソッドの file_name、line_no はそれぞれファイル名と行数で、eval メソッドの引数と同様、スタックトレースの出力などに利用します。
では、実際に試してみましょう。test.rb というファイルを用意します。
このプログラムでは、まずてきとーな Ruby プログラム (ヒアドキュメントで記述してあるもの) をコンパイルし、そのコンパイル結果を逆アセンブルして出力、というものです。その後、そのファイル自身を compile_file メソッドでコンパイルし、出力しています。
実行結果は次のようになります。
まぁ、長々と出てきましたが、なんとなくコンパイルできた感をつかんでいただけたんではないかと思います。多分。
さて、紹介した 2 つの命令列オブジェクトファクトリーメソッドですが、実はオプショナルにもう 1 つ、コンパイルオプションを指定する引数を受け付けます。コンパイルオプションで渡す値は以下のようになっています。
現状でサポートしているコンパイルオプションは以下のとおりです。
指定するときは、上記のコンパイルオプションをシンボルにして、
のように行います。
では、コンパイルオプションを弄って試してみましょう。
実行結果です。
たくさん出てきてわかりづらいですが、とりあえず命令列の長さが短くなっているのはわかるので、なんらかの最適化がかかったんだなぁ、ということはわかると思います。
なお、Ruby プログラムを実行するときに適用されるコンパイルオプション (デフォルト値) は YARVCore::InstructionSequence.compile_option= メソッドにここで説明した値を渡すことで指定できます。多分、将来的には Ruby インタプリタのコマンドライン引数で指定できるようにするんじゃないでしょうか。ただ、この辺を弄れても嬉しい人はほとんどいないような気はします。
さて今回の本題、YARV 命令列のシリアライズです。
命令列オブジェクトは、定数 (数値・文字列・シンボル)、配列、ハッシュオブジェクトなどのプリミティブなデータのみの、取り扱いデータ型へ変換可能です。ここでは、これをもって命令列のシリアライズと称しています。
で、その変換を行うには YARVCore::InstructionSequence::to_a を利用します。実際に試してみましょう。
こんなふうに使います。このプログラムはやる気の無い Ruby プログラムをコンパイルして、YARV 命令列オブジェクトを生成して、その結果を配列に変換し、pp してデータ構造をどかどか出力しています。
眺めただけで、どんなプログラムなのかすぐにわかりますね!
さて、出力したデータ構造は面倒なんで説明はかなり省略しますが、命令列の説明 + いろんな付加データとなっています (省略しすぎ)。
「いろんな付加情報」というのは以下のような配列になっています。
それぞれがどんなデータをあらわしてどのように表現するか、という説明は 面倒くさいので 紙面の都合上行いませんが、名前を見るとぼんやりとわかるんじゃないかな、と思います。ソースを見るのも手です (compile.c、iseq.c)。
この付加情報のうち body 要素が命令列を表しています。body 要素の実体は配列で、その配列の要素は [:”命令名のシンボル”, operand…] という配列もしくは :label_name というシンボルです。つまり、body の値は次のような感じになります。
例えば、p true while true という Ruby プログラムは
こんなふうに表現されます。
命令名などは、命令名に対応する数字を決めてしまえば、このデータ構造はかなり圧縮できたりするのですが、シンボルにしておいたほうが可搬性があるのでこのようなフォーマットにしています。というか、まだ命令セットが fix していないので、ガチガチに固めたくないということもあります。まぁ、シンボルだと (人間にとって) 読み書きがとてもやりやすいというのが一番の理由です。
さて、上記のようにシリアライズしたデータ構造は、もちろんそのまま読み込むことができます。
こんなふうに使います。
また、YARVCore::InstructionSequence#eval というメソッドは (トップレベルタイプの命令列なら) その命令列を実行します。
では YARV のシリアライズ機構の応用例を示しましょう。昔から Ruby には難読化器がないとかなんとか、いろいろ言われてきたので、シリアライズ機構を活用して作ってみました。以下にその難読化器のコードを示します。なお、これとまったく同じものが YARV のソースツリーの中の rb/compile.rb にも同梱してあるので、実際に使いたい場合はそちらを利用してください。
このプログラムは、指定したファイルを a.rb (もしくは、-o オプションで指定した名前) というファイルに__コンパイル__します。
実際にやってみましょう。
これを ruby compile.rb t.rb として実行すると、a.rb というファイルが生成されます。
実際に何をやっているかというと、
いる、というものです。
なんというか、Ruby プログラムを pack(‘m*’) するだけでもいいような気がしないでもないですが、まぁそれだけだと簡単に中身が見えてしまうので、一度 YARV 命令列にコンパイルしてから Base64 エンコーディングしているということです。
さて、ロードしたいときにはデシリアライズするプログラムになっているのでこれをそのまま実行すればよいのですが、具体的に何をするかというと、
という流れになります。
ちなみに、これだけのオーバヘッドがあるのでロード時間短縮のためにこのコンパイラで先にコンパイルしておく、というのは意味がありません (測りました)。というか、遅くなります。unpack のコスト、および Marshal.load のコストが大部分で、これに時間がかかります。後者のコストはシンボルを多用している現在の表現によります。この辺、バイナリでガチガチに固めてしまう、よくある (Java の VM 用クラスファイルのような) 構造を定義すれば可能になるでしょう。そのためには命令セット決めないとなぁ。うーん4。
ところで、結局 YARV 命令列 -> Ruby プログラム変換器 (逆コンパイラ) を作れば中で何をしているのかわかってしまうのですが、とりあえずそんなに簡単ではないとたかをくくっています。誰か作りませんか?
さて、このデシリアライズインターフェースを使うと簡単に YARV 上で実行できる命令列を作成することができます。たとえば、他の言語で YAML を利用して上記フォーマットを記述しておけば、簡単に YARV に読み込ませることが出来ます5。
が、配列をごちゃごちゃ弄るのはめんどくさいです。そもそも、構造の解説がソース読め、という冷たい仕打ち。そこで、yasm というライブラリを用意しました。要するに YARV 用アセンブラで、Ruby の文法で記述します、というか Ruby のプログラムです。アセンブラで色々記述すると、上記のシリアライズした表現、つまり Ruby の配列を作るので、あとはそれをデシリアライズ (ロード) して実行すればいいわけです。
習うより慣れろ、ということで利用例を示します。
YASM.toplevel メソッドが toplevel の YARV 命令列のファクトリーメソッドになります。そして、ブロック中でどんな挙動をするのか、命令を Ruby のメソッドとして作成します6。
ブロックなどは block メソッドで記述します。
さて、YASM の書き方、Ruby の文法としてちょっと不思議に思いませんか。Ruby プログラムとしては完全に valid ですが、putobject などのメソッド、どのクラスに属しているんでしょう。トップレベルで定義されたメソッドでもありません (実際に putobject と読んでみると、NoMethodError になります)。ブロックの中でしか有効ではありません。
この仕組み、instance_eval を利用して作られています。つまり、ブロックの中では self が違うんですね。そのため、うっかりすると間違ってしまう可能性があります。
で、self の置き換えを防ぐために、明示的にビルダーオブジェクトに対してエミットメッセージを送るという書き方にするには次のように書きます。
ブロックの arity を見て、もし arity が 1 ならば instance_eval する代わりにビルダーオブジェクトを渡す、という設計です。この設計、ちょっとどうかなと思わなくもないのですが…。まぁ、とりあえずこのようにしておきましょう。
ちなみに、これは Microsoft の .NET などで用意されている ILGenerator みたいなイメージですね。
今回は YARV 命令列のシリアライズとデシリアライズの機能について紹介しました。今回紹介した仕組み、とくに yasm を利用すると YARV 上で動作するプログラミング言語を気楽に作ることが出来ます (なんといっても、アセンブリ言語が Ruby の表現力を持っているわけですから、その使いやすさは推して知るべし)。
本当は、今回何か簡単な言語のコンパイラを作ろうと思っていたのですが、間に合いませんでした。誰か Scheme あたりで挑戦してみませんか。かなり簡単だと思いますよ。
では、また。
ささだ こういち。非学生。
非学生だが、RubyKaigi 2006 にかまけて仕事が全然できていない。ヤバイ。まぁ、いいか (いや、大変よくない。マジで)。
ちょっとわかりづらいかもしれませんが、たとえばこの命令列が require などで実行された場合を考えてみてください。 ↩
YARV を使ったことがある人がほとんどいねーという噂。 ↩
加えるのは簡単なんですが、インターフェース、API の設計が難しいのです。 ↩
別の選択肢として、「現在の VM が dump したファイルしか読み込めないようにする」という割り切りをしてしまうという方法があります。ビルドバージョンが違ったり、別の VM が dump した、たとえばネットワーク越しのやり取りは一切サポートしない、という方法です。個人的には、現在の冗長表現、それから VM 限定のタイトな表現、どちらもサポートするのがいいのかな、と思っています。 ↩
というか、そういう目的で作ったわけです。 ↩
命令を Ruby のメソッドとして記述可能にするために命令セットの名前を考え直したんですよね。たとえば、end 命令は leave 命令になりました。 ↩