Android環境でRust製ライブラリを直接呼び出して使用する

# 今回は

[前回][previous]は前提知識をなるべく活かす方向でC言語のレイヤーを挟みました。
今回は全体的な構成をシンプルに、なるべく使用言語などを少なくしたいのでRustでJNI用のライブラリを直接作成し呼び出します。

また、Pluginやビルドスクリプトの設定をしてRustのビルドも同時に行いたいと思います。

一応、比較のために今回の構成を図にすると以下の様になります。単純にJNIでRustで作成したライブラリを呼び出す形となります。

```mermaid
flowchart LR
  Java/Kotlin-->|JNI| RustLib[Rust言語ライブラリ]
```

# 手順

早速、実際の手順を確認していきます。
前提として[前回][previous]で作成したJNIを使用したプロジェクトを使用します。

大まかに下記の手順で進めます。

* Rust言語をビルドするgradle plugin (内部的にcargoを呼び出す)の設定
* Rust言語でJNIから呼ばれるライブラリの作成

## Android Studioでプロジェクトのビルド設定

RustのソースファイルをAndroid Studioでビルドするには [mozilla/rust-android-gradle](https://github.com/mozilla/rust-android-gradle) のプラグインを導入します。

気をつける点として下記の項目を全て修正してからビルドスクリプトのsyncを行った方が良いと思われます。

1ファイルずつエラーを確認しながら進めると、1ファイルとして正しく記述されていても、全体として整合性が合わないためのエラーが出たりするので逆に混乱する場合があります。

* `libs.versions.toml` の修正
* プロジェクトルートの `build.gradle.kts` の修正
* モジュールの `build.gradle.kts` の修正
* `local.properties` の修正

### `libs.versions.toml` の修正


`gradle/libs.versions.toml` にプラグインのバージョンとidを指定します。

```diff
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,6 +8,7 @@ espressoCore = "3.6.1"
 appcompat = "1.7.0"
 material = "1.12.0"
 constraintlayout = "2.2.0"
+mozillaRust = "0.9.6"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -21,4 +22,5 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }
 kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+mozilla-rust-android = { id = "org.mozilla.rust-android-gradle.rust-android", version.ref = "mozillaRust" }
```

### プロジェクトルートの `build.gradle.kts` の修正

プロジェクトのルートにある `build.gradle.kts` を下記のように修正します。

```diff
diff a/build.gradle.kts b/build.gradle.kts
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,4 +2,5 @@
 plugins {
     alias(libs.plugins.android.application) apply false
     alias(libs.plugins.kotlin.android) apply false
+    alias(libs.plugins.mozilla.rust.android) apply false
 }
\ No newline at end of file
```

### モジュールの `build.gradle.kts` の修正

`app/build.gradle.kts` を下記のように編集します。

既存のcmakeを使用したC/C++のビルド設定を削除し、Rustのビルド設定をします。

`cargo {...}` 内の `module` はビルド対象の `Cargo.toml` があるパスを指定します。今回のプロジェクトルートから `app/src/main/rust` となります。

同じく `libname` は `Cargo.toml` 内の `[package]` の `name = "myapplication"` と同じ名前を設定します。また、これはJava/Kotlinから呼び出す時の `System.loadLibrary("myapplication")` でも、同じライブラリ名を使用しますので全て同じ名前にする必要があります。

`tasks.configureEach {...}` ではAndroid Studio上でビルドを実行した時、Rustのビルドも実行し、作成されたライブラリを適切にパッケージングできるよに指定しています。

```diff
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,6 +1,7 @@
 plugins {
     alias(libs.plugins.android.application)
     alias(libs.plugins.kotlin.android)
+    alias(libs.plugins.mozilla.rust.android)
 }
 
 android {
@@ -33,12 +34,6 @@ android {
     kotlinOptions {
         jvmTarget = "11"
     }
-    externalNativeBuild {
-        cmake {
-            path = file("src/main/cpp/CMakeLists.txt")
-            version = "3.22.1"
-        }
-    }
     buildFeatures {
         viewBinding = true
     }
@@ -53,4 +48,17 @@ dependencies {
     testImplementation(libs.junit)
     androidTestImplementation(libs.androidx.junit)
     androidTestImplementation(libs.androidx.espresso.core)
+}
+
+cargo {
+    module  = "src/main/rust"
+    libname = "myapplication"
+    targets = listOf("arm", "arm64", "x86", "x86_64")
+}
+
+tasks.configureEach {
+    if (name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders") {
+        dependsOn("cargoBuild")
+        inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
+    }
 }
\ No newline at end of file
```

### `local.properties` の修正

[mozilla/rust-android-gradle](https://github.com/mozilla/rust-android-gradle) ではRustのビルド時にシステムのPythonを実行しています。

最近のUbuntuでは標準で `python` ではなく `python3` と呼び出す必要があります。開発環境にあったPythonコマンドをプロジェクトルートの `local.properties` に追加する必要があります。

```
rust.pythoncommand=python3
```

## Rustライブラリの用意

今回はRustのソースファイルを `app/src/main/rust` に新規作成します。

```shell
$ cd <path to project root>/app/src/main
$ cargo new --lib rust
```

上記のコマンドで `rust` ディレクトリが作成され、その中に作成された以下のファイルを下記の様に修正します。

### `app/src/main/rust/Cargo.toml` の修正

* `[package]` の `name` を `"myapplication"` と変更し
* JNIから呼び出せるように `[lib]` の `crate-type` を変更
* RustからJNIのAPIを呼び出せるように `[dependencies]` に `jni` を追加

```diff
diff --git a/app/src/main/rust/Cargo.toml b/app/src/main/rust/Cargo.toml
--- a/app/src/main/rust/Cargo.toml
+++ b/app/src/main/rust/Cargo.toml
@@ -1,6 +1,10 @@
 [package]
-name = "rust"
+name = "myapplication"
 version = "0.1.0"
 edition = "2021"

+[lib]
+crate-type = ["cdylib"]
+
 [dependencies]
+jni = "0.21.1"
```

### `app/src/main/rust/src/lib.rs` の修正

[前回][previous]、C言語で作成した部分をRustで置き換えます。
Java/KotlinからJNIで呼び出し可能な様に `#[no_mangle]` や `extern "C"` を追加し、関数名もJNIの規則沿ったものに変更します。

```diff
diff --git a/app/src/main/rust/src/lib.rs b/app/src/main/rust/src/lib.rs
index b93cf3f..a7ea608 100644
--- a/app/src/main/rust/src/lib.rs
+++ b/app/src/main/rust/src/lib.rs
@@ -1,14 +1,32 @@
-pub fn add(left: u64, right: u64) -> u64 {
+use jni::JNIEnv;
+use jni::objects::JClass;
+use jni::sys::jstring;
+
+pub fn rustadd(left: u64, right: u64) -> u64 {
     left + right
 }
 
+#[no_mangle]
+pub extern "C" fn Java_com_example_myapplication_MainActivity_stringFromJNI(
+    env: JNIEnv,
+    _: JClass,
+) -> jstring {
+    let x: u64 = 1;
+    let y: u64 = 2;
+    let z: u64 = rustadd(x, y);
+
+    let buf = format!("{}+{}={} from rust library", x, y, z);
+    let output = env.new_string(buf).expect("Couldn't create java string!");
+    output.to_owned()
+}
+
```

# まとめ

以上で、Android Studioでプロジェクト全体をビルドする時にRustのコードもコンパイルしJNI用のライブラリを作成して、適切に配置してapkなどのパッケージファイルにも同梱されるように設定できたかと思います。

環境構築で躓く点はあったものの、一度環境が構築できればビルド自体は既存のプロジェクトに統合されており開発環境としてはシンプルになり良かったと思います。

しかし、現状ではAndroid Studio上でRustのコードの編集時にコード補完などができていない状態で、別のエディタを使用しています。

Android Studioの元となるIntelliJ IDEAを開発しているJetBrainsではRust用IDEのRustRoverもあるのでなんとかなりそうなのですが（又はLSPサーバーであるrust-analyzerとの連携など）、現時点で連携方法がわからず、2つのエディタを行き来している状態となっています。

[previous]: https://qiita.com/TechSavvy_24/items/cdcf962738efad5df9f2