ToB企服应用市场:ToB评测及商务社交产业平台

标题: 利用 Rust 语言编写 Java JNI 实现 [打印本页]

作者: 用户国营    时间: 2024-7-6 23:22
标题: 利用 Rust 语言编写 Java JNI 实现
前言

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

JDK

JDK 是编写 Java 程序须要的开辟组件。笔者利用的是 AdoptOpenJDK 版本 21 ,可在此处下载与你操作体系与架构相匹配的 AdoptOpenJDK 。详细安装过程受限于文章篇幅故略去,请自行检索安装方法。
笔者在此处贴出本身的 Java 版本信息以供读者对照:
  1. $ /opt/adoptopenjdk-21.0.2+13/bin/java -version
  2. openjdk version "21.0.2" 2024-01-16 LTS
  3. OpenJDK Runtime Environment Temurin-21.0.2+13 (build 21.0.2+13-LTS)
  4. 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 版本信息以供读者对照:
  1. $ rustup --version
  2. rustup 1.27.0 (bbb9276d2 2024-03-08)
  3. info: This is the version for the rustup toolchain manager, not the rustc compiler.
  4. 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 ,安装到用户主文件夹下):
  1. $ curl -s "https://get.sdkman.io" | bash
复制代码
等待 SDKMAN! 安装完成后,重启电脑或重新打开 shell ,运行:
  1. $ sdk install gradle
复制代码
稍等片刻, SDKMAN! 就自动安装好 Gradle 了。笔者在此处贴出本身的 Gradle 版本信息以供读者对照:
  1. $ gradle --version                                 
  2. ------------------------------------------------------------
  3. Gradle 8.7
  4. ------------------------------------------------------------
  5. Build time:   2024-03-22 15:52:46 UTC
  6. Revision:     650af14d7653aa949fce5e886e685efc9cf97c10
  7. Kotlin:       1.9.22
  8. Groovy:       3.0.17
  9. Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
  10. JVM:          17.0.10 (Oracle Corporation 17.0.10+11-LTS-240)
  11. OS:           Linux 6.8.2-zen2-1-zen amd64
复制代码
预备:Java 程序的编写

新起一个文件夹作为项目的根文件夹,笔者起名为 rust-jni-demo :
  1. $ mkdir rust-jni-demo
  2. $ cd rust-jni-demo
复制代码
在项目根文件夹下另起一文件夹 java 作为 Java 代码部分的根目录:
  1. $ mkdir java
  2. $ cd java
复制代码
我们对此项目的 Java 代码采用 Gradle 构建工具。在该目录中初始化 Gradle 的相关配置:
  1. $ gradle init --use-defaults --type java-application
复制代码
等待 Gradle 创建 Gradle Wrapper 与相关项目初始文件。完成后利用 Java IDE (笔者利用 IntelliJ IDEA )进入 app 目录并修改相关示例代码文件,编辑主类 App :
  1. /*
  2. * This source file was generated by the Gradle 'init' task
  3. */
  4. package top.srcres.apps.rustjnidemo;
  5. import java.io.File;
  6. import java.nio.file.Path;
  7. public class App {
  8.     static void loadRustLibrary() {
  9.         System.out.println(System.getProperty("java.library.path"));
  10.         System.loadLibrary("rust_jni_demo");
  11.     }
  12.     static native String hello(String input);
  13.     public static void main(String[] args) {
  14.         loadRustLibrary();
  15.         String output = hello("string from Java");
  16.         System.out.println(output);
  17.     }
  18. }
复制代码
其中 hello 方法即为我们要在 Rust 代码中实现的 native 方法。切回 Gradle 项目根目录并运行 ./gradlew build 先构建一下项目。
接下来需要编辑 app 目录下的 build.gradle.kts 文件,为 Gradle 添加天生 JNI 头文件的 Task ,在文件末端加入:
  1. val generateJniHeaders: Task by tasks.creating {
  2.     val jniHeaderDir = file("src/main/generated/jni")
  3.     group = "build"
  4.     dependsOn(tasks.getByName("compileJava"))
  5.     inputs.dir("src/main/java")
  6.     outputs.dir(jniHeaderDir)
  7.     doLast {
  8.         val javaHome = Jvm.current().javaHome
  9.         val javap = javaHome.resolve("bin").walk().firstOrNull { it.name.startsWith("javap") }?.absolutePath ?: error("javap not found")
  10.         val javac = javaHome.resolve("bin").walk().firstOrNull { it.name.startsWith("javac") }?.absolutePath ?: error("javac not found")
  11.         val buildDir = file("build/classes/java/main")
  12.         val tmpDir = file("build/tmp/jvmJni").apply { mkdirs() }
  13.         val bodyExtractingRegex = """^.+\Rpublic \w* ?class ([^\s]+).*\{\R((?s:.+))\}\R$""".toRegex()
  14.         val nativeMethodExtractingRegex = """.*\bnative\b.*""".toRegex()
  15.         println("Beginning to generate JNI headers.")
  16.         println("javaHome is ${javaHome.absolutePath}")
  17.         println("javap is $javap")
  18.         println("javac is $javac")
  19.         buildDir.walkTopDown()
  20.                 .filter { "META" !in it.absolutePath }
  21.                 .forEach { file ->
  22.                     if (!file.isFile) return@forEach
  23.                     val output = ByteArrayOutputStream().use {
  24.                         project.exec {
  25.                             commandLine(javap, "-private", "-cp", buildDir.absolutePath, file.absolutePath)
  26.                             standardOutput = it
  27.                         }.assertNormalExitValue()
  28.                         it.toString()
  29.                     }
  30.                     val (qualifiedName, methodInfo) = bodyExtractingRegex.find(output)?.destructured ?: return@forEach
  31.                     val lastDot = qualifiedName.lastIndexOf('.')
  32.                     val packageName = qualifiedName.substring(0, lastDot)
  33.                     val className = qualifiedName.substring(lastDot+1, qualifiedName.length)
  34.                     val nativeMethods =
  35.                             nativeMethodExtractingRegex.findAll(methodInfo).mapNotNull { it.groups }.flatMap { it.asSequence().mapNotNull { group -> group?.value } }.toList()
  36.                     if (nativeMethods.isEmpty()) return@forEach
  37.                     val source = buildString {
  38.                         appendln("package $packageName;")
  39.                         appendln("public class $className {")
  40.                         for (method in nativeMethods) {
  41.                             if ("()" in method) appendln(method)
  42.                             else {
  43.                                 val updatedMethod = StringBuilder(method).apply {
  44.                                     var count = 0
  45.                                     var i = 0
  46.                                     while (i < length) {
  47.                                         if (this[i] == ',' || this[i] == ')') insert(i, " arg${count++}".also { i += it.length + 1 })
  48.                                         else i++
  49.                                     }
  50.                                 }
  51.                                 appendln(updatedMethod)
  52.                             }
  53.                         }
  54.                         appendln("}")
  55.                     }
  56.                     val outputFile = tmpDir.resolve(packageName.replace(".", "/")).apply { mkdirs() }.resolve("$className.java").apply { delete() }.apply { createNewFile() }
  57.                     outputFile.writeText(source)
  58.                     println("Generating for ${outputFile.absolutePath} into ${jniHeaderDir.absolutePath}")
  59.                     project.exec {
  60.                         commandLine(javac, "-h", jniHeaderDir.absolutePath, outputFile.absolutePath)
  61.                     }.assertNormalExitValue()
  62.                 }
  63.     }
  64. }
复制代码
保存文件后切回 Gradle 项目根目录,运行刚才新添加的 Task , ./gradlew generateJniHeaders 。完成后可在 app/src/main/generated/jni 目录下找到天生的 JNI 头文件,应该能在其中看到如下内容:
  1. /*
  2. * Class:     top_srcres_apps_rustjnidemo_App
  3. * Method:    hello
  4. * Signature: (Ljava/lang/String;)Ljava/lang/String;
  5. */
  6. JNIEXPORT jstring JNICALL Java_top_srcres_apps_rustjnidemo_App_hello
  7.   (JNIEnv *, jclass, jstring);
复制代码
自此 Java 代码部分编写完毕,接下来利用 Rust 代码实现这个预留的 native 方法。
实现:Rust 动态链接库的编写

切回到团体项目的根目录( java 的父目录),另起一目录用以存放 Rust 代码:
  1. $ mkdir rust
  2. $ cargo new --name rust-jni-demo rust
复制代码
注意此处我们用 Rust 自带的包管理器 Cargo 创建了 rust 目录并自动地完成了相应的初始化工作,并将这个 Rust Crate 名称指定为 rust-jni-demo 。修改该目录中的 Cargo.toml 文件,添加依靠 cargo 并指定 Crate 范例为动态链接库:
  1. [dependencies]
  2. jni = "0.21.1"
  3. [lib]
  4. crate-type = ["cdylib"]
复制代码
进入 src 目录,删掉默认的 main.rs 文件,新建 lib.rs 文件,在此文件中编写 native 方法的实现:
  1. use jni::JNIEnv;
  2. use jni::objects::{JClass, JObject, JString, JValue};
  3. use jni::sys::{jint, jlong, jstring};
  4. use std::thread;
  5. use std::time::Duration;
  6. fn create_rust_string(src: &str) -> String {
  7.     format!("Rust-created string, {}", src)
  8. }
  9. #[allow(non_snake_case)]
  10. #[no_mangle]
  11. pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_hello<'a>(
  12.     mut env: JNIEnv<'a>,
  13.     _: JClass<'a>,
  14.     input: JString<'a>
  15. ) -> jstring {
  16.     let input: String = env.get_string(&input).expect("Failed to get Java string.").into();
  17.     let output = env.new_string(create_rust_string(&input)).expect("Failed to create Rust string.");
  18.     output.into_raw()
  19. }
复制代码
回到 Rust Cargo 的根目录,构建 release 版本的 Cargo :
  1. $ cargo build --release
复制代码
构建完成后,在 target/release 目录下应能找到构建天生的动态链接库(笔者在 Linux 体系上构建天生 librust_jni_demo.so )。接下来需要让 Java 程序加载这个动态链接库从而调用其对于 native 方法的实现。
回到 Java 代码目录中,编辑 app/build.gradle.kts 文件,在 application 块中加入运行 Java 程序的 JVM 参数:
  1. application {
  2.     // Define the main class for the application.
  3.     mainClass = "top.srcres.apps.rustjnidemo.App"
  4.     applicationDefaultJvmArgs = listOf(
  5.             "-Djava.library.path=../../rust/target/release/"
  6.     )
  7. }
复制代码
运行 ./gradlew run , Java 程序出现以下输出,代表成功调用 Rust 代码所编写的 native 实现。
  1. Rust-created string, string from Java
复制代码
后续:添加更多不同功能的 native 方法并实现

我们为 Java 程序主类 App 添加更多的静态 native 方法:
  1.     static int testInt;
  2.     static String testString;
  3.     static String testStringFromRust;
  4.     static native String hello(String input);
  5.     static native int helloInt(int input);
  6.     static native int helloFromTestIntField();
  7.     static native String helloFromTestStringField();
  8.     static native void modifyTestStringFromRust(String input);
  9.     static String callFromRust(String input) {
  10.         System.out.println("Method callFromRust was invoked!");
  11.         return "Java-side received: " + input;
  12.     }
  13.     static native String actCallFromRust(String input);
  14.     static native void delayInRust(long timeMillis);
复制代码
并在 main 方法中予以调用:
  1.     public static void main(String[] args) {
  2.         loadRustLibrary();
  3.         String output = hello("string from Java");
  4.         System.out.println(output);
  5.         int outputInt = helloInt(114514);
  6.         System.out.println(outputInt);
  7.         testInt = 514;
  8.         System.out.println(helloFromTestIntField());
  9.         testString = "String static field from Java";
  10.         System.out.println(helloFromTestStringField());
  11.         modifyTestStringFromRust("string from Java #2");
  12.         System.out.println(testStringFromRust);
  13.         System.out.println("actCallFromRust result: " + actCallFromRust("string from Java #3"));
  14.         System.out.println("Delay in Rust for 2 seconds...");
  15.         delayInRust(2000);
  16.         System.out.println("Delay done.");
  17.     }
复制代码
转到 Rust 代码中实现这些 native 方法:
  1. #[allow(non_snake_case)]
  2. #[no_mangle]
  3. pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_helloInt<'a>(
  4.     _: JNIEnv<'a>,
  5.     _: JClass<'a>,
  6.     input: jint
  7. ) -> jint {
  8.     input + 1919810
  9. }
  10. #[allow(non_snake_case)]
  11. #[no_mangle]
  12. pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_helloFromTestIntField<'a>(
  13.     mut env: JNIEnv<'a>,
  14.     class: JClass<'a>
  15. ) -> jint {
  16.     let testInt = env.get_static_field(&class, "testInt", "I")
  17.         .expect("Failed to get static field testInt")
  18.         .i()
  19.         .expect("Failed to convert testInt into jint");
  20.     testInt + 114
  21. }
  22. #[allow(non_snake_case)]
  23. #[no_mangle]
  24. pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_helloFromTestStringField<'a>(
  25.     mut env: JNIEnv<'a>,
  26.     class: JClass<'a>
  27. ) -> jstring {
  28.     let testStringObj = env.get_static_field(&class, "testString", "Ljava/lang/String;")
  29.         .expect("Failed to get static field testString")
  30.         .l()
  31.         .expect("Failed to convert testString into JObject");
  32.     let testString: String = env.get_string(&JString::from(testStringObj))
  33.         .expect("Failed to get the value of testString")
  34.         .into();
  35.     let output = env.new_string(create_rust_string(&testString)).expect("Failed to create Rust string.");
  36.     output.into_raw()
  37. }
  38. #[allow(non_snake_case)]
  39. #[no_mangle]
  40. pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_modifyTestStringFromRust<'a>(
  41.     mut env: JNIEnv<'a>,
  42.     class: JClass<'a>,
  43.     input: JString<'a>
  44. ) {
  45.     let inputStr: String = env.get_string(&input).expect("Failed to receive the argument: input").into();
  46.     let testStringFromRust = env.new_string(create_rust_string(&inputStr)).expect("Failed to create Rust string.");
  47.     let testStringFromRustObj = JObject::from(testStringFromRust);
  48.     let testStringFromRustId = env.get_static_field_id(&class, "testStringFromRust", "Ljava/lang/String;")
  49.         .expect("Failed to get the ID of static field testStringFromRust");
  50.     env.set_static_field(&class, &testStringFromRustId, JValue::from(&testStringFromRustObj))
  51.         .expect("Failed to set static field testStringFromRust");
  52. }
  53. #[allow(non_snake_case)]
  54. #[no_mangle]
  55. pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_actCallFromRust<'a>(
  56.     mut env: JNIEnv<'a>,
  57.     class: JClass<'a>,
  58.     input: JString<'a>
  59. ) -> jstring {
  60.     let inputStr: String = env.get_string(&input).expect("Failed to receive the argument: input").into();
  61.     let testStringFromRust = env.new_string(create_rust_string(&inputStr)).expect("Failed to create Rust string.");
  62.     let testStringFromRustObj = JObject::from(testStringFromRust);
  63.     let callFromRustResult = env.call_static_method(&class, "callFromRust", "(Ljava/lang/String;)Ljava/lang/String;", &[JValue::from(&testStringFromRustObj)])
  64.         .expect("Failed to invoke static method callFromRust");
  65.     let callFromRustResultObj = callFromRustResult.l().expect("Failed to convert the method result into JObject.");
  66.     JString::from(callFromRustResultObj).into_raw()
  67. }
  68. #[allow(non_snake_case)]
  69. #[no_mangle]
  70. pub extern "system" fn Java_top_srcres_apps_rustjnidemo_App_delayInRust<'a>(
  71.     _: JNIEnv<'a>,
  72.     _: JClass<'a>,
  73.     input: jlong
  74. ) {
  75.     let inputU = u64::try_from(input).expect("Attempting to call delayInRust with negative millisecond duration.");
  76.     thread::sleep(Duration::from_millis(inputU));
  77. }
复制代码
先构建 Rust 动态链接库 cargo build --release ,再构建并运行 Java 程序 ./gradlew run 。将会得到如下输出:
  1. ../../rust/target/release/
  2. Rust-created string, string from Java
  3. 2034324
  4. 628
  5. Rust-created string, String static field from Java
  6. Rust-created string, string from Java #2
  7. Method callFromRust was invoked!
  8. actCallFromRust result: Java-side received: Rust-created string, string from Java #3
  9. Delay in Rust for 2 seconds...
  10. Delay done.
复制代码
完备项目

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4