Hands-on with Java and Wasm

Compiling Java code to WebAssembly is an efficient way to get it running in a web browser, and you get a serious performance boost. Let's check it out.

racing speed runners binary compete
Thinkstock

WebAssembly, or Wasm, provides a generalized, lightweight native binary format for any language that can be compiled to it. Once the source language is transformed into WebAssembly, that compact binary can run in a variety of contexts, from cloud virtual machines and desktops to IoT and mobile devices. One of the most interesting environments Wasm supports is the web browser. In this article, we’ll look at how to compile your Java code into WebAssembly, then we’ll run a demo application in a web server and see it in action.

Why use WebAssembly for Java?

Wasm provides a way to execute Java on the front end in the web browser, where your Java API can be called from JavaScript.

WebAssembly compilers generate a binary from the Java source (or from the bytecode) and that binary is executed by the browser using the host operating system capabilities, so you get OS-level performance. The Java code gets turned into a Wasm binary, which exposes hooks you call from JavaScript.

Wasm is available on all the major modern browsers, so you can use it now. It offers superior performance for demanding tasks in the language of your choice. Video encoding, graphics, and other intensive data processing are good candidates for this technology. Some projects are using Wasm to address blockchain's performance limitations.

If you’ve been around Java for a while, you might think about Java applets when you hear about Java in the browser. Rest assured, Wasm is a completely different animal. It is a modern, high-performance technology, designed from the ground up as a secure sandboxed runtime. With portability, performance, security, and polyglot support, Wasm is a vital piece of the technology landscape today.

Java to Wasm: How it works

Transforming a Java program into WebAssembly is a bit tricky. There is not a fully acceptable golden path that I can recommend. Several projects exist, but they are not mature or well-documented. Java as a language presents a few difficult problems for a WebAssembly transpiler to solve, like garbage collection and reflection. These seem to be the source of the delay in arriving at a first-class Java Wasm solution.

GraalVM currently is building support for outputting to Wasm with the native-image tool (as a target binary in the --features flag). If the proposal succeeds, that likely will become the recommended path. GraalVM already has the capacity to execute Wasm, and generally is an excellent project. Once GraalVM can generate Wasm binaries, it will be well worth exploring.

In the meantime, there are a handful of libraries for handling Java-to-Wasm compilation:

We’re going to use TeaVM for our Java Wasm compilation. I asked TeaVM's creator, Alexey Andreev, why Java is lagging behind other languages in Wasm implementations. He told me, “In brief: it's not hard to write your own GC or exception handling. It's hard to implement your own GC in Wasm due to lack of access to stack.” WebAssembly has limited stack access for security reasons. Instead of looking at ways to allow stack access securely, as Andreev has proposed, Wasm is opting to build garbage collection into Wasm itself. (See the WebAssembly project page on GitHub to follow the progress of garbage collection in Wasm.)

Compiling Java to Wasm with TeaVM

TeaVM is meant to be a lightweight library for creating Wasm out of Java. It draws inspiration from Google Web Toolkit but builds off of the bytecode instead of the source. Because of this, it can handle other JVM languages, like Scala. TeaVM gives us a quick way to look at a Java Wasm project with one of its sample projects, which we will use for the demonstration.

Setting up TeaVM and the sample project

To use TeaVM you’ll need a JDK of Java 8 or higher installed. We’re going to build the latest TeaVM version and install it into our local repository, then build one of the sample projects that uses Wasm to see how it accomplishes the Java-to-Wasm compilation in the browser transformation.

You’ll need Git installed to clone the TeaVM project: Git clone https://github.com/konsoletyper/teavm.git. At the time of writing, TeaVM is on 0.8.0-SNAPSHOT

Once the project is checked out, you need to build the TeaVM library itself and install it to your local repository. Go to the root directory, and type: /teavm/gradlew. That’ll use the standalone Gradle wrapper executable to build TeaVM and install it locally.

Once that’s done, you can go to the /teavm/samples/pi directory and type: ../../gradlew war. That tells Gradle to build the Pi sample project to a WAR file. Once that completes, there will be a /teavm/samples/pi/build/libs/pi.war file that we can deploy to a servlet container.

In my case, I’m running Ubuntu for this demo, so I installed Tomcat as a service (sudo apt-get install tomcat9) and then copied the pi.war file into the /webapps directory (sudo cp /teavm-wasm/target/pi.war /var/lib/tomcat9/webapps/). Next, I used sudo systemctl start tomcat9 to start Tomcat. Another way is with the start.sh/.bat script in the /bin directory, which works if you've downloaded the standalone version of Tomcat. 

Note that the Pi sample demonstrates both the JavaScript and Wasm output capabilities of TeaVM. We'll stick with Wasm.

The Pi demo for Wasm

The application should now be running on localhost:8080/pi. If you visit that URL in the browser, you’ll see a simple user interface with two links, one for JavaScript and one for WebAssembly. Click WebAssembly and you’ll see a view like the one shown below.

UI for the Wasm demo application. IDG

Figure 1. The WebAssembly Pi interface

This interface lets you specify the number of digits to calculate pi to. It uses Java that has been compiled to Wasm for the calculation, and it calls the code from JavaScript. Let's see how this calculation is accomplished.

If you look at the /teavm/samples/pi project, it is a standard Maven/Gradle layout that includes both Java and web application sources. These are stored at src/main/java and src/main/webapp, respectively. Look at the single Java class in src/main/java/org/teavm/samples/pi/PiCalculator.java and you’ll see that it is a normal Java class called PiCalculator. The class defines a main method, which takes a single argument from args[] as the number of digits to calculate. It uses an inner class named PiDigitSpigot to do the actual crunching of pi, and relies on a single Java library, java.math.BigInteger. Overall, it’s a typical garden-variety Java program. In fact, if you go to /teavm/samples/pi/build/libs/classes/main and type java org/teavm/samples/pi/PiCalculator 50, it will happily compute pi to 50 digits for you on the command line.

Now let’s look at the web application side of things, in teavm/samples/pi/src/main/webapp. The wasm.html is the one we are interested in. Most of the file is basic HTML, but there’s some interesting JavaScript to consider, shown in Listing 1.

Listing 1. The web application for calculating pi


<script type="application/javascript">
  let runner = null;
  function init() {
    TeaVM.wasm.load("wasm/pi.wasm", {
      installImports(o, controller) {
        function putwchars(address, count) {
          let instance = controller.instance;
          let memory = new Int8Array(instance.exports.memory.buffer);
          let string = "";
            for (let i = 0; i < count; ++i) {
              string += $rt_putStdoutCustom(memory[address++]);
            }
          }
          o.teavm.putwcharsOut = putwchars;
          o.teavm.putwcharsErr = putwchars;
        },
      }).then(teavm => {
        this.instance = teavm.instance;
        runner = n => teavm.main([n.toString()]);
        document.getElementById("run").disabled = false;
      })
  }
  function calculate() {
    var count = parseInt(document.getElementById("digit-count").value);
    runner(count);
  }
  init();
</script>
</head>
</body>
<div>
  Digit count:
  <input type="text" id="digit-count" value="1000">
  <button onclick="calculate()" id="run" disabled>Run</button>
</div>
<div id="stdout"></div>
</body>

To begin with, the line TeaVM.wasm.load("wasm/pi.wasm", { ... begins an import of the pi.wasm file from the server. This is a wrapper for the built-in WebAssembly.instantiate() that tells the browser to load a Wasm file. The object passed as the second parameter of the load method configures, and the .then() call provides a callback function to handle what happens once the WebAssembly program is ready.

No documentation is yet available for TeaVM.wasm.load, but you can find the source code on GitHub

What we are most interested in is the callback function, where we see the line: runner = n => teavm.main([n.toString()]);. That is where we invoke our Java main() function in JavaScript, passing the number of digits to calculate as the argument.

Now let’s see how the PiCalculator.java file made its transformation to pi.wasm. In the /pi/build.gradle.kts file, the TeaVM plugin is configured to point at the Java file, as shown in Listing 2.

Listing 2. A Java-to-Wasm transformation


teavm {
    js {
        addedToWebApp.set(true)
    }
    wasm {
        addedToWebApp.set(true)
    }
    wasi {
        outputDir.set(File(buildDir, "libs/wasi"))
        relativePathInOutputDir.set("")
    }
    all {
        mainClass.set("org.teavm.samples.pi.PiCalculator")
    }
}

You can see the TeaVM plugin is configured with mainClass.set("org.teavm.samples.pi.PiCalculator"), in the all field because it applies to both JavaScript and Wasm (that is, the project outputs both JavaScript and Wasm versions). If you look at build/generated/teavm/wasm/ you can see where TeaVM has dropped the Wasm files.

Conclusion

Wasm is a promising technology that enables Java to run in the web browser with good performance. While it’s still a work in progress, libraries like TeaVM give us a simplified means of working with Java and WebAssembly. TeaVM lets us compile Java code to a compact binary that is executed in the browser using the capabilities of the host operating system. Wasm is an important piece of the Java technology landscape, and it is still in the relatively early stages of development. Integrating garbage collection could expand its potential in several key areas. It's definitely one to watch.

Copyright © 2023 IDG Communications, Inc.