A Weird Imagination

Running Java in JavaScript

Posted in

The problem#

Java and JavaScript are insufficiently easily confused. I had some code I wanted to run on the user's computer and not the server, and it specifically had to be in Java to reference a library written in Java. I found1 Doppio, a JVM written in TypeScript, but it had bitrotted enough that I was having trouble getting it to run, despite confirming it did what I wanted using the official demo, which provides an in-browser console-like interface.

The solution#

I was able to piece together a working simple page using Doppio, modified from the official example. It's in a repository on GitHub and should be straightforward to modify for your needs.

The details#

Official instructions#

Initially, I tried following Doppio's Getting Started, but grunt release showed

$ grunt release
Recompiling /home/anyoneeb/workspace/doppio/tasks/find_native_java.ts...
Recompiling /home/anyoneeb/workspace/doppio/tasks/ice-cream.ts...
[]
Recompiling /home/anyoneeb/workspace/doppio/Grunttasks.ts...
Loading "Gruntfile.js" tasks...ERROR
>> Error: Compilation error: null
>> null
Warning: Task "release" not found. Use --force to continue.

I inspected Grunttasks.ts and tried to see if the way grunt specified tasks had changed, but I wasn't getting anywhere.

Modifying the demo#

I decided didn't have to figure out the build system is supposed to work, since I had the working demo. Unfortunately, the logic of the demo is in a file app.js, which is minified. It does have a source map, so I was able to get a somewhat readable version of it, but the actual source is not in the repo. On top of that the demo does a lot of work to make a nice console-like interface, which is not relevant to just programmatically running some Java code. So, while I did copy some code out, modifying the demo was a dead end.

Copying files from the demo#

But I still had the problem that I couldn't build my own copy of Doppio. So I used the web developer tools to find the files the demo was using and grabbed its copies of doppio.js and browserfs.min.js, which the simple.html example referenced. I also grabbed doppio_home.zip, which, among other things, contained the JRE files from OpenJDK necessary to actually run a Java program.

Selecting the Doppio home path#

What is BrowserFS?#

Stepping back a bit, in order to make an environment that makes sense from Java's point of view, Doppio has to emulate a lot of the context of running as a desktop application that doesn't actually quite apply in a web browser. Part of that is BrowserFS, a library that presents a filesystem for the Java programs to use that supports various backends so the actual files can be in various places that make sense from a browser perspective like in IndexedDB or on Dropbox. (Unfortunately, the version used by Droppio is too old to support the File System API.) In addition to being used for the file system operations of the Java programs run by Droppio, it's also used to store the .java/.class/.jar files Droppio acts on and the libraries they reference including the core Java runtime.

The conclusion of all that is that in order to actually do anything with Droppio, we need a BrowserFS filesystem set up containing the Java runtime libraries and Droppio has to be given the correct path to them.

Setting up BrowserFS#

The Getting Started instructions do explain how to create a basic filesystem, including setting up the listings.json file using make_xhrfs_index. Since there's no HTTP request for getting a directory listing, that just dumps the directory listing to a JSON file, so BrowserFS can have it. Note that means you need to rerun make_xhrfs_index after adding or moving a file or BrowserFS won't know it's there.

I unzipped the doppio_home.zip file from the demo and pulled out the vendor directory, as that's what the instructions said was needed.

BrowserFS console commands#

As it can be confusing what is actually in the BrowserFS filesystem, it can be useful to inspect it from the web developer tools console. In order to get access to BrowserFS, load the fs object (if the script did not already put it in the global variables):

const fs = BrowserFS.BFSRequire("fs")

Then you can list a directory with fs.readdirSync():

fs.readdirSync('/')
(3) ['tmp', 'home', 'sys']

Or read a file with fs.readfileSync():

fs.readFileSync('/sys/listings.json').toString()
'{"doppio":{"app.js":null,"browserfs.min.js":null,…'

doppioHomePath error messages#

If the doppioHomePath is completely wrong, it will throw an exception

TypeError: Cannot read properties of null (reading 'throwNewException')

and there will be a variable with the Java type name "Ljava/lang/Thread;". Since it fails throwing an exception, nothing will be output, so it won't even be obvious anything happening other than it taking a long time to load unless the web developer tools are open.

But following the Getting Started instructions to point doppioHomePath to a directory containing vendor/java_home, it prints the following message to the console:

Error constructing JVM: Failed to initialize Ljava/lang/reflect/Constructor;

Referencing doppio_home.zip#

Since I knew it worked in the demo, in attempt to recreate the conditions of the demo as closely as possible, I was able to fix that error by referencing the entire doppio_home.zip instead of taking just the vendor/ directory.

BrowserFS has a backend for loading a ZIP file and making it look like a directory, which is used by the official demo to load doppio_home.zip, so I figured it would be simplest to just copy that exactly. One minor complication is that then the filesystem isn't initialized until after the request to load that ZIP file has completed, but that's easy to deal with.

The code to load a ZIP file looks like

const mfs = new BrowserFS.FileSystem.MountableFileSystem();
const buffer = BrowserFS.BFSRequire("buffer").Buffer;
const t = new XMLHttpRequest();
t.open("GET", "doppio_home.zip");
t.responseType = "arraybuffer";
t.addEventListener("load", function(e) {
  mfs.mount("/doppio_home",
    new BrowserFS.FileSystem.ZipFS(
      new buffer(t.response)));
  // … then actually run Doppio now that it's loaded.
});
t.send();

Then the correct doppioHomePath is /doppio_home.

Running a program#

doppio_home.zip includes a Java compiler, which the example uses to compile and run the simple program App.java (it has a string of that file and writes to it /tmp/App.java). Here's the code to actually compile and run the program:

Doppio.VM.CLI(
  ['-classpath', '/doppio_home',
   'classes.util.Javac', '/tmp/App.java'],
  {
    doppioHomePath: '/doppio_home'
  }, function(exitCode) {
    if (exitCode === 0) {
      Doppio.VM.CLI(
        ['-classpath', '/tmp', 'App'],
        {
          doppioHomePath: '/doppio_home'
        });
    }
  });

Loading additional files#

I added to the example code that I copied out of the demo to load files from the user's computer into the BrowserFS filesystem, as most likely the reason to be running in the browser is to get access to some data on the user's computer that you don't want to leave their computer. You could modify that code to run a Doppio command after loading the files or add a separate button to do so.


  1. Specifically, I remembered attending the talk at PLDI 2014. 

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.