使用 Rust 语言编写 Java JNI 实现

news/2024/10/6 3:24:58/文章来源:https://www.cnblogs.com/srcres258/p/18288108

前言

Rust 语言是近几年来编程语言界的新秀之子,因其严格的内存安全保障机制而备受众多程序员的青睐与推崇。而 Rust 语言除了可用于编写独立运行的二进制程序以外,亦可用于编写动态链接库并被第三方程序动态加载调用。笔者趁 Rust 学习途中就动手借助 jni crate 从而使用 Rust 语言通过 JNI 实现 Java 程序中的本地方法,并将此练手项目以及其编写过程一字不落地记录于此。

前置:相关环境的设置与必要软件的安装

JDK

JDK 是编写 Java 程序必要的开发组件。笔者使用的是 AdoptOpenJDK 版本 21 ,可在此处下载与你操作系统与架构相匹配的 AdoptOpenJDK 。详细安装过程受限于文章篇幅故略去,请自行检索安装方法。

笔者在此处贴出自己的 Java 版本信息以供读者对照:

$ /opt/adoptopenjdk-21.0.2+13/bin/java -version 
openjdk version "21.0.2" 2024-01-16 LTS
OpenJDK Runtime Environment Temurin-21.0.2+13 (build 21.0.2+13-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.2+13 (build 21.0.2+13-LTS, mixed mode, sharing)

Rust

既然要编写 Rust 代码,首先需要设置好 Rust 开发环境。笔者使用 Rustup 工具安装 Rust ,前往该链接可获取与你操作系统相符的安装工具或 shell 命令行。

笔者在此处贴出自己的 Rustup 版本信息以供读者对照:

$ rustup --version
rustup 1.27.0 (bbb9276d2 2024-03-08)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.76.0 (07dca489a 2024-02-04)`

Gradle

由于笔者使用 Gradle 作为 Java 代码的构建工具,故需先安装 Gradle 。 Gradle 已在其官方文档中给出不同操作系统上的安装方法,不过是英文版。考虑到读者的阅读需要,故记录一下自己的安装方法(Linux 系统上)。

Gradle 官方推荐采用 SDKMAN! 安装 Gradle ,笔者亦使用该方式来安装。前往该链接获取安装 shell 命令并执行(无需 sudo ,安装到用户主文件夹下):

$ curl -s "https://get.sdkman.io" | bash

等待 SDKMAN! 安装完成后,重启电脑或重新打开 shell ,运行:

$ sdk install gradle

稍等片刻, SDKMAN! 就自动安装好 Gradle 了。笔者在此处贴出自己的 Gradle 版本信息以供读者对照:

$ gradle --version                                  ------------------------------------------------------------
Gradle 8.7
------------------------------------------------------------Build time:   2024-03-22 15:52:46 UTC
Revision:     650af14d7653aa949fce5e886e685efc9cf97c10Kotlin:       1.9.22
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          17.0.10 (Oracle Corporation 17.0.10+11-LTS-240)
OS:           Linux 6.8.2-zen2-1-zen amd64

准备:Java 程序的编写

新起一个文件夹作为项目的根文件夹,笔者起名为 rust-jni-demo

$ mkdir rust-jni-demo
$ cd rust-jni-demo

在项目根文件夹下另起一文件夹 java 作为 Java 代码部分的根目录:

$ mkdir java
$ cd java

我们对此项目的 Java 代码采用 Gradle 构建工具。在该目录中初始化 Gradle 的相关配置:

$ gradle init --use-defaults --type java-application

等待 Gradle 创建 Gradle Wrapper 与相关项目初始文件。完成后使用 Java IDE (笔者使用 IntelliJ IDEA )进入 app 目录并修改相关示例代码文件,编辑主类 App

/** This source file was generated by the Gradle 'init' task*/
package top.srcres.apps.rustjnidemo;import java.io.File;
import java.nio.file.Path;public class App {static void loadRustLibrary() {System.out.println(System.getProperty("java.library.path"));System.loadLibrary("rust_jni_demo");}static native String hello(String input);public static void main(String[] args) {loadRustLibrary();String output = hello("string from Java");System.out.println(output);}
}

其中 hello 方法即为我们要在 Rust 代码中实现的 native 方法。切回 Gradle 项目根目录并运行 ./gradlew build 先构建一下项目。

接下来需要编辑 app 目录下的 build.gradle.kts 文件,为 Gradle 添加生成 JNI 头文件的 Task ,在文件末尾加入:

val generateJniHeaders: Task by tasks.creating {val jniHeaderDir = file("src/main/generated/jni")group = "build"dependsOn(tasks.getByName("compileJava"))inputs.dir("src/main/java")outputs.dir(jniHeaderDir)doLast {val javaHome = Jvm.current().javaHomeval javap = javaHome.resolve("bin").walk().firstOrNull { it.name.startsWith("javap") }?.absolutePath ?: error("javap not found")val javac = javaHome.resolve("bin").walk().firstOrNull { it.name.startsWith("javac") }?.absolutePath ?: error("javac not found")val buildDir = file("build/classes/java/main")val tmpDir = file("build/tmp/jvmJni").apply { mkdirs() }val bodyExtractingRegex = """^.+\Rpublic \w* ?class ([^\s]+).*\{\R((?s:.+))\}\R$""".toRegex()val nativeMethodExtractingRegex = """.*\bnative\b.*""".toRegex()println("Beginning to generate JNI headers.")println("javaHome is ${javaHome.absolutePath}")println("javap is $javap")println("javac is $javac")buildDir.walkTopDown().filter { "META" !in it.absolutePath }.forEach { file ->if (!file.isFile) return@forEachval output = ByteArrayOutputStream().use {project.exec {commandLine(javap, "-private", "-cp", buildDir.absolutePath, file.absolutePath)standardOutput = it}.assertNormalExitValue()it.toString()}val (qualifiedName, methodInfo) = bodyExtractingRegex.find(output)?.destructured ?: return@forEachval lastDot = qualifiedName.lastIndexOf('.')val packageName = qualifiedName.substring(0, lastDot)val className = qualifiedName.substring(lastDot+1, qualifiedName.length)val nativeMethods =nativeMethodExtractingRegex.findAll(methodInfo).mapNotNull { it.groups }.flatMap { it.asSequence().mapNotNull { group -> group?.value } }.toList()if (nativeMethods.isEmpty()) return@forEachval source = buildString {appendln("package $packageName;")appendln("public class $className {")for (method in nativeMethods) {if ("()" in method) appendln(method)else {val updatedMethod = StringBuilder(method).apply {var count = 0var i = 0while (i < length) {if (this[i] == ',' || this[i] == ')') insert(i, " arg${count++}".also { i += it.length + 1 })else i++}}appendln(updatedMethod)}}appendln("}")}val outputFile = tmpDir.resolve(packageName.replace(".", "/")).apply { mkdirs() }.resolve("$className.java").apply { delete() }.apply { createNewFile() }outputFile.writeText(source)println("Generating for ${outputFile.absolutePath} into ${jniHeaderDir.absolutePath}")project.exec {commandLine(javac, "-h", jniHeaderDir.absolutePath, outputFile.absolutePath)}.assertNormalExitValue()}}
}

保存文件后切回 Gradle 项目根目录,运行刚才新添加的 Task , ./gradlew generateJniHeaders 。完成后可在 app/src/main/generated/jni 目录下找到生成的 JNI 头文件,应该能在其中看到如下内容:

/** Class:     top_srcres_apps_rustjnidemo_App* Method:    hello* Signature: (Ljava/lang/String;)Ljava/lang/String;*/
JNIEXPORT jstring JNICALL Java_top_srcres_apps_rustjnidemo_App_hello(JNIEnv *, jclass, jstring);

自此 Java 代码部分编写完毕,接下来使用 Rust 代码实现这个预留的 native 方法。

实现:Rust 动态链接库的编写

切回到整体项目的根目录( java 的父目录),另起一目录用以存放 Rust 代码:

$ mkdir rust
$ cargo new --name rust-jni-demo rust

注意此处我们用 Rust 自带的包管理器 Cargo 创建了 rust 目录并自动地完成了相应的初始化工作,并将这个 Rust Crate 名称指定为 rust-jni-demo 。修改该目录中的 Cargo.toml 文件,添加依赖 cargo 并指定 Crate 类型为动态链接库:

[dependencies]
jni = "0.21.1"[lib]
crate-type = ["cdylib"]

进入 src 目录,删掉默认的 main.rs 文件,新建 lib.rs 文件,在此文件中编写 native 方法的实现:

use jni::JNIEnv;
use jni::objects::{JClass, JObject, JString, JValue};
use jni::sys::{jint, jlong, jstring};
use std::thread;
use std::time::Duration;fn create_rust_string(src: &str) -> String {format!("Rust-created string, {}", src)
}#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_hello<'a>(mut env: JNIEnv<'a>,_: JClass<'a>,input: JString<'a>
) -> jstring {let input: String = env.get_string(&input).expect("Failed to get Java string.").into();let output = env.new_string(create_rust_string(&input)).expect("Failed to create Rust string.");output.into_raw()
}

回到 Rust Cargo 的根目录,构建 release 版本的 Cargo :

$ cargo build --release

构建完成后,在 target/release 目录下应能找到构建生成的动态链接库(笔者在 Linux 系统上构建生成 librust_jni_demo.so )。接下来需要让 Java 程序加载这个动态链接库从而调用其对于 native 方法的实现。

回到 Java 代码目录中,编辑 app/build.gradle.kts 文件,在 application 块中加入运行 Java 程序的 JVM 参数:

application {// Define the main class for the application.mainClass = "top.srcres.apps.rustjnidemo.App"applicationDefaultJvmArgs = listOf("-Djava.library.path=../../rust/target/release/")
}

运行 ./gradlew run , Java 程序出现以下输出,代表成功调用 Rust 代码所编写的 native 实现。

Rust-created string, string from Java

后续:添加更多不同功能的 native 方法并实现

我们为 Java 程序主类 App 添加更多的静态 native 方法:

    static int testInt;static String testString;static String testStringFromRust;static native String hello(String input);static native int helloInt(int input);static native int helloFromTestIntField();static native String helloFromTestStringField();static native void modifyTestStringFromRust(String input);static String callFromRust(String input) {System.out.println("Method callFromRust was invoked!");return "Java-side received: " + input;}static native String actCallFromRust(String input);static native void delayInRust(long timeMillis);

并在 main 方法中予以调用:

    public static void main(String[] args) {loadRustLibrary();String output = hello("string from Java");System.out.println(output);int outputInt = helloInt(114514);System.out.println(outputInt);testInt = 514;System.out.println(helloFromTestIntField());testString = "String static field from Java";System.out.println(helloFromTestStringField());modifyTestStringFromRust("string from Java #2");System.out.println(testStringFromRust);System.out.println("actCallFromRust result: " + actCallFromRust("string from Java #3"));System.out.println("Delay in Rust for 2 seconds...");delayInRust(2000);System.out.println("Delay done.");}

转到 Rust 代码中实现这些 native 方法:

#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_helloInt<'a>(_: JNIEnv<'a>,_: JClass<'a>,input: jint
) -> jint {input + 1919810
}#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_helloFromTestIntField<'a>(mut env: JNIEnv<'a>,class: JClass<'a>
) -> jint {let testInt = env.get_static_field(&class, "testInt", "I").expect("Failed to get static field testInt").i().expect("Failed to convert testInt into jint");testInt + 114
}#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_helloFromTestStringField<'a>(mut env: JNIEnv<'a>,class: JClass<'a>
) -> jstring {let testStringObj = env.get_static_field(&class, "testString", "Ljava/lang/String;").expect("Failed to get static field testString").l().expect("Failed to convert testString into JObject");let testString: String = env.get_string(&JString::from(testStringObj)).expect("Failed to get the value of testString").into();let output = env.new_string(create_rust_string(&testString)).expect("Failed to create Rust string.");output.into_raw()
}#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_modifyTestStringFromRust<'a>(mut env: JNIEnv<'a>,class: JClass<'a>,input: JString<'a>
) {let inputStr: String = env.get_string(&input).expect("Failed to receive the argument: input").into();let testStringFromRust = env.new_string(create_rust_string(&inputStr)).expect("Failed to create Rust string.");let testStringFromRustObj = JObject::from(testStringFromRust);let testStringFromRustId = env.get_static_field_id(&class, "testStringFromRust", "Ljava/lang/String;").expect("Failed to get the ID of static field testStringFromRust");env.set_static_field(&class, &testStringFromRustId, JValue::from(&testStringFromRustObj)).expect("Failed to set static field testStringFromRust");
}#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_actCallFromRust<'a>(mut env: JNIEnv<'a>,class: JClass<'a>,input: JString<'a>
) -> jstring {let inputStr: String = env.get_string(&input).expect("Failed to receive the argument: input").into();let testStringFromRust = env.new_string(create_rust_string(&inputStr)).expect("Failed to create Rust string.");let testStringFromRustObj = JObject::from(testStringFromRust);let callFromRustResult = env.call_static_method(&class, "callFromRust", "(Ljava/lang/String;)Ljava/lang/String;", &[JValue::from(&testStringFromRustObj)]).expect("Failed to invoke static method callFromRust");let callFromRustResultObj = callFromRustResult.l().expect("Failed to convert the method result into JObject.");JString::from(callFromRustResultObj).into_raw()
}#[allow(non_snake_case)]
#[no_mangle]
pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_delayInRust<'a>(_: JNIEnv<'a>,_: JClass<'a>,input: jlong
) {let inputU = u64::try_from(input).expect("Attempting to call delayInRust with negative millisecond duration.");thread::sleep(Duration::from_millis(inputU));
}

先构建 Rust 动态链接库 cargo build --release ,再构建并运行 Java 程序 ./gradlew run 。将会得到如下输出:

../../rust/target/release/
Rust-created string, string from Java
2034324
628
Rust-created string, String static field from Java
Rust-created string, string from Java #2
Method callFromRust was invoked!
actCallFromRust result: Java-side received: Rust-created string, string from Java #3
Delay in Rust for 2 seconds...
Delay done.

完整项目

完整的项目源码已上传至 GitHub 仓库 ,使用 MIT 协议开源。由于时间仓促没能就代码细节具体解释实现原理,读者可自行前往阅读研究。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/739621.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

华为od面经(C++)

华为od面经(C++) 流程 5.20 机试 5.21 性格测试(从性格测试到HR面隔了一个月,期间在准备技术面) 6.20 HR面试 6.25 上午11:00 技术一面 6.25 下午6:00 技术二面 6.27 主管面 机考 100分:剩余银饰的重量,字符串序列判定,200分:数组排列求和。前两道题很简单,当时数组排…

算法金 | 一个强大的算法模型,GPR !!

大侠幸会,在下全网同名「算法金」 0 基础转 AI 上岸,多个算法赛 Top 「日更万日,让更多人享受智能乐趣」抱个拳,送个礼 高斯过程回归(GPR)是一种非参数化的贝叶斯方法,用于解决回归问题。与传统的线性回归模型不同,GPR 能够通过指定的核函数捕捉复杂的非线性关系,并提…

[SNCPC2024] 2024 年陕西省大学生程序设计 J题猜质数II 题解

题目链接:CF 或者 洛谷 PS: CF的得等上gym。 前提说明 其实在上个月就见到这题了,当时很想做这题,结果找不到做题链接,也不知道出处,原来是陕西省赛的捧杯题。个人评价觉得是一道很不错的题,难度适中。 讲解 其实题解写的挺不错的,比很多比赛的题解写的详细许多了。这里…

[Redis]持久化

持久化 Redis的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证Redis的数据不会因为故障而丢失,这种机制就是Redis的持久化机制。 Redis的持久化机制有两种,第一种是快照,第二种是AOF日志。 快照是一次全量备份,AOF日志是连续的增量备份。 快…

Body SectionedSolidHorizontal

Body SectionedSolidHorizontal Body SectionedSolidHorizontal是通过使用两个或多个闭合轮廓(可能具有不同的尺寸)来表示产品的三维实体,这些轮廓沿准线在指定位置之间扫掠。应使用保持该几何表示的IfcShapeResentation的以下属性值:IfcShapeRepresentation.Representatio…

04.条件语句

if 语句if 的条件里可以赋值 if 的条件里赋值的变量作用域就在这个 if 语句里使用 if 语句打开 txt 文件package mainimport ("fmt""io/ioutil" )func main() {const filename = "test.txt"//返回两个值([]byte, error)文件内容和出错形式conten…

矢量数据库Chromadb的入门信息

一. 概述Chromadb是比较年轻的矢量数据库,也是LangChain默认使用的矢量数据库,使用简单,上手很容易。 官网地址:https://docs.trychroma.com/ Github:https://github.com/chroma-core/chroma二. 安装官网的指南:https://docs.trychroma.com/getting-started三. 使用模式内…

【Linux系列】Linux 性能调优工具的 9 张图

性能观察工具静态性能工具性能压测工具性能调优工具sarperf-tools追踪工具BPF性能工具

「杂文」算法竞赛之黑话大赏

欢迎投稿。写在前面 欢迎投稿。 罚时 一种根据选手完成题目的耗时,用于对通过题目数量相同的选手,进行排名的指标。 仅有选手成功通过的题目,才会计算罚时。 一道成功通过的题目的罚时为:选手第一次通过该题目时间,距离比赛开始时间之差,再加上未成功提交的罚时惩罚。 选…

汇编语言 5. [BX] 和 loop | 实验 4 : [BX] 和 loop 的使用

1) 2)向内存 0:200 - 0:23F (0020:0 ~ 0020:3f) 依次传递数据 0~63 , 只用9个指令 使用 bx 即用于偏移地址[bx] 也用于普通寄存器 bx assume cs:codecode segmentmov ax,0020hmov ds,ax ; ds : 0020h mov cx,64mov bx, 0s: mov [bx],bxinc bxloop s mov ax,4c00hint …

WAF 大全

WAF 大全 宝塔网站防火墙