oracle / graaljs

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

Replacing ScriptEngine Invocable cast with Context API equivalent #320

Closed KatarinaKulkova closed 4 years ago

KatarinaKulkova commented 4 years ago

Hello!

I am working on porting a rather large code base from nashorn to graal.js. The original code was using javax.script.ScriptEngine.

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

I have decided to replace the ScriptEngine with the polyglot Context API.

private Context cx = Context.newBuilder().allowAllAccess(true).allowHostAccess(HostAccess.ALL).build();

I replaced every call to engine.put() with a call to cx.getBindings("js").putMember() and similarly engine.get() with cx.getBindings("js").getMember() like this:

cx.getBindings("js").putMember("mote", mote);
cx.getBindings("js").putMember("id", id);
//engine.put("mote", mote);
//engine.put("id", id);

However, there is one place in the code where I cannot get rid of the engine. The engine is cast as an Invocable and runs a Thread in a thread group:

scriptThread = new Thread(group, new Runnable() {
  public void run() {
    try {
      ((Invocable)engine).getInterface(Runnable.class).run();
    } catch (RuntimeException e) {
      Throwable throwable = e;
      while (throwable.getCause() != null) {
        throwable = throwable.getCause();
      }

      if (throwable.getMessage() != null &&
          throwable.getMessage().contains("test script killed") ) {
        logger.info("Test script finished");
      } else {
        if (!Cooja.isVisualized()) {
          logger.fatal("Test script error, terminating Cooja.");
          logger.fatal("Script error:", e);
          System.exit(1);
        }

        logger.fatal("Script error:", e);
        deactivateScript();
        simulation.stopSimulation();
        if (Cooja.isVisualized()) {
          Cooja.showErrorDialog(Cooja.getTopParentContainer(),
              "Script error", e, false);
        }
      }
    }
    /*logger.info("test script thread exits");*/
  }
});
scriptThread.start(); /* Starts by acquiring semaphore (blocks) */
while (!semaphoreScript.hasQueuedThreads()) {
  Thread.yield();
}

This is the part I would like to change:

 ((Invocable)engine).getInterface(Runnable.class).run();

Is there a Context equivalent for this? I could not find any documentation regarding Invocables in the Context API. I have, however, read somewhere that it I should use Context to execute JavaScript code when using GraalVM. So my question is, is there a way to change this piece of code, so that it keeps its functionality but I don't have to use the engine anymore and use the Context API instead? If not, what would be a good way to do it?

Thanks.

eleinadani commented 4 years ago

Hi @KatarinaKulkova,

The Context API's Value class provides the as method, which can be used to cast a Value object to some Java type. In your example above, I think you could try using .as(Runnable.class).run()?

KatarinaKulkova commented 4 years ago

Hello.

Which Value exactly do you think should be cast as Runnable? I tried the following:

cx.getBindings("js").as(Runnable.class).run();

but this gave me the following error when trying to run the code:

 java.lang.UnsupportedOperationException: Unsupported operation identifier 'run' and  object 
 'com.oracle.truffle.polyglot.PolyglotLanguageBindings@57302cec'(language: Java, type: 
 com.oracle.truffle.polyglot.PolyglotMap). Identifier is not executable or instantiable.

I don't think I am using any other Value except for the Bindings.

eleinadani commented 4 years ago

I am missing a bit of context, so I am not sure I fully understand what the code in your example is trying to achieve, but if I understand correctly your usage of getInterface() I think that you should be able to achieve the same semantics by using the global object in your script, so e.g., context.eval("js", "this").as(Runnable.class) or context.eval("js", "globalThis").as(Runnable.class) should do the same thing as ((Invocable)engine).getInterface(Runnable.class)

KatarinaKulkova commented 4 years ago

I will try to provide some general context. I am currently working on this document: https://github.com/contiki-ng/cooja/blob/63538bbb882ba06a7b8cf97c11ce2fe4d22e4f88/java/org/contikios/cooja/plugins/LogScriptEngine.java This is a copy of the document before I made any changes. All I did was replace all instances of the engine with the Context API, as I described in my question. There is a function in the code called activateScript(). It parses the JavaScript code, then calls engine.eval(jsCode) which I replaced by cx.eval("js", jsCode), then there are some calls to engine.put() which I replaced by calls to cx.getBindings("js").putMember(). Finally, scriptThread (which is a Thread created at the beginning of the document), is assigned to be a new Thread, with its run function containing the cast to Invocable. This thread is supposed to run the parsed JavaScript. The activateScript function also does some semaphore stuff, but I don't think it is related to the issue I am having.

So I tried context.eval("js", jsCode).as(Runnable.class).run(); as you suggested, but this resulted in the following error: java.lang.NullPointerException at org.contikios.cooja.plugins.LogScriptEngine$4.run(LogScriptEngine.java:328) at java.base/java.lang.Thread.run(Thread.java:834) where LogScriptEngine.java:328 is the line where I do context.eval("js", jsCode).as(Runnable.class).run(); I wonder whether the fact that the engine was cast as an Invocable enabled it to run, and so replacing it with the Context API doesn't work? I am not sure about this.

KatarinaKulkova commented 4 years ago

Nevermind, this solved it: context.eval("js", "this").as(Runnable.class) Thank you very much.

eleinadani commented 4 years ago

OK, happy to hear that it worked (and thanks for the context info) -- your call to ((Invocable)engine).getInterface(Runnable.class).run(); essentially looks for a function called run in your JS global scope and then calls it, which is why context.eval("js", "this").as(Runnable.class).run() (or using "globalThis") should be equivalent

KatarinaKulkova commented 4 years ago

I see. That makes sense. Thank you for clearing that up. It was very helpful.