簡単に言うと…
Rust言語で作成したライブラリをAndroidから簡単に呼び出したい。
- C言語からRust製ライブラリの関数を簡単に呼び出せる
- AndroidのJava/KotlinはJNIでC/C++言語の関数を呼び出せる
よって、
この様に簡単な方法で呼び出せるのでは?と思い、実行してみた結果、呼び出せることを確認しました。
Rust言語とは
Rust言語は、メモリの安全性を確保しやすい言語機能を持っていることからC/C++の代用として昨今Windows、Linux、Android自身を含むさまざまな場面1 2 3 4 で採用されている言語です。
なぜ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++」を選択します。
デフォルトではC++のソースコードがテンプレートとして生成されますが、今回はCに変更してみましょう。
native-lib.cpp
からnative-lib.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
内のファイル名も変更します。
--- 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」が空欄の場合は一度クリックすると表示されます。
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言語のセットアップについてはこちらを参照ください。
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
ファイルを下記のように変更します。
--- 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
と変更しておきます。
--- 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
を下記のように編集します。
--- 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
関数を呼び出し、戻り値で答えを受け取り表示するように変更します。
--- 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製のライブラリの関数を呼び出し、その結果を受け取ったとり表示できたことを確認できます。
まとめ
当初の目論見通り、比較簡単に目的を達成できたと思います。
しかし、特定の前提条件が多かったので、様々な場面で有効な方法とは言いづらいかもしれません。
また、今後の課題としては、Java/KotlinからJNIでC言語のラッパー関数を経由しましたが、Android Studio上でRust言語のビルドも実行できれば、このラッパー関数もRust言語で作成することができる上に、そこからRustライブラリもより簡単に呼び出せるようになるはずです。