この記事はOpal Advent Calendar 2016の23日目の投稿として書いています。
今日はNativeの実装について書こうと思います。
その前にNativeのつかいかたです。
require 'native'
%x{
var foo = new Object();
foo.bar = function() {
return 'bar';
};
foo.baz = 'baz';
}
n = Native(`foo`)
puts n.bar
puts n.baz
こんなプログラムを実行してみましょう。n.barもn.bazもそれぞれ文字列を返します。
Rubyのこれらの呼び出しはすべてメソッド呼び出しになります。
一方、JavaScriptではプロパティに関数だったり、文字列だったりをアサインすることができます。
Nativeは関数であれば関数を実行した結果を返すし、文字列などの値であればその値を返すようになっています。
それでは、これらがどのように実装されているか見てみましょう。
Nativeはstdlib/native.rbに定義されています。
まずはNative()の定義をみてみます。
def Native(obj)
if `#{obj} == null`
nil
elsif native?(obj)
Native::Object.new(obj)
else
obj
end
end
nativeであればNative::Objectを返します。native?はどのように判定しているのでしょうか?
def native?(value)
`value == null || !value.$$class`
end
nullの場合もしくは、$$classが定義されていない場合はnativeであると判定しています。
$$classはRubyのクラスを表す変数です。この定義があればRubyのクラスから生成されたオブジェクトと言えます。
Native::Objectのメソッド呼び出しはmethod_missingでnativeの呼び出しに変換されます。
def method_missing(mid, *args, &block)
%x{
if (mid.charAt(mid.length - 1) === '=') {
return #{self[mid.slice(0, mid.length - 1)] = args[0]};
}
else {
return #{::Native.call(@native, mid, *args, &block)};
}
}
end
メソッド名の最後が=だったらプロパティへの代入になります。
Native::Object#[]はJSのプロパティを返します。
def [](key)
%x{
var prop = #@native[key];
if (prop instanceof Function) {
return prop;
}
else {
return #{::Native.call(@native, key)}
}
}
end
メソッド名が=で終らない場合は、Native#callが呼ばれます。
def self.call(obj, key, *args, &block)
%x{
var prop = #{obj}[#{key}];
if (prop instanceof Function) {
var converted = new Array(args.length);
for (var i = 0, length = args.length; i < length; i++) {
var item = args[i],
conv = #{try_convert(`item`)};
converted[i] = conv === nil ? item : conv;
}
if (block !== nil) {
converted.push(block);
}
return #{Native(`prop.apply(#{obj}, converted)`)};
}
else {
return #{Native(`prop`)};
}
}
end
Native#callの中身はプロパティが関数の場合は引数をnativeに変換して関数を呼び出します。
関数の場合は関数の呼び出し結果、それ以外ならプロパティそのものをNative::Objectでラップして返します。
引数の変換はto_nメソッドでおこなわれます。
def self.try_convert(value)
%x{
if (#{native?(value)}) {
return #{value};
}
else if (#{value.respond_to? :to_n}) {
return #{value.to_n};
}
else {
return nil;
}
}
end
to_nメソッドはnative.rbのなかでいくつか定義されています。
たとえば、Numericなら
class Numeric
def to_n
`self.valueOf()`
end
end
valueOf()でプリミティブを返すようになっています。
native.rbにはほかにもさまざまなクラスにto_nの定義をしています。Array``Hashなどもあります。
もちろん、Native::Object自身もto_nメソッドをもちます。
しかし、to_nが定義されていないクラスに関しては、nativeへの変換は行なわれませんので注意が必要です。
逆にto_nを定義すればnativeのメソッドに引数として渡すことができるようになります。
Nativeの実装についてみてきましたがいかがでしょうか。次回はJSの実装を見てみましょう。
では、Happy hacking!