oracle / graaljs

GraalJS – A high-performance, ECMAScript compliant, and embeddable JavaScript runtime for Java
https://www.graalvm.org/javascript/
Universal Permissive License v1.0
1.81k stars 190 forks source link

Unable to use JavaScript Array in Java method requiring a Collection #768

Open Aerilym opened 1 year ago

Aerilym commented 1 year ago

Versions

Graal Scripting Engine Setup

HostAccess BASIC_ALL = HostAccess.newBuilder(HostAccess.ALL)
       .allowPublicAccess(true)
       .build();

GraalJSScriptEngine engine =
GraalJSScriptEngine.create(null,
       Context.newBuilder("js")
               .allowHostAccess(BASIC_ALL)
               .allowHostClassLookup(s -> true)
               .option("js.ecmascript-version", "2022"));

To run each script file I used the following:

public void runScript(String script) throws FileNotFoundException, ScriptException {
   engine.eval(new FileReader(script));
}

JS Code Example

var HashSet = Java.type("java.util.HashSet");
var MockClass = Java.type("org.example.MockClass");
var JavaClass = new MockClass("testString");

var javaHashSet = new HashSet();
var javascriptArray = [JavaClass];
javaSet.addAll(javascriptArray);

See MockClass:

public class MockClass {
   String name;
   public MockClass(String n) {
       name = n;
   }
   public String getName() {
       return name;
   }
}

Issue

Calling the .addAll() method on a Java HashSet in JavaScript with a JavaScript array as the parameter (See above code example) throws the exception:

javax.script.ScriptException: java.lang.UnsupportedOperationException: Unsupported operation identifier 'iterator' and  object '[JavaObject[org.example.MockClass]]'(language: JavaScript, type: Array). Identifier is not executable or instantiable.

See Stack Trace for more information.

The code example at the top works on Nashorn, and it appears the problem is with Graal not mapping the JavaScript Array to a Collection, which Set.addAll() requires [1].

Nashorn Scripting Engine Setup: (OpenJDK 11.0.19)

ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("nashorn");

As before, I ran the same script in the same way:

public void runScript(String script) throws FileNotFoundException, ScriptException {
   engine.eval(new FileReader(script));
}

In Graal, I could get around this issue by adding explicit type maps [2]

public final class HostAccessPreset {
   public static List transformArray(Value v) {
       List list = new ArrayList();
       for (int i = 0; i < v.getArraySize(); ++i) {
           Value element = v.getArrayElement(i);
           list.add(toObject(element));
       }
       return list;
   }

   public static Object toObject(Value v) {
       if (v.isHostObject()) {
           return v.asHostObject();
       } else if (v.isProxyObject()) {
           return v.asProxyObject();
       }
       return null;
   }

   public static final HostAccess COLLECTION_MAPPING = HostAccess.newBuilder(HostAccess.ALL)
           .allowPublicAccess(true)
           .targetTypeMapping(
                   Value.class, Collection.class,
                   Value::hasArrayElements,
                   HostAccessPreset::transformArray
           )
           .build();
}

This explicit mapping solution came from another issue [2]. Using this type map caused other problems as the type mapping is too generic and caught other data types.

JS Error object no longer being passable to the logger:

InvokeScriptedProcessor[id=350d1c49-b35e-3954-61b3-347c6d73f6f9] Processing halted: yielding [1 sec]: org.graalvm.polyglot.PolyglotException: TypeError: invokeMember (error) on org.apache.nifi.controller.TerminationAwareLogger failed due to: no applicable overload found (overloads: [
Method[public void org.apache.nifi.controller.TerminationAwareLogger.error(java.lang.String,java.lang.Object[])],
Method[public void org.apache.nifi.controller.TerminationAwareLogger.error(java.lang.String)],
Method[public void org.apache.nifi.controller.TerminationAwareLogger.error(java.lang.String,java.lang.Object[],java.lang.Throwable)],
Method[public void org.apache.nifi.controller.TerminationAwareLogger.error(java.lang.String,java.lang.Throwable)],
Method[public default void org.apache.nifi.logging.ComponentLog.error(org.apache.nifi.logging.LogMessage)]
], arguments: [com.oracle.truffle.js.runtime.builtins.JSErrorObject@1e1ab28c (JSErrorObject)])

The issue is caused by the JSErrorObject being passed into a Java logger class method which does not have a method that takes JSErrorObject. I have been unable to find a way to map JSErrorObject to anything else, as it seems to be indistinguishable from an object. From my research, it seems we can’t directly reference JSErrorObject from truffle. Is there a way to explicitly map JSErrorObject to something else?

Is there a proper way to use JavaScript Arrays in Java methods requiring Collections as their parameters?

Stack Trace

javax.script.ScriptException: java.lang.UnsupportedOperationException: Unsupported operation identifier 'iterator' and  object '[JavaObject[org.example.MockClass]]'(language: JavaScript, type: Array). Identifier is not executable or instantiable.
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotEngineException.unsupported(PolyglotEngineException.java:147)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotInteropErrors.invokeUnsupported(PolyglotInteropErrors.java:193)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandler$ProxyInvokeNode.invokeOrExecute(PolyglotObjectProxyHandler.java:261)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandler$ProxyInvokeNode.doCachedMethod(PolyglotObjectProxyHandler.java:214)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandlerFactory$ProxyInvokeNodeGen.executeAndSpecialize(PolyglotObjectProxyHandlerFactory.java:308)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandlerFactory$ProxyInvokeNodeGen.execute(PolyglotObjectProxyHandlerFactory.java:244)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandler$ObjectProxyNode.doDefault(PolyglotObjectProxyHandler.java:150)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandlerFactory$ObjectProxyNodeGen.executeAndSpecialize(PolyglotObjectProxyHandlerFactory.java:124)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandlerFactory$ObjectProxyNodeGen.executeImpl(PolyglotObjectProxyHandlerFactory.java:110)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.HostToGuestRootNode.execute(HostToGuestRootNode.java:124)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandler.invoke(PolyglotObjectProxyHandler.java:105)
    at jdk.proxy1/jdk.proxy1.$Proxy40.iterator(Unknown Source)
    at java.base/java.util.AbstractCollection.addAll(AbstractCollection.java:335)
    at <js>.:program(<eval>:9)
    at org.graalvm.sdk/org.graalvm.polyglot.Context.eval(Context.java:403)
    at org.graalvm.js.scriptengine/com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:485)
    at org.graalvm.js.scriptengine/com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:427)
    at java.scripting/javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:247)
    at org.example.ScriptingEngine.runScript(ScriptingEngine.java:29)
    at org.example.Main.main(Main.java:52)
Caused by: java.lang.UnsupportedOperationException: Unsupported operation identifier 'iterator' and  object '[JavaObject[org.example.MockClass]]'(language: JavaScript, type: Array). Identifier is not executable or instantiable.
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotEngineException.unsupported(PolyglotEngineException.java:147)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotInteropErrors.invokeUnsupported(PolyglotInteropErrors.java:193)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandler$ProxyInvokeNode.invokeOrExecute(PolyglotObjectProxyHandler.java:261)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandler$ProxyInvokeNode.doCachedMethod(PolyglotObjectProxyHandler.java:214)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandlerFactory$ProxyInvokeNodeGen.executeAndSpecialize(PolyglotObjectProxyHandlerFactory.java:308)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandlerFactory$ProxyInvokeNodeGen.execute(PolyglotObjectProxyHandlerFactory.java:244)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandler$ObjectProxyNode.doDefault(PolyglotObjectProxyHandler.java:150)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandlerFactory$ObjectProxyNodeGen.executeAndSpecialize(PolyglotObjectProxyHandlerFactory.java:124)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandlerFactory$ObjectProxyNodeGen.executeImpl(PolyglotObjectProxyHandlerFactory.java:110)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.HostToGuestRootNode.execute(HostToGuestRootNode.java:124)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.executeRootNode(OptimizedCallTarget.java:718)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.profiledPERoot(OptimizedCallTarget.java:641)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.callBoundary(OptimizedCallTarget.java:574)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.doInvoke(OptimizedCallTarget.java:558)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.callIndirect(OptimizedCallTarget.java:486)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.call(OptimizedCallTarget.java:467)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandler.invoke(PolyglotObjectProxyHandler.java:105)
    at jdk.proxy1/jdk.proxy1.$Proxy40.iterator(Unknown Source)
    at java.base/java.util.AbstractCollection.addAll(AbstractCollection.java:335)
    at org.graalvm.truffle/com.oracle.truffle.host.HostMethodDesc$SingleMethod$MHBase.invokeHandle(HostMethodDesc.java:350)
    at org.graalvm.truffle/com.oracle.truffle.host.GuestToHostCodeCache$1.executeImpl(GuestToHostCodeCache.java:96)
    at org.graalvm.truffle/com.oracle.truffle.host.GuestToHostRootNode.execute(GuestToHostRootNode.java:80)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.executeRootNode(OptimizedCallTarget.java:718)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.callInlined(OptimizedCallTarget.java:522)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.GraalRuntimeSupport.callInlined(GraalRuntimeSupport.java:239)
    at org.graalvm.truffle/com.oracle.truffle.host.GuestToHostRootNode.guestToHostCall(GuestToHostRootNode.java:102)
    at org.graalvm.truffle/com.oracle.truffle.host.HostMethodDesc$SingleMethod$MHBase.invokeGuestToHost(HostMethodDesc.java:386)
    at org.graalvm.truffle/com.oracle.truffle.host.HostExecuteNode.doInvoke(HostExecuteNode.java:876)
    at org.graalvm.truffle/com.oracle.truffle.host.HostExecuteNode.doFixed(HostExecuteNode.java:139)
    at org.graalvm.truffle/com.oracle.truffle.host.HostExecuteNodeGen$Inlined.executeAndSpecialize(HostExecuteNodeGen.java:402)
    at org.graalvm.truffle/com.oracle.truffle.host.HostExecuteNodeGen$Inlined.execute(HostExecuteNodeGen.java:362)
    at org.graalvm.truffle/com.oracle.truffle.host.HostObject.invokeMember(HostObject.java:465)
    at org.graalvm.truffle/com.oracle.truffle.host.HostObjectGen$InteropLibraryExports$Cached.invokeMemberNode_AndSpecialize(HostObjectGen.java:7369)
    at org.graalvm.truffle/com.oracle.truffle.host.HostObjectGen$InteropLibraryExports$Cached.invokeMember(HostObjectGen.java:7355)
    at org.graalvm.truffle/com.oracle.truffle.api.interop.InteropLibraryGen$CachedDispatch.invokeMember(InteropLibraryGen.java:8477)
    at com.oracle.truffle.js.nodes.function.JSFunctionCallNode$ForeignInvokeNode.executeCall(JSFunctionCallNode.java:1609)
    at com.oracle.truffle.js.nodes.function.JSFunctionCallNode.executeAndSpecialize(JSFunctionCallNode.java:318)
    at com.oracle.truffle.js.nodes.function.JSFunctionCallNode.executeCall(JSFunctionCallNode.java:263)
    at com.oracle.truffle.js.nodes.function.JSFunctionCallNode$InvokeNode.execute(JSFunctionCallNode.java:764)
    at com.oracle.truffle.js.nodes.JavaScriptNode.executeVoid(JavaScriptNode.java:192)
    at com.oracle.truffle.js.nodes.control.AbstractBlockNode.executeVoid(AbstractBlockNode.java:80)
    at com.oracle.truffle.js.nodes.control.AbstractBlockNode.executeVoid(AbstractBlockNode.java:55)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedBlockNode.executeGeneric(OptimizedBlockNode.java:78)
    at com.oracle.truffle.js.nodes.control.AbstractBlockNode.execute(AbstractBlockNode.java:75)
    at com.oracle.truffle.js.nodes.binary.DualNode.execute(DualNode.java:119)
    at com.oracle.truffle.js.nodes.function.FunctionBodyNode.execute(FunctionBodyNode.java:73)
    at com.oracle.truffle.js.nodes.function.FunctionRootNode.executeInRealm(FunctionRootNode.java:156)
    at com.oracle.truffle.js.runtime.JavaScriptRealmBoundaryRootNode.execute(JavaScriptRealmBoundaryRootNode.java:96)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.executeRootNode(OptimizedCallTarget.java:718)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.profiledPERoot(OptimizedCallTarget.java:641)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.callBoundary(OptimizedCallTarget.java:574)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.doInvoke(OptimizedCallTarget.java:558)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.callDirect(OptimizedCallTarget.java:504)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedDirectCallNode.call(OptimizedDirectCallNode.java:69)
    at com.oracle.truffle.js.lang.JavaScriptLanguage$ParsedProgramRoot.execute(JavaScriptLanguage.java:248)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.executeRootNode(OptimizedCallTarget.java:718)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.profiledPERoot(OptimizedCallTarget.java:641)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.callBoundary(OptimizedCallTarget.java:574)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.doInvoke(OptimizedCallTarget.java:558)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.callIndirect(OptimizedCallTarget.java:486)
    at jdk.internal.vm.compiler/org.graalvm.compiler.truffle.runtime.OptimizedCallTarget.call(OptimizedCallTarget.java:467)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotContextImpl.eval(PolyglotContextImpl.java:1481)
    at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotContextDispatch.eval(PolyglotContextDispatch.java:63)
    ... 6 more
    Suppressed: Attached Guest Language Frames (4)
    Suppressed: java.lang.UnsupportedOperationException: iterator
        at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotFunctionProxyHandler.invokeDefault(PolyglotFunctionProxyHandler.java:203)
        at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotObjectProxyHandler.invoke(PolyglotObjectProxyHandler.java:108)
        ... 52 more
    Caused by: java.lang.IllegalAccessException: no such method: java.util.Collection.iterator()Iterator/invokeSpecial
        at java.base/java.lang.invoke.MemberName.makeAccessException(MemberName.java:972)
        at java.base/java.lang.invoke.MemberName$Factory.resolveOrFail(MemberName.java:1117)
        at java.base/java.lang.invoke.MethodHandles$Lookup.resolveOrFail(MethodHandles.java:3649)
        at java.base/java.lang.invoke.MethodHandles$Lookup.findSpecial(MethodHandles.java:2996)
        at org.graalvm.truffle/com.oracle.truffle.polyglot.PolyglotFunctionProxyHandler.invokeDefault(PolyglotFunctionProxyHandler.java:201)
        ... 53 more
    Caused by: java.lang.AbstractMethodError: 'java.util.Iterator java.util.Collection.iterator()'
        at java.base/java.lang.invoke.MethodHandleNatives.resolve(Native Method)
        at java.base/java.lang.invoke.MemberName$Factory.resolve(MemberName.java:1085)
        at java.base/java.lang.invoke.MemberName$Factory.resolveOrFail(MemberName.java:1114)
        ... 56 more

[1] https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Set.html#addAll(java.util.Collection)

[2] https://github.com/oracle/graaljs/issues/3#issuecomment-555661056

iamstolis commented 1 year ago

I am able to reproduce the missing conversion of JavaScript arrays to Java Collection. I agree that this is unfortunate. I have no idea why the corresponding code in truffle does not do that by default (considering that it is willing to convert arrays to Java List). The consequence is that methods like Collection.addAll()/Collections.min() do not work while methods that take List (like Collections.sort()) work fine.

You are right that you can add the missing conversion through a custom target type mapping. Personally, I would use

targetTypeMapping(Value.class, Collection.class, Value::hasArrayElements, v -> v.as(List.class))

I don't understand your troubles with errors. I don't see the relation to the mentioned target type mapping. Errors do not have array elements, so they should not be affected at all. I am afraid that I cannot help you without a reproducible test-case.