graaljs icon indicating copy to clipboard operation
graaljs copied to clipboard

Scala varargs interop problem

Open provegard opened this issue 4 years ago • 1 comments

I'm in the process of migrating code from Nashorn to Graal. My host language is Scala rather than Java. I've encountered a problem with calling a varargs method from JavaScript code.

Consider the following code:

import org.graalvm.polyglot.{Context, HostAccess}
import scala.annotation.varargs

object GraalScalaTest {
  def main(args: Array[String]): Unit = {
    val hostAccess = HostAccess
      .newBuilder()
      .allowPublicAccess(true)
      .build()

    val ctx = Context.newBuilder("js").allowHostAccess(hostAccess).build
    val bindings = ctx.getBindings("js")
    bindings.putMember("r", new Receiver)
    val script =
      """
        |r.items(r.newItem('a'));
        |r.items(r.newItem('a'), r.newItem('b'));
        |""".stripMargin
    ctx.eval("js", script)
  }

  class Item(str: String) {
    override def toString: String = this.str
  }

  class Receiver {
    def newItem(s: String): Item = new Item(s)

    @varargs
    def items(item1: Item, rest: Item*): Unit = {
      println(s"rest has type: ${rest.getClass}")
      val all = Seq(item1) ++ rest
      dump(all)
    }

    private def dump(items: Seq[Item]): Unit = {
      println(s"Items (${items.size}):")
      items.foreach(item => println(s"* $item"))
    }
  }
}


The script code calls items first with 1 parameter and then with 2. Note that items has the varargs annotation, to force the Scala compiler to generate a method that takes a varargs array as last argument (meant for Java interop).

The output is:

Items (1):
* a
rest has type: class scala.collection.mutable.WrappedArray$ofRef
Items (2):
* a
* b

This is great, but I need more host access. Thus I modify the code as follows:

    val hostAccess = HostAccess
      .newBuilder()
      .allowPublicAccess(true)
      .allowAllImplementations(true) // <--- added
      .build()

Now the output is as follows:

rest has type: class scala.collection.mutable.WrappedArray$ofRef
Items (1):
* a
rest has type: class com.sun.proxy.$Proxy14
Exception in thread "main" Unsupported operation identifier 'foreach' and  object 'b'(language: Java, type: GraalScalaTest$Item). Identifier is not executable or instantiable.
	at com.oracle.truffle.polyglot.PolyglotEngineException.unsupported(PolyglotEngineException.java:134)
	at com.oracle.truffle.polyglot.HostInteropErrors.newUnsupportedOperationException(HostInteropErrors.java:206)
	at com.oracle.truffle.polyglot.HostInteropErrors.invokeUnsupported(HostInteropErrors.java:178)
	at com.oracle.truffle.polyglot.ProxyInvokeNode.invokeOrExecute(HostInteropReflect.java:628)
	at com.oracle.truffle.polyglot.ProxyInvokeNode.doCachedMethod(HostInteropReflect.java:592)
	at com.oracle.truffle.polyglot.ProxyInvokeNodeGen.executeAndSpecialize(ProxyInvokeNodeGen.java:116)
	at com.oracle.truffle.polyglot.ProxyInvokeNodeGen.execute(ProxyInvokeNodeGen.java:64)
	at com.oracle.truffle.polyglot.ObjectProxyNode.executeImpl(HostInteropReflect.java:535)
	at com.oracle.truffle.polyglot.HostToGuestRootNode.execute(HostToGuestRootNode.java:99)
	at com.oracle.truffle.polyglot.ObjectProxyHandler.invoke(HostInteropReflect.java:678)
	at com.sun.proxy.$Proxy14.foreach(Unknown Source)
	at scala.collection.generic.Growable.$plus$plus$eq(Growable.scala:62)
	at scala.collection.generic.Growable.$plus$plus$eq$(Growable.scala:53)
	at scala.collection.mutable.ListBuffer.$plus$plus$eq(ListBuffer.scala:184)
	at scala.collection.mutable.ListBuffer.$plus$plus$eq(ListBuffer.scala:47)
	at scala.collection.TraversableLike.defaultPlusPlus$1(TraversableLike.scala:152)
	at scala.collection.TraversableLike.$plus$plus(TraversableLike.scala:169)
	at scala.collection.TraversableLike.$plus$plus$(TraversableLike.scala:147)
	at scala.collection.immutable.List.$plus$plus(List.scala:210)
	at GraalScalaTest$Receiver.items(GraalScalaTest.scala:33)
	at <js> :program(Unnamed:3:28-66)
	at org.graalvm.polyglot.Context.eval(Context.java:371)
	at GraalScalaTest$.main(GraalScalaTest.scala:20)
	at GraalScalaTest.main(GraalScalaTest.scala)

Thus, the case with 2 items fails - but 1 item works well.

Invoking javap on the Receiver class shows:

public class GraalScalaTest$Receiver {
  public void items(GraalScalaTest$Item, GraalScalaTest$Item...);
  public GraalScalaTest$Item newItem(java.lang.String);
  public void items(GraalScalaTest$Item, scala.collection.Seq<GraalScalaTest$Item>);
  public static final void $anonfun$dump$1(GraalScalaTest$Item);
  public GraalScalaTest$Receiver();
  public static final java.lang.Object $anonfun$dump$1$adapted(GraalScalaTest$Item);
}

The error doesn't happen with Java, so I suppose the problem is that Scala generates 2 items overloads. I tried adding a Value -> Seq target type mapping, but that didn't help.

Is there any workaround for this?

provegard avatar Aug 11 '20 07:08 provegard

It works if I call items with 3 arguments. Thus, the problem is the case when a single argument is passed for the rest array.

I found a workaround:

    @varargs
    def items(item1: Item, rest: Item*): Unit = {
      println(s"rest has type: ${rest.getClass}")

      val restSeq = rest match {
        case p: java.lang.reflect.Proxy =>
          val v = Context.getCurrent.asValue(p)
          val item = v.asHostObject[Item]()
          Seq(item)
        case _ => rest.toSeq
      }

      val all = Seq(item1) ++ restSeq
      dump(all)
    }

It's more of a hack than a workaround though... Also, in the real application I'd like to keep the host code free from Graal-specific types.

provegard avatar Aug 11 '20 08:08 provegard