Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

238 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rustc_codegen_jvm

License: MIT/Apache-2.0 CI Rust: Nightly Java: 8+

A custom Rust compiler backend that compiles Rust directly to Java Virtual Machine (JVM) bytecode, enabling you to compile crates into a runnable .jar compatible with Java 8+.

Demo

This backend transparently compiles Rust constructs to Java classes and interfaces, enabling rich interop between JVM and Rust code at a level mostly unreachable by traditional FFI solutions.

It also enables modern Rust code to run on older platforms outside the reach of current native targets, and has integrated upstream changes into OpenJDK's C2 JIT compiler which can make the JVM faster for everyone, including ~1.85x faster 128-bit multiplication on x86.

By leveraging a "virtual MMU" translation layer, it supports raw pointers with complex pointer arithmetic, transmute, and unions. It also supports key parts of the Rust standard library, including networking, async/await, threading, unwinding, allocation, as well as file system operations, STDIO, and more.

All official Rust coretests pass, with 99.6% verified by CI on every commit (11 slow cases are skipped). Similarly, 99.9% of alloctests pass on the same CI, with only 2 cases skipped due to slowness.

Note

This project is in an active mid-stage of development. While it supports the vast majority of the Rust language, edge-case bugs are continually being ironed out. The ultimate goal is potential upstreaming into main rustc.

Stars, contributions, and feedback are highly welcome and appreciated!

Quickstart

Clone the repo and install cargo-jvm

If on Windows, please use PowerShell so $PWD will work.

git clone https://github.com/IntegralPilot/rustc_codegen_jvm
cd rustc_codegen_jvm
cargo install --path cargo-jvm
cargo jvm setup "$PWD"

Make a new hello_world project

Please make sure you are on the latest Rust nightly. If it isn't your default toolchain, you might have to use cargo +nightly.

cargo new hello_world --bin
cd hello_world

Build and run on JVM

cargo jvm run

You should see "Hello, world!" printed to the console.

Then, head down to Usage to learn how to integrate it into your project.

Table of Contents

  1. Why is this useful?
  2. Demos
  3. Features & Standard Library Support
  4. Current Limitations
  5. How It Works
  6. Interop Model
  7. Prerequisites
  8. Usage
  9. Running Tests
  10. Project Structure
  11. Contributing
  12. License

Why is this useful?

Interop is deeper and more ergonomic than FFI or bridge solutions

Rust enums, generics, function pointers, unions, and traits map directly onto JVM classes and interfaces (see Interop Model). Because of this, rustc_codegen_jvm achieves a level of ergonomic interop with Java that native FFI solutions cannot easily match. For example, you can implement a Rust trait directly on a Java class and pass it as &dyn Trait to Rust (test and demo), or pass a standard Java lambda directly to a Rust function expecting a Fn closure (test and demo).

Java

import org.rustlang.runtime.Utf8View;
import my_crate.NamedCounter;
import my_crate.Accumulator;
import static my_crate.my_crate.*;

public class Main {
    // Implement a Rust trait directly on any Java class
    private static class JavaAccumulator implements Accumulator {
        private int sum = 0;

        @Override
        public int add(int amount) {
            this.sum += amount;
            return this.sum;
        }
    }

    public static void helloWorld() {
        System.out.println("Hello from Rust!");
    }

    public static void main(String[] args) {
        // 1. Interact with Rust types and methods
        NamedCounter counter = NamedCounter.new(Utf8View.fromJavaString("JVM-Counter"));
        counter.increment();
        System.out.println("Counter: " + counter.count);

        // 2. Pass a standard Java lambda directly to a Rust Fn closure
        int result = apply_twice(val -> val * 3, 2);
        System.out.println("Lambda output: " + result);

        // 3. Pass a Java trait implementation to Rust dynamic dispatch
        JavaAccumulator acc = new JavaAccumulator();
        int finalSum = run_accumulation(acc);
        System.out.println("Accumulator sum: " + finalSum);

        // 4. Trigger a foreign function call from Rust back into Java
        trigger_hello();
    }
}

Rust

unsafe extern "C" {
    // Link directly to the static method on the Java Main class
    #[link_name = "jvm:static:Main:helloWorld"]
    fn hello_world();
}

pub struct NamedCounter {
    pub name: &'static str,
    pub count: u32,
}

impl NamedCounter {
    pub fn new(name: &'static str) -> Self {
        NamedCounter { name, count: 0 }
    }
    pub fn increment(&mut self) {
        self.count += 1;
    }
}

pub fn apply_twice(callback: &dyn Fn(i32) -> i32, value: i32) -> i32 {
    callback(callback(value))
}

pub trait Accumulator {
    fn add(&mut self, value: i32) -> i32;
}

pub fn run_accumulation(acc: &mut dyn Accumulator) -> i32 {
    acc.add(10) + acc.add(5)
}

pub fn trigger_hello() {
    unsafe { hello_world(); }
}

Runs everywhere a JVM does, even on legacy systems

Because the compiler targets standard JVM bytecode rather than native machine code, compiled output can run on platforms far outside the reach of modern native Rust targets. It supports any environment with JVM 8+ compatibility.

Operating System Native Rust Minimum JVM 8 (rustc_codegen_jvm)
Windows Windows 10 Windows Vista SP2 / 7 SP1
macOS 10.12 Sierra 10.8.3 Mountain Lion
Linux Kernel 3.2, glibc 2.17 Kernel 2.6.28, glibc 2.9+
Solaris Solaris 11.4 Solaris 10

Compiling directly to JVM bytecode also avoids the deployment friction of native shared libraries in restricted environments. This makes compiled JARs highly portable across sandboxed environments (such as Minecraft mod loaders) and Android platforms (via DEX conversion).

Benefits the wider JVM ecosystem through JIT compiler improvements

Developing this backend helps inspire me to find opportunities to optimise OpenJDK's upstream HotSpot C2 compiler. Contributions benefit the entire JVM ecosystem (including Java and Kotlin).

One merged optimisation (OpenJDK PR #30174) sped up 128-bit multiplication by ~1.85x on x86 targets. Another contribution under review (OpenJDK PR #30485) introduces internal range-check elimination in loops for common compiled patterns.

Facilitates gradual migration of JVM codebases to Rust

Transitioning a large production JVM codebase to native Rust is rarely feasible in a single step. rustc_codegen_jvm enables an incremental migration path where new or refactored components are written in Rust while remaining fully compatible with the existing JVM application. Once a rewrite is complete, the Rust code can either be target-switched to native or kept on the JVM target for fast iteration and cross-platform consistency.

Rapid debugging and hot reloading iteration

Once shared standard-library artifacts are cached, incremental compilation for crates is fast. Leveraging the JVM's mature debugging, hot-reloading, and tracing ecosystem (such as JFR and IDE debuggers) opens up rapid iteration workflows that are traditionally difficult with native Rust targets.

Additionally, the virtual MMU layer can catch raw pointer Undefined Behavior (UB) early, throwing structured Java exceptions with accurate stack traces and LineNumberTable information.

Demos

The following example programs live in tests/, are compiled with the standard library to JVM bytecode, and are verified in CI on every commit:

Standard Library Demonstrations

Example Demonstrates
Alloc Complex allocations: binary trees, heaps, linked lists, vectors, strings, Arc/atomics, and drop/cleanup semantics.
Threads Multi-threading, scoped threads, mutexes (with poisoning), RWLocks, barriers, condition variables, and TLS.
Panic Unwinding, catching static/dynamic panic payloads, resuming unwinds, and custom panic hooks.
Async / Await Multi-poll futures, nested and recursive async work, async closures and trait methods, dyn Future, cross-thread execution, cancellation, and unwinding across suspension points.
STD File system operations, command-line arguments, environment variables, standard I/O, and runtime context.
Network Loopback TCP and UDP, DNS, timeouts, nonblocking sockets, peeking, vectored I/O, multicast, cloning, shutdown, and socket options.

Standalone Rust Programs

Example Demonstrates
Raw Pointers Pointer identity, dereferencing, casts, offset arithmetic, fat pointers, and DST handling.
Unions unsafe union storage, field nesting, and reinterpretation.
Enums & Structs Complex nested data structures, tuples, arrays, and slices.
Traits Trait implementations, trait objects, and dynamic dispatch.
Function Pointers Function pointers as values, struct fields, parameters, returns, and generics.
Iterators Combinators (map, zip, chain, flatten), double-ended traversal, and custom iterators.

Java & Rust Interop

Example Demonstrates
Lambda Callbacks Passing native Java lambdas directly into Rust functions expecting Fn closures.
Trait Implementors Implementing a Rust trait on a Java class and passing it to Rust dynamic dispatch (&dyn Trait).

Features & Standard Library Support

The vast majority of the Rust language is supported, including generics, traits, coroutines, closures, control flow, data structures, and unsafe features (raw pointer arithmetic, transmutes, and unions).

Official upstream test suites

The following pass rates are a quality gate enforced by CI, in both debug and release mode.

Suite Tests passed Tests skipped (too slow) Total tests Pass %
coretests tests & benches 2799 11 2810 99.61%
alloctests tests & benches 1347 2 1349 99.85%

Standard Library Support Matrix

Beyond core and alloc, a large amount of the std is supported too, though a JVM OS implementation which is overlayed on top of upstream.

Subsystem Status Details
Threads & Sync Supported Thread spawning, scoped threads, Mutex, RwLock, Condvar, TLS
Async & Futures Supported Async functions, blocks, closures and trait methods; boxed/recursive dyn Future; cancellation
Panic Unwinding Supported Complete unwinding stack, catch_unwind, panic hooks, and abort-on-double-panic semantics
Stdio & Env Supported println!, eprintln!, stdin, env::args, env::vars
Time & Random Supported SystemTime, Instant, standard entropy seeds
File System (std::fs) Supported Java NIO files, directories, positional and vectored I/O, nanosecond timestamps, opaque file identity, atomic POSIX creation permissions, links, locks, and paths
Networking (std::net) Supported Java NIO TCP/UDP, IPv4/IPv6, DNS, timeouts, nonblocking sockets, vectored I/O, peeking, multicast, and socket cloning
Processes (std::process) Planned Spawning, managing, interacting with, and terminating child processes

Compiled JAR files emit rich JVM metadata (LineNumberTable, parameter names, nested class info), ensuring seamless IDE integration (autocomplete, tooltips, refactoring) in IntelliJ IDEA and detailed stack traces during debugging or profiling with JFR.

Current Limitations

  • Java NIO's DirectoryStream does not expose the native directory-entry type (d_type). DirEntry::file_type therefore performs a no-follow metadata lookup.
  • Java's public socket API does not expose unicast IP_TTL or IPV6_V6ONLY. The corresponding std::net getters and setters return ErrorKind::Unsupported; multicast TTL is supported.
  • Code that heavily relies on raw pointer arithmetic executes through the Virtual MMU translation layer, which introduces extra memory allocations and GC overhead compared to structured stack/heap access.
  • The backend relies heavily on HotSpot's JIT (C2) compiler for runtime optimisation rather than aggressive AOT passes during compilation.
  • Although upstream coretests pass, niche compiler edge cases may still trigger Internal Compiler Errors (ICEs).
  • The quote!() proc macro is currently unsupported.
  • If you pass a Rust object to Java (or another JVM language) code, there's no guarantee it has to follow Rust's rules (i.e. ownership, borrowing, or drop semantics). This matches what happens on native targets when interacting with FFI, but potentially can be improved for the JVM in future.
  • Backtraces on panic currently work by raising a JVM exception on Rust panic (as part of the unwind process, because unwinding uses try-except-finally), and the trackback comes from the JVM, not Rust's std::traceback which is currently unimplemented and emits some warnings during std compile.

How It Works

Compilation Pipeline

graph TD
    A[Rust Source Code] -->|rustc frontend| B(MIR)
    B -->|lower1| C(OOMIR)
    C -->|optimise1| D(Optimised OOMIR)
    D -->|lower2| E[JVM .class files]
    E -->|java-linker| F[Executable .jar]

    style A fill:#f9d0c4,stroke:#333,stroke-width:2px
    style C fill:#d4e6f1,stroke:#333,stroke-width:2px
    style F fill:#d5f5e3,stroke:#333,stroke-width:2px
Loading
  1. rustc Frontend: Parses and type-checks code, lowering it to Mid-level IR (MIR).
  2. lower1: Transforms MIR into a custom "Object-Oriented MIR" (OOMIR) matching JVM constructs.
  3. optimise1: Applies constant folding, constant propagation, dead code elimination, and algebraic simplification.
  4. lower2: Translates OOMIR to bytecode, computes stack map frames, and serialises .class files via ristretto_classfile.
  5. java-linker: Bundles generated .class files and the runtime environment into a self-contained .jar with manifest metadata.

Virtual MMU Layer

To enable unsafe Rust features without violating JVM bytecode verification or breaking garbage collection, the runtime uses a custom translation layer (Pointer.java). The basics are:

  • Safe Rust structures remain standard JVM objects. If memory is accessed via byte-offset raw pointers, the runtime lazily encodes the object into a little-endian byte array, modifies it, and decodes it back.
  • Exposing numeric pointer addresses triggers allocation in a synthetic 64-bit address space tracked by a thread-safe navigable map (ALLOCATION_RANGES).
  • Synthetic allocations use WeakReference entries and ReferenceQueue hooks to prevent tracking metadata memory leaks.
  • Unaligned or arbitrary byte-offset atomic ops map to striped locks (ATOMIC_STRIPES), maintaining thread safety up to SeqCst.

Drop / RAII

Because the JVM uses garbage collection, rustc_codegen_jvm preserves Rust's deterministic RAII semantics by emitting explicit drop calls at compile time.

  • Drop elaboration (scopes, drop order, drop flags) is fully handled by rustc's frontend. The backend emits direct bytecode calls at every MIR Drop terminator, executing cleanup synchronously at scope exit rather than relying on GC finalisation.
  • This emits static calls for structs, unrolled loops for arrays, dynamic helpers (Pointer.dropSlice) for slices, and per-variant virtual methods (_rust_drop_fields) for enums to prevent dropping inactive variant payloads.
  • Types needing drop implement a simple runtime interface (public interface RustDrop { void rustDrop(); }). Dynamic cases like dyn Trait objects or pointers use runtime instanceof checks (Pointer.dropRustValue) to dispatch destructors safely.
  • JVM GC handles raw memory reclamation, but all Rust Drop side effects (closing handles, releasing locks) run eagerly as normal Java method calls at standard Rust scope boundaries.

Interop Model

Rust constructs map directly to JVM structures without requiring JNI wrapper code:

Rust Construct JVM Representation
struct Standard Java class with 1:1 mapped fields and methods
enum Abstract base class with concrete subclasses per variant
union Class backed by contiguous byte-array storage with reinterpretation helpers
trait Java interface
fn(A, B) -> R Single-method Java interface (Functional Interface)
impl methods Class instance methods
&dyn Trait Java interface reference
*const T / *mut T Shared pointer wrapper (org.rustlang.runtime.Pointer)

Prerequisites

  • Rust Nightly (configured automatically via rust-toolchain.toml)
  • JDK 8+ (java, javac, and jar must be available on PATH)
  • Python 3.8+
  • Git
  • cargo-jvm - see install instructions in its README

Usage

cargo-jvm is used to make building and running Rust projects on the JVM as seamless as possible. It wraps the standard Cargo workflow, forwarding all ordinary Cargo selection and feature arguments. For instructions on installing cargo-jvm, see its README.

The following commands assume you are within a Rust project directory that you wish to compile/run using the JVM, and which is configured to use the latest nightly toolchain.

Building

cargo jvm build
cargo jvm build --release --features serde
cargo jvm build --workspace -j 8

Build artifacts are placed under target/jvm-unknown-unknown/debug or release, just as with an explicit Cargo target.

Binary and cdylib artifacts are JARs, but ordinary Rust libraries remain .rlib inputs.

Binaries, cdylib and ordinary libraries can all be packaged into fully self-contained JARs using cargo jvm package (see below).

Running

Build and launch a binary with the correct JAR and classpath automatically:

cargo jvm run
cargo jvm run --release

The launcher defaults to a 16 MiB JVM thread stack. It can be adjusted, and arbitrary Java options and program arguments can be provided:

cargo jvm run --stack 32m --java-arg=-ea -- program-argument

Packaging into self-contained JARs

Create a self-contained distributable JAR with all required org.rustlang.runtime classes.

cargo jvm package --release
cargo jvm package --output dist/my-app.jar

Default outputs go to target/jvm-package/<profile>.

Use --bin or --lib when a package contains both and a single --output is requested:

cargo jvm package --lib --output dist/my-library.jar
cargo jvm package --bin my-app --output dist/my-app.jar

Tests

Rust test targets can also run on the JVM:

cargo jvm test
cargo jvm test --release --workspace
cargo jvm test -- --nocapture

This compiles Cargo's test targets with --no-run, then launches every reported test JAR on the JVM.

Other

cargo jvm doctor reports the cargo-jvm version and source commit (when available), the configured backend's current Git commit, Java, Cargo, rustc, target and runtime paths. Please run this if you are reporting a bug.

cargo jvm update pulls and rebuilds the backend.

Run cargo jvm --help for all options.

Running Tests

Run the binary, multi-crate, Rust/Java integration, and cargo-jvm workflow self-test suite:

python3 Tester.py             # Debug build testing
python3 Tester.py --release   # Release build testing

Run the upstream coretests verification suite (add --include-default-ignored to run really slow cases too):

python3 Coretests.py             # Debug mode
python3 Coretests.py --release   # Release mode

Run the upstream alloctests verification suite (add --include-default-ignored to run the explicitly skipped cases too):

python3 Alloctests.py             # Debug mode
python3 Alloctests.py --release   # Release mode

Project Structure

.
├── src/                      # Compiler backend implementation
│   ├── lower1/               # MIR -> OOMIR lowering
│   ├── optimise1/            # OOMIR optimisation passes
│   ├── lower2/               # OOMIR -> Bytecode generator
│   └── oomir.rs              # OOMIR definitions
├── java-linker/              # JAR packaging and manifest utility
├── cargo-jvm/                # `cargo jvm` build, run, test and package command
├── runtime/                  # Core Java runtime support library
├── std/                      # Standard library JVM patch overlays
├── tests/                    # Integration, binary, and multicrate tests
│   └── cargo_jvm/            # Real build/run/test/package demo projects
├── build.py                  # Master build script
├── test_harness.py           # Shared test execution utilities
├── Tester.py                 # Main test suite runner
├── Coretests.py              # Upstream rustc coretests runner
└── Alloctests.py             # Upstream rustc alloctests runner

Contributing

Contributions, bug reports, and feature requests are welcome!

If you are interested in contributing but unsure where to start, feel free to open a thread on the Discussions board and I can point you in the right direction about what's useful right now.

For significant changes or architecture proposals, please open an issue and/or discussion first to discuss the design.

License

Dual-licensed under either of:

at your option.

About

Toolchain to create JVM-ready Java bytecode from Rust MIR.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages