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 代码中,第一个 JNIEnv
是 JVM
运行环境变量,很多操作需要;第二个是类对象 (静态方法 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()
类似的函数创建。