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ライブラリの詳しい説明、使い方

手順

  1. 下準備

  2. CargoProjectの立ち上げとコンパイラの設定

  3. 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で利用できるようにするためリンカーを手に入れましょう。

  1. AndroidNDKのインストールをする
    AndroidStudioの設定画面の Appearance & Behaviour > System Settings > Android SDK > SDK ToolsからNDKにチェックを入れてインストールする。 AndroidStudio-Setting
    もしくは直接ダウンロード。 NDK のダウンロード  |  Android NDK  |  Android Developers

  2. スクリプトの確認
    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というJavaFFIするための仕様の一つで、詳しくは以下。

Java Native Interface仕様の目次

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内にインストールしていきます。

f:id:totechite:20190106213600p:plain ここまで来たらこんな風になるかとおもいます。

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

参考元・勉強になったページ

Building and Deploying a Rust library on Android

jni - Rust