JDK-8046009 : nashorn object identity is compromised
  • Type: Bug
  • Component: core-libs
  • Sub-Component: jdk.nashorn
  • Priority: P3
  • Status: Closed
  • Resolution: Not an Issue
  • OS: generic
  • CPU: generic
  • Submitted: 2014-06-05
  • Updated: 2014-06-20
  • Resolved: 2014-06-09
Related Reports
Relates :  
Description
While investigating my #2 issue from 
https://bugs.openjdk.java.net/browse/JDK-8006183?focusedCommentId=13504584&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-13504584
I've noticed a problem with identity. I know the identity of Java objects passed to Nashorn and returned back is weaker than it used to be in Rhino - e.g. sometimes objects are not == only equal. But I found a case where the object is not even equal and I consider it a bug. Would you agree that following testIdentity function should never throw the IllegalStateException?

    public static void main(String[] args) throws Exception {
        ScriptEngineManager sem = new ScriptEngineManager();
        ScriptEngine eng = sem.getEngineByMimeType("text/javascript");
        Invocable inv = (Invocable) eng;

        testIdentity(eng, new Object());
        
    }
    
    private static void testIdentity(ScriptEngine eng, Object obj) throws Exception {
        Invocable inv = (Invocable) eng;

        eng.eval("function id(x) { return x; }");
        
        Object returned = inv.invokeFunction("id", obj);
        
        if (returned == obj || obj.equals(returned)) {
            return;
        }
        throw new IllegalStateException(
          "Not equals! obj: " + obj + " returned: " + returned + 
          "\ntypes: " + obj.getClass() + " real: " + returned.getClass()
        );
    }

I would agree with such statement. Having a function that returns is parameter, should return the parameter. At least I hope.
Comments
OK, I can understand now why the identity cannot be preserved. However the suggested fix is problematic. So far my code worked on JDK7, if I use ScriptObjectMirror, it won't work anymore and moreover I no longer compile on JDK7. In addition to that it won't work if content of the array is not object, but for example String. I was trying to find some solution on JavaScript side. Like using Java.to(arr), but surprisingly that does not do deep check - e.g. does not convert objects in the array. So I was trying to do the recursion into the array content myself, but there are some issues. Another flaw is that Java.to method is not general. It does not handle primitives like "Java.to(true)" and rather throws an exception. Why it is so complicated to use it? Can't you just make sure Java.to(anything) works and if applied on an array it makes sure elements are converted to ScriptObjectMirror or kept untouched if primitive? But except here outlines issues (which I believe should be addressed), I guess we can verify that the identity problem is not real problem.
20-06-2014

Example fixed to use ScriptObjectMirror[] instead of Object[]: import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import jdk.nashorn.api.scripting.ScriptObjectMirror; /** * * @author Jaroslav Tulach */ public class NashornIdProblem { public static void main(String[] args) throws Exception { ScriptEngineManager sem = new ScriptEngineManager(); ScriptEngine eng = sem.getEngineByMimeType("text/javascript"); Invocable inv = (Invocable) eng; // oK testIdentity(eng, new Object()); // ok Object jsObj = eng.eval("new Object()"); testIdentity(eng, jsObj); // fails on JDK8b132 defineArraySizeFn(eng); Object[] arr = (Object[]) checkArray(inv, eng.eval("[ new Object() ]")); testIdentity(eng, arr[0]); } private static void testIdentity(ScriptEngine eng, Object obj) throws Exception { Invocable inv = (Invocable) eng; eng.eval("function id(x) { return x; }"); Object returned = inv.invokeFunction("id", obj); if (returned == obj || obj.equals(returned)) { return; } System.out.println("returned " + returned.getClass()); System.out.println("obj " + obj.getClass()); throw new IllegalStateException("Not equals! obj: " + obj + " returned: " + returned + "\ntypes: " + obj.getClass() + " real: " + returned.getClass()); } static Object checkArray(Invocable inv, Object val) throws Exception { final Object fnRes = inv.invokeFunction("toJavaArray", val, null); int length = ((Number) fnRes).intValue(); if (length == -1) { return val; } Object[] arr = new ScriptObjectMirror[length]; inv.invokeFunction("toJavaArray", val, arr); return arr; } static void defineArraySizeFn(ScriptEngine eng) throws ScriptException { eng.eval("function toJavaArray(arr, to) {\n" + "if (to === null) {\n" + " if (Object.prototype.toString.call(arr) === '[object Array]') return arr.length;\n" + " else return -1;\n" + "} else {\n" + " var l = arr.length;\n" + " for (var i = 0; i < l; i++) to[i] = arr[i];\n" + " return l;\n" + "}" + "}" ); } }
09-06-2014

Please see earlier comment for code restructuring suggestions.
09-06-2014

All script engine APIs (ScriptObject.eval, ScriptEngine.get, Bindings.get, Invocable.invokeMethod, Invocable.invokeFunction) wrap underlying script objects as a convenient object of type jdk.nashorn.api.scripting.ScriptObjectMirror. This ScriptObjectMirror allows Java code to manipulate the object via it's super interfaces like jdk.nashorn.api.scripting.JSObject and javax.script.Bindings -- this interface barrier also takes are not exposing implementation detail like jdk.nashorn.internal.runtime.ScriptObject and subclasses. And this takes care of setting the correct nashorn global instance into Nashorn's thread local storage (which is needed for correct script execution by nashorn). This conversion also occurs when your Java method accepts a parameter of type ScriptObjectMirror. When call Invocable.invokeMethod/invokeFunction, if you pass ScriptObjectMirror (created by correct Nashorn global scope object), nashorn will unwrap and use it as a ScriptObject instance. But in the example, you assign a script object into a java array - which means it goes there not being a ScriptObjectMirror - ScriptObject "escapes" unwrapped (this is by design - so that we don't want to check ScriptObject instance crossing Java boundary for *every* Java call). Your example code managed to "mix a ScriptObject and it's corresponding ScriptObjectMirror! Hence "identity crisis" noted. Solution is to convert array of script objects an array of ScriptObjectMirror[] - rather than java Object[]. Whenever nashorn sees ScriptObjectMirror required and script passes ScriptObject, it will to necessary wrapping. Java.to function can be used to convert a script array into any Java array - you could ask it to convert to ScriptObjectMirror[]. If you insist on keeping your array as Object[], in that case you've to do manual wrapping to ScriptObjectMirror. jdk.nashorn.api.scripting.ScriptUtils' wrap can be called from script to do the explicit wrapping. Please see also: https://wiki.openjdk.java.net/display/Nashorn/Nashorn+jsr223+engine+notes
09-06-2014

The same example showing three situations where the last one compromises the object identity. I believe similar reasons causing this issue are behind my problems with "with" described in JDK-8006183
05-06-2014