前言

提到JNI,大家都会想到C,C++.不过如今Rust又给我们增加了一个选项,借助rust的jni库(https://github.com/jni-rs/jni-rs),我们可以很方便的使Android与rust交互.从本章起,我们将逐步地了解使用rust实现一些经典的jni方法.

关于 Rust 环境搭建、配置 Rust Android targets、linker,以及如何在 Android 上如何直接运行 Rust 代码,可以看上篇文章 将 Rust 编译为可在 Android 上使用的二进制文件

本文主要介绍如何使用 Rust 借助 J4RS 方便快捷的编写 Android Jni:

阅读本文,你需要具备、了解一下知识:

  • Android 编写 JNI 的基本流程
  • Rust 代码基本的阅读能力
  • Android 开发的基本流程

Android 集成 Rust

配置 Rust Android Gradle Plugin

Rust Android Gradle Plugin 这个 Gradle 插件的主要功能是帮你自动配置 Rust-Android 交叉编译,并将编译产物自动添加到 Android 项目。

这并不是必须的,你同样可以手动 build rust 项目。然后手动复制到 Android Studio 中使用。

因为我使用的是最新版本的 gradle , 采用 Kotlin kts 编写。在语法上稍有不同,旧版本写法可以在项目的 README 查看

添加插件

在项目根目录settings.gradle.kts添加

pluginManagement {
   repositories {
       google()
       mavenCentral()
+      maven(url = "https://plugins.gradle.org/m2/")
       gradlePluginPortal()
   }
}

build.gradle.kts添加

plugins {
    id("com.android.application") version "8.1.0" apply false
    id("org.jetbrains.kotlin.android") version "1.8.0" apply false
+   id("org.mozilla.rust-android-gradle.rust-android") version "0.9.3"
}

创建 Rust 项目

在项目根目录执行:

cargo new --lib rust

你的项目名称、路径并不需要和我的保持一致。

现在我们的项目目录结构应该是这样的:

配置cargo

在项目级 build.gradle.kts 添加:

添加 cargo 配置:

android{}


cargo {
    module = "../rust"       // Cargo.toml路径
    libname = "rust"          // Cargo.toml 中 [package] 中 name
    targets = listOf("arm", "arm64" /*"x86", "x86_64"*/)
    targetIncludes = arrayOf("rust.so") // 编译后 so 的名字
    targetDirectory = "../rust/target/" // rust 编译产物的目录
}


tasks.whenTaskAdded {
    when (name) {
        "mergeDebugJniLibFolders", "mergeReleaseJniLibFolders" -> {
            dependsOn("cargoBuild")
            this.inputs.dir(buildDir.resolve("rustJniLibs/android")) // 将编译后的 so 复制到 build/rustJniLibs/android 目录
        }
    }
}

tasks.register<Exec>("cargoClean") {
    executable("cargo")     // cargo.cargoCommand
    args("clean")
    // rust 项目的路径
    workingDir("${projectDir.parentFile}${cargo.module?.replace("..", "")}")
}

tasks.clean.dependsOn("cargoClean")

添加 plugins:

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
+   id("org.mozilla.rust-android-gradle.rust-android")
}

添加 android target

rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android

OK 现在同步 Gradle 即可使用插件来自动进行 Rust 的交叉编译了。

编写 Android 调用 Rust 代码

Android 端代码:

class CallRust {
    init {
        System.loadLibrary("rust");
    }

     external fun callback(listener: Instance<RustListener>)
}

interface RustListener {
    fun onStringCallback(msg: String)
    fun onVoidCallback()
}

Rust 本上是兼容 C 接口的,所以我们先使用传统方式写

#![allow(non_snake_case)]
#[no_mangle]
pub extern fn Java_com_apkdv_rustinandroid_CallRust_callback(_env: JNIEnv, _class: JClass,
                                                                 callback: JObject) {
    let hello = "Hello world from Rust";
    // 转为 Java String
    let jni_string_hello = JNIString::from(hello);
    let j_string_hello = _env.new_string(jni_string_hello).unwrap();
    _env.call_method(callback, "onStringCallback", "(Ljava/lang/String;)V", &[j_string_hello.into()]).unwrap();
    _env.call_method(callback, "onVoidCallback", "()V", &[]).unwrap();
}

可以看到,就写法上还是稍微有点麻烦,接下来我们使用 J4RS 来实现:

进阶——使用 J4RS 简化 Java 与 Rust 的相互调用

先添加 j4rs 依赖

cargo add j4rs_derive j4rs

在 Android Gradle 中添加

implementation("io.github.astonbitecode:j4rs:0.17.1")

修改 Android 代码:

class CallRust {
    init {
        System.loadLibrary("rust");
    }

     external fun callback(listener: Instance<RustListener>)
}

interface RustListener {
    fun onStringCallback(msg: String)
    fun onVoidCallback()
}

Rust 端实现:

#[call_from_java("com.apkdv.rustinandroid.CallRust.callback")]
fn rust_to_java_callback(callback: Instance) {
    let jvm = Jvm::attach_thread().unwrap();
    let ia = InvocationArg::try_from("callback from j4rs").unwrap();
    let _ = jvm.invoke(&callback, "onStringCallback", &[ia]).unwrap();
    let _ = jvm.invoke(&callback, "onVoidCallback", &[]).unwrap();
}

这就完成了。可以看到,写起来非常的方便,不需要写冗长的函数签名,直接使用 call_from_java 声明Java 的方法名就可以了,而且也可以直接使用 Rust 的类型了。

J4RS 使用介绍

J4RS 允许从Rust轻松调用Java代码,反之亦然。

j4rs 具有以下特性:

  • 从 Rust 代码中调用 Java 代码。
  • Java 到 Rust 回调。
  • Java 泛型。
  • Java 函数重载。

支持的数据类型

j4rs 支持以下数据类型的转换:

Rust 到 Java:

  • i8、i16、i32、i64、u8、u16、u32、u64
  • f32、f64
  • String
  • Vec
  • Result<T, E>

Java 到 Rust:

  • byte、short、int、long、float、double
  • String
  • List
  • Map<K, V>
  • Optional

向Java传递Rust 数据

j4rs 使用InvocationArg枚举将参数传递给 Java 。使用TryFrom语法可以传递几种基本类型:

let i1 = InvocationArg::try_from("a str")?;      // java.lang.String
let my_string = "a string".to_owned();
let i2 = InvocationArg::try_from(my_string)?;    // java.lang.String
let i3 = InvocationArg::try_from(true)?;         // java.lang.Boolean
let i4 = InvocationArg::try_from(1_i8)?;         // java.lang.Byte
let i5 = InvocationArg::try_from('c')?;          // java.lang.Character
let i6 = InvocationArg::try_from(1_i16)?;        // java.lang.Short
let i7 = InvocationArg::try_from(1_i64)?;        // java.lang.Long
let i8 = InvocationArg::try_from(0.1_f32)?;      // java.lang.Float
let i9 = InvocationArg::try_from(0.1_f64)?;      // java.lang.Double

对于自定义类型:

#[derive(Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
struct MyBean {
    name: String,
    age: i32,
}

let my_bean = MyBean {
    name: "My String In A Bean".to_string(),
    age: 33,
};
let ia = InvocationArg::new(&my_bean, "com.apkdv.rustinandroid.MyBean");

在 Kotlin 端:

data class MyClass(val name: String, val age: Int) {
    // 必须有一个无参构造函数
    constructor() : this("", 0)
}

需要注意的是。j4rs 内部使用 Jackson 解析数据,所以,如果使用 Kotlin 的 data class 需要手动实现一个无参的构造函数。

对于Vec

let my_vec: Vec<String> = vec![
    "abc".to_owned(),
    "def".to_owned(),
    "ghi".to_owned()];
//
let i10 = InvocationArg::try_from(my_vec.as_slice())?;

对于 NULL

let null_string = InvocationArg::from(Null::String);                // A null String
let null_integer = InvocationArg::from(Null::Integer);              // A null Integer
let null_obj = InvocationArg::from(Null::Of("java.util.List"));    // A null object of any other class. E.g. List

Java 调用 Rust

Java 调用 Rust 除了上面提交的需要添加 j4rs 的 gradle 依赖以外,并不需要特别的处理。唯一值得注意的是,所有返回值、参数需要用 Instance包括,比如:

// 返回值
external fun callByJ4rs(): Instance<String>
// 参数与回调
external fun callback(listener: Instance<RustListener>)

从 Instance 取出数据

val callResult = CallRust().callByJ4rs()
val resultString = Java2RustUtils.getObjectCasted<String>(callResult)

Rust 调用 Java

j4rs 自身是支持调用Java 动态方法的。但是需要传入 Jar 路径,所以在 Android 端目前只成功调用了 静态方法。如果有其他方法欢迎留言评论。

调用 Java 静态方法:

Android 端:

需要注意:Java 中不能直接调用 Kotlin 中的静态方法和静态变量,所以需要在 Kotlin 方法上加上@JvmStatic