如何使用 Rust 增强你的 Java 项目 —— 带有完整示例的 JNI 集成实用指南

Greptime Greptime Follow 5 min read · 4 days ago

Listen Share

简介

RustJava 都是广泛使用的语言,每种语言都在不同的领域表现出色。在实际场景中,将它们结合起来以实现更有效的系统级和应用级编程通常是有益的:

在本文中,我们将逐步介绍如何在同一项目中组织和集成 Rust 和 Java。重点是实践,提供具体的代码示例和逐步说明。最后,你将能够创建一个跨语言应用程序,其中 RustJava 可以顺畅地交互。

背景:理解 JNI 和 Java 内存管理

Java Native Interface (JNI)Java 与用 C/C++Rust 编写的本机代码之间的桥梁。虽然它的语法相对简单,但 JNI 在实践中却因其隐式的内存和线程管理规则而臭名昭著。

Java 运行时中的内存段

Java 应用程序跨多个内存段运行:

理解这些边界是编写高性能、内存安全的跨语言代码的关键。

一个实际的集成:rust-java-demo 项目

让我们来看一个真实的例子:我们的开源 rust-java-demo 存储库演示了如何将 Rust 无缝集成到 Java 应用程序中。

将特定于平台的 Rust 库打包到单个 JAR 中

Java 字节码是平台独立的,但 Rust 二进制文件不是。将 Rust 动态库嵌入 JAR 中会引入平台依赖性。虽然为每个架构构建单独的 JAR 是可能的,但这会使分发和部署复杂化。

更好的解决方案是将特定于平台的 Rust 库打包到单个 JAR 中的不同文件夹中,并在运行时动态加载正确的库。

在提取我们的多平台 JAR (jar xf rust-java-demo-2c59460-multi-platform.jar) 之后,你将找到如下结构:

我们使用一个简单的实用程序来根据主机平台加载正确的库:

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

这种方法确保了平台灵活性,而不会牺牲开发人员或运营的便利性。

统一 Rust 和 Java 的日志

如果没有统一的日志记录,跨语言项目可能会很快变成调试的噩梦。我们通过将所有日志(RustJava)通过相同的 SLF4J 后端进行处理来解决这个问题。

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) { inner.error(msg); }  public void info(String msg) { inner.info(msg); }  public void debug(String msg) { inner.debug(msg); }  // ...}

然后,Rust 使用 JNI 调用此记录器。这是一个简化的 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()]      );    }  }}

我们将其注册为全局记录器:

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

现在,来自两种语言的日志都可以在相同的输出流中看到,从而简化了诊断和监视。

从 Java 调用 Rust 异步函数

Rust 的突出特点之一是其强大的异步运行时。不幸的是,JNI 方法不能声明为 async,因此直接从 Java 调用异步 Rust 代码并不简单:

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

为了弥合这一点,我们必须手动创建一个运行时(例如,使用 Tokio)并自己管理异步执行:

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()}

但是 block_on() 会阻塞当前线程(包括 Java 的线程)。相反,我们利用一种更惯用的方法:异步任务生成与 Java 端的 CompletableFuture 相结合,从而实现非阻塞集成。

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}

这种模式(在 Apache OpenDAL 中使用)让 Java 开发人员可以决定何时以及是否阻塞,从而使集成更加灵活。

将 Rust 错误映射到 Java 异常

为了统一跨语言的异常处理,我们将 RustResult::Err 转换为 JavaRuntimeException

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);}

这确保了 Java 代码可以统一处理所有异常,无论它们是来自 Rust 还是 Java

结论

在本文中,我们探讨了 Rust-Java 互操作性的关键方面:

有关更多详细信息和完整的演示,请查看我们的开源存储库

我们欢迎来自社区的反馈、问题和 PR

关于我们

GreptimeDB 是一个开源的、云原生的数据库,专为实时可观测性而构建。它用 Rust 构建并针对云原生环境进行了优化,为指标、日志和跟踪提供统一的存储和处理 - 从边缘到云,以任何规模提供亚秒级的洞察力。

🧑💻 想贡献吗?从 good first issue 开始,加入我们不断壮大的开源社区。

GitHub: GreptimeDB

🌐 greptime.cn | 📚 docs.greptime.cn

💬 Slack | 🐦 @Greptime | 💼 LinkedIn