スクリプト解析処理まで進んだのでいよいよJavaバイトコードへの変換部分を読んでいきたいと思います。
スクリプトの解析が終わると解析結果のNodeを引数にrunNormallyメソッドが呼び出されます。JRubyでのスクリプト実行にはコンパイル式とインタプリタ式がありますがデフォルトではJavaバイトコードにコンパイルされてから実行されるのでコンパイル式のみ見ることにします。
tryCompileメソッドはいくつかありますが順に追っていくとコンパイル処理の主要クラスはASTCompilerクラスであることがわかります。また、ASTCompiler.compileRootメソッドにはStandardASMCompilerクラスのインスタンスが渡されており、このクラスも重要な役割をしそうなことがわかります。
JRubyのRuby互換バージョンが1.9の場合はASTCompiler19クラスのインスタンスが生成され一部の処理がオーバーライドされているようです。
compileRootメソッドに移ります。まずStandardASMCompilerインスタンスはcompileRootメソッドにはScriptCompilerインターフェースとして渡されていることがわかります。
compileRootメソッドでは以下の処理が行われています。
compileメソッドは巨大なswitch文です。Nodeの種類に応じてcompileXxxメソッドが呼ばれコンパイル処理が進行していくようです。
クラスの生成にはASMが利用されています。startScriptメソッドを見ると生成されるクラスのスーパークラスとしてorg.jruby.ast.executable.AbstractScriptクラスが使われていることがわかります。
ところでstartScriptメソッドを見ているとpとかsigというメソッドを見かけますがStandardASMCompilerクラスには該当メソッドは定義されていません。Eclipseの場合、メソッドにカーソルを合わせるとorg.jruby.util.Codegenのメソッドであることがわかります。どういうカラクリかというとJava 5.0から導入されたstaticインポート機能を使用しているようです。
このメソッドではスクリプトのトップレベルに書かれているコードに対応するメソッドとして__file__メソッドの作成を開始しています(__file__メソッドを構築するためのMethodBodyCompilerが返されます)。
このメソッドでは引数により生成するクラスにloadメソッドとmainメソッドを追加しています。loadメソッドでは__file__メソッドの呼び出しを行っています。Ruby.runScriptメソッドを見ると生成されたクラス(Scriptインターフェース)のloadメソッドを呼び出しています。これによりコンパイルしたトップレベルのコードが実行されるというカラクリのようです。
それではスクリプト解析を読むで構築したNodeをコンパイルしてみることにします。ちなみにjrubyに--bytecodeオプションを指定すると生成されたクラスのバイトコードが出力されます。montecarlo.rbをコンパイルした結果はこちらになります。montecarlo.bytecode.txt
先ほど説明したstartFileMethodメソッド中でMethodBodyCompiler.beginMethodメソッドが呼び出されます。beginMethodメソッドは以下のようになっています。
1
2
3
4
5
6
7
8
9
10
| - - | | | | - ! ! ! |
|
variableCompilerはスーパークラスであるBaseBodyCompilerのコンストラクタで設定されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| - - | | | | | | | | | | | | | ! ! |
|
委譲されてMethodBodyCompilerに戻ってきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | - - - | - | - | ! ! ! - - - ! ! ! |
|
トップレベルではクロージャ(ブロック)もscope awareなメソッドも使っていないためStackBasedVariableCompilerが生成されるはずです。
で、StackBasedVariableCompiler.beginMethodメソッド。
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| - | | | | | | - | | ! | - - - - | | | - | | - | | ! ! | - ! ! | - | ! ! | - - | ! | | ! ! |
|
あちこち飛びますがloadNilメソッドは以下の通りです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| - - | - | - | ! ! | - | ! ! |
|
何をしているかというと、ローカル変数にnilを代入するという処理を行っています。結果として、__file__メソッドの先頭に以下のコードが埋め込まれます。
ALOAD 1 // ThreadContextロード GETFIELD org/jruby/runtime/ThreadContext.nil : Lorg/jruby/runtime/builtin/IRubyObject; // nilを取得 DUP // 取得したnilを複製 ASTORE 9 // ローカル変数nにnilを設定 ASTORE 10 // ローカル変数piにnilを設定
さてローカル変数の設定ができたのでNodeのコンパイルに取りかかります。まず始めにあるのはClassNodeです。対応するASTCompiler.compileClassメソッドではクラスパス、スーパークラス、クラス定義本体のコールバックを作成しBodyCompiler.defineClassメソッドを呼んでいます。defineClassメソッドはBaseBodyCompilerクラスで定義されています。
defineClassメソッドはけっこう長いですがやっていることは以下になります。
クラス定義本体のNodeをたどっていくとDefnNodeに行き当たります。引数、メソッド本体に対するコールバックを作成した上でBodyCompiler.defineNewMethodメソッドを呼んでいます。
defineNewMethodメソッドでは以下のことが行われています。
メソッド定義用のBodyCompilerを生成する際にVariableCompiler.beginMethodメソッドが呼ばれます。トップレベルと違い今回は引数のargsCallbackがnullではないのでcallメソッドが呼び出されます。その結果、ASTCompiler.compileArgsメソッドが呼ばれ引数設定処理のコンパイルが行われます。ちなみに、compileArgsメソッドはASTCompiler19クラスでオーバーロードされているので注意が必要です。
ASTCompiler19.compileArgsメソッドはcompileMethodArgsメソッドに処理を委譲します。compileMethodArgsメソッドでは各種引数に対するコールバックを作成しVariableCompiler.assignMethodArguments19メソッドを呼び出しています。
Rubyには引数の種類が複数あるので引数設定処理は単純ではありません。それがassignMethodArguments19メソッドに現れています。特にオプション引数は実際に渡された引数の数によって処理を変える必要があります。実際に生成されるコードを使って解説した方がいいので一瞬別のスクリプトを使います。
def bar 2 end def foo(a, b = 1, c = bar) # 実はデフォルト値としてメソッド呼ぶことで来ます end
このfooメソッドの引数設定処理は以下のようになります。
// 引数aの設定 ALOAD 3 ICONST_0 ALOAD 1 GETFIELD org/jruby/runtime/ThreadContext.nil : Lorg/jruby/runtime/builtin/IRubyObject; INVOKESTATIC org/jruby/javasupport/util/RuntimeHelpers.elementOrNil ([Lorg/jruby/runtime/builtin/IRubyObject;ILorg/jruby/runtime/builtin/IRubyObject;)Lorg/jruby/runtime/builtin/IRubyObject; ASTORE 9 // 引数bの設定 // 2つ目の引数が渡されているかのチェック ALOAD 3 ICONST_1 ICONST_0 INVOKESTATIC org/jruby/javasupport/util/RuntimeHelpers.optElementOrNull ([Lorg/jruby/runtime/builtin/IRubyObject;II)Lorg/jruby/runtime/builtin/IRubyObject; DUP // 2つ目の引数が渡されてない場合はデフォルト値設定処理へ IFNULL L1 // 渡されている場合はそれを設定 ASTORE 10 // 引数cの設定 // 3つ目の引数が渡されているかのチェック ALOAD 3 ICONST_2 ICONST_0 INVOKESTATIC org/jruby/javasupport/util/RuntimeHelpers.optElementOrNull ([Lorg/jruby/runtime/builtin/IRubyObject;II)Lorg/jruby/runtime/builtin/IRubyObject; DUP // 2つ目の引数が渡されてない場合はデフォルト値設定処理へ IFNULL L2 // 渡されている場合はそれを設定 ASTORE 11 // 引数が3つ渡されているのでデフォルト値設定処理をスキップ GOTO L3 // 引数bのデフォルト値設定処理 L1 FRAME FULL [opt org/jruby/runtime/ThreadContext org/jruby/runtime/builtin/IRubyObject [Lorg/jruby/runtime/builtin/IRubyObject; org/jruby/runtime/Block T T T T org/jruby/runtime/builtin/IRubyObject org/jruby/runtime/builtin/IRubyObject org/jruby/runtime/builtin/IRubyObject] [org/jruby/runtime/builtin/IRubyObject] ALOAD 1 GETFIELD org/jruby/runtime/ThreadContext.runtime : Lorg/jruby/Ruby; INVOKESTATIC org/jruby/RubyFixnum.one (Lorg/jruby/Ruby;)Lorg/jruby/RubyFixnum; ASTORE 10 // 引数cのデフォルト値設定処理 L2 FRAME SAME1 org/jruby/runtime/builtin/IRubyObject ALOAD 0 INVOKEVIRTUAL opt.getCallSite0 ()Lorg/jruby/runtime/CallSite; ALOAD 1 ALOAD 2 ALOAD 2 INVOKEVIRTUAL org/jruby/runtime/CallSite.call (Lorg/jruby/runtime/ThreadContext;Lorg/jruby/runtime/builtin/IRubyObject;Lorg/jruby/runtime/builtin/IRubyObject;)Lorg/jruby/runtime/builtin/IRubyObject; ASTORE 11
今回対象としているスクリプトではメソッドの引数はシンプルなので以下のように渡された引数をそのまま格納するというコードが生成されます。ちなみに、piメソッドはクロージャを持っているためVariableCompilerの実体はHeapBaseVariableCompilerになっています。(というわけでASTOREではなく、メソッド呼び出しになっています)
ALOAD 3 // 1つ目の引数ロード ALOAD 5 // DynamicScopeロード SWAP INVOKEVIRTUAL org/jruby/runtime/DynamicScope.setValueZeroDepthZero (Lorg/jruby/runtime/builtin/IRubyObject;)Lorg/jruby/runtime/builtin/IRubyObject; // 引数をローカル変数nに設定
メソッド内に入って1つ目にあるのはLocalAsgnNodeですがまあこれはあまり面白くないので飛ばしてメソッド呼び出しをしているCallNoArgNodeを見てみましょう。処理はInvocationCompilerのinvokeDynamicメソッドに委譲されます。InvocationCompilerの実体は通常StandardInvocationCompilerのようです。
invokeDynamicメソッドではレシーバのコンパイル、引数のコンパイル、ブロックのコンパイルをした後、メソッド呼び出しを行っています。メソッドの呼び出しはCallSiteというクラスが鍵になっているようですがそれは後から見ることにしてブロックのコンパイルに移りましょう。
IterNodeに対応するcompileIterメソッドもASTCompiler19クラスでオーバーロードされています。そのため、ブロックのコンパイルはBodyCompiler.createNewClosure19メソッドに委譲されます。
createNewClosure19メソッドではBlockBodyクラスとブロックを関連づけた後、ブロック本体のコンパイルを行っています。
ブロックに入るとDAsgnNodeがあります。もっともそのブロックで定義されたブロック変数の場合はLocalAsgnNodeの処理と変わりがないので飛ばします。
IfNodeを処理するcompileIfではいくつか最適化が行われています。常にtrueやfalseなif、条件部がグローバル変数や定数時とすると主にデバッグ用途ですかね。
通常はBodyCompiler.performBooleanBranch2メソッドに処理が委譲されています。分岐の処理自体は単純ですね。
条件部で生成されるコードがちょっとわかりにくいので解説します。まず、条件部のNodeはこんな感じです。
condition=CallOneArgNode receiverNode=CallOneArgNode receiverNode=CallOneArgNode receiverNode=DVarNode name='x' location=0 name='*' arg1=DVarNode name='x' location=0 name='+' arg1=CallOneArgNode receiverNode=CallOneArgNode receiverNode=DVarNode name='y' location=1 name='*' arg1=DVarNode name='y' location=1 name='<=' arg1=FixNumNode value=1
で、生成されるコードはこんな感じです。
ALOAD 0 INVOKEVIRTUAL montecarlo.getCallSite3 ALOAD 1 ALOAD 2 ALOAD 0 INVOKEVIRTUAL montecarlo.getCallSite4 ALOAD 1 ALOAD 2 ALOAD 0 INVOKEVIRTUAL montecarlo.getCallSite5 ALOAD 1 ALOAD 2 ALOAD 9 ALOAD 9 INVOKEVIRTUAL org/jruby/runtime/CallSite.call ALOAD 0 INVOKEVIRTUAL montecarlo.getCallSite6 ALOAD 1 ALOAD 2 ALOAD 10 ALOAD 10 INVOKEVIRTUAL org/jruby/runtime/CallSite.call INVOKEVIRTUAL org/jruby/runtime/CallSite.call LDC 1 INVOKEVIRTUAL org/jruby/runtime/CallSite.call
何回もALOADされててわかりにくいですがStandardInvocationCompiler.invokeDynamicメソッドを手がかりに読み解いていくと、メソッド呼び出しは
となっています。それを考えると、
ALOAD 0 ----------+ INVOKEVIRTUAL montecarlo.getCallSite3 | ALOAD 1 | ALOAD 2 | ALOAD 0 --------+ | INVOKEVIRTUAL montecarlo.getCallSite4 | | ALOAD 1 | | ALOAD 2 | | ALOAD 0 -+ | | INVOKEVIRTUAL montecarlo.getCallSite5 | | | ALOAD 1 | | | ALOAD 2 |x * x | | ALOAD 9 | | | ALOAD 9 | | | INVOKEVIRTUAL org/jruby/runtime/CallSite.call -+ |x * x + y * y ALOAD 0 -+ | | INVOKEVIRTUAL montecarlo.getCallSite6 | | |x * x + y * y <= 1 ALOAD 1 | | | ALOAD 2 |y * y | | ALOAD 10 | | | ALOAD 10 | | | INVOKEVIRTUAL org/jruby/runtime/CallSite.call -+ | | INVOKEVIRTUAL org/jruby/runtime/CallSite.call --------+ | LDC 1 | INVOKEVIRTUAL org/jruby/runtime/CallSite.call ----------+
という入れ子関係になっていることがわかります。ちなみに、一番外側の「x * x + y * y <= 1」はinvokeDynamicではなくinvokeBinaryBooleanFixnumRHSメソッドが使われています。最適化の一環でしょう。
さて次にthen節のコンパイル処理を見ていきましょう。「count += 1」は以下のようなNodeになります。
thenBody=LocalAsgnNode name='count' location=1 depth=1 valueNode=CallOneArgNode receiverNode=LocalVarNode name='count' location=1 depth=1 name='+' arg1=FixNumNode value=1
先ほどはLocalAsgnNodeは面白くないと飛ばしましたがそれは通常時のローカル変数代入の話です。ブロック内でブロックの外のローカル変数(depthでどれだけさかのぼるかを管理)にアクセスするとなると話は面白くなります。
StackBasedVariableCompilerのassignLocalVariableメソッドは次のようになっています。
assignHeapLocalメソッドはAbstractVariableCompilerクラスに書かれています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | - - | | | | | | ! | ! - - ! - | | ! ! |
|
というわけでdepth分スコープをさかのぼる処理をした後、そのスコープに値を設定している雰囲気です。以上を考えて生成されるコードを読むと理解が容易になります。(LocalVarNodeも話は同じです)
ALOAD 5 --------------+ INVOKEVIRTUAL org/jruby/runtime/DynamicScope.getNextCapturedScope | ALOAD 0 ------------+ | INVOKEVIRTUAL montecarlo.getCallSite7 | | ALOAD 1 | | ALOAD 2 | | ALOAD 5 -+ | | INVOKEVIRTUAL org/jruby/runtime/DynamicScope.getNextCapturedScope | | | ALOAD 1 |count参照 | | GETFIELD org/jruby/runtime/ThreadContext.nil | |count+1 INVOKEVIRTUAL org/jruby/runtime/DynamicScope.getValueOneDepthZeroOrNil -+ | | LDC 1 | |count代入 INVOKEVIRTUAL org/jruby/runtime/CallSite.call ------------+ | INVOKEVIRTUAL org/jruby/runtime/DynamicScope.setValueOneDepthZero --------------+
ここまで読めば後は大体わかるでしょう。
これでNodeからJavaバイトコードへの変換が終了しました。Rubyクラスまで戻った後、runScriptメソッドで生成されたクラスのloadメソッドを呼び出すことで実行が開始されます。(ちなみに、バイトコードをダンプした場合はloadメソッドは作られません)
今回はスクリプト解析で作成したNodeをJavaバイトコードに変換する処理を見てきました。Java VMのバイトコードを生成するため、CRubyやmrubyのようなRuby実行に特化した命令コードよりもやっていることがわかりにくいと感じました。それでも一つ一つ読み解いていくことで次第に「このまとまりがメソッド呼び出しで、それが入れ子になってるのか ということがわかるようになりました。
さて、ここまででバイトコードに変換され後は実行するだけなのですが、メソッド呼び出しやらブロック呼び出しやらがどう動いてるのか全く解決していませんね。引き続きそこら辺を読んでいきたいと思います。