Implementing WebWorkers with J2V8

May 28, 2015 | 5 min Read

J2V8 is a set of Java bindings for Google’s popular JavaScript engine, V8. As we push towards J2V8 3.0, the focus has been on multithread support. We’ve already discussed how to use multiple threads with V8. In this tutorial we will show you some new API coming in 3.0, and how this can be used to implement web workers.

V8Executor

Each thread can have its own V8 runtime (isolated from all other runtimes). To make it even easier to run scripts in isolation, a V8Executor was added. The V8Executor will execute a script on a separate thread (and in a separate isolated runtime)  and make the results available to the caller. In addition to executing scripts, the V8Executor supports a terminateExecution() method to stop long running scripts. The terminateExecution() method can be called from any thread.

 V8Executor executor = new V8Executor("script to execute...");
 executor.start();
 executor.join();
 String result = executor.getResult();

V8Executors can marked as longlived which means after the original script is processed, they will wait in an event loop for messages to arrive. When messages arrive (using the sendMessage(String s) method), the messages will be processed and the V8Executor will return to the waiting state.

The V8Executor can also track other threads that were started. This way, if the first worker terminates, other workers will be shutdown too.

Receiver in Callback

When a Java method is called from JavaScript, J2V8 now uses the V8Object that the JS Function was called on as the first parameter.  Consider the following JavaScript:

var array1 = [{first:'Ian'}, {first:'Jordi'}, {first:'Holger'}];
for ( var i = 0; i < array1.length; i++ ) {
  print.call(array1[i]);
}

If print is Java method, the array element will now be passed as the first argument.

 class PersonPrinter implements JavaVoidCallback {
  @Override
  public void invoke(final V8Object receiver, final V8Array parameters) {
   System.out.println(receiver.getString("first"));
  }
}

V8Map

J2V8 uses native handles (pointers) to C++ objects. These native handles must be released when they are no longer needed. For objects with a long life, especially for those used as keys in a map, managing them can be a pain. A V8Map has been added in which V8Objects can be used as keys. You can then release the handle to the V8Object immediately, and the V8Map will take over the memory management for you. For example:

 V8Map map = new V8Map();
 class PersonMapper implements JavaVoidCallback {
  @Override
  public void invoke(final V8Object receiver, final V8Array parameters) {
   map.put(receiver, receiver.getString("first"));
   receiver.release();
  }
}

Items can be retrieved from this map in another callbacks. When the Map is no longer needed, it can be released freeing all the memory it manages. To make it even easier to manage, the map can be associated with a V8 Runtime, and when the runtime is released, so will the map.

Putting it all together

Using these three new features of J2V8, building WebWorkers is relatively easy. You will need the latest snapshot version of J2V8, which is now available in Maven Central.


 
  snapshots-repo
  https://oss.sonatype.org/content/repositories/snapshots
  false
  true
 


 
  com.eclipsesource.j2v8
  j2v8_win32_x86
  3.0.0-SNAPSHOT
  compile
 

First register the Worker, terminate(), and postMessage() function handlers. The terminate and postMessage functions are added to the prototype of Worker. The start function is mapped directly to the Worker constructor. This way when new Worker() is called, the start method will be invoked.

private void configureWorker(V8 runtime) {
  runtime.registerJavaMethod(this, "start", "Worker", new Class[] { V8Object.class, String[].class }, true);
  V8Object worker = runtime.getObject("Worker");
  V8Object prototype = runtime.executeObjectScript("Worker.prototype");
  prototype.registerJavaMethod(this, "terminate", "terminate", new Class[] { V8Object.class, Object[].class }, true);
  prototype.registerJavaMethod(this, "postMessage", "postMessage", new Class[] {V8Object.class, String[].class}, true);
  worker.setPrototype(prototype);
  worker.release();
  prototype.release();
}

The Java implementation of Start is the most interesting. Web workers typically specify a file from which to load the script, however, for simplicity sake we will just pass the script as a string. In the start method, we seed the V8Executor with the script, and define a message handler called messageHandler. The Executor is marked as longliving. We then configure the worker so it can create additional workers. Finally, we register the worker with the existing runtime and start it.

public void start(V8Object worker, String... s) {
  String script = (String) s[0];
  V8Executor executor = new V8Executor(script, true, "messageHandler") {
    @Override
    protected void setup(V8 runtime) {
      configureWorker(runtime);
    }
  };
  worker.getRutime().registerV8Executor(worker, executor);
  executor.start();
}

The terminate and post message handlers are fairly straight forward. They both lookup the Executor from the current runtime and send a message. The postMessage only handles strings, but a more advanced version could serialize a complex object to JSON and pass it between runtimes.

  public void terminate(V8Object worker, Object... s) {
    V8Executor executor = worker.getRutime().removeExecutor(worker);
    if (executor != null) {
      executor.shutdown();
    }
  }
 
  public void postMessage(V8Object worker, String... s) {
    V8Executor executor = worker.getRutime().getExecutor(worker);
    if (executor != null) {
      executor.postMessage(s);
    }
  }

Finally, we put this all together by creating a main executor which creates a worker and executes a simple script on that worker. The script prints messages to the console. Finally, two messages are posted to the worker and then the worker is terminated.

  public void start() throws InterruptedException {
    V8Executor mainExecutor = new V8Executor(""
    + "var w = new Worker('messageHandler = function(e) { console.print(e[0]); }');\n"
    + "w.postMessage('message to send.');\n"
    + "w.postMessage('another message to send.');\n"
    + "w.terminate();\n") {
    @Override
    protected void setup(V8 runtime) {
      configureWorker(runtime);
    }
  };
  mainExecutor.start();
  mainExecutor.join();
}

The complete example is available on GitHub.

This is a release candidate for J2V8 3.0. Try it out and let me know if you hit problems.

For more J2V8 updates, follow me on twitter.

Ian Bull

Ian Bull

Ian is an Eclipse committer and EclipseSource Distinguished Engineer with a passion for developer productivity.

He leads the J2V8 project and has served on several …