Android環境でRust製ライブラリをC/C++経由で使用する

# 簡単に言うと…

Rust言語で作成したライブラリをAndroidから簡単に呼び出したい。
* C言語からRust製ライブラリの関数を簡単に呼び出せる
* AndroidのJava/KotlinはJNIでC/C++言語の関数を呼び出せる

よって、

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

この様に簡単な方法で呼び出せるのでは？と思い、実行してみた結果、呼び出せることを確認しました。

# Rust言語とは

Rust言語は、メモリの安全性を確保しやすい言語機能を持っていることからC/C++の代用として昨今Windows、Linux、Android自身を含むさまざまな場面[^1] [^2] [^3] [^4] で採用されている言語です。

[^1]: https://forest.watch.impress.co.jp/docs/news/insiderpre/1516147.html
[^2]: https://forest.watch.impress.co.jp/docs/serial/yajiuma/1536253.html
[^3]: https://japan.zdnet.com/article/35193491/
[^4]: https://forest.watch.impress.co.jp/docs/news/1462573.html

# なぜC/C++のラッパー関数を経由するのか？

前提として下記のような理由がありました。

* 既にLinux環境向けにRust言語でとある機能を開発している
* 上記の機能をAndroidでそのまま動作させたい
* CからRust言語で作成したライブリ関数の呼び出し方法を知っている
* Android(Java/Kotlin)からCの呼び出し方法(JNI)をある程度知っている

→ 新しいことを覚える事が少なく最低限の作業でAndroid移植ができるのでは？

上記を踏まえたメリット/デメリットは以下のようになると思います。

## メリット

* 既知の情報を活かせる
    * CからRust関数の呼び出し
    * Java/KotlinからのJNIでC関数の呼び出し
* Rust側のコードはほぼ変更の必要がない
* Android Studioがデフォルトで対応しているC/C++を使用することで  
  Android Studioのサポートを受けられる （例：リファクタリングなど）

## デメリット

* データのやり取りが多い場合、ラッパー関数を挟むためオーバーヘッドが多くなる  
  性能の要件が厳しい場合には、これだけで今回の方法は採用できないクリティカルな問題になり得ます
* 構成が複雑になる  
  簡単なC/C++言語とはいえ、使用する言語がJava/Kotlin、C/C++、Rustと3言語(以上)になってしまう  
  → 今回はC/C++を使用している箇所をRust言語に置き換えることも可能なので次回解説したいと思っています

# 手順

それでは実際の手順を説明します。

大まかにRust製ライブラリのクロスコンパイルの設定と、Android側からRust製ライブラリを呼び出せるようにすることで完了となります。

今回は、Android StudioとNDKを使用したビルドまでの手順を説明したいと思います。

## Android Studioでプロジェクト作成

新規作成からプロジェクトを新規作成し「Native C++」を選択します。

<img width="80%" alt="プロジェクト新規作成" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3791092/e8cb22be-2fae-3e7c-3e48-ae16c73152e2.png">

デフォルトではC++のソースコードがテンプレートとして生成されますが、今回はCに変更してみましょう。

`native-lib.cpp`から`native-lib.c`に変更しソースコードを以下のように変更します。

```c
#include <jni.h>
#include <string.h>

JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject this) {
    const char* hello = "Hello from C";
    (void)this;
    return (*env)->NewStringUTF(env, hello);
}
```

Nativeコードのビルドに使用される`CMakeLists.txt`内のファイル名も変更します。

```diff
--- a/app/src/main/cpp/CMakeLists.txt
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -26,7 +26,7 @@ project("myapplication")
 # used in the AndroidManifest.xml file.
 add_library(${CMAKE_PROJECT_NAME} SHARED
         # List C/C++ source files with relative paths to this CMakeLists.txt.
-        native-lib.cpp)
+        native-lib.c)
 
```

この時点でビルドできることを確認しておきます。

またビルドに使用しているSDKやNDKのバーションを確認しておきます。  
F4キーで起動できる「プロジェクト構造」ダイアログの「Modules」の「Compile SDK Version」と 「NDK Version」から確認できます。  
「NDK Version」が空欄の場合は一度クリックすると表示されます。

<img width="80%" alt="NDK Version" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3791092/39bef903-e511-8b12-f9ad-043104ec7d5e.png">

## Android NDKのインストールと設定

今回は既にRustのビルド環境があるLinuxにコマンドラインのNDKをインストールします。

まず、NDKやその他のAndroid関連のファイルをインストールするための `sdkmanager`をインストールします。

下記のURLからダウンロードします。
https://developer.android.com/studio?hl=ja#command-line-tools-only  

ダウンロードされたファイルを展開すると`cmdline-tools`というディレクトリに展開されますが`cmdline-tools/latest`といパスにしないと実行時に以下のようにエラーが表示され実行できないので、方法は問いませんが指定されたパスにします。

```
$ sdkmanager "ndk;27.0.12077973"
Error: Could not determine SDK root.
Error: Either specify it explicitly with --sdk_root= or move this package into its expected location: <sdk>/cmdline-tools/latest/
```

以下の様に変更しました。

```
cd ~/
mkdir android-sdk && cd android-sdk
# move zip file here
unzip commandlinetools-linux-11076708_latest.zip 
mv cmdline-tools latest
mkdir cmdline-tools
mv latest cmdline-tools
```

`sdkmanager`の実行にJavaのランタイムが必要なりますのでインストールしていない場合はインストールします。

```
sudo apt install openjdk-21-jdk
```

環境変数PATHの設定

`~/.profile`などに下記のように追記します。

```
export ANDROID_HOME=${HOME}/android-sdk
export PATH=$PATH:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools/bin
```

`sdkmanager`でNDKをインストールします。
Android Studioで使用している「NDK Version」を指定します。

```
sdkmanager "ndk;27.0.12077973"
```

## RustのAndroid向けクロスコンパイル設定

Rust言語のセットアップについては[こちら](https://www.rust-lang.org/ja/tools/install)を参照ください。

Androidには複数のアーキテクチャがあり、Android Studio上のビルドには下記のtargetが必要になりますので`rustup`コマンドでインストールします。

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

`~/.cargo/config`に下記の様に`sdkmanager`でインストールしたNDKのリンカーへのパスの指定を追記してください。
これは共有ライブラリをビルドするときにNDKのリンカーを使用する必要があるためです。  
また、リンカーファイル名の数値には、前述の「Compile SDK Version」にあった数値を指定してください。

```
[target.aarch64-linux-android]
linker = "android-sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang"

[target.armv7-linux-androideabi]
linker = "android-sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi35-clang"

[target.i686-linux-android]
linker = "android-sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android35-clang"

[target.x86_64-linux-android]
linker = "android-sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android35-clang"
```

## Rustのライブラリを作成とビルド

Rust言語でライブラリの雛形を作成します。  
そこからAndroidのJNIで呼び出すC言語から呼び出せるようにします。

```
cargo new --lib rustadd
```

これで雛形として プロジェクトの設定ファイルの`Cargo.toml`とソースファイルの`src/lib.rs`が 作成されます。  
`src/lib.rs`には2つの引数を足して値を戻す`add`関数が生成されます。

C言語の共有ライブラリを作成しつつ、リリースビルド時にファイルサイズを抑えるstripの設定をするため
`Cargo.toml`ファイルを下記のように変更します。

```diff_toml
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,3 +4,9 @@ version = "0.1.0"
 edition = "2021"

 [dependencies]
+
+[lib]
+crate-type = ["cdylib"]
+
+[profile.release]
+strip = true
```

C言語から呼び出せるように `src/lib.rs`を下記のように変更します。
ついでに関数名も `add`から、わかりやすく`rustadd`と変更しておきます。

```diff_rust
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,5 @@
-pub fn add(left: u64, right: u64) -> u64 {
+#[no_mangle]
+pub extern "C" fn rustadd(left: u64, right: u64) -> u64 {
     left + right
 }
```

各アーキテクチャ向けにビルド

```
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release
cargo build --target x86_64-linux-android --release
```

するとアーキテクチャごとに共有ライブラリが作成されます

```
$ ls target/*/release/*.so
target/aarch64-linux-android/release/librustadd.so    target/i686-linux-android/release/librustadd.so
target/armv7-linux-androideabi/release/librustadd.so  target/x86_64-linux-android/release/librustadd.so
```

これでAndroid Sutdioで動作するエミュレータ環境や実機に必要なライブラリがクロスコンパイルできました。

## `CMakeLists.txt`の修正とライブラリの配置

Rust製のライブラリをリンクできるように`CMakeLists.txt`を下記のように編集します。

```diff
--- a/app/src/main/cpp/CMakeLists.txt
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -28,10 +28,17 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
         # List C/C++ source files with relative paths to this CMakeLists.txt.
         native-lib.c)
 
+add_library(imported-lib SHARED IMPORTED)
+
+set_target_properties(imported-lib
+        PROPERTIES IMPORTED_LOCATION
+        ${CMAKE_CURRENT_SOURCE_DIR}/libs/${ANDROID_ABI}/librustadd.so)
+
 # Specifies libraries CMake should link to your target library. You
 # can link libraries from various origins, such as libraries defined in this
 # build script, prebuilt third-party libraries, or Android system libraries.
 target_link_libraries(${CMAKE_PROJECT_NAME}
         # List libraries link to the target library
+        imported-lib
         android
         log)
\ No newline at end of file
```

`CMakeLists.txt`内の`${CMAKE_CURRENT_SOURCE_DIR}`は`CMakeLists.txt`があるパスが格納されます 
`${ANDROID_ABI}`はAndroid Studio側で設定される各ABIの文字列です。  
Rust言語のtarget名と`ANDROID_ABI`のマッピングは以下のとおりです。

|CPU アーキテクチャ|Rust言語のTarget名     |ANDROID_ABI|
|------------------|-----------------------|-----------|
| ARM v7           |armv7-linux-androideabi|armeabi-v7a| 
| ARM v8 64bit     |aarch64-linux-android  |arm64-v8a  | 
| x86 32bit        |i686-linux-android     |x86        | 
| x86 64bit        |x86_64-linux-android   |x86_64     | 


下記のように`CMakeLists.txt`がある場所に`lib`ディレクトリを作成し、更にその中にANDROID_ABI名のディレクトリを作成し、その中に共有ライブラリを配置します。

```
lib/armeabi-v7a/librustadd.so
lib/arm64-v8a/librustadd.so
lib/x86/librustadd.so
lib/x86_64/librustadd.so
```

## `native-lib.c`からRust製ライブラリの関数を呼び出す

最後に`native-li.c`からRust製ライブラリの中の`rustadd`関数を呼び出し、戻り値で答えを受け取り表示するように変更します。

```diff_c
--- a/app/src/main/cpp/native-lib.c
+++ b/app/src/main/cpp/native-lib.c
@@ -2,11 +2,22 @@
 #include <string.h>
 #include <stdio.h>
 
+#include <inttypes.h>
+
+int64_t rustadd(int64_t x, int64_t y);
+
 JNIEXPORT jstring JNICALL
 Java_com_example_myapplication_MainActivity_stringFromJNI(
         JNIEnv *env,
         jobject this) {
-    const char *buf = "Hello from C";
+    char buf[100];
     (void)this;
+    int64_t x, y, z;
+
+    x = 1;
+    y = 2;
+    z = rustadd(x, y);
+    snprintf(buf, sizeof(buf),
+             "%"PRId64"+%"PRId64"=%"PRId64" from rust library", x, y, z);
     return (*env)->NewStringUTF(env, buf);
 }
```

ビルドし、エミュレータまたは実機で下記の様に表示されれば、Rust製のライブラリの関数を呼び出し、その結果を受け取ったとり表示できたことを確認できます。

<img width="50%" alt="実行結果" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3791092/2d70c49c-fe22-3f12-84b8-7b8534aefc0c.png">

## まとめ

当初の目論見通り、比較簡単に目的を達成できたと思います。  
しかし、特定の前提条件が多かったので、様々な場面で有効な方法とは言いづらいかもしれません。

また、今後の課題としては、Java/KotlinからJNIでC言語のラッパー関数を経由しましたが、Android Studio上でRust言語のビルドも実行できれば、このラッパー関数もRust言語で作成することができる上に、そこからRustライブラリもより簡単に呼び出せるようになるはずです。