Project Write-up

RustJava: An Embeddable JVM in Rust

RustJava is a Java virtual machine implementation written entirely in Rust. It targets early Java (roughly version 1.2), compiles to WebAssembly, and is designed to be embedded inside larger applications rather than run as a standalone JVM. This page is a walkthrough of why the project exists, how it's structured, and what trade-offs come with running a JVM under those constraints.

What It Is

RustJava is not a re-implementation of OpenJDK. It's not trying to keep up with modern Java releases, run a full standard library, or pass the TCK. The goal is much narrower: provide a small, embeddable Java runtime that can be linked into other Rust applications, compiled to WebAssembly, and extended with custom classes defined in Rust rather than Java.

The target version is Java 1.2. That's deliberate. Most of the historical software the project was built to support was written against 1.2-era APIs, and freezing the language target at that level avoids a long tail of features โ€” generics, lambdas, modules, value types โ€” that would massively expand the implementation surface without changing what RustJava can actually run.

Why It Exists

RustJava grew out of a concrete need. The WIE emulator needed a JVM that could be linked directly into the emulator binary, run in a no_std environment so it could be compiled to WebAssembly, and let WIPI and MIDP APIs be implemented in Rust rather than as a pile of Java glue code. Standard JVMs failed all three requirements: they expect a host operating system, they expect to be the top-level process, and they expect new classes to be defined as .class files at runtime.

The host environment for WIE is the browser, where shipping a native JVM is not practical to begin with, and the runtime requirements include features โ€” like callback-driven scheduling and cooperative async tasks โ€” that don't map cleanly onto an off-the-shelf JVM's threading model. RustJava became reusable enough that it now stands on its own outside the emulator.

Workspace Layout

The project is split into a small set of crates, each owning one piece of the runtime.

  • jvm โ€” the JVM core. Class loading, classes and instances, method and field metadata, value representations, the garbage collector, and the thread abstraction. Compiled as no_std; everything dynamic goes through alloc.
  • jvm_rust โ€” the execution engine. Bytecode interpretation, stack frame management, and the Rust-backed implementations of the JVM core traits. This is where actual Java code gets executed instruction by instruction.
  • classfile โ€” the .class file parser. Constant pool, attributes, fields, methods, and the opcode table. Decoupled from the rest of the runtime so it can also be used as a standalone analysis tool.
  • java_class_proto โ€” the Rust-defined class prototype system. This is the layer that lets a host application register Java classes whose methods are actually Rust functions.
  • java_runtime โ€” the bundled Java standard library, implemented as Rust-defined class prototypes. Covers java.lang, java.io, java.util, and the other 1.2-era packages an embedded app is likely to touch.

An embedding application can pull in just the pieces it needs. A user that only wants to parse class files can depend on classfile alone; a user that wants a full embedded JVM pulls in jvm, jvm_rust, and java_runtime.

Rust-Defined Class Prototypes

The most distinctive part of RustJava is how it lets host applications add classes to the runtime. In a normal JVM, every class either comes from a .class file on the classpath or from a JNI-implemented native library. RustJava adds a third option: a class prototype defined in Rust, where the class's fields and method signatures are declared as Rust data and the method bodies are async Rust functions.

From the running Java code's perspective, these classes are indistinguishable from classes loaded from a .class file. They participate in method dispatch, can be referenced by name, can be extended, and can throw and catch Java exceptions. Underneath, when the interpreter dispatches a method call to a prototype class, it hands off to the registered Rust function instead of fetching a bytecode array to interpret.

For embedders, this is the feature that makes the project worth building. Implementing java.lang.String or org.kwis.msp.lcdui.Graphics in Java and then having to plumb every primitive operation back out to the host through JNI is enormously more painful than writing the class once in Rust and registering it. The standard library shipped with RustJava itself is built this way.

The no_std and WebAssembly Story

Core crates are written as no_std, using the alloc crate for heap-allocated types. This means there's no implicit dependency on filesystems, threading primitives, or any other capability that the operating system would normally provide. The host application supplies whatever external integrations the JVM needs through explicit trait implementations.

On native targets the project links against tokio with the multi-threaded runtime; on WebAssembly it links against tokio with the single-threaded runtime instead. Apart from the executor swap, the same code runs in both environments. This matters because WIE's browser-hosted use case needs the JVM to coexist with the rest of the emulator on a single thread without needing host-level threading.

Async All the Way Through

Method dispatch in RustJava is async. The Method::run trait method returns a future, and the interpreter awaits the result. Bytecode interpretation itself is async; calls into Rust-defined prototype methods are async; class loading is async. This is unusual for a JVM and is a direct consequence of the embedding model.

The host needs to be able to suspend Java execution from outside โ€” for example, when a WIPI API call has to wait on a host-side audio buffer to drain, or when ARM code on the other side of an emulator boundary needs to run before Java can continue. Making the whole call stack async makes those suspensions cheap to express. A Rust-defined Java method just .awaits whatever it needs and the interpreter handles the rest. No additional thread-juggling, no callback inversion, no separate "blocking Java thread" abstraction.

What It Doesn't Try to Be

It's worth being explicit about scope. RustJava is not aiming to run modern Java applications. It does not aim for spec-conformance. It is not a replacement for OpenJDK or for any other production JVM. The standard library implementation is exactly as large as the embedding applications have required, and no larger.

Within that scope, though, the project does the thing it set out to do: run early-era Java code from inside another Rust program, on either native or WebAssembly, with the ability to extend the runtime entirely from the Rust side. That's a narrow niche, but it's one that no existing JVM fills well.

This page is a best-effort write-up โ€” some details may be inaccurate or out of date. If you spot an error, please flag it via the email on the home page.

Source

RustJava is MIT-licensed and developed on GitHub. The code, issue tracker, and contribution guidelines live there.