#contents *はじめに [#weec78da] スクリプト解析処理まで進んだのでいよいよJavaバイトコードへの変換部分を読んでいきたいと思います。 *org.jruby.Ruby [#z56c5253] **runNormallyメソッド [#ua202c8a] スクリプトの解析が終わると解析結果のNodeを引数にrunNormallyメソッドが呼び出されます。JRubyでのスクリプト実行にはコンパイル式とインタプリタ式がありますがデフォルトではJavaバイトコードにコンパイルされてから実行されるのでコンパイル式のみ見ることにします。 **tryCompileメソッド [#hcccbf4c] tryCompileメソッドはいくつかありますが順に追っていくとコンパイル処理の主要クラスはASTCompilerクラスであることがわかります。また、ASTCompiler.compileRootメソッドにはStandardASMCompilerクラスのインスタンスが渡されており、このクラスも重要な役割をしそうなことがわかります。 JRubyのRuby互換バージョンが1.9の場合はASTCompiler19クラスのインスタンスが生成され一部の処理がオーバーライドされているようです。 *org.jruby.compiler.ASTCompiler [#t8e1884f] **compileRootメソッド [#v0b7ba19] compileRootメソッドに移ります。まずStandardASMCompilerインスタンスはcompileRootメソッドにはScriptCompilerインターフェースとして渡されていることがわかります。 compileRootメソッドでは以下の処理が行われています。 +開始処理(ScriptCompiler.startScriptメソッド) +トップレベルのコードをメソッドとしてコンパイル ++BodyCompilerインターフェースの取得(実際に返されるのはorg.jruby.compiler.impl.MethodBodyCompiler) ++RootNodeの子要素に対してcompileメソッドを適用 +終了処理(ScriptCompiler.endScriptメソッド) **compileメソッド [#jbcb30fb] compileメソッドは巨大なswitch文です。Nodeの種類に応じてcompileXxxメソッドが呼ばれコンパイル処理が進行していくようです。 *org.ruby.compiler.impl.StandardASMCompiler [#a57779c0] **startScriptメソッド [#j9370ddd] クラスの生成には[[ASM>http://asm.ow2.org/]]が利用されています。startScriptメソッドを見ると生成されるクラスのスーパークラスとしてorg.jruby.ast.executable.AbstractScriptクラスが使われていることがわかります。 ***余談:staticインポート [#s169074e] ところでstartScriptメソッドを見ているとpとかsigというメソッドを見かけますがStandardASMCompilerクラスには該当メソッドは定義されていません。Eclipseの場合、メソッドにカーソルを合わせるとorg.jruby.util.Codegenのメソッドであることがわかります。どういうカラクリかというとJava 5.0から導入されたstaticインポート機能を使用しているようです。 **startFileMethodメソッド [#t0a7e94f] このメソッドではスクリプトのトップレベルに書かれているコードに対応するメソッドとして__file__メソッドの作成を開始しています(__file__メソッドを構築するためのMethodBodyCompilerが返されます)。 **endScriptメソッド [#m2323e46] このメソッドでは引数により生成するクラスにloadメソッドとmainメソッドを追加しています。loadメソッドでは__file__メソッドの呼び出しを行っています。Ruby.runScriptメソッドを見ると生成されたクラス(Scriptインターフェース)のloadメソッドを呼び出しています。これによりコンパイルしたトップレベルのコードが実行されるというカラクリのようです。 *コンパイルしてみる [#a148822c] それでは[[スクリプト解析を読む>JRuby/スクリプト解析を読む]]で構築したNodeをコンパイルしてみることにします。ちなみにjrubyに--bytecodeオプションを指定すると生成されたクラスのバイトコードが出力されます。montecarlo.rbをコンパイルした結果はこちらになります。&ref(montecarlo.bytecode.txt); **MethodBodyCompiler.beginMethodメソッド [#i8e085dd] 先ほど説明したstartFileMethodメソッド中でMethodBodyCompiler.beginMethodメソッドが呼び出されます。beginMethodメソッドは以下のようになっています。 #code(Java){{ public class MethodBodyCompiler extends RootScopedBodyCompiler { public void beginMethod(CompilerCallback args, StaticScope scope) { method.start(); variableCompiler.beginMethod(args, scope); // visit a label to start scoping for local vars in this method method.label(scopeStart); } } }} variableCompilerはスーパークラスであるBaseBodyCompilerのコンストラクタで設定されます。 #code(Java){{ public abstract class BaseBodyCompiler implements BodyCompiler { public BaseBodyCompiler(StandardASMCompiler scriptCompiler, String methodName, String rubyName, ASTInspector inspector, StaticScope scope, int scopeIndex) { this.script = scriptCompiler; this.scope = scope; this.inspector = inspector; this.methodName = methodName; this.rubyName = rubyName; this.argParamCount = getActualArgsCount(scope); method = new SkinnyMethodAdapter(script.getClassVisitor(), ACC_PUBLIC | ACC_STATIC, methodName, getSignature(), null, null); createVariableCompiler(); invocationCompiler = OptoFactory.newInvocationCompiler(this, method); this.scopeIndex = scopeIndex; } } }} 委譲されてMethodBodyCompilerに戻ってきます。 #code(Java){{ public class MethodBodyCompiler extends RootScopedBodyCompiler { protected void createVariableCompiler() { if (inspector == null) { variableCompiler = new HeapBasedVariableCompiler(this, method, scope, specificArity, StandardASMCompiler.ARGS_INDEX, getFirstTempIndex()); } else if (inspector.hasClosure() || inspector.hasScopeAwareMethods()) { variableCompiler = new HeapBasedVariableCompiler(this, method, scope, specificArity, StandardASMCompiler.ARGS_INDEX, getFirstTempIndex()); } else { variableCompiler = new StackBasedVariableCompiler(this, method, scope, specificArity, StandardASMCompiler.ARGS_INDEX, getFirstTempIndex()); } } } public abstract class BaseBodyCompiler implements BodyCompiler { protected int getFirstTempIndex() { // さわだ追記:ARGS_INDEX = 3, argParamCount = 1, FIRST_TEMP_OFFSET = 5 return StandardASMCompiler.ARGS_INDEX + argParamCount + StandardASMCompiler.FIRST_TEMP_OFFSET; } } }} トップレベルではクロージャ(ブロック)もscope awareなメソッドも使っていないためStackBasedVariableCompilerが生成されるはずです。 で、StackBasedVariableCompiler.beginMethodメソッド。 #code(Java){{ public class StackBasedVariableCompiler extends AbstractVariableCompiler { public StackBasedVariableCompiler( BaseBodyCompiler methodCompiler, SkinnyMethodAdapter method, StaticScope scope, boolean specificArity, int argsIndex, int firstTempIndex) { super(methodCompiler, method, scope, specificArity, argsIndex, firstTempIndex); this.baseVariableIndex = firstTempIndex; } public void beginMethod(CompilerCallback argsCallback, StaticScope scope) { // fill in all vars with nil so compiler is happy about future accesses if (scope.getNumberOfVariables() > 0) { // if we don't have opt args, start after args (they will be assigned later) // this is for crap like def foo(a = (b = true; 1)) which numbers b before a // FIXME: only starting after required args, since opt args may access others // and rest args conflicts with compileRoot using "0" to indicate [] signature. if (scope.getRequiredArgs() < scope.getNumberOfVariables()) { int start = scope.getRequiredArgs(); methodCompiler.loadNil(); for (int i = start; i < scope.getNumberOfVariables(); i++) { if (i + 1 < scope.getNumberOfVariables()) methodCompiler.method.dup(); assignLocalVariable(i, false); } } // temp locals must start after last real local tempVariableIndex += scope.getNumberOfVariables(); } if (argsCallback != null) { argsCallback.call(methodCompiler); } } public void assignLocalVariable(int index, boolean expr) { if (expr) { method.dup(); } method.astore(baseVariableIndex + index); } } }} あちこち飛びますがloadNilメソッドは以下の通りです。 #code(Java){{ public class MethodBodyCompiler extends RootScopedBodyCompiler { public void loadNil() { loadThreadContext(); if (Options.COMPILE_INVOKEDYNAMIC.load()) { method.invokedynamic("nil", sig(IRubyObject.class, ThreadContext.class), InvokeDynamicSupport.getContextFieldHandle()); } else { method.getfield(p(ThreadContext.class), "nil", ci(IRubyObject.class)); } } public void loadThreadContext() { method.aload(StandardASMCompiler.THREADCONTEXT_INDEX); } } }} 何をしているかというと、ローカル変数に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を設定 **ASTCompiler.compileClassメソッド [#y557aa83] さてローカル変数の設定ができたのでNodeのコンパイルに取りかかります。まず始めにあるのはClassNodeです。対応するASTCompiler.compileClassメソッドではクラスパス、スーパークラス、クラス定義本体のコールバックを作成しBodyCompiler.defineClassメソッドを呼んでいます。defineClassメソッドはBaseBodyCompilerクラスで定義されています。 defineClassメソッドはけっこう長いですがやっていることは以下になります。 +JRuby的にRubyクラスを定義(RubyModule.defineOrGetClassUnderメソッドの呼び出しまで) +クラス定義文に対応するメソッドの呼び出し(先頭で定義しているclassMethodNameの呼び出しまで) +クラス定義文に対応するメソッドのコンパイル(ClassBodyCompilerインスタンスを生成しているところ以降) **ASTCompiler.compileDefnメソッド [#ofe264c0] クラス定義本体のNodeをたどっていくとDefnNodeに行き当たります。引数、メソッド本体に対するコールバックを作成した上でBodyCompiler.defineNewMethodメソッドを呼んでいます。 defineNewMethodメソッドでは以下のことが行われています。 +JRuby的にメソッドの登録(RuntimeHelpers.defの呼び出しまで) +メソッド定義のコンパイル(BodyCompiler取得しているところ以降) **ASTCompiler19.compileArgsメソッド [#w59faf6e] メソッド定義用の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に設定 **ASTCompiler.compileCallメソッド [#d4068719] メソッド内に入って1つ目にあるのはLocalAsgnNodeですがまあこれはあまり面白くないので飛ばしてメソッド呼び出しをしているCallNoArgNodeを見てみましょう。処理はInvocationCompilerのinvokeDynamicメソッドに委譲されます。InvocationCompilerの実体は通常StandardInvocationCompilerのようです。 invokeDynamicメソッドではレシーバのコンパイル、引数のコンパイル、ブロックのコンパイルをした後、メソッド呼び出しを行っています。メソッドの呼び出しはCallSiteというクラスが鍵になっているようですがそれは後から見ることにしてブロックのコンパイルに移りましょう。 **ASTCompiler19,compileIterメソッド [#e293e94d] **ASTCompiler19.compileIterメソッド [#e293e94d] IterNodeに対応するcompileIterメソッドもASTCompiler19クラスでオーバーロードされています。そのため、ブロックのコンパイルはBodyCompiler.createNewClosure19メソッドに委譲されます。 createNewClosure19メソッドではBlockBodyクラスとブロックを関連づけた後、ブロック本体のコンパイルを行っています。 **ASTCompiler.compileIfメソッド [#rf971c6d] ブロックに入ると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メソッドを手がかりに読み解いていくと、メソッド呼び出しは +CallSiteのキャッシュ(ALOAD 0とINVOKEVIRTUAL montecarlo.getCallSiteX) +ThreadContextのロード(ALOAD 1) +selfのロード(ALOAD 2) +引数の処理 +Rubyメソッドの呼び出し(INVOKEVIRTUAL CallSite.call となっています。それを考えると、 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メソッドが使われています。最適化の一環でしょう。 **ASTCompiler.compileLocalAsgnメソッド、compileLocalVarメソッド [#vb7e9194] さて次に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メソッドは次のようになっています。 #code(Java){{ public void assignLocalVariable(int index, int depth, CompilerCallback value, boolean expr) { if (depth == 0) { assignLocalVariable(index, value, expr); } else { assignHeapLocal(value, depth, index, expr); } } }} assignHeapLocalメソッドはAbstractVariableCompilerクラスに書かれています。 #code(Java){{ protected void assignHeapLocal(CompilerCallback value, int depth, int index, boolean expr) { switch (index) { case 0: unwrapParentScopes(depth); value.call(methodCompiler); method.invokevirtual(p(DynamicScope.class), "setValueZeroDepthZero", sig(IRubyObject.class, params(IRubyObject.class))); break; ... } ... } protected void unwrapParentScopes(int depth) { // unwrap scopes to appropriate depth method.aload(methodCompiler.getDynamicScopeIndex()); while (depth > 0) { method.invokevirtual(p(DynamicScope.class), "getNextCapturedScope", sig(DynamicScope.class)); depth--; } } }} というわけで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 --------------+ ここまで読めば後は大体わかるでしょう。 *再度org.jruby.Ruby [#u7d69444] **runScriptメソッド [#i1b27b12] これでNodeからJavaバイトコードへの変換が終了しました。Rubyクラスまで戻った後、runScriptメソッドで生成されたクラスのloadメソッドを呼び出すことで実行が開始されます。(ちなみに、バイトコードをダンプした場合はloadメソッドは作られません) *おわりに [#pc77250f] 今回はスクリプト解析で作成したNodeをJavaバイトコードに変換する処理を見てきました。Java VMのバイトコードを生成するため、CRubyやmrubyのようなRuby実行に特化した命令コードよりもやっていることがわかりにくいと感じました。それでも一つ一つ読み解いていくことで次第に「このまとまりがメソッド呼び出しで、それが入れ子になってるのか ということがわかるようになりました。 さて、ここまででバイトコードに変換され後は実行するだけなのですが、メソッド呼び出しやらブロック呼び出しやらがどう動いてるのか全く解決していませんね。引き続きそこら辺を読んでいきたいと思います。