はじめに †
今回はJRubyを読みます。JRubyではRubyスクリプトをJavaVMのバイトコードに変換して実行するということができるのでそれがどのように実現されているかを見ていきたいと思います。
なお、対象としたバージョンは1.7.3です。
org.jruby.Main †
まずはmainメソッドが定義されているクラスを探します。org.jrubyパッケージの中を眺めるとそのものずばりにMainというクラスがあるので見てみるとmainメソッドおよびそこから呼び出されているコードで
- Mainインスタンスの生成
- RubyInstanceConfigインスタンスの生成
- runメソッドの実行
- 引数の解析(RubyInstanceConfigインスタンスに委譲)
- Rubyインスタンスの生成、以後の処理はRubyインスタンスに委譲
という処理が行われています。
org.jruby.RubyInstanceConfig †
RubyInstanceConfigは各種の情報を格納するクラスです。RubyInstanceConfigでは以下のようにVMの実行に関する情報と実行するスクリプトに関する情報を保持しています。
- VMに関するもの
- 互換バージョン
- 実行方法(コンパイルするのかインタープリトするのか)など
- スクリプトに関するもの
- スクリプトの入力元(ファイルかコマンドライン引数か)
- スクリプトへの引数など
org.jruby.Ruby †
initメソッド †
Rubyインスタンスが生成される際、initメソッドが呼ばれます。initメソッドではRubyのクラスヒエラルキーの構築が行われます。
initRootメソッド †
initRootメソッドではまずRubyソース愛読者にはおなじみのObject-Module-Classのクラス階層構築が行われています。なお、RubyのクラスはRubyObjectのように「Ruby + <Rubyのクラス名>」という命名規則になっているようです。
その後、Kernelモジュール、top self、Nilクラス、Falseクラス、Trueクラスが構築されています。
bootstrapメソッド †
bootstrapメソッドはinitCoreメソッドとinitExceptionsメソッドを呼び出しています。それぞれ、組み込みクラス・モジュールの生成と組み込み例外クラスの生成が行われています。
initBuiltinsメソッド †
initBuiltinsメソッドではビルトインライブラリの登録が行われています。
1
|
| addLazyBuiltin("java.rb", "java", "org.jruby.javasupport.Java");
|
見た感じ、java.rbをrequireするとorg.jruby.javasupport.Javaが読み込まれそうな雰囲気です。実際、initメソッドに戻って先を見てみるとリフレクションが使える場合、javaをrequireしています。つまり、RubyのクラスをJavaで定義することができるようです。ここら辺はCRubyの拡張ライブラリみたいな感じですね。JRubyの場合、org.jruby.runtime.load.Libraryインターフェースを実装してloadメソッドを作成するという拡張ライブラリ規約のようです。
initRubyKernelメソッド †
initRubyKernelメソッドではKernelモジュールにメソッドを定義しています。どうやらこのメソッドではKernelモジュールの全メソッドを定義するわけではなく、一部のメソッドだけ定義しているようです。おもしろいのは読み込むrbファイルがjarの中に含まれていてそれを読み込んでいるというところでしょう。(前述のjavaライブラリでも一部の機能はRubyで書かれているようです)
メソッドの定義 †
ところでKernelモジュールを構築しているRubyKernel.createKernelModuleメソッドを見るとKernelモジュールのメソッド群を定義しているコードが見当たりません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| -
|
|
|
|
|
|
|
-
|
-
|
!
!
|
|
|
|
|
|
!
| public static RubyModule createKernelModule(Ruby runtime) {
RubyModule module = runtime.defineModule("Kernel");
runtime.setKernel(module);
module.defineAnnotatedMethods(RubyKernel.class);
module.setFlag(RubyObject.USER7_F, false);
runtime.setPrivateMethodMissing(new MethodMissingMethod(module) {
@Override
public IRubyObject methodMissing(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args, Block block) {
return RubyKernel.methodMissing(context, self, name, PRIVATE, CallType.NORMAL, args, block);
}
});
(中略)
recacheBuiltinMethods(runtime);
return module;
}
|
それっぽいのはdefineAnnotateMethodsメソッドを呼んでいるところぐらいです。私も始めわからなかったのですがコードを読んでいると、
1
2
3
4
|
-
|
!
| @JRubyMethod(name = "p", rest = true, module = true, visibility = PRIVATE)
public static IRubyObject p(ThreadContext context, IRubyObject recv, IRubyObject[] args) {
(省略)
}
|
のように書いてあるのでどうやらアノテーションでRubyのメソッド情報を記述しておいてその情報を用いてRubyのメソッドを定義しているらしいということがわかりました。
というわけで、
org.jruby.RubyModule.defineAnnotateMethods
→ org.jruby.RubyModule.defineAnnotatedMethodsIndividually
→ org.jruby.anno.TypePopulator.DefaultTypePopulator.populate
とコードを追っていくと、
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
46
47
48
49
50
51
52
| -
-
!
|
|
|
|
|
-
-
|
|
-
-
-
!
-
!
!
!
!
|
-
|
-
|
!
!
|
-
|
-
|
!
!
|
-
|
-
|
!
!
|
-
|
-
|
!
!
|
|
!
| public void populate(RubyModule clsmod, Class clazz) {
MethodFactory methodFactory = MethodFactory.createFactory(clsmod.getRuntime().getJRubyClassLoader());
Ruby runtime = clsmod.getRuntime();
RubyModule.MethodClumper clumper = new RubyModule.MethodClumper();
clumper.clump(clazz);
for (Map.Entry> entry : clumper.getAllAnnotatedMethods().entrySet()) {
for (JavaMethodDescriptor desc : entry.getValue()) {
JRubyMethod anno = desc.anno;
if (anno.frame() || (anno.reads() != null && anno.reads().length >= 1) || (anno.writes() != null && anno.writes().length >= 1)) {
ASTInspector.addFrameAwareMethods(anno.name());
ASTInspector.addScopeAwareMethods(anno.name());
}
}
}
for (Map.Entry> entry : clumper.getStaticAnnotatedMethods().entrySet()) {
clsmod.defineAnnotatedMethod(entry.getKey(), entry.getValue(), methodFactory);
for (JavaMethodDescriptor desc : entry.getValue()) {
if (!desc.anno.omit()) runtime.addBoundMethod(desc.declaringClassName, desc.name, entry.getKey());
}
}
for (Map.Entry> entry : clumper.getAnnotatedMethods().entrySet()) {
clsmod.defineAnnotatedMethod(entry.getKey(), entry.getValue(), methodFactory);
for (JavaMethodDescriptor desc : entry.getValue()) {
if (!desc.anno.omit()) runtime.addBoundMethod(desc.declaringClassName, desc.name, entry.getKey());
}
}
for (Map.Entry> entry : clumper.getStaticAnnotatedMethods1_8().entrySet()) {
clsmod.defineAnnotatedMethod(entry.getKey(), entry.getValue(), methodFactory);
for (JavaMethodDescriptor desc : entry.getValue()) {
if (!desc.anno.omit()) runtime.addBoundMethod(desc.declaringClassName, desc.name, entry.getKey());
}
}
for (Map.Entry> entry : clumper.getAnnotatedMethods1_8().entrySet()) {
clsmod.defineAnnotatedMethod(entry.getKey(), entry.getValue(), methodFactory);
for (JavaMethodDescriptor desc : entry.getValue()) {
if (!desc.anno.omit()) runtime.addBoundMethod(desc.declaringClassName, desc.name, entry.getKey());
}
}
(1.9と2.0用メソッドについて同様)
}
|
とアノテーション情報を収集してメソッドを定義している様子が見られます。ちなみにアノテーション情報以外にjava.lang.reflect.Methodインスタンスから取れる情報も利用してインスタンスメソッドなのかクラスメソッドなのか、引数の数などの情報を設定しているようです。その辺りについてはorg.jruby.anno.JavaMethodDescriptorクラスのコンストラクタを参照してください。
RubyModule.defineAnnotatedMethodメソッドに進みます。
1
2
3
4
5
6
7
8
9
10
11
| -
|
-
|
-
|
|
|
|
!
!
| public boolean defineAnnotatedMethod(String name, List methods, MethodFactory methodFactory) {
JavaMethodDescriptor desc = methods.get(0);
if (methods.size() == 1) {
return defineAnnotatedMethod(desc, methodFactory);
} else {
DynamicMethod dynamicMethod = methodFactory.getAnnotatedMethod(this, methods);
define(this, desc, dynamicMethod);
return true;
}
}
|
メソッド数が1つか複数かで処理が分かれています。メソッド数が複数になる場合というのはどういう場合かというと、
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
| -
|
|
-
|
!
|
-
-
|
|
!
|
!
|
|
-
|
!
|
-
|
|
|
!
|
!
| public class RubyArray extends RubyObject implements List, RandomAccess {
...
@JRubyMethod(name = {"[]", "slice"}, compat = RUBY1_9)
public IRubyObject aref19(IRubyObject arg0) {
return arg0 instanceof RubyFixnum ? entry(((RubyFixnum)arg0).getLongValue()) : arefCommon(arg0);
}
private IRubyObject arefCommon(IRubyObject arg0) {
if (arg0 instanceof RubyRange) {
long[] beglen = ((RubyRange) arg0).begLen(realLength, 0);
return beglen == null ? getRuntime().getNil() : subseq(beglen[0], beglen[1]);
}
return entry(RubyNumeric.num2long(arg0));
}
@JRubyMethod(name = {"[]", "slice"}, compat = RUBY1_9)
public IRubyObject aref19(IRubyObject arg0, IRubyObject arg1) {
return arefCommon(arg0, arg1);
}
private IRubyObject arefCommon(IRubyObject arg0, IRubyObject arg1) {
long beg = RubyNumeric.num2long(arg0);
if (beg < 0) beg += realLength;
return subseq(beg, RubyNumeric.num2long(arg1));
}
...
}
|
みたいな場合です。上の[]はself[nth]とself[range]、下の[]はself[start, length]です。つまり、メソッド実装内で引数分けを書かないで引数ごとにメソッド実装を用意するということができるようです。まあ、オブジェクト指向のオーバーロード思想的には正しいですね。
MethodFactoryは抽象クラスでその実体はデフォルトではInvocationMethodFactoryです。というわけでInvocationMethodFactory.getAnnotatedMethodメソッドに進むと以下のようにAnnotatedMethodClassというものを取得しています。
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
| -
|
|
|
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-
|
|
!
!
| public DynamicMethod getAnnotatedMethod(RubyModule implementationClass, List descs) {
JavaMethodDescriptor desc1 = descs.get(0);
String javaMethodName = desc1.name;
try {
Class c = getAnnotatedMethodClass(descs);
DescriptorInfo info = new DescriptorInfo(descs);
JavaMethod ic = (JavaMethod)c.getConstructor(new Class[]{RubyModule.class, Visibility.class}).newInstance(new Object[]{implementationClass, desc1.anno.visibility()});
TypePopulator.populateMethod(
ic,
Arity.optional().getValue(),
javaMethodName,
desc1.isStatic,
CallConfiguration.getCallConfig(info.isFrame(), info.isScope()),
desc1.anno.notImplemented(),
desc1.getDeclaringClass(),
desc1.name,
desc1.getReturnClass(),
desc1.getParameterClasses());
return ic;
} catch(Exception e) {
e.printStackTrace();
throw implementationClass.getRuntime().newLoadError(e.getMessage());
}
}
|
getAnnotatedMethodClassメソッドに進むと非常に面白いことがわかります。なんと、Rubyメソッド実装を呼び出すためのクラスを動的に生成しているのです。その様子は、addAnnotatedMethodInvokerメソッド→createAnnotatedMethodInvocationメソッドと眺めるとわかります。callメソッドにて、レシーバ、引数を積んだ上でRubyメソッドを実装するJavaメソッドを呼び出すという処理を生成しているようです。
ところでメソッド数が1つの場合でも複数の場合でも結局同じような処理をしているように見えるのですが私の理解が足りないのでしょうかね。
おわりに †
今回はJRubyのうち、初期化の部分を読みました。その結果、以下のことがわかりました。
- Rubyメソッド実装はアノテーションで定義
- 引数の数が異なる場合、メソッド実装をオーバーロードすることが可能
- アノテーションで定義されたメソッド実装の呼び出しはクラスを動的に生成することで対応
実はまだメソッド実装がオーバーロードされている場合にどう動くのかわかっていませんがそれはメソッド呼び出しを読んでいくとわかるでしょう。
というわけで次はスクリプト解析→実行の部分に進みます。