【一起学Rust | 进阶篇 | jni库】JNI实现Java与Rust进行交互

简介: 【一起学Rust | 进阶篇 | jni库】JNI实现Java与Rust进行交互



前言

在Rust语言中文社区中看到了大佬metaworm的这样一篇帖子《Rust与Java交互-JNI模块编写-实践总结》,里面详细阐述了Rust如何使用JNI与Java进行交互,在本人的学习过程中也是发现了一些小的错误,经过调整后,文章的例子得以运行。本文旨在推广其实战经验,修复其存在的一些影响读者阅读的小问题,推动Rust开发生态的普及。

JNI是一套Java与其他语言互相调用的标准,主要是C语言,官方也提供了基于C的C++接口。理论上支持C API的语言都可以和Java语言互相调用,Rust就是其中之一。

Rust 与 Java 相互调用可以使用原始的 JNI 接口,但是操作过程较为繁琐;Rust 社区已经有人基于原始的 JNI 接口封装了一套safe接口,crate 名字就叫 jni,便于开发。

在原文的评论区就看到了这样的问题

不知道为啥, -Djava.library.path=target/debug 用这个指定dll路径, java执行提示找不到路径

在本人亲自实践去运行作者提供的案例时,确实出现了该问题,并且已经修复,这里除了总结其经验以外还有修正其错误。

本文要求你懂 Maven 和 Cargo 项目的配置


一、工程配置

如果你比较熟悉Maven 和 Cargo,建议直接去看作者的仓库

1. Rust工程配置

  1. 创建一个新的工程
    打开终端并运行一下命令
cargo new java-rust-demo
  1. 进入java-rust-demo文件夹,编辑Cargo.toml文件
[package]
name = "rust-java-demo"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ['cdylib']
[dependencies]
jni = {version = '0.19'}

lib这一节意思时Rust项目时动态库类型的,在编译后,如果你是Windows系统,就会在target/debug生成dll文件,如果你是Linux系统,就会生成so文件。

实际上,作者在后面还写了个宏来处理全局引用和对象缓存的问题,这里依赖应该添加个 anyhow 才对,因此,最终的配置文件应该是这样的

[package]
name = "java-rust-demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ['cdylib']
[dependencies]
jni = {version = '0.19'}
anyhow = "1.0.65"
  1. 修改src的main.rs为lib.rs,(Rust库类型的项目编译入口为lib.rs),然后添加代码
use jni::objects::*;
use jni::JNIEnv;
#[no_mangle]
pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_init(env: JNIEnv, _class: JClass) {
    println!("rust-java-demo inited");
}

这里导出了一个C语言的函数init,函数名为Java_pers_metaworm_RustJNI_init,是有一定讲究的,格式一般是这样的

Java_<类完整路径>_<方法名>,这里所演示的函数导出的就是java中pers.metaworm.RustJNI类的一个native的静态方法init,这里只输出一句调试信息。

  1. 编译生成动态库
    打开终端执行以下命令来生成动态库
cargo build

如果执行正常的话,就会在target/debug生成动态库,如果你是Windows就是rust_java_demo.dll,如果你是Linux就是rust_java_demo.so,到此Rust项目就算是配置成功。

2. Java工程配置

  1. 创建pom.xml文件
    由于使用的是Maven,先Maven的配置文件,如果你的java想要使用依赖,都在可以在这里进行配置。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>pers.metaworm</groupId>
    <artifactId>RustJNI</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <exec.mainClass>pers.metaworm.RustJNI</exec.mainClass>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    </properties>
    <dependencies>
    </dependencies>
    <build>
        <sourceDirectory>java</sourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  1. 创建对应的Java类
    先创建java目录,然后依次创建pers/metaworm/RustJNI.java文件,创建好以后是这样的

    然后在创建的java文件中写入以下内容
package pers.metaworm;
public class RustJNI {
    static {
        System.loadLibrary("rust_java_demo");
    }
    public static void main(String[] args) {
        init();
    }
    static native void init();
}

这里main方法是Java调用的入口函数,static代码块在类一加载就会调用,其中

System.loadLibrary("java_rust_demo");

的意思是加载一个库,如果是Windows系统就是java_rust_demo.dll,如果是Linux就是java_rust_demo.so

注意,在原文中就是此处出现了小问题,导致学习中加载不到这个库,这里不是path问题,而是作者给的是rust_java_demo,但是调用的是java_rust_demo,因此jvm找不到这个库文件,就会出错了。

  1. 编写编译运行的命令行

这里作者给出的是

java -Djava.library.path=target/debug -classpath target/classes pers.metaworm.RustJNI

直接命令行方式运行确实可行,但是较为麻烦,因此这里写了两个脚本

  1. 创建build.bat
    创建好以后,在文件中写入以下内容
@echo off
echo rust compiling
cargo build
echo java compiling
mvn compile
  1. 创建run.bat
    创建好以后,在该文件中写入以下内容
@echo off
set cpath=%~dp0
set library_path=%cpath%target\debug
set class_path=%cpath%target\classes
echo output
java -Djava.library.path=%library_path% -classpath %class_path% pers.metaworm.RustJNI

创建好以后,项目目录应该是这样的

4. 编译运行

此时执行build.bat进行编译,然后再执行run.bat运行,就会输出

rust-java-demo inited

表示此时动态库已经加载完毕,因为init函数确实调用了,到此为止Rust和Java进行交互的环境已经搭建好了。

二、Java调用Rust

在前面已经介绍过Rust如何给Java暴露一个native方法,就是导出名称为Java_<类完整路径>_<方法名>的函数,然后在Java对应的类里声明对应的native方法。

拓展

除了本文用到的方法,还有一种动态注册的方法,就是导出JNI_Onload函数,在其中调用JNIEnv::register_native_methods进行动态注册。

如果你想要动态注册,建议看看

#[no_mangle]
pub fn test_func(_env: JNIEnv, _class: JClass){
    println!("register_native_methods test_func")
}
#[no_mangle]
pub unsafe extern "C" fn JNI_Onload(_env: JNIEnv, _class: JObject){
    let fn_ptr = test_func;
    let nmd: jni::NativeMethod = jni::NativeMethod{
        name: JNIString::from("test_func"),
        sig: JNIString::from("Ljava/lang/Void;"),
        fn_ptr: fn_ptr as *mut c_void
    };
    JNIEnv::register_native_methods(&_env, _class, &[nmd]).expect("register_native_methods");
}

其中JNI_Onload会在jvm加载JNI的时候执行,自动将其中的native方法注册进去。

当在Java里首次调用native方法时,JVM就会寻找对应名称的导出的或者动态注册的native函数,并将Java的native方法和Rust的函数关联起来;如果JVM没找到对应的native函数,则会报java.lang.UnsatisfiedLinkError异常。

这里引入作者提供的例子

use jni::objects::*;
use jni::sys::{jint, jobject, jstring};
use jni::JNIEnv;
#[no_mangle]
pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_addInt(
    env: JNIEnv,
    _class: JClass,
    a: jint,
    b: jint,
) -> jint {
    a + b
}
#[no_mangle]
pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_getThisField(
    env: JNIEnv,
    this: JObject,
    name: JString,
    sig: JString,
) -> jobject {
    let result = env
        .get_field(
            this,
            &env.get_string(name).unwrap().to_string_lossy(),
            &env.get_string(sig).unwrap().to_string_lossy(),
        )
        .unwrap();
    result.l().unwrap().into_inner()
}

以上代码导出了两个函数addInt和getThisField,addInt就是最常见的两个整数加和,getThisField用来获取class中字段的值,此时修改RustJNI.java

package pers.metaworm;
public class RustJNI {
    static {
        System.loadLibrary("rust_java_demo");
    }
    public static void main(String[] args) {
        init();
        System.out.println("test addInt: " + (addInt(1, 2) == 3));
        RustJNI jni = new RustJNI();
        System.out.println("test getThisField: " + (jni.getThisField("stringField", "Ljava/lang/String;") == jni.stringField));
        System.out.println("test success");
    }
    String stringField = "abc";
    static native void init();
    static native int addInt(int a, int b);
    native Object getThisField(String name, String sig);
}

这里和上面是一样的,在类中申明native 方法,如果返回值是复杂类型的,那就用Object

此时编译运行就会输出

rust-java-demo inited
test addInt: true
test getThisField: true

证明Java调用Rust成功了。

参数传递

JNI函数一般前两个参数是JNIEnv和类对象,JNIEnv提供了交互操作,类对象根据方法不同有不同的含义,如果是静态native方法那这里取到的就是类对象,如果是实例native方法,那么获取到的就是this实例,从第三个参数开始就是申明的方法所提供的参数了。

对于基础的类型可以直接用use jni::sys::*提供的j开头的系列类型来声明,这里给出作者提供的对照表

如果是复杂类型(引用类型)用jni::objects::JObject来申明

抛异常

一般来说,是这样抛异常的

env.exception_clear().expect("clear");
env.throw_new("Ljava/lang/Exception;", format!("{err:?}"))
                .expect("throw");
            std::ptr::null_mut()

可以看到在抛异常之前,调用了env.exception_clear()来清除异常,这是因为前面的get_field已经抛出一个异常了,当env里已经有一个异常的时候,后续再调用env的函数都会失败,这个异常也会继续传递到上层的Java调用者,所以其实这里没有这两句,直接返回null的话,Java也可以捕获到异常;但我们通过throw_new可以自定义异常类型及异常消息.

这里给出作者提供的一个经典的抛异常的例子

#[no_mangle]
pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_divInt(
    env: JNIEnv,
    _class: JClass,
    a: jint,
    b: jint,
) -> jint {
    if b == 0 {
        env.throw_new("Ljava/lang/Exception;", "divide zero")
            .expect("throw");
        0
    } else {
        a / b
    }
}

三、Rust调用Java

对于如何在Rust中调用Java对象,类,访问字段等操作,作者提供了以下代码

#[allow(non_snake_case)]
fn call_java(env: &JNIEnv) {
    match (|| {
        let File = env.find_class("java/io/File")?;
        // 获取静态字段
        let separator = env.get_static_field(File, "separator", "Ljava/lang/String;")?;
        let separator = env
            .get_string(separator.l()?.into())?
            .to_string_lossy()
            .to_string();
        println!("File.separator: {}", separator);
        assert_eq!(separator, format!("{}", std::path::MAIN_SEPARATOR));
        // env.get_static_field_unchecked(class, field, ty)
        // 创建实例对象
        let file = env.new_object(
            "java/io/File",
            "(Ljava/lang/String;)V",
            &[JValue::Object(env.new_string("")?.into())],
        )?;
        // 调用实例方法
        let abs = env.call_method(file, "getAbsolutePath", "()Ljava/lang/String;", &[])?;
        let abs_path = env
            .get_string(abs.l()?.into())?
            .to_string_lossy()
            .to_string();
        println!("abs_path: {}", abs_path);
        jni::errors::Result::Ok(())
    })() {
        Ok(_) => {}
        // 捕获异常
        Err(jni::errors::Error::JavaException) => {
            let except = env.exception_occurred().expect("exception_occurred");
            let err = env
                .call_method(except, "toString", "()Ljava/lang/String;", &[])
                .and_then(|e| Ok(env.get_string(e.l()?.into())?.to_string_lossy().to_string()))
                .unwrap_or_default();
            env.exception_clear().expect("clear exception");
            println!("call java exception occurred: {err}");
        }
        Err(err) => {
            println!("call java error: {err:?}");
        }
    }
}
#[no_mangle]
pub unsafe extern "C" fn Java_pers_metaworm_RustJNI_callJava(env: JNIEnv) {
    println!("call java");
    call_java(&env)
}

你可以看到,获取一个类,字段是这么操作的

let File = env.find_class("java/io/File")?;
let separator = env.get_static_field(File, "separator", "Ljava/lang/String;")?;

如果要调用实例方法,那么就需要

let abs = env.call_method(file, "getAbsolutePath", "()Ljava/lang/String;", &[])?;
let abs_path = env
            .get_string(abs.l()?.into())?
            .to_string_lossy()
            .to_string();

这里作者总结了常用的方法

  • 创建对象 new_object
  • 创建字符串对象 new_string
  • 调用方法 call_method call_static_method
  • 获取字段 get_field get_static_field
  • 修改字段 set_field set_static_field

此外还可以捕获异常

let except = env.exception_occurred().expect("exception_occurred");
let err = env
          .call_method(except, "toString", "()Ljava/lang/String;", &[])
          .and_then(|e| Ok(env.get_string(e.l()?.into())?.to_string_lossy().to_string()))
          .unwrap_or_default();
env.exception_clear().expect("clear exception");
println!("call java exception occurred: {err}");

总结

好了,本期内容到此为止。本文主要是写了Rust利用JNI实现与Java的相互调用,内容很多是取自大佬metaworm的文章,本文并没有将其全部内容写出,还有一些全局对象引用,异常处理等相关内容,如果你对其感兴趣,更建议去阅读原文,这里主要是修正一些大佬写的文章中一些导致读者难以理解的小问题。

目录
相关文章
|
3月前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
|
13天前
|
Java API Apache
|
29天前
|
JSON JavaScript Java
在Java中处理JSON数据:Jackson与Gson库比较
本文介绍了JSON数据交换格式及其在Java中的应用,重点探讨了两个强大的JSON处理库——Jackson和Gson。文章详细讲解了Jackson库的核心功能,包括数据绑定、流式API和树模型,并通过示例演示了如何使用Jackson进行JSON解析和生成。最后,作者分享了一些实用的代码片段和使用技巧,帮助读者更好地理解和应用这些工具。
在Java中处理JSON数据:Jackson与Gson库比较
|
1月前
|
人工智能 缓存 Java
深入解析Spring AI框架:在Java应用中实现智能化交互的关键
【10月更文挑战第12天】Spring AI 是 Spring 框架家族的新成员,旨在满足 Java 应用程序对人工智能集成的需求。它支持自然语言处理、图像识别等多种 AI 技术,并提供与云服务(如 OpenAI、Azure Cognitive Services)及本地模型的无缝集成。通过简单的配置和编码,开发者可轻松实现 AI 功能,同时应对模型切换、数据安全及性能优化等挑战。
|
1月前
|
Rust 监控 编译器
解密 Python 如何调用 Rust 编译生成的动态链接库(一)
解密 Python 如何调用 Rust 编译生成的动态链接库(一)
37 2
|
1月前
|
Rust 安全 Python
解密 Python 如何调用 Rust 编译生成的动态链接库(二)
解密 Python 如何调用 Rust 编译生成的动态链接库(二)
29 1
|
2月前
|
安全 Java API
【性能与安全的双重飞跃】JDK 22外部函数与内存API:JNI的继任者,引领Java新潮流!
【9月更文挑战第7天】JDK 22外部函数与内存API的发布,标志着Java在性能与安全性方面实现了双重飞跃。作为JNI的继任者,这一新特性不仅简化了Java与本地代码的交互过程,还提升了程序的性能和安全性。我们有理由相信,在外部函数与内存API的引领下,Java将开启一个全新的编程时代,为开发者们带来更加高效、更加安全的编程体验。让我们共同期待Java在未来的辉煌成就!
63 11
|
2月前
|
安全 Java API
【本地与Java无缝对接】JDK 22外部函数和内存API:JNI终结者,性能与安全双提升!
【9月更文挑战第6天】JDK 22的外部函数和内存API无疑是Java编程语言发展史上的一个重要里程碑。它不仅解决了JNI的诸多局限和挑战,还为Java与本地代码的互操作提供了更加高效、安全和简洁的解决方案。随着FFM API的逐渐成熟和完善,我们有理由相信,Java将在更多领域展现出其强大的生命力和竞争力。让我们共同期待Java编程新纪元的到来!
94 11
|
1月前
|
JSON Java 数据格式
Java Jackson-jr库使用介绍
Jackson-jr是专为资源受限环境设计的轻量级JSON处理库,适用于微服务、移动应用及嵌入式系统。它通过牺牲部分高级功能实现了更小体积和更快启动速度,非常适合对库大小敏感的项目。本文将介绍如何使用Jackson-jr进行JSON序列化与反序列化,并演示处理嵌套对象与数组的方法。此外,还介绍了自定义序列化与反序列化的技巧以及性能与功能的权衡。通过示例代码,展示了Jackson-jr在常见任务中的高效与灵活性。
24 0
|
2月前
|
数据采集 存储 前端开发
Java爬虫开发:Jsoup库在图片URL提取中的实战应用
Java爬虫开发:Jsoup库在图片URL提取中的实战应用