graaljs icon indicating copy to clipboard operation
graaljs copied to clipboard

[21.3.0][JDK11][javax.script] Java static members lost in scoped bindings

Open ctabin opened this issue 4 years ago • 4 comments

Hello,

We are migrating to GraalJS as scripting engine behind the javax.script API in replacement of Nashorn.

In our case we need to execute multiple script blocks in isolation, but all have some common data/tools used. By doing this, we are hitting some strange issues. Find below the detailed data to reproduce the case.

Initialization script (INIT). We want BigDecimal and MathContext to be available in all the lines that will be executed. This script is executed in the ENGINE_SCOPE that will be the 'super-scope' for all our lines.

var BigDecimal = Java.type("java.math.BigDecimal");
var MathContext = Java.type("java.math.MathContext");

The ScriptEngine is initialized like this:

ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = engineManager.getEngineByName("graal.js");
        
//init GraalJS
Bindings defaultBindings = engine.createBindings();
defaultBindings.put("polyglot.js.allowHostAccess", true);
defaultBindings.put("polyglot.js.allowHostClassLookup", true);
engine.setBindings(defaultBindings, ScriptContext.ENGINE_SCOPE);
        
engine.eval(INIT); //provides BigDecimal and MathContext

Here is the sample script used (SCRIPT):

var a = new BigDecimal(15);
var b = new BigDecimal(3);
a.divide(b, MathContext.DECIMAL64)

When the script is executed in the ENGINE_SCOPE, there is no problem:

engine.put(ScriptEngine.FILENAME, "script");
Compilable compiler = (Compilable)engine;
CompiledScript cs = compiler.compile(SCRIPT);
return (BigDecimal)cs.eval(); //returns a BigDecimal(5)

However, when an isolated scope is created, then an error is thrown

Bindings defaults = engine.getBindings(ScriptContext.ENGINE_SCOPE);
SimpleBindings localBindings = new SimpleBindings(new HashMap<>(defaults));

engine.put(ScriptEngine.FILENAME, "script");
Compilable compiler = (Compilable)engine;
CompiledScript cs = compiler.compile(SCRIPT);
return (BigDecimal)cs.eval(localBindings); //error
javax.script.ScriptException: org.graalvm.polyglot.PolyglotException: TypeError: invokeMember (divide) on java.math.BigDecimal@12bfd80d failed due to: Multiple applicable overloads found for method name divide (candidates: [Method[public java.math.BigDecimal java.math.BigDecimal.divide(java.math.BigDecimal,java.math.MathContext)], Method[public java.math.BigDecimal java.math.BigDecimal.divide(java.math.BigDecimal,java.math.RoundingMode)]], arguments: [JavaObject[3 (java.math.BigDecimal)] (HostObject), DynamicObject<undefined>@b2c5e07 (Nullish)])
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.toScriptException(GraalJSScriptEngine.java:483)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:460)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.access$400(GraalJSScriptEngine.java:83)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine$10.eval(GraalJSScriptEngine.java:628)
        at java.scripting/javax.script.CompiledScript.eval(CompiledScript.java:89)
        at ch.astorm.graaljs.ScopeTest.evaluate(ScopeTest.java:53)
        at ch.astorm.graaljs.ScopeTest.testFailureDivision(ScopeTest.java:45)
Caused by: org.graalvm.polyglot.PolyglotException: TypeError: invokeMember (divide) on java.math.BigDecimal@12bfd80d failed due to: Multiple applicable overloads found for method name divide (candidates: [Method[public java.math.BigDecimal java.math.BigDecimal.divide(java.math.BigDecimal,java.math.MathContext)], Method[public java.math.BigDecimal java.math.BigDecimal.divide(java.math.BigDecimal,java.math.RoundingMode)]], arguments: [JavaObject[3 (java.math.BigDecimal)] (HostObject), DynamicObject<undefined>@b2c5e07 (Nullish)])
        at <js>.:program(script:3)
        at org.graalvm.polyglot.Context.eval(Context.java:379)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458)
        ... 23 more

Here is a small project that reproduces the case. Just type mvn clean package in graaljs-bug dir to see it.

ctabin avatar Oct 22 '21 09:10 ctabin

After digging a little bit more, it seems the problem is related to static member of classes. Here the script I tested:

var mc = new MathContext(5);
print(mc.getPrecision());
print(MathContext.DECIMAL64);

When executed in ScriptContext.ENGINE_SCOPE:

5
precision=16 roundingMode=HALF_EVEN

When executed in an isolated context:

5
undefined

Hence, the class MathContext seems to be still there, but the static members are unknown:

  • static constants are undefined
  • static methods raise an error:
javax.script.ScriptException: org.graalvm.polyglot.PolyglotException: TypeError: invokeMember (getStaticValue) on ch.astorm.graaljs.TestClass failed due to: Unknown identifier: getStaticValue
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.toScriptException(GraalJSScriptEngine.java:483)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:460)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.access$400(GraalJSScriptEngine.java:83)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine$10.eval(GraalJSScriptEngine.java:628)
        at java.scripting/javax.script.CompiledScript.eval(CompiledScript.java:89)
        at ch.astorm.graaljs.ScopeTest.evaluate(ScopeTest.java:48)
        at ch.astorm.graaljs.ScopeTest.testFailureDivision(ScopeTest.java:41)
Caused by: org.graalvm.polyglot.PolyglotException: TypeError: invokeMember (getStaticValue) on ch.astorm.graaljs.TestClass failed due to: Unknown identifier: getStaticValue
        at <js>.:program(script:2)
        at org.graalvm.polyglot.Context.eval(Context.java:379)
        at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458)
        ... 23 more

ctabin avatar Oct 22 '21 21:10 ctabin

The root of the problem is that Java classes can be represented by two kinds of objects in JavaScript:

  • as a class object (an instance of java.lang.Class)
  • as a static object

The former representation has members/properties that correspond to public instance members of java.lang.Class object (like getConstructors()). The latter has members/properties that correspond to static members of the corresponding class (like DECIMAL64 for MathContext). Java.type() returns the static object, but you can get the other representation using its class property. Similarly, you can get the static object from the class object using its static property.

Unfortunately, there is no representation of the static object in Java. So, both the class and static objects map to the corresponding instance of java.lang.Class when passed to Java (like in your case). Therefore, the object in the localBindings is not a static object (as you expect) but a class object.

This behaviour is unfortunate but expected (= matches the current design ideas) I am afraid. So, the best I can suggest you is to tweak your scripts to expect that your super-scope exports class objects (not static objects), i.e., use MathContext.static.DECIMAL64 instead of MathContext.DECIMAL64 there.

iamstolis avatar Oct 24 '21 18:10 iamstolis

Hi @iamstolis and thanks for your reply.

As suggested, I tried different options and find out what causes the problem: it seems that java types are not reloaded correctly depending on the bindings.

Consider the following piece of code:

engine.eval("var MathContext = java.type(\"java.math.MathContext\");");
engine.eval("print(\"MathContext.DECIMAL64\")"); //ok

Now, the same thing, with new bindings:

var bindings = new SimpleBindings(new HashMap<>(defaults));
engine.eval("var MathContext = java.type(\"java.math.MathContext\");", bindings);
engine.eval("print(\"MathContext.DECIMAL64\")", bindings); //ok

So far, so good ! And finally:

engine.eval("var MathContext = java.type(\"java.math.MathContext\");");
engine.eval("print(\"MathContext.DECIMAL64\")"); //ok

//continue with the same ScriptEngine, but with a child context
var bindings = new SimpleBindings(new HashMap<>(defaults));
engine.eval("var MathContext = java.type(\"java.math.MathContext\");", bindings);
engine.eval("print(\"MathContext.DECIMAL64\")", bindings); //FAILURE

So it looks that the second evaluation of var MathContext = java.type(...) did not take priority over the MathContext variable declared in the parent scope. Maybe, is there some cache behind the java.type function ? Would it be possible to somewhat *force" a reload ? I tried to use delete MathContext before the second evaluation, but still hit the same error.

However, the following works:

engine.eval("var MathContext = java.type(\"java.math.MathContext\");");
engine.eval("print(\"MathContext.DECIMAL64\")"); //ok

//continue with the same ScriptEngine, but with a child context
var bindings = new SimpleBindings(new HashMap<>(defaults));
engine.eval("var MathContext = java.type(\"java.math.MathContext\");\n
             print(\"MathContext.DECIMAL64\")", bindings); //ok

ctabin avatar Oct 24 '21 22:10 ctabin

So it looks that the second evaluation of var MathContext = java.type(...) did not take priority over the MathContext variable declared in the parent scope.

Yes, this seems to be the case. We do not handle SimpleBindings very well. I guess that you can work around it if you replace

Bindings bindings = new SimpleBindings(new HashMap<>(defaults));

by

Bindings bindings = engine.createBindings();
bindings.putAll(defaults);

iamstolis avatar Oct 25 '21 18:10 iamstolis