6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Androidアプリ内の複数のモジュールにRustを導入する

Posted at

現代のAndroidアプリ開発でNDKが必要になった場合、やはりネイティブ側の開発言語としてはRustを採用したいだろうと思います。

Androidネイティブ開発言語としてRustを導入すること自体はすでにいくつか例があるし、だいたいはmozilla/rust-android-gradleのREADME通りにやればできます。

ところが、現代のAndroidアプリはモジュール分割して開発されていることが多い。
複数のモジュールでNDKが必要になることもあるでしょう。

ネイティブ開発が必要なモジュール全てに上記のプラグインを導入すれば複数のモジュールでRust開発を行うことができるのですが、それではモジュールという分割されたハコの中に別々のRustクレートを作成し、別々のバイナリを生成することになります。

こういうイメージです
multi-plugins-image.png

最小限のRustコードしかないうちは特に困らないのですが、ネイティブ開発の規模が大きくなるといずれはネイティブコードから他モジュールのネイティブコードへアクセスする必要が出てきます。
各モジュールごとにネイティブのバイナリが生成される開発環境ではそれは難しいです。不可能ではないだろうとは思いますが。

そこでもっと簡単にネイティブコードから他モジュールのネイティブコードへアクセスできる開発環境を構築してみます。

この記事の内容は私が趣味の個人開発で行ったものです。
私の所属企業でこの手法を採用しているということではありませんので、あしからず。

結論

CargoのWorkspaceを使います。
普段Gradleで複数のモジュールを統合してひとつのAndroidアプリをビルドしているのと非常に似たCargoの機能です。複数のRustパッケージを統合してひとつのバイナリを生成できます。

このようなディレクトリ構造になります。

[project root]
 ├ settings.gradle.kts
 ├ build.gradle.kts
 ├ Cargo.toml
 ├ app
 │  ├ build.gradle.kts
 │  ├ Cargo.toml
 │  └ src
 │     ├ main
 │     │  └ 略
 │     └ rust
 │        └ 略
 └ modules
    ├ featureA
    │  ├ build.gradle.kts
    │  ├ Cargo.toml
    │  └ src
    │     ├ main
    │     │  └ 略
    │     └ rust
    │        └ 略
    ├ featureB
    │  ├ build.gradle.kts
    │  ├ Cargo.toml
    │  └ src
    │     ├ main
    │     │  └ 略
    │     └ rust
    │        └ 略
    └ featureC
       ├ build.gradle.kts
       └ src
          └ main
             └ 略

さっきのイメージと同じじゃんと思われた方もいるかもしれません。
しかし違います。

さっきのイメージは一つのデカいGradleプロジェクトの中に分割されたGradleモジュールがあり、その中にそれぞれCargoプロジェクトがあります。
multi-plugins-file-tree.png

私がお見せしたディレクトリ構造は、一つのデカいGradleプロジェクトであると同時に一つのデカいCargoプロジェクトでもあります。
single-plugin-file-tree.png

Gradle目線だとこのディレクトリツリーは複数のGradleモジュールから成る一つのGradleプロジェクトであり、Cargo目線だとこのディレクトリツリーは複数のCargoパッケージから成る一つのCargoプロジェクトなわけですね。
single-plugin-file-tree.png

導入手順まとめ

理屈はいいから何をすればいいのかだけ教えろという人向けのまとめを先に置いておきます

local.properties
+ rust.pythonCommand=python3
gradle/libs.versions.toml
  [versions]
  agp = "8.5.2"
  kotlin = "2.0.20"
+ ndk = "(記事執筆時点では27.1.12297006)"

  [plugins]
  android-application = { id = "com.android.application", version.ref = "agp" }
  jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
  android-library = { id = "com.android.library", version.ref = "agp" }
+ mozilla-rust-android = { id = "org.mozilla.rust-android-gradle.rust-android", version = "(記事執筆時点では0.9.4)" }
settings.gradle.kts
  pluginManagement {
     repositories {
        mavenCentral()
+       maven("https://plugins.gradle.org/m2/")
     }
  }
build.gradle.kts
  plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.jetbrains.kotlin.android) apply false
+   alias(libs.plugins.mozilla.rust.android) apply false
  }
app/build.gradle.kts
  plugins {
     alias(libs.plugins.android.application)
     alias(libs.plugins.jetbrains.kotlin.android)
+    alias(libs.plugins.mozilla.rust.android)
  }

  android {
     namespace = "com.example.rust"
     compileSdk = 34
+    ndkVersion = libs.versions.ndk.get()
  }

+ fun isXterm() = System.getenv("TERM")?.startsWith("xterm") ?: false
+
+ cargo {
+    module = ".."
+    libname = "rust_example"
+    targets = listOf("arm", "arm64", "x86", "x86_64")
+    if (isXterm()) {
+       extraCargoBuildArguments = listOf("--color=always")
+    }
+ }
+
+ tasks.matching { it.name.matches(Regex("merge.*JniLibFolders")) }.configureEach {
+    inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
+    dependsOn("cargoBuild")
+ }
Cargo.toml
+ [workspace]
+ members = [
+    "app",
+    "modules/featureA",
+    "modules/featureB",
+ ]
+ resolver = "2"
+
+ [workspace.dependencies]
+ jni = "(記事執筆時点では0.21.1)"
+ feature_a = { path = "modules/featureA" }
+ feature_b = { path = "modules/featureB" }
app/Cargo.toml
+ [package]
+ name = "rust_example"
+ edition = "2021"
+
+ [lib]
+ path = "src/rust/lib.rs"
+ crate-type = ["cdylib"]
+
+ [dependencies]
+ jni.workspace = true
+ feature_a.workspace = true
+ feature_b.workspace = true
modules/featureA/Cargo.toml
+ [package]
+ name = "feature_a"
+ edition = "2021"
+
+ [lib]
+ path = "src/rust/lib.rs"
+
+ [dependencies]
+ jni.workspace = true
modules/featureB/Cargo.toml
+ [package]
+ name = "feature_b"
+ edition = "2021"
+
+ [lib]
+ path = "src/rust/lib.rs"
+
+ [dependencies]
+ jni.workspace = true
app/src/main/java/com/example/rust/MainApplication.kt
  class MainApplication : Application() {
+    init {
+       System.loadLibrary("rust_example")
+    }
  }
app/src/main/java/com/example/rust/Test.kt
+ package com.example.rust
+
+ external fun increment(current: Int): Int
app/src/rust/lib.rs
+ use jni::JNIEnv;
+ use jni::objects::JClass;
+ use jni::sys::jint;
+
+ pub use feature_a;
+ pub use feature_b;
+
+ #[no_mangle]
+ extern "C" fn Java_com_example_rust_TestKt_increment(
+    _env: JNIEnv,
+    _class: JClass,
+    current: jint
+ ) -> jint {
+    feature_a::increment(current)
+ }
modules/featureA/src/rust/lib.rs
+ pub fn increment(current: i32) -> i32 {
+    current + 1
+ }

導入手順

見ての通り、やるべきことはそこそこ多いです。
一歩ずつ進め、どのコードが何のために必要なのかしっかり把握しながら導入するとよいでしょう。

Rustでは、Kotlinと同じ用語が全く違う意味で使われることがあります。
この記事ではややこしさを軽減するために、厳密には正しくないことを許容し、以下の呼び方をします。

「ワークスペース」: アプリのソースコードのうち、ネイティブ(Rust)側全体
「モジュール」: アプリのソースコードを一定の大きさで分割したもの。app, featureAなど

まだNDKが導入されていない下記の構成のAndroidアプリにRustを導入してみます。

[project root]
 ├ settings.gradle.kts
 ├ build.gradle.kts
 ├ app
 │  ├ build.gradle.kts
 │  └ src
 │     └ main
 │        └ 略
 └ modules
    ├ featureA
    │  ├ build.gradle.kts
    │  └ src
    │     └ main
    │        └ 略
    ├ featureB
    │  ├ build.gradle.kts
    │  └ src
    │     └ main
    │        └ 略
    └ featureC
       ├ build.gradle.kts
       └ src
          └ main
             └ 略

Rustをインストール

Rust自体のインストールが済んでいない場合公式の手順に従ってインストールしましょう。

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

クロスコンパイル用ターゲットを追加

Android用のターゲットを追加しましょう。
以下はmozilla/rust-android-gradleのREADMEからコピーしてきたものです。
必要なものだけでよいとは思いますが私はすべて追加しました

$ rustup target add armv7-linux-androideabi   # for arm
$ rustup target add i686-linux-android        # for x86
$ rustup target add aarch64-linux-android     # for arm64
$ rustup target add x86_64-linux-android      # for x86_64
$ rustup target add x86_64-unknown-linux-gnu  # for linux-x86-64
$ rustup target add x86_64-apple-darwin       # for darwin x86_64 (if you have an Intel MacOS)
$ rustup target add aarch64-apple-darwin      # for darwin arm64 (if you have a M1 MacOS)
$ rustup target add x86_64-pc-windows-gnu     # for win32-x86-64-gnu
$ rustup target add x86_64-pc-windows-msvc    # for win32-x86-64-msvc

Workspaceを準備

プロジェクトのルートディレクトリにCargo.tomlを作成します。

Cargo.toml
[workspace]
members = [
]
resolver = "2"

[workspace.dependencies]

この時点ではビルドできません。

$ cargo b
error: manifest path `/home/wcaokaze/Projects/wcaokaze/rustexample` contains no package: The manifest is virtual, and the workspace has no members.

まずはappモジュールをメンバに追加しましょう。

プロジェクトルートのCargo.tomlにメンバを宣言します。

Cargo.toml
  [workspace]
  members = [
+    "app",
  ]
  resolver = "2"

appモジュールにCargo.tomlを作成します。

app/Cargo.toml
[package]
name = "rust_example"
edition = "2021"

[lib]
path = "src/rust/lib.rs"

[dependencies]

package.nameは生成されるライブラリファイル名になるのでよしなに付けましょう。

lib.pathは "src/rust/lib.rs" にします。デフォルトでは src/lib.rs ですが、今回は同じディレクトリにGradleプロジェクトが混在しているので、Rustコード用のディレクトリを分けておかないと大変なことになります。
lib.pathを設定するとsrcディレクトリにはmain, test, androidTest, rustが置かれることになり綺麗です。

余談

Q. src/main/javaというディレクトリがあるのでRustコードはsrc/main/rustに置いた方が綺麗では?
A. Rustでは通常テストコード用のディレクトリを分けないので、src/main/rustにソースコードを置くとsrc/test/rustがないことに違和感が出てしまう。
Kotlin Multiplatformで開発してる場合だとsrc/commonMain, src/androidMain, src/desktopMainみたいに分かれるのでそこでも似た問題が起きますね。
まあ気持ちの問題なので俺はsrc/main/rustに置くぜというのもアリだと思います。

余談おわり

app/src/rust/lib.rsを作成します。とりあえず空でいいです

app/src/rust/lib.rs

ビルドできるようになります。

$ cargo b
   Compiling rust_example v0.0.0 (/home/wcaokaze/Projects/wcaokaze/rustexample/app)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s

NDKを導入

gradle/libs.versions.toml
  [versions]
  agp = "8.5.2"
  kotlin = "2.0.20"
+ ndk = "(記事執筆時点では27.1.12297006)"
app/build.gradle.kts
  android {
     namespace = "com.example.rust"
     compileSdk = 34
+    ndkVersion = libs.versions.ndk.get()
  }

Rust Android Gradle Pluginを導入

mozilla/rust-android-gradleを追加します。

gradle/libs.versions.toml
  [plugins]
  android-application = { id = "com.android.application", version.ref = "agp" }
  jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
  android-library = { id = "com.android.library", version.ref = "agp" }
+ mozilla-rust-android = { id = "org.mozilla.rust-android-gradle.rust-android", version = "(記事執筆時点では0.9.4)" }
settings.gradle.kts
  pluginManagement {
     repositories {
        mavenCentral()
+       maven("https://plugins.gradle.org/m2/")
     }
  }
build.gradle.kts
  plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.jetbrains.kotlin.android) apply false
+   alias(libs.plugins.mozilla.rust.android) apply false
  }
app/build.gradle.kts
  plugins {
     alias(libs.plugins.android.application)
     alias(libs.plugins.jetbrains.kotlin.android)
+    alias(libs.plugins.mozilla.rust.android)
  }

  android {
       :
       :
  }

+ cargo {
+    module = ".."
+    libname = "rust_example"
+    targets = listOf("arm", "arm64", "x86", "x86_64")
+ }

moduleにはワークスペースのパスを指定しています。appモジュールから見るとワークスペースのルートディレクトリは .. にあるのでこうなります。
libnameはapp/Cargo.tomlで宣言したパッケージ名にしましょう。

また、プラグインはデフォルトではpythonを使用する際に python コマンドを叩くようになっています。
python3 コマンドを使う場合はlocal.propertiesに下記を追記しましょう。

local.properties
+ rust.pythonCommand=python3

JNIクレートを追加

jniクレートを追加します。

Cargo.toml
  [workspace.dependencies]
+ jni = "(記事執筆時点では0.21.1)"
app/Cargo.toml
  [dependencies]
+ jni.workspace = true

ライブラリをロードする

アプリケーションの起動後、動的ライブラリをロードする必要があります。

crate-typeをcdylibに変更します。

app/Cargo.toml
  [lib]
  path = "src/rust/lib.rs"
+ crate-type = ["cdylib"]

ライブラリをロードします。私はApplicationのコンストラクタでロードしています。

app/src/main/java/com/example/rust/MainApplication.kt
  class MainApplication : Application() {
+    init {
+       System.loadLibrary("rust_example")
+    }
  }

ネイティブ関数を呼び出してみる

ネイティブの関数を書いてみましょう!

app/src/main/java/com/example/rust/Test.kt
package com.example.rust

external fun increment(current: Int): Int
app/src/rust/lib.rs
use jni::JNIEnv;
use jni::objects::JClass;
use jni::sys::jint;

#[no_mangle]
extern "C" fn Java_com_example_rust_TestKt_increment(
   _env: JNIEnv,
   _class: JClass,
   current: jint
) -> jint {
   current + 1
}

単純に引数 + 1の数値を返却する関数です。

JNIはC形式の関数を呼び出すので、C形式のバイナリを生成させるため extern "C" を付与し、マングリングを抑止するための #[no_mangle] を付与します。

関数名は Java_ の後にクラスの完全修飾名、その後にメソッド名となります。
Kotlinではクラスに属さないトップレベルの関数を記述できますが、この場合JVM上では ファイル名+Kt がクラス名となるため、 Test.kt に書いた increment 関数はJVM上では TestKt クラス内のstaticメソッド increment です。
結果として Java_com_example_rust_TestKt_increment がRust側の関数名となります。

第一引数はJNI関数を呼び出すための JNIEnv
JVM側のメソッドを呼び出したいとき、JVM側のインスタンスを生成したいとき、JVMのStringインスタンスをRustのStringに変換したいときなど、JVM側との橋渡しになる関数がほとんど全てここにあります。

第二引数は JClass
非staticメソッド(すなわちKotlinコード上でclassやobjectに属する関数)の場合、ここは JObject 型となり this インスタンスが渡されますが、staticメソッドではJClassとなります。

そして第三引数以降がKotlin側の関数宣言で宣言した引数となります。
プリミティブ型、配列型、java.lang.Stringには対応する型がjniクレートにあります( jint , JIntArray , JString ...)が、それ以外のインスタンスは JObject となり、事実上動的型付けです。
もっとも、その事情を抜きにしても、もともとJVMからもらった値を生のまま扱い続けると可読性が著しく落ちるので推奨できません。

説明が長くなりましたが、これがネイティブ関数を記述する際の定型文となります。

#[no_mangle]
extern "C" fn Java_{パッケージ名}_{クラス名/ファイル名+Kt}_{関数名}(
   env: JNIEnv,
   {class: JClass / obj: JObject},
   {引数...}
) -> {返り値の型} {
}

ちなみに関数を置くファイルはどこでもいいので、たとえばTest.ktのexternal funに対応するRust関数はtest.rsに書くなど、管理しやすい方法を自由に使えばいいと思います。

cargoBuildを実行する

さて、ネイティブ関数を書いたので早速実行してみると、UnsatisfiedLinkErrorがスローされるはずです。

FATAL EXCEPTION: main
Process: com.example.rust, PID: 20562
java.lang.UnsatisfiedLinkError: dlopen failed: library "librust_example.so" not found
	at java.lang.Runtime.loadLibrary0(Runtime.java:1081)
	at java.lang.Runtime.loadLibrary0(Runtime.java:1003)
	at java.lang.System.loadLibrary(System.java:1765)
	at com.example.rust.MainApplication.<init>(MainApplication.kt:7)

いつもの感覚でAndroidアプリをビルドしてもRustライブラリはビルドされていないのです。
一度cargoBuildを実行しておいてから再度アプリをビルドすると正常に動作するようになります。

$ ./gradlew app:cargoBuild

と言っても、毎回cargoBuildを実行するのはもちろん無理があります。
Gradleのビルド時にcargoBuildが実行されるようにタスクの依存関係を設定しておきましょう。

app/build.gradle.kts
  cargo {
       :
       :
  }

  dependencies {
       :
       :
  }

+ tasks.matching { it.name.matches(Regex("merge.*JniLibFolders")) }.configureEach {
+    inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
+    dependsOn("cargoBuild")
+ }

これでいつもの感覚でアプリをビルドしてもRustライブラリが同時にビルドされてアプリに含まれるようになります。

Cargoの出力に色をつける

Rustacean達はご存知かと思いますが、Rustのコンパイルエラーはめちゃくちゃ見やすいです。
ところが、Rust Android Gradleプラグインで追加されるcargoBuildタスクではCargoの出力に色がつかないではありませんか! 色がなければメッセージの見やすさも半減です。
Cargoの出力に色をつけましょう。

色つきの文字を出力できるかどうかは使用しているターミナル次第です。色つきの文字を表示できないターミナルもありえますし、極論、シェルスクリプト等から実行されている場合ターミナルそのものが存在しないということもありえます。
おそらくGradleからCargoを叩くとき、Cargoは色を出力できない環境と判断しているのだと思います。
cargoコマンドのオプション --colors=always を指定すれば色つきの出力を強制できます。

app/build.gradle.kts
  cargo {
     module = ".."
     libname = "rust_example"
     targets = listOf("arm", "arm64", "x86", "x86_64")
+    extraCargoBuildArguments = listOf("--color=always")
  }

しかし先述の通り、色を出力できるかどうかは実行環境によります。
Gradleそのものが色を出力できない環境で実行されている場合、 --colors=always は指定しない方がいいです。
なので私はこんな感じにしています。

app/build.gradle.kts
+ fun isXterm() = System.getenv("TERM")?.startsWith("xterm") ?: false

  cargo {
     module = ".."
     libname = "rust_example"
     targets = listOf("arm", "arm64", "x86", "x86_64")
+    if (isXterm()) {
        extraCargoBuildArguments = listOf("--color=always")
+    }
  }

…と、ここまで書いておいてから言うのもアレですが、Android開発においてGradleをターミナルから叩くことはほとんどないので、あまり恩恵はないかもしれません。
まあやっておいて損はないので、やっておくといいと思います。

他のモジュールにもRustを導入する

本題ですね。
ここまででappモジュールではRustコードを書けるようになりました。
他のモジュールでもRustを書きたいというのが今回の主題です。

:modules:featureAでもNDKが必要になったとします。
appモジュールのときと同様にCargo.tomlを作成します。

modules/featureA/Cargo.toml
[package]
name = "feature_a"
edition = "2021"

[lib]
path = "src/rust/lib.rs"

[dependencies]
jni.workspace = true

ワークスペースへの追加もお忘れなく。

Cargo.toml
  [workspace]
  members = [
     "app",
+    "modules/featureA",
  ]

appモジュールと同様にJNIを利用して関数を書けます。

modules/featureA/src/main/java/com/example/rust/featurea/Test.kt
external fun featureATest()
modules/featureA/src/rust/lib.rs
use jni::JNIEnv;
use jni::objects::JClass;
use jni::sys::jint;

#[no_mangle]
extern "C" fn Java_com_example_rust_featurea_TestKt_featureATest(
   _env: JNIEnv,
   _class: JClass
) {
}

他のモジュールの関数を呼んでみる

たとえばfeatureAモジュールにある関数をappモジュールから呼びたいとします。

一旦featureAに適当な関数を作ります

modules/featureA/src/rust/lib.rs
  #[no_mangle]
  extern "C" fn Java_com_example_rust_featurea_TestKt_featureATest(
     _env: JNIEnv,
     _class: JClass
  ) {
  }

+ pub fn increment(current: i32) -> i32 {
+    current + 1
+ }

この関数をappモジュールから呼びましょう。

Workspaceを使った開発環境を構築したので、完全にWorkspaceの仕組みで実現できます。
appモジュールのdependenciesにfeatureAモジュールを追加します。

Cargo.toml
  [workspace.dependencies]
  jni = "(記事執筆時点では0.21.1)"
+ feature_a = { path = "modules/featureA" }
app/Cargo.toml
  [dependencies]
  jni.workspace = true
+ feature_a.workspace = true

featureAモジュールの関数を呼べるようになります。簡単ですね。

app/src/rust/lib.rs
  #[no_mangle]
  extern "C" fn Java_com_example_rust_TestKt_increment(
     _env: JNIEnv,
     _class: JClass,
     current: jint
  ) -> jint {
-    current + 1
+    feature_a::increment(current)
  }

モジュールを統合する

さて、ここからがWorkspaceを使わない場合と違うところです。

Workspaceを使わない場合

おさらい用にもう一度イメージを置いておきます。
multi-plugins-file-tree.png

各モジュールごとにRust Android Gradleプラグインを導入していて、各モジュールごとにライブラリファイルが生成されます。

この場合、appモジュールのcargoBuildが生成したバイナリとfeatureAモジュールのcargoBuildが生成したバイナリの両方をロードする必要があります。

modules/featureA/Cargo.toml
  [lib]
  path = "src/rust/lib.rs"
+ crate-type = ["cdylib"]
main/src/main/java/com/example/rust/MainApplication.kt
  class MainApplication : Application() {
     init {
        System.loadLibrary("rust_example")
+       System.loadLibrary("feature_a")
     }
  }

今回構築するWorkspaceを使う開発環境では、全てのモジュールを一つのライブラリファイルに統合することができます。

たとえば、前項でappモジュールからfeatureAモジュールを利用しているので、appモジュールが生成するバイナリにはfeatureAモジュールも含まれています。
したがって、特になにもしなくてもfeatureAモジュールのバイナリはすでにロードされています。

問題はappモジュールから依存されないモジュールの扱いです。
たとえばfeatureBにもNDKを利用したネイティブコードが追加されたとします。

appモジュールのRust側コードにとってはfeatureBモジュールは必要ないので、appモジュールのCargo.tomlのdependenciesにはfeatureBは追加されません。
しかしappモジュールのKotlin側コードにとってはfeatureBモジュールが必要なので、appモジュールのbuild.gradle.ktsのdependenciesにはfeatureBが追加されるし、featureBのKotlin側からはfeatureBのRust側のコードが必要なので、featureBのRust側のバイナリはアプリに含まれなければなりません。

間接的にfeatureBのRust側のバイナリが必要なのに直接必要ではないためdependenciesに含まれない状態です。
この状態ではfeatureBのネイティブ関数を実行しようとした際にUnsatisfiedLinkErrorがスローされます。

dependencies.png
▲含まれなければならないfeatureBのRustコードが含まれない様子

appモジュールにfeatureBモジュールを統合しなければなりません。

つまり、appモジュールにとって直接的に必要でなくても、間接的に依存しているfeatureBモジュールもCargo.tomlに宣言しなければなりません。

app/Cargo.toml
  [dependencies]
  jni.workspace = true
  feature_a.workspace = true
+ feature_b.workspace = true

dependencies宣言しても実際に使っていなければバイナリには含まれないので、適当にuse宣言を記述しておきます。

app/src/rust/lib.rs
+ pub use feature_b;

これでappモジュールにfeatureBモジュールが統合され、正常に動作するようになります。

use宣言すべきモジュール

ここでひとつ議論のタネが生まれます。
appモジュールではどのモジュールをuse宣言するべきなのか?という点です。

例としてこのような依存関係を仮定します。
suppose.png

1. 全て

一番簡単な方法はワークスペース内の(app以外の)全てのモジュールをappモジュールでuse宣言しておくことです。

app/src/rust/lib.rs
pub use feature_a;
pub use feature_b;
pub use feature_c;
pub use feature_d;

workspace.membersを追加する際にappモジュールにもuse宣言を追記する必要がありますが、その他に考えることはありません。
問題は強いて言えばappモジュールにとっては明らかに必要ではない use feature_d; などが宣言されるという気持ちの問題でしょうか。

2. 浮いたものだけ

一方で一番大変な方法は、appモジュールから依存されないモジュール、つまり上記の図のCargo目線の依存関係で「浮いている」ものだけをuse宣言することです。

app/src/rust/lib.rs
pub use feature_b;
pub use feature_c;

appモジュールはすでにfeatureAを使用しているので、別途use宣言をしなくてもfeatureAはappモジュールのバイナリに含まれます。featureBとfeatureCはappモジュールから依存されないのでuse宣言が必要です。

この時点で少し面倒なのですが、もっと厄介なのはfeatureCがfeatureDに依存していることです。
featureCをuse宣言した時点でfeatureCを経由して間接的にfeatureDに依存するので、featureDは浮いていません。
なのでfeatureDのuse宣言は要らないということになるのですが、これはあまりにも大変だし、すっきりしない考え方です。

3. Gradleのdependencies宣言に合わせる

ひとつの妥協案は、Gradleでdependencies宣言されているモジュールを全てuse宣言することです。

app/src/rust/lib.rs
pub use feature_a;
pub use feature_b;

appモジュールのbuild.gradle.ktsにはfeatureAとfeatureBに依存すると宣言されているので、Rustコードでもそれに合わせてuse宣言を記述しておくという感じ。

同様に、featureBはbuild.gradle.ktsでfeatureCとfeatureDに依存すると宣言しているので、featureBのRustコードではこの2つをuse宣言します。

modules/featureB/src/rust/lib.rs
pub use feature_c;
pub use feature_d;

これにより、appモジュールは use feature_c; を宣言しなくてもfeatureCが統合されます。

これはあまり大変じゃないし、比較的納得できるコードになります。

まとめ

必要なコード変更まとめです。
この記事の半ばで一度貼ったものと同じです。

local.properties
+ rust.pythonCommand=python3
gradle/libs.versions.toml
  [versions]
  agp = "8.5.2"
  kotlin = "2.0.20"
+ ndk = "(記事執筆時点では27.1.12297006)"

  [plugins]
  android-application = { id = "com.android.application", version.ref = "agp" }
  jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
  android-library = { id = "com.android.library", version.ref = "agp" }
+ mozilla-rust-android = { id = "org.mozilla.rust-android-gradle.rust-android", version = "(記事執筆時点では0.9.4)" }
settings.gradle.kts
  pluginManagement {
     repositories {
        mavenCentral()
+       maven("https://plugins.gradle.org/m2/")
     }
  }
build.gradle.kts
  plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.jetbrains.kotlin.android) apply false
+   alias(libs.plugins.mozilla.rust.android) apply false
  }
app/build.gradle.kts
  plugins {
     alias(libs.plugins.android.application)
     alias(libs.plugins.jetbrains.kotlin.android)
+    alias(libs.plugins.mozilla.rust.android)
  }

  android {
     namespace = "com.example.rust"
     compileSdk = 34
+    ndkVersion = libs.versions.ndk.get()
  }

+ fun isXterm() = System.getenv("TERM")?.startsWith("xterm") ?: false
+
+ cargo {
+    module = ".."
+    libname = "rust_example"
+    targets = listOf("arm", "arm64", "x86", "x86_64")
+    if (isXterm()) {
+       extraCargoBuildArguments = listOf("--color=always")
+    }
+ }
+
+ tasks.matching { it.name.matches(Regex("merge.*JniLibFolders")) }.configureEach {
+    inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
+    dependsOn("cargoBuild")
+ }
Cargo.toml
+ [workspace]
+ members = [
+    "app",
+    "modules/featureA",
+    "modules/featureB",
+ ]
+ resolver = "2"
+
+ [workspace.dependencies]
+ jni = "(記事執筆時点では0.21.1)"
+ feature_a = { path = "modules/featureA" }
+ feature_b = { path = "modules/featureB" }
app/Cargo.toml
+ [package]
+ name = "rust_example"
+ edition = "2021"
+
+ [lib]
+ path = "src/rust/lib.rs"
+ crate-type = ["cdylib"]
+
+ [dependencies]
+ jni.workspace = true
+ feature_a.workspace = true
+ feature_b.workspace = true
modules/featureA/Cargo.toml
+ [package]
+ name = "feature_a"
+ edition = "2021"
+
+ [lib]
+ path = "src/rust/lib.rs"
+
+ [dependencies]
+ jni.workspace = true
modules/featureB/Cargo.toml
+ [package]
+ name = "feature_b"
+ edition = "2021"
+
+ [lib]
+ path = "src/rust/lib.rs"
+
+ [dependencies]
+ jni.workspace = true
app/src/main/java/com/example/rust/MainApplication.kt
  class MainApplication : Application() {
+    init {
+       System.loadLibrary("rust_example")
+    }
  }
app/src/main/java/com/example/rust/Test.kt
+ package com.example.rust
+
+ external fun increment(current: Int): Int
app/src/rust/lib.rs
+ use jni::JNIEnv;
+ use jni::objects::JClass;
+ use jni::sys::jint;
+
+ pub use feature_a;
+ pub use feature_b;
+
+ #[no_mangle]
+ extern "C" fn Java_com_example_rust_TestKt_increment(
+    _env: JNIEnv,
+    _class: JClass,
+    current: jint
+ ) -> jint {
+    feature_a::increment(current)
+ }
modules/featureA/src/rust/lib.rs
+ pub fn increment(current: i32) -> i32 {
+    current + 1
+ }

推薦図書

JNI tips (Android Developers)
Rust導入後のAndroidアプリ開発でテストを書く

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?