更新 2005/2/28
Ruby という言葉は既存の ruby インタプリタの仕様もしくは 既存の Ruby の仕様を差します。
ClassName#method_name は ClassName クラスのインスタンスメソッド method_name を指し、
ClassName.method_name は ClassName クラスのクラスメソッド method_name を指します。
以下の説明においては Ruby.NET コンパイラが実際に
作成する IL コードを根本的な動作に変わりがない程度で簡略化して、C# で表現しています。
.NET の中間言語であるMSIL(以下IL)は Java のバイトコードと同じように オブジェクト思考なアセンブラです。C#のコードとほぼ一対一の対応が可能です。 例えば以下のようなC#コードは
namespace Test
{
class Class1
{
[STAThread]
static void Main(string[] args)
{
}
void test()
{
}
}
}
以下のようなILコードにコンパイルされます。
.namespace Test
{
.class private auto ansi beforefieldinit Class1
extends [mscorlib]System.Object
{
.method private hidebysig static void
Main(string[] args) cil managed
{
.entrypoint
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
.maxstack 0
IL_0000: nop
IL_0001: ret
}
.method private hidebysig instance void
test() cil managed
{
.maxstack 0
IL_0000: nop
IL_0001: ret
}
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
.maxstack 1
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
}
}
}
このようにC#と対応が可能ですので以下の説明ではILで記述する変わりにC#のコードを説明に用います。class Test def test0 end def Test.test1 end endRuby.NETコンパイラではこれを次のようにコンパイルします。
class Test
{
IObject test0(IObject[] args, IObject proc)
{
return Object.nil_;
}
}
class cTest
{
IObject test1(IObject[] args, IObject proc)
{
return Object.nil_;
}
}
すべてのメソッドのシグニチャは同じ形をしており、 IObject[] と IObject を引数に取り IObject を返します。class Test def test0 end def Test.test1 end end t = Test.new t.test0 # Test#test0 が呼ばれる Test.test1 # Test.test1 が呼ばれる t = Test t.test1 # Test.test1 が呼ばれるこのコードは以下のようにコンパイルされます。
class Test {}
class cTest {}
IObject t = new Test();
IObject Test = new cTest();
t.test0(EmptyArgs, null);
Test.test1(EmptyArgs, null);
t = Test
t.Test1(EmptyArgs, null);
Rubyではクラスを変数に代入することが可能です。
よって Ruby.NET コンパイラでは Ruby のクラスが2つのクラスにコンパイルされます。
上の例ではC#でいうインスタンスメソッドはTestに、C#でいうスタティックメソッド(クラスメソッド)は
cTest に定義され、Test という名前の変数に cTest のインスタンスが代入されます。
cTest のインスタンスはプログラム中にただ1つしか存在しません。
class Test
def test0
return true
end
end
class Test
def test0
return false
end
end
class Test
def test1
return true
end
end
以上のコードは Ruby.NET コンパイラでは次のようにコンパイルされます。
class Test
{
IObject test0(IObject[] args, IObject proc)
{
return Object.false_;
}
IObject test1(IObject[] args, IObject proc)
{
return Object.true_;
}
}
class cTest
{
}
Ruby ではクラスを途中で再定義することが可能ですが、
Ruby.NET コンパイラではクラスの最終状態をそのクラスの定義としています。
class Test
def test0
true
end
end
t = Test.new
puts t.test0 # => true
class Test
def test0
false
end
end
t = Test.new
puts t.test0 # => false
Ruby では以上のコードでは test0 の結果がクラスの再定義により変わりますが、
Ruby.NET コンパイラではどちらも false を表示します。
コンパイラの複雑さの軽減とプログラムの高速化のためにこのような実装にしました。
今後 Ruby と同じ結果になるように変更するかもしれません。
class Array
def size
10
end
end
a = Array.new
puts a.size # => 10
Ruby.NET コンパイラではライブラリで定義されているすべてのクラスにラッパクラスが作られます。
上のコードでは Array のラッパクラスにおいて size メソッドが再定義されることになります。
class Fixnum
{
IObject to_s(IObject[] args, IObject proc)
{
return new String( value );
}
}
ライブラリで定義されているクラス Fixnum に to_s というメソッドがあるとして、
このメソッドは同じくライブラリで定義されている String クラスのインスタンスを返すとします。
Ruby のプログラムで次のように String クラスを再定義します。
class String
def length
10
end
end
t = 1 # IObject t = new Fxinum(1);
s = t.to_s # IObject s = t.to_s(EmptyArgs, null);
このとき、ライブラリで定義されている Fixnum#to_s は再定義された新しい String
クラスのインスタンスを返さなければなりません。
class Fixnum
{
IObject to_s(IObject[] args, IObject proc)
{
return main_.StringFactory( value );
}
}
StringFactory は仮想関数として定義されます。
新しい String を返す StringFactory が定義されたクラスがコンパイラにより作られ、
スタティック変数 main_ にそのインスタンスが代入されています。
このため、常に main_.StringFactory() の呼び出しにより新しい String のインスタンスが返されます。
Ruby において呼び出されるメソッドは実行時に決定されます。 例えば以下のようなコードにおいて、実行時に cond の真偽によって t には Test1 のインスタンスか Test2 のインスタンスが代入されます。このため t.test の呼び出しで Test1#test が呼び出されるか Test2#test が呼び出されるかコンパイル時には判断できません。
class Test1
def test
true
end
end
class Test2
def test
false
end
end
if cond
t = Test1.new
else
t = Test2.new
end
t.test # => Test1#test or Test2#test
メソッドディスパッチの実現するために、Ruby.NET コンパイラでは、
仮想関数 ___send を用いて実行時に正しい関数を呼び出します。
interface IObject
{
IObject ___send( string name, Object[] args ) { }
}
class C : IObject
{
virtual IObject ___send( string name, Object[] args )
{
if( name == “hoge” )
return this.hoge (args);
}
virtual IObject hoge(Object[] args) {}
}
Object t = null;
if( cond )
t = new C();
else
t = new D();
t.___send (“hoge”, null );
このように、すべてのクラスは interface IObject の仮想関数 ___send を実装し、
___send に呼び出すメソッド名と引数を渡すことで目的のメソッドを呼び出します。
___send は仮想関数ですので、常に現在のオブジェクトの ___send が呼ばれ、
各オブジェクトの ___send メソッドは自分のオブジェクトの全メソッドを把握していますので、
___send メソッドはどのメソッドを呼ぶべきか判断できます。
interface IObject
{
IObject ___send(int mid, string methodName, IObject[] args, IObject proc);
}
class Object : IObject
{
virtual IObject ___send(int mid, string methodName, IObject[] args, IObject proc)
{
if( has_singleton_method? ) {
Method method = getSingletonMethod(methodName);
if( method != null )
return method.call(args, proc);
}
if( mid > 50 ) {
if( mid > 75 ) {
if( mid == 80 )
return this.hoge(args, proc);
if( mid == 90 )
return this.test1(args, proc);
}
....
}
.......
}
}
この実装は、ハッシュテーブルを使うのに比べ非常に高速に動作します。
has_singleton_method? は 特異メソッドの有無を調べています。
Ruby にはインスタンス固有のメソッドである特異メソッドという物があります。
class Test
def test0
1
end
end
t = Test.new
t.test0 # => 1
def t.test1
2
end
t.test1 # => 2
def t.test0
3
end
t.test0 # => 3
この機能を実現するため、Ruby.NETコンパイラでは次のようなRubyコードに対して以下のようなコードを生成します。
class Test end t = Test.new def t.test puts "hello" end
class Test
{
}
class Singleton_test : Method
{
public Singleton_test(IObject self)
{
self_ = self;
}
override IObject call( IObject[] args, IObject proc )
{
self_.puts( "hello" );
}
}
IObject t = new Test();
t.addSingletonMethod( "test", new Singleton_test(this) );
特異メソッドは関数オブジェクトとして扱われ、
各インスタンスの特異メソッドテーブルに追加されます。
特異メソッドテーブルはハッシュテーブルを用いて実装されています。Ruby にはブロック付きメソッド呼び出しという機能があります。
def test()
yield
end
test() {
puts "hello"
}
{ puts "hello" } の部分がメソッド test に渡され、test 内で yield を呼び出すことにより、
test は渡されたブロックを実行します。
virtual IObject test(IObject[] args, IObject proc)
{
proc.yield( Object.EmptyArgs, null);
}
class Proc_001 : Proc
{
override IObject yield(IObject[] args, IObject proc)
{
self_.puts( "hello" );
}
}
test( EmptyArgs, new Proc_001(this) );
すべてのメソッドは IObject methodName(IObject[] args, IObject proc) という形をしており、
この二番目の引数 proc としてブロックがオブジェクト化され渡されます。
class Test
def save &proc
@proc = proc
end
def call
@proc.yeild
end
end
a = 1
t = Test.new
t.save {
puts a
}
a = 2
t.call # => 2
このプログラムでメソッド save に渡されるブロック内でロール変数 a を参照しています。
ここでこのローカル変数はブロックの外で a = 1 として定義される a と同じ物を参照します。
save にブロックを渡したとで a = 2 とこのローカル変数の値を変更しています。
この後 t.call と先ほど save に渡されたブロックを実行すると、puts a が実行され、
2 が表示されます。
a = 1
t = Test.new
t.save {
puts a
}
a = 2
これは以下のようにコンパイルされます。
IObject[] locals = new IObject[1]; a[0] = new Fixnum(1); t.save( EmptyArgs, new Proc_001(this, locals) ); a[0] = new Fixnum(2);ブロックの中で参照されているローカル変数だけがヒープに割り付けられ、その他のローカル変数は、 通常通りローカル変数としてスタックに割り付けられます。
a0 = 1
a1 = 2
1.times {
b0 = 3
b1 = 4
1.times {
a1 = 5
b1 = 6
c0 = 7
}
}
上のようにブロックがネストしている場合も同様に、それより内側のブロックで参照されている
a1、b1 はヒープに割り付けられ、その他はスタックに割り付けられます。
このとき一番内側のブロックのコンストラクタは Proc_002(this, locals0, locals1) となります。
locals0 は aX のレベルのローカル変数が割り付けられるヒープ、locals1 は bX のレベルのローカル変数が割り付けられるヒープへのポインタです。
Ruby には C# でいうスタティック関数(クラス関数)は存在しません。 一見組み込み関数はスタティック関数のように見えますが、 実際には Object (がincludeしているKernel)に定義されているインスタンスメソッドです。
class Test
def test
puts "hello"
end
end
上のコードの Test#test の中で呼ばれている puts は組み込み関数と呼ばれるメソッドですが、
これは Test クラスが暗黙に継承している Object クラスに定義されているインスタンスメソッドです。
def puts( arg )
print "puts start\n"
print arg + "\n"
pritn "puts end\n"
end
class Test
def test
puts "hello"
end
end
上のコードでは、組み込み関数 puts が再定義されています。
ところで、Test#test の中で呼ばれている puts は Test クラスのベースクラスである Object
に定義されているインスタンスメソッドでした。
上のコードで、Test#test 内の puts の呼び出しでは再定義された puts が呼び出されます。
class Object : RubyLibrary.Object
{
override IObject puts(IObject[] args, IObjec proc)
{
print(...);
print(...);
print(...);
return Object.nil_;
}
}
class Test : Object
{
virtual IObject test(IObject[] args, IObject proc)
{
puts( new IObject[] {"hello"}, null );
return Object.nil_;
}
}
クラスの再定義の項目で説明したように、ライブラリで定義されているすべてのクラスに対して、
Ruby.NET コンパイラはラッパクラスを作成します。
上のコードで、class Object : RubyLibrary.Object は ライブラリの Object クラスに対するラッパクラスです。
この中で、Object のメソッド puts を再定義しています。
Test クラスでは、このラッパクラス Object を継承しているので、
Test#test 内の puts の呼び出しでは、再定義された puts を呼び出すことになります。
def puts( arg )
print "puts start\n"
print arg + "\n"
pritn "puts end\n"
end
class Array
def new_method
puts "hello"
end
end
Array#new_method 内の puts は先ほどと同様に Object クラスで定義されている puts を呼び出します。
Ruby.NET コンパイラは Array クラスをライブラリで定義されている Array
クラスのラッパクラスとして以下のように生成します。
class Array : RubyLibrary.Array
{
virtual IObject new_method(IObject[] args, IObject proc)
{
puts( new IObject[] { "hello" }, null );
return Object.nil_;
}
}
ここで puts は Array のベースクラス RubyLibary.Array より親に定義されたメソッドということになります。
ライブラリの中では以下のような継承関係があります。
namespace RubyLibrary
{
interface IObject {}
class Object : IObject {}
class Array : Object {}
}
結局 Array#new_method 内の puts 呼び出しは、Array のベースクラス RubyLibary.Array のベースクラス RubyLibary.Object
で定義されているインスタンスメソッドが呼び出されることになります。
しかし、元の Ruby コードで puts が再定義されているので、Array#new_method 内の puts 呼び出しは、
ライブラリで定義された puts ではなく再定義された puts が呼ばれなくてはなりません。
Test クラスの例では、Test クラスが RubyLibrary.Object のラッパクラス Object を継承することで
再定義された puts を呼び出すことが可能でしたが、Array の場合は ラッパクラス Object を継承していません。
class Array : RubyLibrary.Array
{
override IObject puts(IObject[] args, IObjec proc)
{
print(...);
print(...);
print(...);
return Object.nil_;
}
virtual IObject new_method(IObject[] args, IObject proc)
{
puts( new IObject[] { "hello" }, null );
return Object.nil_;
}
}
つまり、puts の実装コードが、ラッパクラス Object だけでなく、ラッパクラス Array にもコピーされます。
このようにトップレベルで定義されたメソッド(再定義された組み込み関数を含む)は、
ライブラリで定義されているすべてのクラスのラッパクラス内にコピーされます。
Ruby.NET コンパイラでは Ruby から .NET のクラスのインスタンスを作成したり、そのメソッドを呼び出すことが可能です。 以下のように自然な形で .NET クラスライブラリのクラスを使用できます。
require 'System' Console = System.Console.type # static メソッド呼び出しのために type オブジェクトを作成 Console.WriteLine(“Hello, world”) # static メソッドの呼び出し a = System.Collections.ArrayList.new # インスタンスの作成 a.Add( “Ruby.NET” ) # インスタンスメソッドの呼び出し a.Add( “C#” ) puts a.Count # プロパティの参照このとき、Ruby の String は .NET の System.String に、Ruby の Fixnum は .NET の System.Int32 に、 Ruby の Float は .NET の System.Double に、Ruby の true 及び false は .NET の true 及び false に、 Ruby の nil は .NET の null に相互に自動変換されます。
type = ", System.Windows.Forms, Version=1.0.5000.0,Culture=neutral, PublicKeyToken=b77a5c561934e089"
Point = "System.Drawing.Point, System.Drawing, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
Font = "System.Drawing.Font, System.Drawing, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
Label = "System.Windows.Forms.Label" + type
Form = "System.Windows.Forms.Form" + type
Button = "System.Windows.Forms.Button" + type
form1 = DotNet.new(Form)
label1 = DotNet.new(Label)
label1.Text = ""
label1.Location = DotNet.new(Point, 40, 30);
font = DotNet.new(Font, "Arial", 22)
label1.Font = font
label1.AutoSize = true
button1 = DotNet.new(Button)
button1.Text = "OK"
button1.Location = DotNet.new(Point, 90, 90);
button1.add_Click { label1.Text = "Hello, Ruby.NET" }
form1.Text = "未踏ユースデモ"
fsp = DotNet.type("System.Windows.Forms.FormStartPosition" + type )
cs = fsp.get_field("CenterScreen")
form1.AcceptButton = button1
form1.StartPosition = cs
form1.Controls.Add(label1)
form1.Controls.Add(button1)
form1.ShowDialog()
クラスの再定義で述べた点の他に、既存の Ruby と Ruby.NET では異なる点があります。
"あいうえお"
'あい\nうえお'
%q(あい\nうえお)
%Q(あいう\nえお)
a = [ 1, 2, [3, 4]]
h = { "one" => 1, "two" => 2 }
1...20
1..19
:symbol
/pattern/im
%r{pattern}
以下の変数が使用できます。
Ruby と完全に同じです。多重代入も可能です。
a, (b, c) = 1, [2, 3]
Ruby の予約語の他に Ruby.NET では include と require も予約語です。
defined? が未実装であること以外、優先順位や評価順等 Ruby と同じです。
a[0] = 1
2 ** 4
~123
123 << 2
1 <=> 3
1..4
1 > 3 ? true : false
a *= 4
if a == 3
...
else
...
end
a = 4 unless b != 4
while i < 100
...
break if c == 11
end
for i in [1,2,3,4] # スコープルールも同じです
puts i
end
r = case e
when 0..3
"ok"
else
"no"
end
until e != 3
...
if d <= 4
redo
elsif d > 6
next # redo は未実装
end
end
次のようなコードの時、Rubyでは Class3 のベースクラスの指定に、クラスを表す物なら なんでも指定できますが、Ruby.NET では、コンパイル時にそのクラスが完全に特定できる形、 つまり :: で区切られれるクラスのパスしか指定できません。 Ruby.NET では、Ruby のクラスを .NET のクラスとして表現しているため、 コンパイル時にベースクラスが特定できる必要があるためです。
class Class1 class Class2 end end class Class3 < Class1::Class2 endまた次のようなコードは Ruby.NET で有効ですが、 Class3 を定義するスコープがコンパイル時に判断できず、 Class3 はトップレベルに定義されます。 従って、Class3 を継承する場合には Class1::Class3 とは書けず、 Class3 もしくは ::Class3 と記述する必要があります。
class Class1 class Class2 end end t = Class1 class t::Class3 < Class1::Class2 endinclude ににおいても、include するModuleがコンパイル時に完全に特定できる形、 つまり :: 区切りのパスを指定する必要があります。 include は Module のソースをその位置に貼り付けるような形で実装されています。 また、requrie に指定できるものも、 コンパイル時に決定できる名前である必要があります。 require もその位置にファイルを貼り付けます。 include や require はどのコントロールパスを通ろうと、コンパイル時に必ず実行されます。
Ruby.NET コンパイラでは eval 等、実行時にパースが必要がである機能は使用できません。 このほかにも、実行時にコンテキストをオブジェクト化するなどの動的な機能が使用できません。 クラスの再定義についての制限は module の使用においても同様に制限があります。 例外をキャッチする begin ... end は値を返せません。 例外をキャッチしなければ(raise のみ)Ruby と同様です。
多倍長演算ができません。
メソッドのアクセス制御 private, public 等が指定できません。
alias, undef が使えません。
ヒアドキュメントが使えません。
標準ライブラリが半分ほどしか実装されていません。
これらは、いずれ実装する予定です。
retry が使用できません。これを実現するのは難しそうです。
Ruby.NET コンパイラの開発では、コンパイラの中心部分の開発に時間を割いたので、 高速化には力を入れていませんが、Ruby と同等以上の速度を達成するためにいくつかの最適化を行いました。
Ruby.NET コンパイラでは、Fixnum はオブジェクト型のため、 整数演算の度にメソッド呼び出しのコストが発生します。
t = 1 + 1例えば上のようなコードに対して、Ruby.NET コンパイラは下のようなコードを生成します。
IObject t = (new Fixnum(1)).___send( 123, "+", new IObject[] { new Fixnum(1) }, null );
メソッドテーブルを検索するコストや、___send メソッドを呼び出すコストをさけるために、
特異メソッドとして演算子が再定義されていない場合、以下のようなコードを生成します。
IObject t = (new Fixnum(1)).___op_Add(new IObject[] { new Fixnum(1) }, null );
ここで ___op_Add は interface IObject に宣言されている仮想関数です。
すべてのオブジェクトはメソッド ___op_Add を持つので、___send を介さず直接 ___op_Add を呼び出すことが可能です。
他の演算子も同様に interface IObject に仮想関数として宣言されています。
IObject t0 = new Fixnum(1);
IObject t1 = new Fixnum(2);
IObject t3;
if( t0 is Fixnum && t1 is Fixnum )
t3 = new Fixnum( t0.value_ + t1.value_ );
else
t3 = t0.___op_Add( new IObject[] { t1 }, null );
if( t0 is Fixnum && t1 is Fixnum ) のようなチェックはすべての2項演算に対して行われるので、
実際に t0 や t1 が Fixnum 型でない場合が多いと、それがオーバーヘッドになります。
while i < 1000 i = i + 1 endというプログラムでは、1000という整数オブジェクトが比較の度に生成され、 1という整数オブジェクトが繰り返しの度に作られ、 i+1の結果の整数オブジェクトが足し算の度に作られます。 これにより上記プログラムでは、new を 3000 回行うことになり、 これにかなりの時間がかかっています。
public static Fixnum GetFixnum(int val)
{
if( val == -1 || val == 0 || val == 1 )
return (Fixnum)fixnumPool_[val+1];
int pos = val % 253 + 3;
if( pos < 0 ) pos = -pos;
Fixnum f = (Fixnum)fixnumPool_[pos];
if( f != null && f.value_ == val )
return (Fixnum)fixnumPool_[pos];
f = main_.___fixnumFactory(val);
fixnumPool_[pos] = f;
return f;
}
先ほどの Ruby コードで以下のような最適化を行わなかったのは
tmp1 = new Finxum(1000); tmp2 = new Finxum(1); while i < tmp1 i = i + tmp2 end先ほどのGetFixnumの場合、以下のようなコードでもオブジェクトの生成回数を減らせるためです。
def get_one return 1 end while i < 1000 i = i + get_one end
method 1,2,3上のコードから Ruby.NET コンパイラでは以下ようなコードを生成します。
this.___send( mid, "method", new IObject[] { new Fixnum(1), new Fixnum(2), new Fixnum(3) }, null );
つまりメソッド呼び出しの度に要素数3の配列を作成しています。
これは無駄なので、要素数が0から10間での配列をあらかじめ作成しておき、
引数の数に応じてその配列を使用するようにしました。引数の数が11より多い場合はあらたに作成します。
ruby Ruby.NET 倍率
空のメソッド呼び出し 0.68 0.016 x42
ブロック付きメソッド呼び出し+yield 1.54 0.23 x6.7
整数の足し算 0.59 0.29 x2.0
while 0.60 0.56(0.14) x1.1(x4.3)
フィボナッチ数列 2.72 0.81 x3.3
特異メソッド呼び出し 0.68 0.23 x2.9
tak 20.5 3.36 x6.1
tarai 17.5 0.031 x650
matrix 4.93 3.31 x1.5
( ) は --fixnum-inline を有効にした場合