【一起学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的文章,本文并没有将其全部内容写出,还有一些全局对象引用,异常处理等相关内容,如果你对其感兴趣,更建议去阅读原文,这里主要是修正一些大佬写的文章中一些导致读者难以理解的小问题。

目录
相关文章
|
2月前
|
Rust 安全 开发者
Rust之旅:打造并发布你的首个Rust库
本文将引导读者走进Rust的世界,从基础概念讲起,逐步深入到如何创建、测试、打包和发布自己的Rust库。通过实际操作,我们将理解Rust的独特之处,并学会如何分享自己的代码到Rust社区,从而为开源世界做出贡献。
|
4天前
|
SQL Java 数据库连接
Java从入门到精通:2.3.1数据库编程——学习JDBC技术,掌握Java与数据库的交互
ava从入门到精通:2.3.1数据库编程——学习JDBC技术,掌握Java与数据库的交互
|
16天前
|
JSON 前端开发 Java
JWT解密:探秘令牌魔法与Java的完美交互
JWT解密:探秘令牌魔法与Java的完美交互
26 0
JWT解密:探秘令牌魔法与Java的完美交互
|
1月前
|
数据采集 JavaScript 前端开发
使用HtmlUnit库的Java下载器:下载TikTok视频
使用Java和HtmlUnit构建TikTok视频下载器,模拟浏览器行为,绕过访问限制。通过爬虫代理配置代理服务器,隐藏真实IP,多线程技术提升下载效率。示例代码展示如何设置HtmlUnit,创建代理,启用JavaScript,下载并处理视频链接。学习了页面模拟、JavaScript交互、代理使用及多线程技术,为实际爬虫项目提供参考。
使用HtmlUnit库的Java下载器:下载TikTok视频
|
1月前
|
存储 监控 安全
Java基于物联网技术的智慧工地云管理平台源码 依托丰富的设备接口标准库,快速接入工地现场各类型设备
围绕施工安全、质量管理主线,通过物联感知设备全周期、全覆盖实时监测,将管理动作前置,实现从事后被动补救到事前主动预防的转变。例如塔吊运行监测,超重预警,升降机、高支模等机械设备危险监控等,通过安全关键指标设定,全面掌握现场安全情况,防患于未然。
148 5
|
1月前
|
XML Java API
JAVA标准库
JAVA标准库
|
2月前
|
Rust 安全 Java
Rust与Java:性能与效率的较量
本文将对比Rust和Java两种编程语言在性能和效率方面的差异。我们将探讨Rust如何通过其独特的内存管理、并发模型和编译时优化来实现高性能,同时分析Java如何在虚拟机(JVM)的支持下实现高效运行。通过比较这两种语言的特性和应用场景,我们可以为开发者在选择编程语言时提供有益的参考。
|
2月前
|
Rust 前端开发 JavaScript
Rust与JavaScript的跨语言交互:探索与实践
本文旨在探讨Rust与JavaScript之间的跨语言交互方法。我们将深入了解WebAssembly(Wasm)的角色,以及它如何使得Rust与JavaScript能够在Web应用中和谐共处。此外,我们还将介绍Rust与JavaScript的集成方式,包括Rust编译到Wasm、使用wasm-bindgen进行Rust与JavaScript的绑定,并通过实际案例展示如何实现两者之间的交互。
|
2月前
|
存储 Rust 安全
Rust标准库概览:集合、IO、时间与更多
本文将带领读者深入了解Rust标准库中的一些核心模块,包括集合类型、输入/输出处理、时间日期功能等。我们将通过实例和解释,探讨这些模块如何使Rust成为高效且安全的系统编程语言。
|
2月前
|
存储 缓存 安全
Guava:Java开发者的全方位工具库
Guava:Java开发者的全方位工具库
72 0