Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Aerilym opened this issue Sep 26, 2023 · 1 comment
Open
Assignees

Comments

@Aerilym
Copy link

Aerilym commented Sep 26, 2023

Versions

  • GraalVM JDK 17.0.8
  • GraalJS 23.0.1

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] #3 (comment)

@iamstolis
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants