RustをクロスコンパイルしてAndroidで動かす
RustのコードがAndroidで動くとか動かないとかいう話をtwitterでみて興味が湧きました。
調べたところmozillaが記事を書いててくれてたので読みながら手を動かしてみた記事です。
読みながらといっても自分はフィーリングでやってた感が多分にあるので、英語読める方は元記事みたほうがいいと思います。
Building and Deploying a Rust library on Android
はじめに
環境
- Windows10 Home
- Android Studio3.2.1
- Rust 1.31.1
やること
RustのプログラムをAndroidのネイティブコードにクロスコンパイルする
やらないこと
JNIライブラリの詳しい説明、使い方
手順
下準備
CargoProjectの立ち上げとコンパイラの設定
Rustコードから共有ライブラリを生成する
下準備
Rustコンパイラツールチェインの取得
対象のAndroidデバイスのCPUアーキテクチャに向けてツールチェインが用意されているのでrustupでインストールする。
ネイティブコードにコンパイルするのに必要になります。
ここではとりあえずメジャー所のARMv7とARM64、x86をインストールする。
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android
公式サイトに対応プラットフォームの一覧があるので適宜書き換えてほしい。
forge.rust-lang.org
AndroidNDKのインストール
先のツールチェインにより各CPUのネイティブコードにコンパイルできるようになったぽい。
次にこれをAndroidで利用できるようにするためリンカーを手に入れましょう。
AndroidNDKのインストールをする
AndroidStudioの設定画面のAppearance & Behaviour > System Settings > Android SDK > SDK Tools
からNDKにチェックを入れてインストールする。
もしくは直接ダウンロード。 NDK のダウンロード | Android NDK | Android Developersスクリプトの確認
NDKのディレクトリndk-bundle\build\tools
に、make-standalone-toolchain.sh
またはmake-standalone-toolchain.py
があることを確認しましょう。
(ndk-bundle
の配置ですが、AndroidStudioでインストールした場合、WindowsであればC:\User\<username>\AppData\Local\Android\sdk
に配置されています。)
このスクリプトを実行することでツールチェインをインストールできます。ツールチェインはのちにRustのコンパイラにリンカーとして指定するのに必要となります。
スタンドアロン ツールチェーン | Android NDK | Android Developers
CargoProjectの立ち上げとコンパイラの設定
デスクトップかどこかにcargo new --lib greetings
とかしてCargoプロジェクトを作る。
Cargo.tomlは以下のように追記する。
[package] name = "greetings" version = "0.1.0" authors = ["hogehuga <hogehuga@gmail.com>"] [target.'cfg(target_os="android")'.dependencies] jni = { version = "0.10.2", default-features = false } [lib] name = "greetings" crate-type = ["cdylib"]
依存クレートにjniとありますが、これはJava Native InterfaceというJavaがFFIするための仕様の一つで、詳しくは以下。
JNI | Java | IT用語辞典 | 日立ソリューションズ
これにのお陰でJVM言語からRustが生成したネイティブコードを呼んだり、ネイティブコードからJavaバイトコードを呼んだりできるらしい。すごい(こなみ)
.cargo/config
cargo buildの際NDKのリンカ―と連携させるようにcargoの設定ファイルをつくります。
プロジェクトのルートディレクトリに.cargo
フォルダとその下にconfig
ファイルをつくります。このとき.cargo/config
となります。config
ファイルは拡張子無しです。
config
の記述は以下の通りです。ちなみに僕はWindows10でやってるのでMacとか他のOSと拡張子が違ったりするかも。いい感じに読み替えてください。
[target.aarch64-linux-android] ar = "NDK/arm64/bin/aarch64-linux-android-ar" linker = "NDK/arm64/bin/aarch64-linux-android-clang.cmd" [target.armv7-linux-androideabi] ar = "NDK/arm/bin/arm-linux-androideabi-ar" linker = "NDK/arm/bin/arm-linux-androideabi-clang.cmd" [target.i686-linux-android] ar = "NDK/x86/bin/i686-linux-android-ar" linker = "NDK/x86/bin/i686-linux-android-clang.cmd"
toml風な記法だけどconfig.toml
と書いても読み込まれないので注意。
NDKツールチェインのインストール
次に先ほど確認したndk-bundle\build\tools
にあるmake-standalone-toolchainスクリプトを実行し、ARMv7とARM64、x86のツールチェインをインストールしていきましょう。
シェルでCargoプロジェクトに入って作業します。
下記は例です。
set NDK_HOME=C:\Users\<your_name>\AppData\Local\Android\sdk\ndk-bundle cd greetings mkdir NDK %NDK_HOME%/build/tools/make_standalone_toolchain.py --api 26 --arch arm64 --install-dir NDK/arm64 %NDK_HOME%/build/tools/make_standalone_toolchain.py --api 26 --arch arm --install-dir NDK/arm %NDK_HOME%/build/tools/make_standalone_toolchain.py --api 26 --arch x86 --install-dir NDK/x86
プロジェクトのルートディレクトリにツールチェインを置きたいのでフォルダをつくります。名前はNDK
にします。
次にmake_standalone_toolchainスクリプトを実行してNDK
内にインストールしていきます。
ここまで来たらこんな風になるかとおもいます。
Rustコードから共有ライブラリを生成する
ということで<cargo_project_name>\src\lib.rs
にコードを書きましょう。
デモに適当なプログラムを用意します。
use std::os::raw::c_int; #[no_mangle] pub extern "C" fn rust_fibo(n: *mut c_int) -> *mut c_int { if n <= 1 as *mut i32 { n } else { let x = n as i32 - 2; let y = n as i32 - 1; (rust_fibo(x as *mut i32) as i32 + rust_fibo(y as *mut i32) as i32) as *mut i32 } } #[cfg(target_os = "android")] pub mod android { extern crate jni; use self::jni::objects::{JClass, JString}; use self::jni::sys::{jint, jstring}; use self::jni::JNIEnv; use super::*; #[no_mangle] pub unsafe extern "C" fn Java_com_totechite_rustpractice_RustGreetings_greeting( env: JNIEnv, _: JClass, input: JString, ) -> jstring { let input: String = env .get_string(input) .expect("invalid pattern string") .into(); let output = env .new_string(format!("Android meets {}", input)) .expect("Couldn't create java string!"); output.into_inner() } #[no_mangle] pub unsafe extern "C" fn Java_com_totechite_rustpractice_RustGreetings_fibo( _env: JNIEnv, _: JClass, java_int: jint, ) -> jint { let fibo = rust_fibo(java_int as *mut i32); fibo as jint } }
書きました。
mod android
内にあるのがJava等から呼び出し可能なネイティブ関数の部分です。
ナントカgreeting関数はStringをとって、"Android meets"という文字列をくっつけて返します。
ナントカfibo関数はフィボナッチ数を求めるやつです。
命名が超長いのは恐らくJNIの仕様で、呼び出し元のprojectのディレクトリ構造とクラス名、メソッド名に対応させるぽいです。
AndroidProjectであれば<android_project_name>\app\src\main
以下のディレクトリになります。
例えばfn Java_com_totechite_rustpractice_RustGreetings_greeting()
なら、app\src\main
以下のjava\com\totechite\rustpractice
にあるRustGreetings
クラス内にgreeting()
外部呼び出しメソッドが定義されていることを期待してます。
関数名とAndroidProject側の構造が合致しないとno implementation
みたいなエラーになるので注意しましょう。
コンパイルしてみる
cargo build --target aarch64-linux-android --release cargo build --target armv7-linux-androideabi --release cargo build --target i686-linux-android --release
targetフォルダに各プラットフォームのフォルダと共有ライブラリの.soファイルが出力されていれば完了です。
次の記事で共有ライブラリをAndroidで動かします。 totechite.hatenablog.com