たくさんの自由帳

Android と Rust

投稿日 : | 0 日前

文字数(だいたい) : 16687

どうもこんばんわ。
スカイコード 攻略しました。OP曲がかっこよくて気になってたやつ

この先どうなるんや、、が続くゲームでよかった(語彙力。作中じゃないけどシナリオに惹かれた
?な発言もちゃんと回収してた

Imgur

天使ちゃんかわいい。
重いシーンが何個かあって個別あるか心配だった、あります。

Imgur

Imgur

Imgur

Imgur

!!!あがっちゃうシンジュちゃん
かわいい!!

Imgur

Imgur

Imgur

あと天使ちゃんルートの中間曲がとてもいい!!シンジュちゃんのところ
しばらくリピートしてる、CD買ってよかった。

Imgur

えちえちシーンも結構良かった!!!、、(けど本筋の話が気になってそれどころじゃないよ;;)

Imgur

それはそうと、何回か(も?)気持ちが揺さぶられて疲れた、、、。休み明けのシンジュちゃんのやつは結構効いた
シンジュちゃんEDの後はどうなったのか、、な

本題

ふと、Rust 言語を書いてみたくなった。
というのも、Android 開発者は難しいC++で処理を書いてJNIで繋いで呼び出せば速くなると盲目的に思ってる(節がある)(私だけか。)

ただC++難しいそうだなあ~思ってたところ、RustAndroid の CPU向けにクロスコンパイル出来るらしく、しかも簡単な方法があると聞いた。
本当に速いか確かめます。

はじめに

この記事で言うネイティブコードは、クロスプラットフォームの人たちが言う 各プラットフォームの開発言語 (Swift / Kotlin) の事では無く、
C++Rustのような Android NDK が必要なコードのことを指します。

ネイティブライブラリも同様にRustをコンパイルしたやつを指します。

Android で Rust を呼び出す方法

調べた感じ、2つくらいRustコードを呼び出す方法があるっぽい。

前者の古い方がC++と同じような感じで、Java Native Interface (JNI)の形式で関数を書いてコンパイルしてって感じのやつ。
後者がRustコードを少し書き足すだけでRustKotlinを繋ぐバインディングを自動で生成してくれるというもの。自動生成とは言え使うのは難しくない。

ただ、前者はJNIなので複雑、後者はJNIとは別のJava Native Access (JNA)を採用しているためか速度が出ないという欠点があります。
今回のように速度を出して欲しいときはJNIの方を使う必要があるかも。

環境

Windowsを使いますが、どうやらWindowsRustするにはVisual Studio Installerを経由して数GBのビルドツールを入れないといけないらしい。(本当?)
without Visual Studio Installerでセットアップする方法ないのかな...
https://www.rust-lang.org/ja/tools/install

数回しか使わないのに面倒すぎるので、今回はWSL 2をインストールしLinuxRustすることにします。今回Android向けにビルドするため、別にWindowsで動く必要ないので。容量無くなったらすぐ消したいしで
また、Rustを書く際にVSCodeを使います。何でも良いです。

なまえあたい
パソコンWindows 10 Pro
Rust 開発WSL 2 ( Ubuntu )
Android 端末Pixel 8 Pro / Xperia 1 V
Rustrustc 1.85.0 (4d91de4e4 2025-02-17)
UniFFI0.29
Android NDK27 ( WSL2Rustをビルドする場合はWindows側は不要 )

DiskInfo3のお陰で、Cドライブを少し開放できたのでWSL 2を入れます。ちょいまって
古いAndroid Studioの残骸を消したら空いた。
Imgur

Rust

Rustほとんどやったこと無いので、まずはチュートリアルをこなしてみる。
Rustのマスコットキャラクター?のカニが挨拶をしてくれるプログラムを作るらしいです。

Rust を入れる

Linuxなのでこっち。

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

Imgur

そのままエンターでいいはず。

1) Proceed with standard installation (default - just press enter)
2) Customize installation
3) Cancel installation

Rust is installed now. Great!って表示されれば良いはず。
ターミナルを再起動します。面倒なのでexitで抜けて再起動するか。

あと私の場合はerror: linker cc not foundエラーが表示されたため、以下のコマンドを叩く必要がありました。

sudo apt update
sudo apt upgrade
sudo apt install -y build-essential

HelloWorld

https://www.rust-lang.org/ja/learn/get-started

これをやってRustの感覚を掴んでみる。
カニのマスコットが挨拶してくれるプログラムを作るらしい。

新しいプロジェクトを作って

cargo new hello-rust

移動して実行

cd hello-rust
cargo build

Hello Worldできた

Imgur

VSCode

さすがにWSL2上でvimを使うのは厳しいので、VSCodeWSL2を接続してVSCodeで開発できるようにします。
https://code.visualstudio.com/docs/remote/wsl

VSCode側に拡張機能をあらかじめインストールしておいて、

Imgur

WSL2上のUbuntuで、code .コマンドを叩くと、WSL2と接続したVSCodeが開きます。左下にWSL: Ubuntuって出てる!!!
すごく統合されていてこの手の開発者は嬉しそう、わたしはAndroidなんで,,,

Imgur

カニさんに挨拶されたい

https://www.rust-lang.org/ja/learn/get-started

これの依存関係のところから。
Cargo.tomlを開きdependenciesに1行足します。

[dependencies]
ferris-says = "0.3.1"

そしたらcargo buildを入力することでライブラリを取ってきてくれるそう。

最後に挨拶してくるプログラムを書きます。srcの中のmain.rsをこの用に書き換えて

use ferris_says::say; // from the previous step
use std::io::{stdout, BufWriter};
 
fn main() {
    let stdout = stdout();
    let message = String::from("Hello fellow Rustaceans!");
    let width = message.chars().count();
 
    let mut writer = BufWriter::new(stdout.lock());
    say(&message, width, &mut writer).unwrap();
}

出来たら、ターミナルでもう一度cargo runを叩きます。
カニさんが出てきたら成功です。

Imgur

Imgur

アスキーアート、かわいい。
バックスラッシュが¥マークになってるけど気にしないことに。

Android Kotlin から Rust を呼び出す

AndroidRustを呼び出すためには、冒頭の通りC++時代のようにJNIを使うか、UniFFIKotlinバインディングを自動生成するかの2択っぽいです。
両方試してみますが、速度が必要ない場合はUniFFIが簡単で良かったです。

Android NDK を入れる

ビルドするためにはAndroid NDKRustをコンパイルするマシンに入れておく必要があります。
JNIにしろ、UniFFIにしろ必要です。

ダウンロードと展開は以下のコマンドで出来ます。が、利用規約を読まず直接 DL することになります、
というわけで利用規約一応おいておきます→ https://developer.android.com/ndk/downloads

cd /opt
sudo wget https://dl.google.com/android/repository/android-ndk-r27c-linux.zip
sudo apt install unzip
sudo unzip android-ndk-r27c-linux.zip

ダウンロード先はどこにするのがいいのか知らないので、他プロジェクトにならって/optにしてみた。
多分sudoが必要です。

unzipコマンドが使える場合はapt installの行はスキップできます。

JNI

https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-21-rust-on-android.html

少し古いですが、この記事通りにやることが出来ます。

JNI Android プロジェクトを作成

C++Androidでやったことある方ならわかるかもしれませんが、JNIの関数はクソ長いです。
JNIの関数の命名規則は、パッケージ名、クラス名、関数名を知っている必要があるので、先にAndroidプロジェクトを作ります。ちなみに後者のUniFFIならもっと簡単です。

今回はWSL2Rustをコンパイルするため、Nativeを選択する必要はありません
WindowsRustをコンパイルする場合もSDK ManagerからAndroid NDKをインストールすればいいはずな気がするので、Nativeを選ぶ必要はないと思います。

Imgur

次に、Kotlinexternal fun greeting(message: String)関数を作ります。今回はMainActivityで。
せっかくなので挨拶文を引数で設定できるようにしてみました。また、カニさんのアスキーアートを文字列で受け取るよう返り値はStringです。

この関数の中身をRustで実装する形になります。

package io.github.takusan23.androidrustjni
 
class MainActivity : ComponentActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AndroidRustJniTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
 
    // これ
    private external fun greeting(message: String): String
 
}

で、なんで先にプロジェクトを作ったかと言うと、この関数名、クラス、パッケージ名を確定させておく必要があるからです。
とりあえず次はRustコードを書きましょう。

JNI Rust 側も作成

まずはそれ用のRustプロジェクトを作ります。VSCodeを開きます。

cargo new android-rust-jni
cd android-rust-jni/
code .

JNI Cargo.toml

Cargo.tomlを開き

[lib]の2行も書き足します。
dylibにするとビルド時に.soが生成されるようになります。

次に、同様にカニさんに挨拶されたいのでライブラリを入れます。同じ用に[dependencies]の下に足します。
また、JNI用のライブラリも追加します。下の2行ですね。

[lib]
crate-type = ["dylib"]
 
[dependencies]
ferris-says = "0.3.1"
 
[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.21.1", default-features = false }

JNI lib.rs

次にmain.rsの名前をlib.rsにします。
cargo newの時点でlibの方を作る方法があったような気もします。)

ここからRust + JNIコードを書いていきます。
で、ここで、さっき作ったAndroidプロジェクトを確認する必要があります。

というのもここから追加で書く関数が、Kotlin側のexternal funと紐付くわけですが、
名前にルールがあります。こちらです。

Java_{パッケージ名。ドットはアンダーバーに置き換え}_{クラス名}_{関数名}

例えば、このKotlinコードだと

package io.github.takusan23.androidrustjni
 
class MainActivity : ComponentActivity() {
    // これ
    private external fun greeting(message: String): String
}
  • パッケージ名
    • io.github.takusan23.androidrustjni
  • クラス名
    • MainActivity
  • 関数名
    • greeting

になるので、これをルール通りに当てはめるとこうなります。

Java_io_github_takusan23_androidrustjni_MainActivity_greeting

関数名が分かったところでコードを書いていきましょう。lib.rsに書き足します。
Java_io_github_takusan23_androidrustjni_MainActivity_greetingの部分は、各自パッケージ名と関数名を直してください。

use ferris_says::say;
 
fn rust_greeting(message: String) -> String {
    let width = message.chars().count();
    let mut writer = Vec::new();
    say(&message, width, &mut writer).unwrap();
    return String::from_utf8(writer).unwrap();
}
 
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
pub mod android {
    extern crate jni;
 
    use jni::JNIEnv;
    use jni::objects::{JClass, JString};
 
    use crate::rust_greeting;
 
    #[unsafe(no_mangle)]
    pub extern "C" fn Java_io_github_takusan23_androidrustjni_MainActivity_greeting<'local>(
        mut env: JNIEnv<'local>,
        _class: JClass<'local>,
        message: JString<'local>,
    ) -> JString<'local> {
        // Rust String へ
        let rust_string: String = env
            .get_string(&message)
            .expect("Couldn't get java string!")
            .into();
 
        // カニさん
        let greeting_string = rust_greeting(rust_string);
 
        // JString を返す
        return env.new_string(greeting_string).unwrap();
    }
}

JNIの関数はJNI用のプリミティブ型を使う必要があります。
これはRustに限らずC++で書いてもjstringとか言うのになる。

JNI ビルド

そしたらビルドします。
と、その前にAndroidをターゲットに追加します。多分この4つ?

上からARM 64ビットARM 32 ビットx64x86。のハズ?
x64とかはWindowsでエミュレータを動かした時にIntel CPUなので多分いる。

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

Imgur

次に、Cargo.tomlの階層に.cargoフォルダを作成し、config.tomlを作成します、
そしたら、以下を貼り付けます。NDKのパスが違う場合は直してください。

[target.aarch64-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang"
 
[target.armv7-linux-androideabi]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang"
 
[target.x86_64-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android21-clang"
 
[target.i686-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android21-clang"

Imgur

そしたら各CPU向けにビルドできるようになっているはずです。
以下のコマンドを順番に叩けばいいはず。

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

Finished release profile [optimized] target(s) in 8.72sみたいなのが出ればいいはず。

Imgur

JNI 共有ライブラリを回収

あともうちょっと!

.soファイルを回収します。targetフォルダ内に各 CPU向けのフォルダがあるので開いて、releaseの中のlib から始まる 拡張子 soのファイルを取り出します。
WSL2ならエクスプローラーから見れるのでラクラクです。

Imgur

Imgur

JNI AndroidStudio のプロジェクトに共有ライブラリを入れる

appsrcmainフォルダへ進み、jniLibsフォルダを作成します。
また、その中に以下4つの名前でフォルダを作ってください。

  • armeabi-v7a
  • arm64-v8a
  • x86
  • x86_64

ファイルツリーの表示をProjectにした時に、この場所にフォルダが作られていれば大丈夫です。

Imgur

Imgur

そしたら今作った4つのフォルダに、それぞれさっき取り出したsoファイルを配置します。

  • armeabi-v7a なら armv7-linux-androideabi
  • arm64-v8a なら aarch64-linux-android
  • x86 なら i686-linux-android
  • x86_64 なら x86_64-linux-android

arm64-v8aならaarch64-linux-androidフォルダのreleaseの中にあったsoファイルといった感じです。

Imgur

JNI 呼び出してみる

カニさんに挨拶されたいことを忘れかけてた。もう見れますよ。
MainActivity.kt共有ライブラリをロードするようにすれば完了です。あとはexternal fun greeting()を呼び出して使いましょう。

System.loadLibrary()ですが、共有ライブラリの名前から先頭のlib.soを消した名前を渡す必要があります。
ちなみに、RustKotlin (JNI)名前が間違ってる場合はjava.lang.UnsatisfiedLinkError: No implementation found見たいな例外が投げられます。

class MainActivity : ComponentActivity() {
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AndroidRustJniTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    // Rust のカニさんが挨拶してくれる
                    Text(
                        text = greeting("Hello Android + JNI + Rust"),
                        modifier = Modifier.padding(innerPadding),
                        fontFamily = FontFamily.Monospace
                    )
                }
            }
        }
    }
 
    // Rust 側の実装
    private external fun greeting(message: String): String
 
    // Rust でビルドした共有ライブラリをロード
    companion object {
        init {
            System.loadLibrary("android_rust_jni")
        }
    }
}

これで完了です。早速実行してみましょう。

JNI 実行

カニさん出た。

Imgur

試しに32ビットAndroid端末でも動かしてみた。(Xperia Z3 Compact
動いてます。

Imgur

画像送るほうが大変、、、Android ビーム (懐かしい)を経由してQuickShare
x86_64.soファイルもちゃんと同梱したので、Windowsでエミュレーターを使ったときの開発でも特に問題なく実行できるはずです。

JNI は長すぎる

これが嫌な場合はUniFFIを採用するべきです、少しはラクできるはず。

UniFFI

https://mozilla.github.io/uniffi-rs/latest/

こちらはRustKotlinを繋ぐ部分を自動で作ってくれます(Kotlin バインディング)。
しかも簡単。

UniFFI 同様にカニさんプロジェクトを作る

JNIと同じように、それ用のプロジェクトをcargo newします。

cargo new android-rust-uniffi
cd android-rust-uniffi

次にCargo.tomlを開いて書き足します。
cdylibで共有ライブラリをビルド、uniffiは記述時時点最新版を入れます。あとはカニさんのライブラリを。

[lib]
crate-type = ["cdylib"]
name = "android_rust_uniffi"
 
[dependencies]
uniffi = { version = "0.29", features = [ "cli" ] }
 
[build-dependencies]
uniffi = { version = "0.29", features = [ "build" ] }

Imgur

詳しくは本家
https://mozilla.github.io/uniffi-rs/latest/tutorial/Prerequisites.html

UniFFI カニさんコードを書く

srcの中のmain.rslib.rsにリネームして、カニさんプログラムを書きます。
JNIのそれと同じです。カニさんのアスキーアートを文字列で返す。

use ferris_says::say;
 
pub fn rust_greeting(message: String) -> String {
    let width = message.chars().count();
    let mut writer = Vec::new();
    say(&message, width, &mut writer).unwrap();
    return String::from_utf8(writer).unwrap();
}

UniFFI Kotlin で使いたい関数に目印をつける

今回はpub fn rust_greeting()関数をKotlinから呼び出したいので、#[uniffi::export]をつけます。
もう一つ、本家ではUDL ファイルを作る方法も紹介されてますが、#[uniffi::export]付けるのが楽だと思います。

#[uniffi::export]
pub fn rust_greeting(message: String) -> String {
    // 以下省略...
}

それから、lib.rsの一番最初にuniffi::setup_scaffolding!();の一行を書き足します。
これが、ここまでの状態のコードです。

uniffi::setup_scaffolding!();
 
use ferris_says::say;
 
#[uniffi::export]
pub fn rust_greeting(message: String) -> String {
    let width = message.chars().count();
    let mut writer = Vec::new();
    say(&message, width, &mut writer).unwrap();
    return String::from_utf8(writer).unwrap();
}

UniFFI Kotlin バインディングを生成する準備

https://mozilla.github.io/uniffi-rs/latest/tutorial/foreign_language_bindings.html

もうゴールは近い。
次はKotlinRustコードを呼び出すバインディングを生成するために使うファイルを作ります。

Cargo.tomlと同じ階層にuniffi-bindgen.rsファイルを作成して、以下を貼り付けます。

fn main() {
    uniffi::uniffi_bindgen_main()
}

次に、Cargo.tomlに書き足します。

[[bin]]
# This can be whatever name makes sense for your project, but the rest of this tutorial assumes uniffi-bindgen.
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"

UniFFI バインディングを生成する

バインディングのために使う.soファイルは多分ターゲット Android向けである必要がない?
以下の2つのコマンドを実行します。soファイルのパスは各自直してください。

cargo build --release
cargo run --bin uniffi-bindgen generate --library target/release/libandroid_rust_uniffi.so --language kotlin --out-dir out

終わると、outフォルダの中に.kt(Kotlin)コードが入ってるはず!
Imgur

UniFFI ビルド

JNIと同じように各 CPU (アーキテクチャ)向けにビルドして、.soファイルを作成する必要があります。
JNI のときと同じ #JNIビルド と同じです。

rustup targetで一回も追加したことない場合は呼び出します。

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

次に.cargoフォルダを作りconfig.tomlを作成し、以下を貼り付けます。
NDKのパスが違う場合は直してください。

[target.aarch64-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang"
 
[target.armv7-linux-androideabi]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang"
 
[target.x86_64-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android21-clang"
 
[target.i686-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android21-clang"

Imgur

これでAndroid向けにビルド出来るようになりました。
4つのコマンドを順番に叩いて、各アーキテクチャ向けにビルドした共有ライブラリを作ります。

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

UniFFI AndroidStudio のプロジェクトに共有ライブラリを入れる

Android Studioでプロジェクトを作ってください。Jetpack Composeのやつを選んでいいです。
Nativeである必要もないです。ビルド済みRustを使うので。

次に、ビルドした.soファイルを置くためのフォルダを作ります。 これも JNI AndroidStudio のプロジェクトに共有ライブラリを入れる と同じです。
詳しくはそっちに譲ります。ざっくり。スクショのとおりにフォルダを作ります。

Imgur

そしたら今作った4つのフォルダに、soファイルを配置します。
soファイルはtarget/{各アーキテクチャ}/releaseの中にあります。

  • armeabi-v7a なら armv7-linux-androideabi
  • arm64-v8a なら aarch64-linux-android
  • x86 なら i686-linux-android
  • x86_64 なら x86_64-linux-android

例えばarm64-v8aならtarget/aarch64-linux-android/release/の中にあるsoファイルを置けばいいです。

Imgur

UniFFI バインディングをコピーしてくる

と、その前に、
UniFFIを動かすにはJava Native Access (JNA)というライブラリに依存しているので、まずapp/build.gradle.ktsを開いてJNAを追加します。そしたらGradle syncね。

dependencies {
    implementation("net.java.dev.jna:jna:5.6.0@aar")
 
    // 以下省略...

次にUniFFIが生成したバインディングを持ってきましょう。
outフォルダを探すとあるはず。out/uniffi/android_rust_uniffiにありました。これをMainActivity.ktと同じ階層に貼り付ける。

Imgur

package 名を直してもいいはず。
JNAJNIのときとは違い、パッケージ名が違ったとしても動くはず。

UniFFI 呼び出してみる

バインディングがあるので、対応する関数が見つかるはず。
rustGreeting()関数ですね。JNIのときの用にしてみる。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AndroidRustUniffiTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Text(
                        text = rustGreeting("Android UniFFI + Rust !!!"), // 今回作ったコードはトップレベルにあった
                        modifier = Modifier.padding(innerPadding),
                        fontFamily = FontFamily.Monospace
                    )
                }
            }
        }
    }
}

UniFFI 実行

UniFFIでもカニさんが挨拶してくれました。良かった。

Imgur

Imgur

また、使いたい関数にに少し書き出すだけでKotlin (JNI)で呼び出せることに気付きましたか?
JNIのときはJavaのプリミティブ型jstringとかを使う必要があったのに対し、UniFFI気にする必要がない。

また、JNIの長い関数名のルールもありません。が、ほんとはJNIでも長い関数名を回避できる、、、

カニさんソースコード

Android NDK だと速いの?

というわけで、今回はひたすら計算する処理をRustで書いて、更にSIMD 命令の恩恵を受けようと思います。

SIMD

CPUにはSIMD命令とか言うのがあって、複数の値に対して同時に四則演算ができるとかなんとか。
どういうことかというと、こんな感じにInt 配列の中身を2倍にしたい場合はまあこうですよね。

val a = listOf(1, 2, 3, 4).map { it * 2 }

しかし、これでは掛け算が4回必要です。
そこでSIMDです。以下のコードは擬似コードなので動かないですが、どうやら一回の掛け算処理で、4つの値全てに対して計算ができる、らしい。

val a = listOf(1, 2, 3, 4) * 2

しかし、このSIMD命令をJavaから使う方法はない?ので、Rustで書いて使うことにします。

SIMD 命令のプログラムは難しくないんですか

そう思ってたのですが、、、、

実は単純なforループの場合はコンパイラが勝手にSIMDを使ったものに置き換えてくれるそうです。
というか今回はコンパイラにやらせる作戦で行きます。自分でSIMD用の関数を呼ぶくらいならやらない。

単純というか副作用がないコードじゃないとならないらしい?
forの中で関数呼び出しとかするとダメそう。

関数

これまでと同じようにcargo newでプロジェクトを作って、lib.rsに関数を作りました。 2つのバイト配列から取り出して引き算するやつ。テストも正解だけだけど書いた。

use std::cmp;
 
// 2つのバイト配列を取って、引き算した結果を返す
fn sub_two_bytearray(a: Vec<u8>, b: Vec<u8>) -> Vec<u8> {
    let size = cmp::min(a.len(), b.len());
    let mut result: Vec<u8> = vec![0; size];
 
    for i in 0..size {
        result[i] = a[i] - b[i];
    }
    return result;
}
 
// テストコード
#[cfg(test)]
mod tests {
    use std::vec;
 
    use crate::sub_two_bytearray;
 
    #[test]
    fn it_works() {
        let a: Vec<u8> = vec![10, 10, 10];
        let b: Vec<u8> = vec![1, 2, 3];
 
        let result = sub_two_bytearray(a, b);
        assert_eq!(result, vec![9, 8, 7]);
    }
}

これをJNIUniFFIで試して速いか確かめます。

付録 ところでコンパイラが SIMD 命令を使っているか確認したい

そのためには、アセンブリコードを読む必要があるのですが、cargo buildはデフォルトでは生成されないのでオプション付きでコマンドを叩きます。
aarch64で作ってみます。なお、releaseの場合は多分SIMDに書き直すのが有効になります。

RUSTFLAGS='--emit asm' cargo build --release --target aarch64-linux-android

target/aarch64-linux-android(アーキテクチャ)/release/deps/の中に.sファイルがあるはず。
これを読みます。一部を抜き出してみました。

.LBB0_16:
	ldp	q0, q3, [x12, #-16]
	subs	x14, x14, #32
	ldp	q1, q2, [x13, #-16]
	add	x12, x12, #32
	add	x13, x13, #32
	sub	v0.16b, v1.16b, v0.16b
	sub	v1.16b, v2.16b, v3.16b
	stp	q0, q1, [x11, #-16]
	add	x11, x11, #32
	b.ne	.LBB0_16
	cmp	x21, x10
	b.eq	.LBB0_6
	tst	x21, #0x18
	b.eq	.LBB0_4

、、、、が、この辺さっぱり分からんのでAIに聞いた。

Imgur

Geminiさん曰く、ARM NEON(ARM の SIMD の名前)の場合は、アセンブリを見てq0みたいな名前の付くレジスタを使っているらしい。
これを見るとldp q0ってところでq0レジスタを使ってるので確かにそうなのかもしれない?

付録 SIMD を無効にしてビルドすると

cargoRUSTFLAGS="-C target-feature=+hogehoge"CPUの機能が有効にできますが、逆にマイナス-hogehogeすると無効にできるらしい。
というわけでこの場合のアセンブリコードもみてみます。SIMD 命令ARM アーキテクチャだとNEONって名前なのでneonIntelだとまた別。

RUSTFLAGS='--emit asm -C target-feature=-neon' cargo build --release --target aarch64-linux-android

同じ様にアセンブリコードを見てみると、q0とかの文字がなくなってそう。

付録 ブラウザでアセンブリコードを見たい

Compiler Explorer
https://godbolt.org/

で、アセンブリコードを見ることが出来る。Rustを選ぶ。
Compiler Options

-C opt-level=3 -C target-feature=+neon --target aarch64-linux-android

を入れると見れるはず。neon無効は-neonで。

SIMD 実験

というわけで、今回は結構前にやったボーカルあり曲とカラオケ曲を使ってボーカルのみ曲を作る記事の、
ボーカル曲からカラオケ曲を引き算し、ボーカルだけにしてる計算部分だけをRustで書きます。

https://takusan.negitoro.dev/posts/summer_vacation_music_vocal_only/

音は波なので、足したり引いたり出来ます。
これを応用して、aacPCMにデコードして、引き算して、aacにエンコードすれば出来るはず。

試す内容としてはJNI SIMD ありUniFFI SIMD ありKotlin リリースビルドJNI SIMD無効で試します。
あと、そもそもRustC++を使ったネイティブコードが、本当に速いのか確かめるためにKotlinも。あとSIMDの有り無しを見たい。

使う端末はXperia 1 VGoogle Pixel 8 Pro
バイト配列の大きさは47290528 バイト (45MB)。これを引き算していく。

アプリ作った

というわけで作りました。
音声のエンコード、デコードは前回の記事のままなので、そちらを見るか、後述するGitHubでソースコードを見てください。

Imgur

実験結果

Snapdragon 8 Gen 2 (Xperia 1 V)

新しいのほちい

Rust + JNIRust + UniFFIKotlinRust + JNI ( SIMD 未使用 )
66 ms446 ms128 ms94 ms
59 ms459 ms22 ms103 ms
72 ms440 ms20 ms96 ms
61 ms444 ms21 ms100 ms
57 ms440 ms21 ms94 ms

Google Tensor G3 (Pixel 8 Pro)

同期のSoCよりも性能がちょっと低い。

Rust + JNIRust + UniFFIKotlinRust + JNI ( SIMD 未使用 )
131 ms572 ms213 ms172 ms
121 ms567 ms78 ms192 ms
121 ms563 ms70 ms181 ms
151 ms572 ms58 ms177 ms
122 ms561 ms53 ms199 ms

まとめ

この規模だとKotlin (JVM)が一番速かった。(というか題材にこれを選んだのが良くなかった?)
JVMが初回を除いてなぜか速い。もっと大規模だと違うのかもしれない。

C++で試せてないのであれですが、別にネイティブコードを書いても速くなるわけじゃないのか、、、

Android の JVMすごい。

UniFFIJava Native Access (JNA)都合で遅くなってる?
JNI独自の型(jstring)とかを使わずに呼び出せるので、それで時間がかかってるのかも?

実験ソースコード

Q&A 16KB ページサイズに対応してないんですけど

.soファイルをapk / aabの中に入れることになるのでこの問題にぶち当たります
が、この記事もう長いので次回にまわします。 → 書きました: https://takusan.negitoro.dev/posts/android_15_16kb_page_size/

takusan23@DESKTOP-ULEKIDB:~$ chmod +x check_elf_alignment.sh
takusan23@DESKTOP-ULEKIDB:~$ ./check_elf_alignment.sh app-release.apk
 
Recursively analyzing app-release.apk
 
NOTICE: Zip alignment check requires build-tools version 35.0.0-rc3 or higher.
  You can install the latest build-tools by running the below command
  and updating your $PATH:
 
    sdkmanager "build-tools;35.0.0-rc3"
 
=== ELF alignment ===
/tmp/app-release_out_6q4W9/lib/x86_64/libandroid_jni.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86_64/libandroid_jni_without_simd.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86_64/libandroidx.graphics.path.so: ALIGNED (2**14)
/tmp/app-release_out_6q4W9/lib/x86_64/libjnidispatch.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86_64/libandroid_rust_uniffi.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/armeabi/libjnidispatch.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/arm64-v8a/libandroid_jni.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/arm64-v8a/libandroid_jni_without_simd.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/arm64-v8a/libandroidx.graphics.path.so: ALIGNED (2**14)
/tmp/app-release_out_6q4W9/lib/arm64-v8a/libjnidispatch.so: ALIGNED (2**16)
/tmp/app-release_out_6q4W9/lib/arm64-v8a/libandroid_rust_uniffi.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86/libandroid_jni.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86/libandroid_jni_without_simd.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86/libandroidx.graphics.path.so: ALIGNED (2**14)
/tmp/app-release_out_6q4W9/lib/x86/libjnidispatch.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86/libandroid_rust_uniffi.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/mips64/libjnidispatch.so: ALIGNED (2**16)
/tmp/app-release_out_6q4W9/lib/mips/libjnidispatch.so: ALIGNED (2**16)
/tmp/app-release_out_6q4W9/lib/armeabi-v7a/libandroid_jni.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/armeabi-v7a/libandroid_jni_without_simd.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/armeabi-v7a/libandroidx.graphics.path.so: ALIGNED (2**14)
/tmp/app-release_out_6q4W9/lib/armeabi-v7a/libjnidispatch.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/armeabi-v7a/libandroid_rust_uniffi.so: UNALIGNED (2**12)
Found 16 unaligned libs (only arm64-v8a/x86_64 libs need to be aligned).
=====================
takusan23@DESKTOP-ULEKIDB:~$

Imgur

今回作った.soが軒並みUNALIGNED。。。

おわりに

Java / Kotlinで十分に速かった。
Android NDKを入れて、Java Native Interfaceのバインディングを書いて、各 CPU アーキテクチャ向けにコンパイルして、、、って面倒だしクラッシュした時に直せる自信がない。
GitHubのコードでNDKが必要って言われた瞬間に目を逸らしてる(偏見)

おわりに2

そーいえば昔というか、32bit時代の、ネイティブコードを使っててarm64-v8aARM 64bit)が入ってないアプリ、すでに動かなくなってそう。
Google TensorG2 ( Pixel 7 )から32 ビットネイティブコードのサポート無し、
Snapdragon8 Gen 332 ビットネイティブコードのサポートが無くなった。

もっとも、targetSdkが古すぎてインストールが弾かれてしまいそうなので、こんなことにはならないと思います、が。