欢迎参与 8 月 1 日中午 11 点的线上分享,了解 GreptimeDB 联合处理指标和日志的最新方案! 👉🏻 点击加入

Skip to content
On this page
技术
2025-5-8

如何用 Rust 强化你的 Java 项目:JNI 实践与完整示例详解

这篇文章分享了 Rust 与 Java 互操作的完整实践方案,包括调用方式、日志统一、异步处理与异常管理,并提供了开源示例项目 rust-java-demo,帮助用户快速搭建跨语言应用。

Introduction

Rust 和 Java 是两种在不同领域广泛应用的主流语言,各自擅长处理特定类型的任务。但在实际项目中,我们常常希望能够将它们结合使用,实现跨语言协同

  • 在 Java 项目中,我们可能希望绕过 GC,对关键内存区域进行手动管理;
  • 或者将核心算法迁移至 Rust,以提高性能并保护实现细节;
  • 而在 Rust 项目中,我们也可能需要将某些能力通过 Java 封装对外暴露,打包成 JAR 供 Java 应用使用。

因此,在这篇文章中,我们会介绍如何在一个项目里同时组织 Rust 和 Java 的代码。本文偏重实践操作,有较多的代码讲解。同时我们提供了一个开源的完整可运行示例,在此基础上大家可以轻松实现一个 Rust 与 Java 互相调用的项目。

基础知识:JNI 和 Java 内存管理

JNI(Java Native Interface)是 Java 与本地代码交互的桥梁。它允许 Java 程序调用 C/C++、Rust 等语言编写的本地库。JNI 本身是一种规范,语法简单但坑点不少。

Java 的内存划分

Java 应用程序运行时的内存大致可分为三类:

  • Java Heap:Java 代码自己创建的对象所使用的内存,由 GC 管理;
  • Native Memory:本地代码(如 Rust)分配的内存,不受 GC 管理,要特别注意内存泄漏;
  • Others:Java 程序的其他内存,如编译器生成代码的缓存或者各种符号元数据等。

理解 JNI 与 Java 的内存管理,是编写稳定、高效、无内存泄漏的跨语言代码的关键。下面我们进入正题,工程实践。

实践项目:Rust 与 Java 的集成

开源的项目 demo 为例,欢迎大家使用。

构建多平台合一的 JAR 包

纯 Java 代码的编译是平台无关的,在 x86 平台上编译出的 JAR 包完全可以在 aarch64 平台上执行。但 Rust 不是。Rust 编译后的 Lib 被放到 JAR 包,就破坏了它的平台无关性。我们也可以给每个平台打一个单独的 JAR 包,但这会造成管理上的麻烦。

有没有方法打出多平台合一的 JAR 包呢?其实非常简单,将不同平台的 Rust lib 放在 JAR 的不同目录下,运行时根据当前平台动态加载即可。

解压一个多平台合一的 JAR 包 jar xf rust-java-demo-2c59460-multi-platform.jar 可见,如下所示:

(图 1:解压 JAR 包示例)
(图 1:解压 JAR 包示例)

封装好的方法可以根据运行环境加载对应的 Rust Lib,运行时自动定位并加载匹配的 Rust 动态库,简化发布和使用流程:

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

统一 Rust 和 Java 两端的日志

在跨语言项目中,统一日志输出便于排查问题和集中管理。我们通过 Java 封装一个 SLF4J Logger,并在 Rust 中调用。

在实践中,我们可以在 Java 端定义一个特殊的 Logger 类:

java
public class Logger {

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

Java 侧简单地将所有打日志的方法代理给 slf4j logger;Rust 侧则可以调用这个 Logger 类,把日志用同一个 slf4j logger 输出到同一个地方:

rust
impl log::Log for Logger {
    fn log(&self, record: &log::Record) {
        // 获取 JNI env
        let env = ...
    
        // 获取 Java 侧的 Logger 类
        let java_logger = find_java_side_logger();

        // Rust log lever 和 Java log level 一一对应上,找到相应的方法
        let logger_method = java_logger.methods.find_method(record.level());
        
        // 调用 Java 侧的方法打 Rust 日志,确保方法签名对应得上
        unsafe {
            env.call_method_unchecked(
                java_logger,
                logger_method,
                ReturnType::Primitive(Primitive::Void),
                &[JValue::from(format_msg(record)).as_jni()]
            );
        }
    }
}

在 Rust Lib 初始化时,可以将上述 Rust Log 的自定义实现设置为一个全局 Logger:

rust
log::set_logger(&LOGGER).expect("unable to set `Logger` as the global logger");

异步调用 Rust 的 Async 函数

Rust 相比 Java 很好用的一个特性是异步函数(Async Function)。然而我们在写 Rust 侧的 Java FFI 方法时,不能声明为异步,所以无法直接调用异步函数:

rust
#[no_mangle]
pub extern "system"
/* 不能加 async!*/ fn Java_io_greptime_demo_RustJavaDemo_hello(
    mut env: JNIEnv,
    _class: JClass,
    ...
) {
    // ❌ 编译错误,不能直接调用 async function
    foo.await;
}

async foo() {}

在 Rust 中,跨越 Sync 和 Async 的方法就是创建一个 Async Runtime 然后等待 Async Function 执行完毕。

以 Tokio 为例:

rust
async async_add_one(x: i32) -> i32 { x + 1 }

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

block_on 会一直阻塞当前线程。如果用这种方法,线程的阻塞会一直传导到 Java 中,显然并不是我们所希望的。那么有什么其他方法吗?

spawn 之后,Async Function 就在 Runtime 中执行了,block_on 只是为了等待结果。Java 中的类似机制是 CompletableFuture:Rust 的 spawn对应 Java 线程池的 submitblock_on 则对应 CompletableFutureget。于是我们可以利用 CompletableFuture 的一些特性,让 Java 上层应用自由选择获取结果,也就是“阻塞”的时机,绕过前面调用 Rust Async Function 时必须等待的限制。

具体方法如下:

  • Java 侧
java
public class AsyncRegistry {
    // 保证 future id 全局唯一
    private static final AtomicLong FUTURE_ID = new AtomicLong();
    // 一个全局唯一的 future id 到 CompletableFuture 的 Map
    private static final Map<Long, CompletableFuture<?>> FUTURE_REGISTRY = new ConcurrentHashMap<>();
}

// 返回一个 CompletableFuture
public CompletableFuture add_one(int x) {
    // 调用 native 方法,但不直接获取结果,而是获取一个 "future id"
    long futureId = native_add_one(x);
    // 根据 "future id" 去获取对应的 CompletableFuture
    return AsyncRegistry.take(futureId);
}

这个调用 Rust Async Function 的方法参考 Apache OpenDAL

异常处理:Rust 错误转为 Java 异常

为了更一致的异常处理体验,我们可以将 Rust 的 Err 以 Java 的 `RuntimeException``抛出:

rust
fn throw_runtime_exception(env: &mut JNIEnv, msg: String) {
    let msg = if let Some(ex) = env.exception_occurred() {
        env.exception_clear();
        // 获取已有的异常的信息,通常是 Rust 回调 Java 时,jni-rs 返回的 "JavaException" Err
        let exception_class_and_message = ...
        format!("{}. Java exception occurred: {}", msg, exception_class_and_message)
    } else {
        msg
    };
    // 将 Rust 中的 "Err" 以 Java 的 RuntimeException 抛出,这样异常处理就统一了
    env.throw_new("java/lang/RuntimeException", &msg);
}

这样所有 Rust 层的错误都能被 Java 捕获和处理。

总结

本文系统介绍了 Rust 与 Java 互操作的常见实践,包括:跨平台 JAR 包打包策略;日志统一输出;异步调用支持和异常处理机制。

我们将完整代码和更多实践封装在开源项目中,欢迎社区同学尝试、反馈,也欢迎提交 PR 与我们共建!


关于 Greptime

Greptime 格睿科技专注于打造新一代可观测数据库,服务开发者与企业用户,覆盖从从边缘设备到云端企业级部署的多样化需求。

  • GreptimeDB 开源版:开源、云原生,统一处理指标、日志和追踪数据,适合中小规模 IoT,个人项目与可观测性场景;
  • GreptimeDB 企业版:面向关键业务,提供更高性能、高安全性、高可用性和智能化运维服务;
  • GreptimeCloud 云服务:全托管云服务,零运维体验“企业级”可观测数据库,弹性扩展,按需付费。

欢迎加入开源社区参与贡献与交流!推荐从带有 good first issue 标签的任务入手,一起共建可观测未来。

Star us on GitHub | 📚 官网 | 📖 文档

🌍 Twitter | 💬 Slack | 💼 LinkedIn

加入我们的社区

获取 Greptime 最新更新,并与其他用户讨论。