📖

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

に公開

今回は

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

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

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

手順

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

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

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

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

RustのソースファイルをAndroid Studioでビルドするには 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 --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 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 となります。

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

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

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 ではRustのビルド時にシステムのPythonを実行しています。

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

rust.pythoncommand=python3

Rustライブラリの用意

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

$ 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 --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 の修正

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

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つのエディタを行き来している状態となっています。

Discussion