Join us for a virtual meetup on Zoom at 8 PM, July 31 (PDT) about using One Time Series Database for Both Metrics and Logs 👉🏻 Register Now

Skip to content
On this page
Technical
May 13, 2025

How to Supercharge Your Java Project with Rust - A Practical Guide to JNI Integration with a Complete Example

This article provides a hands-on guide to integrating Rust and Java through the Java Native Interface (JNI), covering everything from cross-language invocation and unified logging to asynchronous execution and error handling. It’s accompanied by a fully working open-source demo project, rust-java-demo, to help you quickly get started building cross-language applications.

Introduction

Rust and Java are both widely used languages, each excelling in different domains. In real-world scenarios, it’s often beneficial to combine them for more effective system-level and application-level programming:

  • In a Java application, you may want to bypass the Garbage Collector (GC) and manually manage memory in performance-critical regions.
  • You might port a performance-sensitive algorithm to Rust for speed and implementation hiding.
  • Or in a Rust application, you may wish to expose functionality to the Java ecosystem by packaging it as a JAR.

In this post, we’ll walk through how to organize and integrate Rust and Java in the same project. The focus is practical, with concrete code examples and step-by-step explanations. By the end, you'll be able to create a cross-language application where Rust and Java interact smoothly.

Background: Understanding JNI and Java Memory Management

The Java Native Interface (JNI) is the bridge between Java and native code written in C/C++ or Rust. While its syntax is relatively simple, JNI is notoriously tricky in practice due to its implicit memory and thread management rules.

Memory Segments in Java Runtime

Java applications operate across several memory segments:

  • Java Heap: Where Java objects live, automatically managed by the Garbage Collector.
  • Native Memory: Memory allocated by native code (e.g., Rust), not directly managed by the GC—requiring explicit care to avoid memory leaks.
  • Others: Miscellaneous segments, such as code caches and metadata for compiled classes. Understanding these boundaries is key to writing performant, memory-safe cross-language code.

A Practical Integration: The rust-java-demo Project

Let’s walk through a real-world example: our open-source rust-java-demo repository demonstrates how to integrate Rust into Java applications seamlessly.

Packaging Platform-Specific Rust Libraries into a Single JAR

Java bytecode is platform-independent, but Rust binaries are not. Embedding a Rust dynamic library inside a JAR introduces platform dependency. While building separate JARs for each architecture is possible, it complicates distribution and deployment.

A better solution is to package platform-specific Rust libraries into different folders within a single JAR, and dynamically load the correct one at runtime.

After extracting our multi-platform JAR (jar xf rust-java-demo-2c59460-multi-platform.jar), you’ll find a structure like this:

(Figure 1: The Example)
(Figure 1: The Example)

We use a simple utility to load the correct library based on the host platform:

java
static {
    JarJniLoader.loadLib(
        RustJavaDemo.class,
        "/io/greptime/demo/rust/libs",
        "demo"
    );
}

This approach ensures platform flexibility without sacrificing developer or operational convenience.

Unifying Logs Across Rust and Java

Cross-language projects can quickly become debugging nightmares without unified logging. We tackled this by funneling all logs—Rust and Java—through the same SLF4J backend.

On the Java side, we define a simple Logger wrapper:

java
public class Logger {
    private final org.slf4j.Logger inner;
    public Logger(org.slf4j.Logger inner) {
        this.inner = inner;
    }
    public void error(String msg) { inner.error(msg); }
    public void info(String msg) { inner.info(msg); }
    public void debug(String msg) { inner.debug(msg); }
    // ...
}

Rust then calls this logger using JNI. Here's a simplified Rust implementation:

rust
impl log::Log for Logger {
    fn log(&self, record: &log::Record) {
        let env = ...; // Obtain the JNI environment
        let java_logger = find_java_side_logger();
        let logger_method = java_logger.methods.find_method(record.level());
        unsafe {
            env.call_method_unchecked(
                java_logger,
                logger_method,
                ReturnType::Primitive(Primitive::Void),
                &[JValue::from(format_msg(record)).as_jni()]
            );
        }
    }
}

And we register it as the global logger:

rust
log::set_logger(&LOGGER).expect("Failed to set global logger");

Now, logs from both languages are visible in the same output stream, simplifying diagnostics and monitoring.

Calling Rust Async Functions from Java

One of Rust’s standout features is its powerful async runtime. Unfortunately, JNI methods cannot be declared async, so calling async Rust code directly from Java is not straightforward:

rust
#[no_mangle]
pub extern "system" fn Java_io_greptime_demo_RustJavaDemo_hello(...) {
    // ❌ This won't compile
    foo().await;
}
async fn foo() { ... }

To bridge this, we must manually create a runtime (e.g., using Tokio) and manage async execution ourselves:

rust
async fn async_add_one(x: i32) -> i32 {
    x + 1
}
fn sync_add_one(x: i32) -> i32 {
    let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
    let handle = rt.spawn(async_add_one(x));
    rt.block_on(handle).unwrap()
}

But block_on() blocks the current thread—including Java’s. Instead, we leverage a more idiomatic approach: asynchronous task spawning combined with CompletableFuture on the Java side, allowing non-blocking integration.

On the Java side:

java
public class AsyncRegistry {
    private static final AtomicLong FUTURE_ID = new AtomicLong();
    private static final Map<Long, CompletableFuture<?>> FUTURE_REGISTRY = new ConcurrentHashMap<>();
}
public CompletableFuture<Integer> add_one(int x) {
    long futureId = native_add_one(x); // Call Rust
    return AsyncRegistry.take(futureId); // Get CompletableFuture
}

This pattern—used in Apache OpenDAL —lets Java developers decide when and whether to block, making integration more flexible.

Mapping Rust Errors to Java Exceptions

To unify exception handling across languages, we convert Rust Result::Err into Java RuntimeExceptions:

rust
fn throw_runtime_exception(env: &mut JNIEnv, msg: String) {
    let msg = if let Some(ex) = env.exception_occurred() {
        env.exception_clear();
        let exception_info = ...; // Extract exception class + message
        format!("{}. Java exception occurred: {}", msg, exception_info)
    } else {
        msg
    };
    env.throw_new("java/lang/RuntimeException", &msg);
}

This ensures Java code can uniformly handle all exceptions, regardless of whether they originate from Rust or Java.

Conclusion

In this article, we explored key aspects of Rust-Java interoperability:

  • Packaging platform-specific native libraries into a single JAR.
  • Unifying logs across Rust and Java.
  • Bridging async Rust functions with Java’s CompletableFuture.
  • Mapping Rust errors into Java exceptions.

For more details and a full working demo, check out our open-source repo.

We welcome feedback, issues, and PRs from the community!


About Greptime

GreptimeDB is an open-source, cloud-native database purpose-built for real-time observability. Built in Rust and optimized for cloud-native environments, it provides unified storage and processing for metrics, logs, and traces—delivering sub-second insights from edge to cloud —at any scale.

  • GreptimeDB OSS – The open-sourced database for small to medium-scale observability and IoT use cases, ideal for personal projects or dev/test environments.

  • GreptimeDB Enterprise – A robust observability database with enhanced security, high availability, and enterprise-grade support.

  • GreptimeCloud – A fully managed, serverless DBaaS with elastic scaling and zero operational overhead. Built for teams that need speed, flexibility, and ease of use out of the box.

🚀 We’re open to contributors—get started with issues labeled good first issue and connect with our community.

GitHub | 🌐 Website | 📚 Docs

💬 Slack | 🐦 Twitter | 💼 LinkedIn

Join our community

Get the latest updates and discuss with other users.