Posted at
OpalDay 23

OpalのNativeはどのように実装されているのか

More than 1 year has passed since last update.

この記事は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.barn.bazもそれぞれ文字列を返します。

Rubyのこれらの呼び出しはすべてメソッド呼び出しになります。

一方、JavaScriptではプロパティに関数だったり、文字列だったりをアサインすることができます。

Nativeは関数であれば関数を実行した結果を返すし、文字列などの値であればその値を返すようになっています。

それでは、これらがどのように実装されているか見てみましょう。

Nativestdlib/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の定義をしています。ArrayHashなどもあります。

もちろん、Native::Object自身もto_nメソッドをもちます。

しかし、to_nが定義されていないクラスに関しては、nativeへの変換は行なわれませんので注意が必要です。

逆にto_nを定義すればnativeのメソッドに引数として渡すことができるようになります。

Nativeの実装についてみてきましたがいかがでしょうか。次回はJSの実装を見てみましょう。

では、Happy hacking!