Skip to content

Doppio Developer Guide

John Vilk edited this page Jan 22, 2015 · 38 revisions

So you want to hack on doppio: maybe to integrate it into your project more closely, or to add support for JVM features that are currently busted, or whatever your heart desires. Start here to save yourself a lot of head-scratching.

Contents:

Adding Native Methods Using doppioh

Native methods allow doppio to run Javascript code from Java classes. They're critical to the execution of the JVM, but they can also be used to provide web functionality to your Java programs. For example, you might write a native method to pop up an alert message, or manipulate DOM elements, or anything else you can do with Javascript.

Step 1

First, identify the native method that you want to write: is it in the Java Class Library somewhere, or perhaps in some Java code you wrote? For this example, we'll assume we wrote the file classes/util/Test.java like so:

package classes.util;
class Test {
  static native int addTwo(int x);
  public static void main(String[] args) {
    System.out.println(addTwo(5));
  }
}

Step 2

doppio ships with a utility called doppioh, which we liken to the official javah. If you are in the parent directory of classes, you can point it as your compiled class file classes/util/Test.class to generate a native method stub like so:

./doppioh -classpath . -d natives classes/util/Test

The above command will create the file natives/classes_util_Test.js with a stub for the native method addTwo. If you want to write your native methods in TypeScript, you can do that too, but you'll need to point it to a directory with doppio interface definitions for typing purposes:

./doppioh -classpath . -typescript build/dev-cli -d natives classes/util/Test

You can also generate native method stubs for an entire package, e.g. java.lang:

./doppioh -classpath vendor/java_home/classes -d natives java.lang

... which will define the file natives/java_lang.js.

ProTip: Make sure you perform a grunt release-cli so you have a doppioh shortcut in your doppio directory.

Step 3

Next, you'll need to write your native method implementation. Here's the stub generated by doppioh:

registerNatives({
  'classes/util/Test': {
    'addTwo(I)I': function(thread, arg0) {
      thread.throwNewException('Ljava/lang/UnsatisfiedLinkError;', 'Native method not implemented.');
    }
  }
});

Let's implement that! Our method will add 2 to the specified integer, which is fairly straightforward:

registerNatives({
  'classes/util/Test': {
    'addTwo(I)I': function(thread, arg0) {
      // The |0 ensures that we return a 32-bit integer quantity. :)
      return (arg0 + 2)|0;
    }
  }
});

Step 4

Now that you have a new native defined, you'll need to make sure that doppio can find it at runtime. With the Node frontend, specify the directories with the -Xnative-classpath option, delimited by colons (:), much like the regular classpath. The browser frontend has a similar configuration option.

Asynchronous Native Methods

It's easy to write a doppio native method that invokes asynchronous browser functionality:

  1. Put the doppio thread invoking your native method into the ASYNC_WAITING state.
  2. Perform the desired operation.
  3. When your operation completes, invoke the doppio thread's asyncReturn function with the function's return value (if any), which will resume the thread with the specified return value.

Here's an example that 'sleeps' the thread for 50 milliseconds:

// Native methods can pull in doppio modules using 'require' and the path to the module.
// Even if you already have RequireJS or some other module system included on the page,
// this will work as we evaluate your native methods in an environment with our own
// `require` function.
var enums = require('./src/enums');
registerNatives({
  'classes/util/Test': {
    'sleep50()V': function(thread) {
      // This informs the thread to ignore the return value from this JavaScript function.
      thread.setStatus(enums.ThreadStatus.ASYNC_WAITING);
      // Sleep for 50 milliseconds.
      setTimeout(function() {
        // Wake up the thread by returning from this method.
        thread.asyncReturn();
      }, 50);
    }
  }
});

While this doppio thread is waiting for your sleep operation to complete, other doppio threads may run.

Troubleshooting

If the above instructions aren't enough to get your native running, try looking at other native method implementations in src/natives/*.ts. These contain examples of how to do all sorts of tricky things, like getting/setting fields on objects, converting between JVM and Javascript strings, working with long integers, and more.

Plugging doppio into a Frontend

As a full-featured JVM, doppio requires a number of files available at runtime to run properly. This guide will show you how to configure your environment to run doppio on your webpage.

Step 0: Build doppio.js

Follow the instructions in README.md to clone and build a release version of doppio. You will need the following files from the build/release directory when you deploy doppio:

  • ./doppio.js: The actual fully-built doppio library.
  • ./src/natives/*.js: Doppio's native methods. It dynamically loads these from the file system at runtime, much like class files.
  • ./vendor/java_home/classes: The Java Class Library.
  • ./vendor/java_home: Doppio's Java Home. Some of the Java Class Library classes rely on files in here (for e.g. locale information).

Step 1: Include and set up BrowserFS

doppio requires the in-browser filesystem, BrowserFS. Include it on your webpage before doppio.js, and ensure that you have the files described above included somewhere in the file system (most likely, you will set up an XmlHttpRequest file system that pulls in files via downloads).

For example, you might do the following to set up temporary storage at /tmp, system files at /sys, and writable storage at /home:

<script type="text/javascript" src="browserfs.min.js"></script>
<script type="text/javascript">
    // Wrap in a closure; don't pollute the global namespace.
    (function() {
      var mfs = new BrowserFS.FileSystem.MountableFileSystem(),
          fs = BrowserFS.BFSRequire('fs');
      BrowserFS.initialize(mfs);
      // Temporary storage.
      mfs.mount('/tmp', new BrowserFS.FileSystem.InMemory());
      mfs.mount('/home', new BrowserFS.FileSystem.LocalStorage());
      // The first argument is the relative URL to your listings file generated by the BrowserFS XHR
      // listings tool. In this example, the URL is <thiswebpage>/browser/listings.json
      mfs.mount('/sys', new BrowserFS.FileSystem.XmlHttpRequest('browser/listings.json', ''));
    })();
  </script>

BrowserFS also supports a wide variety of browser-local storage technologies, including HTML5 and IndexedDB, and butt storage like Dropbox. Doppio is able to read files, classes, and native methods from any of these storage mediums through BrowserFS.

Step 2: Invoking the JVM

First, make sure you include doppio.js on your webpage:

<script type="text/javascript" src="doppio.js"></script>

When you want to invoke the JVM, you can do so through a command-line style interface, or through a more traditional JavaScript interface. We'll describe the JavaScript interface first.

new doppio.JVM({
    // Paths to the Java Class Library (JCL) and other essential system classes, e.g. [/sys/vendor/java_home/classes]
    bootstrapClasspath: string[],
    // Non-JCL paths on the class path, e.g. [/path/to/my/classes]
    classpath: string[],
    // Path to JAVA_HOME, e.g. /sys/vendor/java_home
    javaHomePath: string,
    // Path where we can extract JAR files, e.g. /tmp
    extractionPath: string,
    // Paths where native methods are located, e.g. [/sys/src/natives, /path/to/my/natives]
    nativeClasspath: string[]
}, function(err, jvmObject) {
    // Called once initialization completes.
    jvmObject.runClass('classes.mypackage.MyClass', ['argument1', 'argument2'], function(success) {
      if (success) {
        // Execution terminated successfully
      } else {
        // Execution failed. :(
      }
    }); 
});

The command-line style interface emulates how you might invoke Java on the command line. For example, the command line java -classpath classes classes.mypackage.MyClass 43 would look like the following:

doppio.javaCli.java(
  // Arguments to the 'java' command.
  ['-classpath', 'classes', 'classes.mypackage.MyClass', '43'],
// Essential JVM information.
{
  // Path to the Java Class Library and other essential system classes, e.g. [/sys/vendor/java_home/classes]
  bootstrapClasspath: string[],
  // Path to Java home, e.g. /sys/vendor/java_home
  javaHomePath: string,
  // Path in the file system to extract zip files, e.g. /tmp
  extractionPath: string,
  // Location of native methods, e.g. [/sys/src/natives].
  nativeClasspath: string[]
}, function(success) {
  if (success) {
    // Class finished executing successfully.
  } else {
    // Execution failed. :(
  }
}, function(jvmObject) {
  // [Optional callback] Called once the JVM is instantiated.
});

Standard out/error/input

Your webpage can easily hook into the JVM's standard out, standard error, and standard input streams:

process.stdout.on('data', function(data) {
  // data is a Node Buffer, which BrowserFS implements in the browser.
  // http://nodejs.org/api/buffer.html
  alert("Received the following output: " + data.toString());
});
process.stderr.on('data', function(data) {
  // data is a Node Buffer, which BrowserFS implements in the browser.
  // http://nodejs.org/api/buffer.html
  alert("Received the following error: " + data.toString());
});
process.stdin.on('_read', function() {
  // Something is looking for stdin input.
  // You can write to stdin *at any time* to provide input. Thus, you can asynchronously
  // prompt the user for data on the webpage, and then write to stdin once input is provided.
  process.stdin.write(new Buffer(prompt("Input?")));
});

The JVM will print error messages to standard error (e.g. fatal exceptions), so you might want to hook into that stream for debugging purposes. Note that the data you receive on these streams is not neatly broken up, so if you redirect stderr to console.error, you could have many single character messages! You will want to introduce buffering to the nearest line before forwarding it to console.error.

Clone this wiki locally