コードも生成できたので最後にコードを実行している部分を読みます。mrb_run()がエントリポイントになります。
いきなりmrb_run()に入る前にmrubyVM*1がどんな実行モデルなのかについて説明します。
mrubyVMの実行モデルはレジスタマシンです。ちなみにYARVはスタックマシンです。レジスタマシンとスタックマシンの違いはWikipediaあたりをご参照ください。
例えば以下の単純なRubyスクリプトの場合、
def foo(a, b) a * b end f = foo(1, 2)
irep 116 nregs=6 nlocals=3 pools=0 syms=1 000 OP_TCLASS R3 001 OP_LAMBDA R4 I(117) 1 002 OP_METHOD R3 'foo' 003 OP_LOADSELF R3 004 OP_LOADI R4 1 005 OP_LOADI R5 2 006 OP_LOADNIL R6 007 OP_SEND R3 'foo' 2 008 OP_MOVE R1 R3 009 OP_STOP irep 117 nregs=7 nlocals=5 pools=0 syms=1 000 OP_ENTER 2:0:0:0:0:0:0 001 OP_MOVE R5 R1 002 OP_MOVE R6 R2 003 OP_LOADNIL R7 004 OP_SEND R5 '*' 1 005 OP_RETURN R5
f = foo(1, 2)の部分は以下のように実行されます。
mrubyではレジスタの確保場所としてスタックを使用しています。そのため、mrubyVMはスタックマシンであると勘違いしてしまう危険があるので注意してください。さわだもソースだけ見ていてスタックマシンだと勘違いしていました。
レジスタの確保場所としてスタックを使うとはどういうことかというと、以下のようなイメージです。(OP_SEND '*'実行直前の状態)
トップレベル実行時のスタックベース→| nil |top_self | |ローカル変数fの格納領域 | nil |よくわからない。特殊変数用? メソッドfoo実行時のスタックベース→| nil |'foo'のレシーバ | 1 |'foo'の引数1 & ローカル変数a | 2 |'foo'の引数2 & ローカル変数b | nil |'foo'に対するブロック引数 | nil |よくわからない。特殊変数用? | 1 |'*'のレシーバ | 2 |'*'の引数1 | nil |'*'に対するブロック引数
以上の前提を持ってmrb_run()に挑むと理解が深まると思います。
では、mrb_run()に見ていくことにしましょう。mrb_run()は一言で言うと一つ一つ命令を実行するループと各命令の処理に振り分ける巨大なswitch文です。ただし、処理効率化のためにちょっとカラクリが施されています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| - - | | ! | - | | | | | | | ! | | ! |
|
INIT_DISPACTH*2, CASE, NEXTの定義はmrb_run()の少し上に書かれています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| - | | | ! |
|
gccかどうかで定義が変ってます。めんどくさいけどちゃんとマクロ展開されたコードを示すことにします。
gccじゃない場合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| - - | | ! | - | | | | | | | ! | | ! ! |
|
gccの場合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| - - | | ! | - | | | | | | | ! | | ! |
|
というわけでgccじゃない場合は無限ループ & switch文ですが、gccの場合は命令コードで決まるジャンプ先に直接飛んでいます。こうすることで命令ごとに条件分岐をするコストがなくなるため高速化が実現できます。YARVでも同じことが行われていました。
総論は終わったので後は各論、いつものように各命令についてどのような処理が行われているかを見ていくことにします。なお、例外処理関係は別で扱う予定なので飛ばします。
mrb_run()を上から見ていくと初めはLOAD系の命令が並んでいます。その中でややわかりにくいのがLOADSELFだと思います。
1
2
3
4
5
| - | | | ! |
|
何でこれでselfを設定したことになるのか?というと、スタックベースにはselfのオブジェクトが格納されているからです。上に書いたスタックのイメージを再掲すると、
メソッドfoo実行時のスタックベース→| nil |'foo'のレシーバ | 1 |'foo'の引数1 & ローカル変数a | 2 |'foo'の引数2 & ローカル変数b | nil |'foo'に対するブロック引数
というわけでstack[0]がselfになっていることがご理解いただけると思います。
このstack[0]がselfであるということは他でも使われているので覚えておくようにしてください。例えば以下のようにインスタンス変数を取得するコードでもしれっと使われています。
1
2
3
4
5
| - | | | ! |
|
src/variable.c
1
2
3
4
5
6
| - | | ! |
|
次にメソッド呼び出しを行うOP_SENDを見てみましょう。長いので例によって区切って解説します。
1
2
3
4
5
6
7
8
9
| - | | | | | | | | |
|
コメントに書いてあるように、OP_SENDは3引数型の命令でそれぞれ以下の内容になっています。
と、これがmrubyのメソッド呼び出し規約となっているようです。スタックイメージを再々掲すると、
| nil |R(A) 'foo'のレシーバ | 1 |R(A+1) 'foo'の引数1 | 2 |R(A+2) 'foo'の引数2
となっており、適切に呼び出す準備がされていることがわかります。*3
先に進みます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| - | | | | - | ! - | | | ! ! |
|
呼び出すメソッドの検索をしています。メソッドがない場合はmethod_missingを呼ぶようにしています。
1 2 3 4 5 6 7 8 9 |
|
呼び出し情報はmrb_callinfo構造体に格納されるようです。ここで覚えておくといいのはstackidxとpcがメソッドから返ってきたときの復元に使われる情報ということでしょうか。
1 2 |
|
スタックベースを調整してレシーバが入っているスタック位置をstack[0]、すなわちselfになるようにしています。
1
2
3
4
5
6
7
8
9
| - | | | | | | | ! |
|
メソッドがCで書かれている場合です。Cで書かれたメソッドの処理はまた別に見ます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| - | | | | | | | | | - | ! - | ! | | | ! ! |
|
メソッドがRubyで書かれている場合はこちらが実行されます。mrb_callinfo.accは先に書いてしまうとメソッドの戻り値をどこに入れるかの情報です。第1引数が保存されているのでレシーバを設定したレジスタに戻り値が格納されることがわかります。その後、各種実行時情報を呼び出し先のものに切り替えています。注意が必要なのはここでは呼び出すだけでメソッドから返ってくるのはOP_RETURNが実行されたときであるということです。
メソッド呼び出しについて見たので次はメソッドの受け側であるOP_ENTERを見ます。
1
2
3
4
5
6
7
8
9
10
11
12
13
| - | | | | | | | - | | | ! |
|
まず、命令コードのオペランドから各種引数の数を取得しています。
1 2 3 |
|
次に実際に渡された引数の取得です。
1
2
3
4
5
6
| - | | | | ! |
|
引数が配列(foo(*a)みたいなの)で渡されたときの処理です。ん?上記のコードを見るとメソッド呼び出し時に存在する不自然な空きレジスタって引数配列退避用なのか?
引数が少ない、もしくは多過ぎるときのエラー処理です。
1
2
3
4
| - | | ! |
|
yieldの場合、*を付けなくても配列展開されるようです。上記でやっているのはその処理です。
1
2
3
4
5
6
7
8
9
10
| - | | | - | ! | ! |
|
オプション引数の処理です。引数の数からメソッドの開始位置を決定しています。実行コード生成に載せたサンプルを再掲します。
def foo(a = 1, b = 'xxx') end
irep 117 nregs=6 nlocals=5 pools=1 syms=0 000 OP_ENTER 0:2:0:0:0:0:0 001 OP_JMP 004 002 OP_JMP 005 003 OP_JMP 006 004 OP_LOADI R1 1 005 OP_STRING R2 'xxx' 006 OP_RETURN R4
1
2
3
4
5
6
7
8
9
| - | - | ! | | | ! |
|
こっちは引数が足りているときです。余った分は残余引数に詰め込んでいます。
1 2 | ! |
|
最後に、渡された引数と想定している引数から計算されたpc*4から実行を開始です。
続いて、メソッドからの復帰時に実行されるOP_RETURNを見てみましょう。なお、前半で行われている例外処理は省略します。
まず、メソッドの戻り値をレジスタから取り出しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| - | - | | ! | | | - | | ! | | | - | | ! | | | | | ! |
|
ブロック中でbreakを実行した場合も命令コードはOP_RETURNになります。上記ではどこまで呼び出しを巻き戻るかを調整しています。
1 2 3 4 |
|
各種実行時情報をメソッド呼び出し前に戻しています。
1
2
3
| - | ! |
|
ensure節の処理です。例外処理についてはまた今度説明します。
1
2
3
4
| - | | ! |
|
Cで書かれたメソッドから呼ばれた場合、accは-1になっています。つまり、ここではCで書かれたメソッドに戻り値を返すということをしています。
1 2 3 4 5 6 7 8 9 10 | ! | ! |
|
各種実行情報復元その2。戻り値をレジスタに格納してメソッド呼び出しを行ったOP_SENDの次の命令から処理を再開しています。
次にブロック周りを見てみましょう。yieldをコンパイルするとOP_BLKPUSHが埋め込まれることがわかります。
$ ./mruby.exe --verbose -e 'def foo; yield(1); end' irep 117 nregs=5 nlocals=3 pools=0 syms=1 000 OP_ENTER 0:0:0:0:0:0:0 001 OP_BLKPUSH R3 0:0:0:0 002 OP_LOADI R4 1 003 OP_LOADNIL R5 004 OP_SEND R3 'call' 1 005 OP_RETURN R3
というわけでOP_BLKPUSHを見てみます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| - | | | | | | | | | | - | | ! | | ! |
|
m1, r, m2はyieldを実行するメソッドの引数情報です。オプション引数が含まれていないように見えますがm1は通常引数の数とオプション引数の数の合計になっています。lvはfor中でyieldすると0以外になるようです。ともかく、OP_BLKPUSHの目的はメソッド呼び出し時に積まれたブロックをレジスタに設定することです。忘れていると思いますが、mrubyのメソッド呼び出し規約ではメソッドを呼ぶ際に以下のようにself、引数、ブロックを積むことになっています。
regs→|self | |引数1 | |・・・ | |引数n | |ブロック|
stack[m1+r+m2]というのがちょうどブロックの位置を指すことになります。
(以下、執筆中)
yieldで使われる。OP_SEND("call")→OP_CALLという流れ
数値、文字列の場合は直接処理、それ以外はOP_SENDへ
Cで書かれたメソッドが引数を取得するための関数