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

  • Post author:
  • Post category:java






前言

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



参考


  1. 参考文章

  2. 代码



版权声明:本文为weixin_47754149原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。