Help us understand the problem. What is going on with this article?

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

More than 3 years have 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!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした