Nashornでホストオブジェクトを追加するには

RhinoではScriptableObjectを継承することで簡単にプロパティの拡張が可能なホストオブジェクトを実装できた。Nashornではそのような公開インターフェイスは用意されていないが、jdk.nashorn.internal.objects.ScriptObjectを継承し、nasgenというツールを使うことで内部的には実現できている。

Nashornのホストオブジェクト実装方法はJavaScriptで擬似的にクラスを作るのに近い。JavaScriptでクラスを実現するには一般には以下のように行う。

function Greeting() {}
Greeting.prototype.sayHello = function (name) { return 'Hello, ' + name; };
var greeting = new Greeting();

JavaScriptでクラスのインスタンスを生成するには、インスタンスであるオブジェクト自身のほかにコンストラクターとなる関数オブジェクトとプロトタイプオブジェクトの2つのオブジェクトがかかわってくる。

コンストラクターとなる関数オブジェクトはただの関数オブジェクトで、利用者がどう使うかの違いでしかない。関数オブジェクトをコンストラクターとして使うにはnew演算子を使う。関数オブジェクトはprototypeプロパティを持っていて、初期値は空のオブジェクトが設定されている。new演算子を使ってオブジェクトを生成すると、生成したオブジェクトのプロトタイプ内部プロパティがコンストラクターのprototypeプロパティの値に設定される。プロトタイプ内部プロパティとprototypeプロパティは違うものなので、greeting.prototypeundefinedだ。プロトタイプ内部プロパティを取得するにはObject.getPrototypeOf(greeting)を使う。

greeting.prototype // -> undefined
Object.getPrototypeOf(greeting) // -> { sayHello: function () { } }

プロトタイプ内部プロパティはオブジェクトのひな形として機能する。greeting自身にはsayHelloプロパティを設定してはいないが、greeting.sayHelloはプロトタイプ内部プロパティはオブジェクトのsayHelloプロパティの値を返してくれる。

Nashornでホストオブジェクトを実装するには、コンストラクター、プロトタイプ、オブジェクトの実装の3つを用意してやればよい。NashornでもRhinoと同様にアノテーションを使って実装する。ホストオブジェクトのタイプごとに1つのJavaクラスを作ってやればよいのだが、コンストラクターやプロトタイプを自分で扱うため面倒になる。

試しに実装してみたホストオブジェクトの実装はこんな感じだ、Nashornではホストオブジェクトを生成するための公開インターフェイスは用意されておらず、実装はNashorn自身のパッケージであるjdk.nashorn.internal.objectsを使わなければならない。

package jdk.nashorn.internal.objects;

import jdk.nashorn.internal.objects.annotations.Attribute;
import jdk.nashorn.internal.objects.annotations.Constructor;
import jdk.nashorn.internal.objects.annotations.Function;
import jdk.nashorn.internal.objects.annotations.ScriptClass;
import jdk.nashorn.internal.runtime.PropertyMap;
import jdk.nashorn.internal.runtime.ScriptObject;

@ScriptClass("Greeting")
public class NativeGreeting extends ScriptObject {

    // initialized by nasgen
    private static PropertyMap $nasgenmap$;

    static PropertyMap getInitialMap() {
        return $nasgenmap$;
    }

    @Constructor
    public static Object constructor(final boolean isNew, final Object self, final Object... args) {
        return new NativeGreeting(Global.instance());
    }

    NativeGreeting(final Global global) {
        super(global.getGreetingPrototype(), global.getGreetingMap());
    }

    public String sayHello(String name) {
        return "Hello, " + name;
    }

    @Function(attributes = Attribute.NOT_ENUMERABLE)
    public static Object sayHello(final Object self, final Object name) {
        return ((NativeGreeting) self).sayHello((String) name);
    }
}

Nashornのビルド時には、Nativeの実装がコンパイルされた後でnasgenツールがjdk/nashorn/internal/objects/NativeGreeting.classを読み込み、
jdk/nashorn/internal/objects/NativeGreeting$Constructor.classjdk/nashorn/internal/objects/NativeGreeting$Prototype.classの2つのクラスファイルを自動生成する。

jdk.nashorn.internal.objects.GlobalクラスはJavaScriptのグローバルオブジェクトを実装するJavaクラスで、グローバルオブジェクトの初期時に、initConstructor()メソッドを使って、jdk.nashorn.internal.objects.NativeGreeting$Constructorクラスからコンストラクターの関数オブジェクトを生成する。

        this.builtinGreeting  = (ScriptFunction)initConstructor("Greeting");

このコンストラクターはjdk.nashorn.internal.objects.NativeGreeting$Prototypeで実装されるプロトタイプを持っていて、以下のように取得できる。

    ScriptObject getGreetingPrototype() {
        return ScriptFunction.getPrototype(builtinGreeting);
    }

あとはthis.builtinGreetingをJavaScriptから見えるようにすればJavaScriptでGreetingオブジェクトを生成できる。

print(Greeting);
var greeting = new Greeting();
print(greeting);
print(greeting.sayHello('Alice'));

以下、今回使ったパッチ

diff -r 18edd7a1b166 src/jdk/nashorn/internal/objects/Global.java
--- a/src/jdk/nashorn/internal/objects/Global.java	Wed Dec 11 18:09:34 2013 +0100
+++ b/src/jdk/nashorn/internal/objects/Global.java	Sun Sep 14 08:07:01 2014 +0900
@@ -313,6 +313,9 @@
     @Property(name = "__LINE__", attributes = Attribute.NON_ENUMERABLE_CONSTANT)
     public Object __LINE__;
 
+    @Property(name = "Greeting", attributes = Attribute.NOT_ENUMERABLE)
+    public volatile Object greeting;
+
     /** Used as Date.prototype's default value */
     public NativeDate   DEFAULT_DATE;
 
@@ -364,6 +367,7 @@
     private ScriptObject   builtinUint32Array;
     private ScriptObject   builtinFloat32Array;
     private ScriptObject   builtinFloat64Array;
+    private ScriptObject   builtinGreeting;
 
     /*
      * ECMA section 13.2.3 The [[ThrowTypeError]] Function Object
@@ -399,6 +403,7 @@
     private PropertyMap    anonymousFunctionMap;
     private PropertyMap    strictFunctionMap;
     private PropertyMap    boundFunctionMap;
+    private PropertyMap    nativeGreetingMap;
 
     // Flag to indicate that a split method issued a return statement
     private int splitState = -1;
@@ -999,6 +1004,10 @@
         return ScriptFunction.getPrototype(builtinFloat64Array);
     }
 
+    ScriptObject getGreetingPrototype() {
+        return ScriptFunction.getPrototype(builtinGreeting);
+    }
+
     // Builtin PropertyMap accessors
     PropertyMap getAccessorPropertyDescriptorMap() {
         return accessorPropertyDescriptorMap;
@@ -1124,6 +1133,10 @@
         return typeErrorThrower;
     }
 
+    PropertyMap getGreetingMap() {
+        return nativeGreetingMap;
+    }
+
     /**
      * Called from compiled script code to test if builtin has been overridden
      *
@@ -1669,6 +1682,8 @@
         this.builtinNumber    = (ScriptFunction)initConstructor("Number");
         this.builtinRegExp    = (ScriptFunction)initConstructor("RegExp");
         this.builtinString    = (ScriptFunction)initConstructor("String");
+        this.builtinGreeting  = (ScriptFunction)initConstructor("Greeting");
+	System.out.println(this.builtinGreeting);
 
         // initialize String.prototype.length to 0
         // add String.prototype.length
@@ -1883,6 +1898,7 @@
         this.uint32Array       = this.builtinUint32Array;
         this.float32Array      = this.builtinFloat32Array;
         this.float64Array      = this.builtinFloat64Array;
+        this.greeting          = this.builtinGreeting;
     }
 
     private void initDebug() {
@@ -1975,6 +1991,7 @@
         this.anonymousFunctionMap = ScriptFunctionImpl.getInitialAnonymousMap().duplicate();
         this.strictFunctionMap = ScriptFunctionImpl.getInitialStrictMap().duplicate();
         this.boundFunctionMap = ScriptFunctionImpl.getInitialBoundMap().duplicate();
+        this.nativeGreetingMap = NativeGreeting.getInitialMap().duplicate();
 
         // java
         if (! env._no_java) {

参考資料

紹介 kiidax
元、環境非依存な職業プログラマー。Blenderで遊んでいます。

コメントを残す