真正的救赎,并非厮杀后的胜利,而是能在苦难之中,找到生的力量和内心的安宁。
——加缪Albert Camus
一、Rust + Java = ?
Java 和 Rust 是两种现代编程语言,各自具有独特的优势,适用于不同的应用场景。
1、Java 的优势
- 跨平台性:Java 的“写一次,运行到处”的理念使得它能够在各种操作系统上运行,只要有 JVM(Java Virtual Machine)支持即可。
- 丰富的生态系统:Java 拥有庞大的标准库和第三方库生态系统,涵盖了几乎所有的开发需求,从 web 开发、数据库访问到大数据处理等。
- 成熟的工具链:Java 具有成熟的开发工具链,包括 IDE(如 IntelliJ IDEA、Eclipse)、构建工具(如 Maven、Gradle)和调试工具,这些工具极大地提高了开发效率。
- 强大的社区支持:Java 社区非常活跃,有大量的开源项目、文档和教程,可以帮助开发者快速解决问题。
- 垃圾回收机制:Java 的自动垃圾回收机制简化了内存管理,减少了内存泄漏和其他内存相关错误的风险。
- 企业级应用:Java 在企业级应用开发中占据重要地位,特别是在金融、电信和大型分布式系统中,Java 被广泛使用。
- 多线程支持:Java 提供了强大的多线程支持,方便开发并发和并行程序。
2、Rust 的优势
- 内存安全:Rust 的所有权系统和借用检查器在编译时保证了内存安全,防止了常见的内存错误,如空指针引用、悬挂指针和缓冲区溢出。
- 高性能:Rust 生成的代码接近 C 和 C++ 的性能,同时提供了更高的安全性和更少的运行时开销。
- 无畏并发:Rust 的所有权模型使得编写并发代码更加安全和简单,避免了数据竞争和死锁等问题。
- 现代语言特性:Rust 支持模式匹配、闭包、泛型、trait 等现代编程语言特性,使得代码更加简洁和可维护。
- 强类型系统:Rust 的强类型系统在编译时捕获更多的错误,提高了代码的可靠性和可维护性。
- 零成本抽象:Rust 提供了高层次的抽象,但这些抽象在编译后不会引入额外的运行时开销,实现了所谓的“零成本抽象”。
- 良好的文档和工具链:Rust 提供了优秀的文档和工具链,包括 Cargo(包管理和构建工具)、rustfmt(代码格式化工具)和 Clippy(代码审查工具),这些工具极大地提高了开发体验。
- 嵌入式和系统编程:Rust 非常适合嵌入式和系统级编程,能够直接操作硬件,同时保持高安全性和性能。
3、Rust + Java 的应用思路
Java 和 Rust 各有其独特的优势,适用于不同的应用场景:
- Java 适合需要跨平台兼容性、丰富生态系统和成熟工具链的企业级应用和大型分布式系统。
- Rust 则适合需要高性能、内存安全和并发控制的系统级编程、嵌入式开发以及对性能和安全性要求极高的应用。
通过结合 Java 和 Rust 的优势,可以在实际项目中实现更高效、安全和功能强大的解决方案。例如,可以使用 Java 实现业务逻辑和用户界面,而使用 Rust 实现性能关键的底层模块和系统组件。
4、应用场景
- 性能关键模块:在 Java 应用中,某些性能关键的部分可以用 Rust 编写,以提高执行效率。例如,计算密集型算法、数据处理或图像处理等任务可以用 Rust 实现,然后通过 JNI(Java Native Interface)调用这些 Rust 函数。
- 内存安全和并发控制:Rust 的所有权机制和借用检查器可以帮助避免内存泄漏和数据竞争问题。在需要严格内存管理和并发控制的场景下,可以用 Rust 编写核心逻辑,并通过 FFI(Foreign Function Interface)与 Java 进行交互。
- 系统级编程:对于需要直接操作系统资源或硬件的功能,例如文件系统操作、网络通信或设备驱动程序,可以用 Rust 编写这些底层代码,然后通过 JNI 或 JNA(Java Native Access)与 Java 应用进行交互。
- 跨平台开发:Rust 可以编译成多种平台的二进制文件,而 Java 本身具有良好的跨平台特性。在需要支持多种操作系统的应用中,可以利用 Rust 编写跨平台的库,并通过 Java 调用这些库,从而实现跨平台兼容性。
- 安全性要求高的应用:在金融、医疗等对安全性要求极高的领域,可以用 Rust 编写关键的安全模块,如加密算法、身份验证等,然后通过 JNI 与 Java 应用集成,以确保系统的整体安全性。
- 微服务架构:在微服务架构中,不同的服务可以用不同的语言实现。如果某些微服务用 Rust 编写,而其他服务用 Java 编写,可以通过 RESTful API 或 gRPC 等方式进行通信,实现互操作。
- 游戏开发:游戏引擎或性能要求高的游戏逻辑可以用 Rust 编写,而游戏的业务逻辑、界面等可以用 Java 编写,通过 JNI 调用 Rust 实现高效的游戏运行。
二、JNI:实现 Rust 与 Java 互调的桥梁
1、如何理解 JNI
Java Native Interface(JNI)是 Java 平台的一部分,它允许 Java 代码与用其他编程语言(如 C、C++ )编写的本地代码进行交互。JNI 的角色可以理解为桥梁或接口,连接了 Java 虚拟机(JVM)和本地代码库,使得两者能够相互调用和通信。
跨语言调用:JNI 使得 Java 程序能够调用本地代码中的函数,也使得本地代码能够调用 Java 方法。这种双向调用能力使得开发者可以在 Java 应用中利用其他语言的特性和性能优势。
性能优化:在某些情况下,Java 代码可能无法提供足够的性能,例如在处理大量数据或执行复杂计算时。通过 JNI,可以将这些性能关键的部分用更高效的语言(如 C、C++)实现,然后在 Java 中调用,从而提升整体应用的性能。
访问底层系统资源:Java 本身是一个高级语言,通常不直接提供对底层系统资源(如硬件设备、操作系统 API)的访问。通过 JNI,Java 程序可以调用本地代码来访问这些底层资源,实现系统级编程。
复用现有库和代码:很多已有的库和代码是用其他语言编写的。通过 JNI,Java 程序可以直接调用这些现有的库,而无需重新实现,从而节省开发时间和成本。
内存管理:JNI 提供了一套机制来管理 Java 和本地代码之间的内存交互。虽然 Java 有自动垃圾回收机制,但本地代码需要手动管理内存。JNI 提供了必要的工具和方法来确保内存安全和有效管理。
错误处理和调试:JNI 提供了一些工具和方法来处理和调试 Java 与本地代码之间的交互问题。例如,通过 JNI 可以捕获和处理本地代码中的异常,并将其转换为 Java 异常,从而在 Java 层面进行处理。
平台独立性:尽管 JNI 允许调用本地代码,这些本地代码往往是平台相关的。然而,JNI 本身作为一个标准接口,使得开发者可以编写跨平台的 Java 代码,同时针对不同的平台编写相应的本地代码库,从而实现一定程度的跨平台兼容性。
总结来说,JNI 的角色是充当 Java 和本地代码之间的桥梁,提供了一种机制,使得 Java 应用能够利用其他语言的特性和性能优势,同时保持一定的灵活性和扩展性。JNI 既然是 C 语言接口,那么理论上支持 C ABI 的语言都可以和 Java 语言互相调用,Rust 就是其中之一。
关于 JNI 的历史背景以及更详细的介绍可以参考 官方文档
2、JNI 相关概念
Java 本地方法:在 Java 中声明但由本地代码实现的方法。
JNI 环境指针 (JNIEnv *
):用于访问 JNI 提供的函数和 Java 虚拟机(JVM)的接口。
本地库:包含本地方法实现的共享库或动态链接库(如 .dll
、.so
文件)。
Java 本地方法(Native Method)、JNI 环境指针(JNIEnv *)和本地库在一起工作时,通常遵循以下流程:
-
声明本地方法:
在 Java 代码中,你需要声明一个本地方法。这个方法使用native
关键字,并且没有方法体。例如:public class MyClass {public native void myNativeMethod();static {System.loadLibrary("MyNativeLib");} }
System.loadLibrary("MyNativeLib")
用于加载包含本地方法实现的本地库。 -
生成头文件:
使用javac
编译 Java 文件,然后使用javah
工具生成对应的 C/C++ 头文件。例如:javac MyClass.java javah -jni MyClass
这会生成一个名为
MyClass.h
的头文件,其中包含 JNI 函数签名。 -
实现本地方法:
在生成的头文件基础上,用 C 或 C++ 实现本地方法。例如:#include <jni.h> #include "MyClass.h"JNIEXPORT void JNICALL Java_MyClass_myNativeMethod(JNIEnv *env, jobject obj) {// 本地方法的实现 }
-
编译本地库:
将实现本地方法的 C/C++ 代码编译成共享库或动态链接库。例如,在 Linux 上可以使用gcc
:gcc -shared -o libMyNativeLib.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux MyClass.c
在 Windows 上可以使用类似的命令生成
.dll
文件。 -
加载本地库:
当 Java 程序运行并调用System.loadLibrary("MyNativeLib")
时,JVM 会加载指定的本地库。 -
调用本地方法:
当 Java 代码调用myNativeMethod()
时,JVM 通过 JNI 调用对应的本地方法实现。此时,JNI 环境指针 (JNIEnv *
) 被传递给本地方法,允许它访问 JNI 提供的函数和 JVM 的接口。 -
执行本地代码:
本地方法在本地库中执行,可以进行各种操作,包括调用底层系统 API、处理硬件设备、执行高性能计算等。 -
返回结果:
如果本地方法有返回值,它会将结果返回给 Java 层。如果发生异常,本地方法可以通过JNIEnv *
抛出 Java 异常。
整个流程如下图所示:
Java 层:MyClass.java (声明本地方法)|v编译 -> MyClass.class|v生成头文件 -> MyClass.h|v
C/C++ 层:MyClass.c (实现本地方法)|v编译 -> libMyNativeLib.so / MyNativeLib.dll|v
Java 层:System.loadLibrary("MyNativeLib")|v调用本地方法 -> JNI 调用 -> 执行本地代码|v返回结果 / 抛出异常
通过这种方式,Java 程序可以与本地代码进行交互,实现更底层的功能或优化性能。
3、JNI 的 Rust 绑定
在 Rust 中和 Java 互相调用,可以使用原始的 JNI 接口,也就是自己声明 JNI 的 C 函数原型,在Rust 里按照 C 的方式去调用,但这样写起来会很繁琐,而且都是 unsafe 的操作。不过 Rust 社区里已经有人基于原始的 JNI 接口,封装好了一套 safe 的接口,crate 的名字就叫 jni ,用这个库来开发就方便多了。
三、互调示例
Step1、定义一个异步耗时操作 CostTimeOption
Step2、Java 端调用 Rust 端,由 Rust 端执行一个异步耗时操作 CostTimeOption
Step3、Rust 端调用 Java 端报告操作已消耗时长和进度
Step 1、创建一个 Maven 项目
import java.time.Duration;
import java.time.LocalDateTime;/*** @version: V1.0* @author: 余衫马* @description: MyJobAgent* @data: 2024-11-12 15:58**/
public class MyJobAgent {/*** 声明一个本地方法* 将 MyJobAgent 实例作为参数传递给 Rust 端*/private static native void runCostTimeFuncAsync(MyJobAgent callback);/*** 开始执行时间*/private static LocalDateTime startDatetime;// 用于加载包含本地方法实现的本地库。static {System.loadLibrary("rust_jni_bingding_demo");}public static void main(String[] args) {startDatetime = LocalDateTime.now();runCostTimeFuncAsync(new MyJobAgent());}/*** 回调方法,由 Rust 端回调 progress 进度*/public void asyncCallback(float progress) {// 计算两个时间点之间的 DurationDuration duration = Duration.between(startDatetime, LocalDateTime.now());// 获取分钟、秒和毫秒long minutes = duration.toMinutes();long seconds = duration.getSeconds();long millis = duration.toMillis();System.out.printf("当前进度:%f,已耗时:%d 分 %d 秒 %d 毫秒\n", progress, minutes, seconds, millis);}
}
我们创建了一个 MyJobAgent 代理工作类,声明了一个本地方法 runCostTimeFuncAsync ,参数是 MyJobAgent 实例是为了让 Rust 端能调用该实例的回调方法 asyncCallback,
private static native void runCostTimeFuncAsync(MyJobAgent callback);
而回调方法 asyncCallback 需要接收一个参数 progress 输出进度,并计算目前已耗时多久,
/*** 回调方法,由 Rust 端回调 progress 进度*/public void asyncCallback(float progress) {// 计算两个时间点之间的 DurationDuration duration = Duration.between(startDatetime, LocalDateTime.now());// 获取分钟、秒和毫秒long minutes = duration.toMinutes();long seconds = duration.getSeconds();long millis = duration.toMillis();System.out.printf("当前进度:%f,已耗时:%d 分 %d 秒 %d 毫秒", progress, minutes, seconds, millis);}
Linux 下的动态库 librust_jni_bingding_demo.so 只需要填 rust_jni_bingding_demo,
// 用于加载包含本地方法实现的本地库。static {System.loadLibrary("rust_jni_bingding_demo");}
Step 2、创建一个 Rust 项目
cargo new rust_jni_bingding_demo --lib
修改 Cargo.toml,添加 Rust jni 绑定库,并且设置 crate_type = ["cdylib"]
[dependencies]
jni = "0.21.1"[lib]
crate_type = ["cdylib"]
修改 lib.rs,
use std::sync::mpsc; // 引入多生产者单消费者通道模块
use std::thread::{self}; // 引入线程模块
use std::time::Duration; // 引入时间模块use jni::objects::*; // 引入JNI对象模块
use jni::sys::jfloat; // 引入JNI浮点数类型
use jni::JNIEnv; // 引入JNI环境模块// 定义一个外部函数,供Java调用
#[no_mangle]
pub extern "system" fn Java_MyJobAgent_runCostTimeFuncAsync<'local>(mut env: JNIEnv<'local>, // JNI环境参数_class: JClass<'local>, // 调用该方法的Java类callback: JObject<'local>, // 回调对象
) {// 获取当前Java虚拟机实例let jvm = env.get_java_vm().unwrap();// 创建全局引用,以便在其他线程中使用回调对象let mycallback = env.new_global_ref(callback).unwrap();// 创建一个多生产者单消费者通道let (tx, rx) = mpsc::channel();// 启动一个新线程let _ = thread::spawn(move || {// 在线程中发送一个信号,表示线程已经启动tx.send(()).unwrap();// 将当前线程附加到JVMlet mut myenv = jvm.attach_current_thread().unwrap();// 模拟一个耗时操作,并定期调用回调函数报告进度for i in 0..100 {let progress = i as jfloat; // 将进度转换为JNI浮点数类型// 调用回调方法,将进度传递给Java层 // 函数签名为(float)voidmyenv.call_method(&mycallback, "asyncCallback", "(F)V", &[progress.into()]).unwrap();// 休眠100毫秒,模拟耗时操作thread::sleep(Duration::from_millis(100));}});// 接收线程启动信号,确保线程已成功启动rx.recv().unwrap();
}
代码解析:
- 引入必要的模块:首先引入了标准库中的
mpsc
、thread
和time
模块,以及JNI相关的模块。 - 定义外部函数:定义了一个外部函数
Java_MyJobAgent_runCostTimeFuncAsync
,这个函数将被Java代码调用。 - 获取JVM实例:通过
env.get_java_vm()
获取当前的Java虚拟机实例。 - 创建全局引用:将回调对象创建为全局引用,以便在线程中使用。
- 创建通道:创建一个多生产者单消费者通道,用于线程间通信。
- 启动新线程:启动一个新线程,在新线程中执行耗时操作。
- 附加线程到JVM:将新线程附加到JVM,以便在新线程中调用Java方法。
- 模拟耗时操作:在循环中模拟一个耗时操作,每次循环都会调用一次回调方法,将当前进度传递给Java层,并休眠100毫秒。
- 接收线程启动信号:主线程等待接收子线程发送的启动信号,确保子线程已成功启动。
这样,通过上述步骤,我们实现了一个异步任务,并在任务执行过程中定期向Java层报告进度。
Step 3、编译代码
编译 Rust 库
cargo build
我这里用的 linux 系统,动态库文件名为 librust_jni_bingding_demo.so,如果是 Windows 系统,文件名为 librust_jni_bingding_demo.dll,
编译 Java 代码
修改 pom.xml,在构建配置文件中指定主类,
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.2.0</version><configuration><archive><manifest><addDefaultImplementationEntries>true</addDefaultImplementationEntries><mainClass>MyJobAgent</mainClass></manifest></archive></configuration></plugin></plugins></build>
然后执行 maven 打包指令,
mvn package
在 target 目录下生成 classes 字节码文件与 RustJniDemo-1.0-SNAPSHOT.jar 。
Step 4、运行效果
这里使用的是 Open JDK8,
(base) sam@sam-PC:~/AwesomeWorkSpace/RustStudy/jni/rust_jni_bingding_demo$ java -version
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (build 1.8.0_212-8u212-b01-1~deb9u1-b01)
OpenJDK 64-Bit Server VM (build 25.212-b01, mixed mode)
# 临时添加环境变量,否则会报错找不到库文件
# export PATH=$PATH:~/AwesomeWorkSpace/RustStudy/rust_jni_binding_demo/target/debug
java -Djava.library.path=target/debug -classpath target/classes MyJobAgent
# 或者直接跑 jar 包
java -Djava.library.path=target/debug -jar target/RustJniDemo-1.0-SNAPSHOT.jar
-
-Djava.library.path=target/debug
:这是一个系统属性设置,-D
选项用于定义系统属性。在这里,java.library.path
属性被设置为target/debug
,这通常是用来指定本地库(如JNI库)的搜索路径。 -
-classpath target/classes
:这是指定类路径的选项,-classpath
或-cp
用于告诉JVM在哪里可以找到用户定义的类和包。在这个例子中,类路径被设置为target/classes
,这意味着JVM会在target/classes
目录下查找需要的类文件。 -
MyJobAgent
:这是要运行的主类的名称。这个类应该包含一个public static void main(String[] args)
方法,这是Java应用程序的入口点。
四、总结
通过以上示例,我们成功实现了 Rust 与 Java 的互调。利用 JNI 技术,可以充分发挥 Rust 的性能优势,同时保持 Java 的跨平台特性。这种技术组合适用于对性能要求较高的应用场景,如图像处理、数据分析和系统级编程等。
参考资料
Rust与Java交互-JNI模块编写-实践总结 - Rust语言中文社区
Leveraging Rust in our high-performance Java database
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/
jni - Rust
https://github.com/jni-rs/jni-rs