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
可见,如下所示:

封装好的方法可以根据运行环境加载对应的 Rust Lib,运行时自动定位并加载匹配的 Rust 动态库,简化发布和使用流程:
static {
JarJniLoader.loadLib(
RustJavaDemo.class,
"/io/greptime/demo/rust/libs",
"demo"
);
}
统一 Rust 和 Java 两端的日志
在跨语言项目中,统一日志输出便于排查问题和集中管理。我们通过 Java 封装一个 SLF4J Logger,并在 Rust 中调用。
在实践中,我们可以在 Java 端定义一个特殊的 Logger 类:
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
输出到同一个地方:
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:
log::set_logger(&LOGGER).expect("unable to set `Logger` as the global logger");
异步调用 Rust 的 Async 函数
Rust 相比 Java 很好用的一个特性是异步函数(Async Function)。然而我们在写 Rust 侧的 Java FFI 方法时,不能声明为异步,所以无法直接调用异步函数:
#[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 为例:
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 线程池的 submit
,block_on
则对应 CompletableFuture
的 get
。于是我们可以利用 CompletableFuture
的一些特性,让 Java 上层应用自由选择获取结果,也就是“阻塞”的时机,绕过前面调用 Rust Async Function 时必须等待的限制。
具体方法如下:
- 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``抛出:
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 | 📚 官网 | 📖 文档