Rust 与其他语言进行交互,基本是以 C ABI 作为规范(当然也支持其他 ABI 规范,只是 C ABI 使用得更广泛)。在和Python 或者 javascript 之间的FFI 的编写是非常简单的,Python 有PyO3,而javascript 有 wasm 和 napi, 这些框架基本做到开箱既用。
那和 Java 交互(或者 JVM 平台 )呢?
Java 和 C/C++ 的交互使用的是一个叫JNI的技术 。JNI(Java Native Interface) 是 Java 提供的一种标准编程接口,允许 Java 代码与本地代码(如 C、C++、Rust)交互。它的核心目标是为 Java 程序提供与操作系统底层或高性能本地库交互的能力。或许在 Java web 上使用 jni 的情况比较少,但是,在 Android 开发上,却是非常的普遍。
在 Rust 上,有一个叫 jni-rs的库可以让 Rust 与 jvm 平台的语言进行交互。但是 jni-rs 并不算是开箱既用的框架,它仅仅是对jni的一些封装。我们必须要学习 JNI 的细节才能够掌握如何用rust为 jvm 平台实现函数。
对照表
一份关于 jni 与 java 的类型对照表:
| jni-rs | Java |
|---|---|
| jboolean | boolean |
| jbyte | byte |
| jchar | char |
| jshort | short |
| jint | int |
| jlong | long |
| jfloat | float |
| jdouble | double |
| jstring、JString | String |
| jobject、JObject | Object |
| jclass、JClass | Object |
| jarray | 数组 |
| jbooleanArray | boolean[] |
| jbyteArray | byte[] |
| jcharArray | char[] |
| jshortArray | short[] |
| jintArray | int[] |
| jlongArray | long[] |
| jfloatArray | float[] |
| jdoubleArray | double[] |
| jobjectArray | Object[] |
函数命名空间
我们都知道 java 的命名空间是基于目录的,比如 com.kkch.example.HelloWorld, 对应的文件名是 com/kkch/example/HelloWorld.java。而且, Java 是纯面向对象语言。
因此, JNI 的导出函数必须要包涵有 Java、命名空间、类名、方法名 四大要素,比如下面这个签名:
pub extern "C" fn Java_com_kkch_example_HelloWorld_new(env: JNIEnv, _class: JClass, java_pattern: JString ) -> jstring {}
它非常明确地描述着一个叫 com.kkch.example.HelloWorld.new() 这个方法。需要注意的是,导出的函数中,前2个参数 env: JNIEnv, _class: JClass, 是必须而且是固定的。
有了上面的基础,一个只传递普通数据类型的函数就可以做到了。比如:
#[unsafe(no_mangle)]
pub extern "C" fn Java_com_kkch_example_HelloWorld_new(env: JNIEnv, _class: JClass, java_pattern: JString ) -> jstring {
let s = env.new_string("new function").unwrap();
s.into_raw()
}
我这里以 Android 调用作为例子。
准备工作有下面几个:
- Rust 这边需要下载适合安卓的 target
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android
- android studio 需要下载 NDK, 我们需要使用 NDK 作为交叉编译器, 在 Rust 这边配置编译器如下:
[target.aarch64-linux-android]
ar = "/home/kkch/Android/Sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/home/kkch/Android/Sdk/ndk/27.2.12479018/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang"
#[target.'cfg(any(target_arch = "arm", target_arch = "aarch64"))']
#rustflags = ["-C", "target-feature=+fp16"]
- 我们需要创建类:
com.kkch.example.HelloWorld,我这里直接采用kotlin语言:
package com.kkch.example
class HelloWorld {
init {
System.loadLibrary("example")
}
external fun new(): String
}
example 名称要看你编译出来的名字是什么,比如这里so 库名称是 libexample.so。
最后调用:
val hello = HelloWorld()
val r = hello.new()
Log.d("output new",r)
高阶用法
上面的用法都是 java 层调用 rust 代码,反过来也是可以的。这里又有一套规则需要学习如何在 rust/C/C++ 调用 java 代码:
JNI字段描述符:
JNI 类型描述符对照表
| 描述符 | 类型 | 示例及说明 |
|---|---|---|
V | void | ()V:无参数且返回 void 的方法 |
I | int | (I)V:接收一个 int 参数并返回 void 的方法 |
B | byte | (B)V:接收一个 byte 参数并返回 void 的方法(注意:原词应为 byte) |
C | char | (C)V:接收一个 char 参数并返回 void 的方法 |
D | double | (D)V:接收一个 double 参数并返回 void 的方法 |
F | float | (F)V:接收一个 float 参数并返回 void 的方法 |
J | long | (J)V:接收一个 long 参数并返回 void 的方法 |
S | short | (S)V:接收一个 short 参数并返回 void 的方法 |
Z | boolean | (Z)V:接收一个 boolean 参数并返回 void 的方法 |
[元素类型 | 数组 | ([I)V:接收一个 int[] 参数并返回 void 的方法 |
L类全限定名; | 类对象 | Ljava/lang/String;:表示 String 类对象 |
- 二维数组用
[[,比如[[I表示Vec<Vec<i32>> - 对象类型,必须是
L打头,以;作为结尾 下面我们在 java/kotlin 层写一个接口:
interface Callback{
fun ok(msg: String)
}
external fun call(callback: Callback)
rust 调用:
#[unsafe(no_mangle)]
pub extern "C" fn Java_com_kkch_example_HelloWorld_call(
mut env: JNIEnv,
_class: JClass,
callback: JObject,
) {
let output = env.new_string("callback is ok").unwrap();
let args = [JValue::Object(&output)];
env.call_method(callback, "ok", "(Ljava/lang/String;)V", &args)
.unwrap();
}
整体上, rust 与jni的交互要比其他语言麻烦一些。