Rust Java JNI 调用简介

2024-01-26 language rust java

JNI 是 Java 提供的一套与其它语言相互调用的标准,主要是 C 语言,所以,从理论上只要支持 C ABI 的语言都可以和 Java 语言相互调用,而 Rust 就是其中之一。

简介

通过 Rust 调用 Java 语言时,可以使用原始的 JNI 接口,也就是自己声明 JNI 的 C 函数原型,然后在 Rust 中按照 C 的方式去调用,不过这样操作起来比较繁琐,而且都是 unsafe 类型,社区的 jni 包已经封装了原始 JNI 接口,可以直接使用。

类型描述符

在 JVM 虚拟机中,数据类型的名称通过描述符保存,而非习惯的 int float 等类型名,基础类型包括了:int(I)long(J)byte(B)short(S)char(C)float(F)double(D)boolean(Z)void(V),其它的如引用类型 L+类全名+;、数组 []、方法 (参数)返回值 等。

如下是简单示例。

Java: java.lang.String
 JNI: Ljava/lang/String;

Java: String[]
 JNI: [Ljava/lang/String;
Java: int[][]
 JNI: [[I

Java: long hello(int n, String s, int[] array);
 JNI: (ILjava/lang/String;[I)J
Java: void hello();
 JNI: ()V

在类似 env.get_field()env.call_method() 等方法中会使用。

类型转换

jni::sys 中声明了各种基础类型,与对应的 Java 类型一一对应,例如 jboolean jbyte jchar jshort jint jlong jfloat jdouble 等等,都可以直接使用,这些都是原始类型 (Primitive Type)。

jni::objects 中的 JObject JString 是对 jobject jstring 的简单封装,可以通过 JString::from(obj) obj.into() 进行转换,如果不确定对象的类型,那么在转换前可以通过 JNIEnv::is_instance_of() 检查。

当通过 env.get_field() env.call_method() 获取到某个对象后,接着可以通过类似 l() i() 等方法转换,然后再通过 env.get_string() 类似方法转换为 Rust 对象,可以参考如下访问 Java 对象的示例。

项目初始化

----- 创建 Rust 项目,支持动态库,同时将 main.rs 修改为 lib.rs
$ cargo new demo
$ cargo add jni
$ cat Cargo.toml
... ...
[lib]
crate-type = ['cdylib']
... ...

----- 创建 Java 相关的项目,会在 example 目录下生成
$ mvn archetype:generate -DgroupId=com.foobar.demo -DartifactId=example -Dversion=1.0 \
    -DarchetypeArtifactId=maven-archetype-quickstart
----- 打包并运行,还可以是 mvn compile、mvn test 以及 mvn clean 等
$ mvn package
$ java -cp target/example-1.0.jar com.foobar.demo.App
----- 也可以编译运行
$ mvn compile
$ java -classpath target/classes com.foobar.demo.App

此时会在同一目录下存在 demo(Rust)example(Java) 两个子目录。如下以此为基础,介绍常用的示例,还可以参考 github 中的示例,有很多不错的使用技巧,包括新建对象、结构体、异步调用等。

Java 调用 Rust

修改 src/lib.rs 文件,然后通过 cargo build 编译,会生成 target/debug/libdemo.so 文件。

use jni::objects::JClass;
use jni::JNIEnv;

#[no_mangle]
pub extern "system" fn Java_com_foobar_demo_App_hello(_env: JNIEnv, _class: JClass) {
    println!("Hello Java!!!");
}

然后修改 Java 项目中的 src/main/java/com/foobar/demo/App.java 文件,通过 mvn compile 编译。

package com.foobar.demo;

public class App {
    static {
        System.loadLibrary("demo");
    }
    static native void hello();

    public static void main(String[] args) {
        hello();
    }
}

最后通过如下命令运行代码。

java -Djava.library.path=demo/target/debug -classpath example/target/classes com.foobar.demo.App

此时,在终端会打印 Hello Java!!! 信息。

在 Rust 中通过 Java_<类完整路径>_<方法名> 将函数暴露出来,然后在对应 Java 类中声明对应的方法。在 Rust 代码中,第一个 JNIEnvJVM 运行环境变量,很多操作需要;第二个是类对象 (静态方法 JClass)或者 this 对象(实例方法 JObject)。

基础类型参数

use jni::objects::JClass;
use jni::sys::jint;
use jni::JNIEnv;

#[no_mangle]
pub extern "system" fn Java_com_foobar_demo_App_sum(
    _env: JNIEnv,
    _class: JClass,
    a: jint,
    b: jint,
) -> jint {
    a + b
}
package com.foobar.demo;

public class App {
    static {
        System.loadLibrary("demo");
    }
    static native int sum(int a, int b);

    public static void main(String[] args) {
        System.out.println("Sum(1, 2)=" + sum(1, 2));
    }
}

关于 String 类型的使用,可以参考下面关于本地全局引用的讨论。

访问 Java

包括了访问 Java 中的成员变量以及调用成员函数。

use jni::objects::{JObject, JValue};
use jni::sys::jstring;
use jni::JNIEnv;

#[no_mangle]
pub extern "system" fn Java_com_foobar_demo_App_hello(mut env: JNIEnv, obj: JObject) -> jstring {
    let result = env
        .get_field(&obj, "name", "Ljava/lang/String;")
        .expect("Couldn't get name field")
        .l()
        .expect("Couldn't convert to object");

    env.call_method(
        &obj,
        "hey",
        "(Ljava/lang/String;)V",
        &[JValue::from(&result)],
    )
    .expect("Call hey method failed");

    let name: String = env
        .get_string(&result.into())
        .expect("Couldn't convert to rust string")
        .into();
    let output = env
        .new_string(format!("Hello {}!!!", name))
        .expect("Couldn't create java string");
    output.into_raw()
}
package com.foobar.demo;

public class App {
    static {
        System.loadLibrary("demo");
    }

    void hey(String name) {
        System.out.println("Hey " + name);
    }

    String name = "Andy";
    native String hello();

    public static void main(String[] args) {
        System.out.println(new App().hello());
    }
}

其它常用的方法还有,创建对象 new_object() new_string()、调用方法 call_method() call_static_method()、获取字段 get_field() get_static_field()、修改字段 set_field() set_static_field() 等。

抛出异常

上述的代码都是通过 expect() 或者 unwrap() 方式处理异常,适用于示例代码,不适合线上。

use jni::errors::Result;
use jni::objects::JObject;
use jni::sys::jstring;
use jni::JNIEnv;

fn do_hello(env: &mut JNIEnv, obj: &JObject) -> Result<jstring> {
    let result = env.get_field(obj, "name", "Ljava/lang/String;")?.l()?;
    let name: String = env.get_string(&result.into())?.into();
    let output = env.new_string(format!("Hello {}!!!", name))?;
    Ok(output.into_raw())
}

#[no_mangle]
pub extern "system" fn Java_com_foobar_demo_App_hello(mut env: JNIEnv, obj: JObject) -> jstring {
    do_hello(&mut env, &obj).unwrap_or_else(|_| JObject::null().into_raw())
}

注意,在 Rust 代码中并没有显示调用 env.throw() env.throw_new() 等函数,暂不确定是否是因为不同版本中有优化。

package com.foobar.demo;

public class App {
    static {
        System.loadLibrary("demo");
    }

    String iname = "Andy";
    native String hello();

    public static void main(String[] args) {
        System.out.println(new App().hello());
    }
}

注意几个调整,使用单独函数处理业务逻辑,这样方便处理 Rust 中的 Result 结果;在函数传参过程中,其中 JNIEnv 是可变引用。

常用技巧

Local VS. Global

JNI 中存在 Local Reference 和 Global Reference 两类,当从 Java 切换到 Native 时会创建一个本地引用表,用于维护在 Native 中使用的对象,最大 512 个,当返回到 Java 环境时,该引用表会被删除。

在 Rust 中,可以通过 <'local> 告知编译器返回的是本地引用,可以确保不被销毁,如下是常用示例。

package com.foobar.demo;

public class App {
    static {
        System.loadLibrary("demo");
    }
    static native String hello(String name);

    public static void main(String[] args) {
        System.out.println(hello("Andy"));
    }
}

如下的两个版本是都允许的。

use jni::objects::{JClass, JString};
use jni::sys::jstring;
use jni::JNIEnv;

#[no_mangle]
pub extern "system" fn Java_com_foobar_demo_App_helloV0(
    mut env: JNIEnv,
    _class: JClass,
    name: JString,
) -> jstring {
    let input: String = env
        .get_string(&name)
        .expect("Couldn't get java name")
        .into();
    let output = env
        .new_string(format!("Hello {}!!!", input))
        .expect("Couldn't create java string");
    output.into_raw()
}

#[no_mangle]
pub extern "system" fn Java_com_foobar_demo_App_hello<'local>(
    mut env: JNIEnv<'local>,
    _class: JClass<'local>,
    name: JString<'local>,
) -> JString<'local> {
    let input: String = env
        .get_string(&name)
        .expect("Couldn't get java name")
        .into();
    env.new_string(format!("Hello {}!!!", input))
        .expect("Couldn't create java string")
}

按照谁使用谁管理的原则,对于像 jstring 这类需要动态申请内存的,要么完全在 Native 中管理,要么通过 env.new_string() 类似的函数创建。