たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 16687
目次
本題
はじめに
Android で Rust を呼び出す方法
環境
Rust
Rust を入れる
HelloWorld
VSCode
カニさんに挨拶されたい
Android Kotlin から Rust を呼び出す
Android NDK を入れる
JNI
JNI Android プロジェクトを作成
JNI Rust 側も作成
JNI Cargo.toml
JNI lib.rs
JNI ビルド
JNI 共有ライブラリを回収
JNI AndroidStudio のプロジェクトに共有ライブラリを入れる
JNI 呼び出してみる
JNI 実行
JNI は長すぎる
UniFFI
UniFFI 同様にカニさんプロジェクトを作る
UniFFI カニさんコードを書く
UniFFI Kotlin で使いたい関数に目印をつける
UniFFI Kotlin バインディングを生成する準備
UniFFI バインディングを生成する
UniFFI ビルド
UniFFI AndroidStudio のプロジェクトに共有ライブラリを入れる
UniFFI バインディングをコピーしてくる
UniFFI 呼び出してみる
UniFFI 実行
カニさんソースコード
Android NDK だと速いの?
SIMD
SIMD 命令のプログラムは難しくないんですか
関数
付録 ところでコンパイラが SIMD 命令を使っているか確認したい
付録 SIMD を無効にしてビルドすると
付録 ブラウザでアセンブリコードを見たい
SIMD 実験
アプリ作った
実験結果
Snapdragon 8 Gen 2 (Xperia 1 V)
Google Tensor G3 (Pixel 8 Pro)
まとめ
実験ソースコード
Q&A 16KB ページサイズに対応してないんですけど
おわりに
おわりに2
どうもこんばんわ。
スカイコード 攻略しました。OP曲
がかっこよくて気になってたやつ
この先どうなるんや、、が続くゲームでよかった(語彙力。作中じゃないけどシナリオに惹かれた
?な発言もちゃんと回収してた
天使ちゃんかわいい。
重いシーンが何個かあって個別あるか心配だった、あります。
!!!あがっちゃうシンジュちゃん
かわいい!!
あと天使ちゃんルートの中間曲がとてもいい!!シンジュちゃんのところ
しばらくリピートしてる、CD買ってよかった。
えちえちシーンも結構良かった!!!、、(けど本筋の話が気になってそれどころじゃないよ;;)
それはそうと、何回か(も?)気持ちが揺さぶられて疲れた、、、。休み明けのシンジュちゃんのやつは結構効いた
シンジュちゃんEDの後はどうなったのか、、な
ふと、Rust 言語
を書いてみたくなった。
というのも、Android 開発者
は難しいC++
で処理を書いてJNI
で繋いで呼び出せば速くなると盲目的に思ってる(節がある)(私だけか。)
ただC++
難しいそうだなあ~思ってたところ、Rust
をAndroid の CPU
向けにクロスコンパイル出来るらしく、しかも簡単な方法があると聞いた。
本当に速いか確かめます。
この記事で言うネイティブコードは、クロスプラットフォームの人たちが言う 各プラットフォームの開発言語 (Swift / Kotlin) の事では無く、
C++
やRust
のような Android NDK が必要なコードのことを指します。
ネイティブライブラリも同様にRust
をコンパイルしたやつを指します。
調べた感じ、2つくらいRust
コードを呼び出す方法があるっぽい。
前者の古い方がC++
と同じような感じで、Java Native Interface (JNI)
の形式で関数を書いてコンパイルしてって感じのやつ。
後者がRust
コードを少し書き足すだけでRust
とKotlin
を繋ぐバインディングを自動で生成してくれるというもの。自動生成とは言え使うのは難しくない。
ただ、前者はJNI
なので複雑、後者はJNI
とは別のJava Native Access (JNA)
を採用しているためか速度が出ないという欠点があります。
今回のように速度を出して欲しいときはJNI
の方を使う必要があるかも。
Windows
を使いますが、どうやらWindows
でRust
するにはVisual Studio Installer
を経由して数GBのビルドツールを入れないといけないらしい。(本当?)
without Visual Studio Installer
でセットアップする方法ないのかな...
https://www.rust-lang.org/ja/tools/install
数回しか使わないのに面倒すぎるので、今回はWSL 2
をインストールしLinux
でRust
することにします。今回Android
向けにビルドするため、別にWindows
で動く必要ないので。容量無くなったらすぐ消したいしで
また、Rust
を書く際にVSCode
を使います。何でも良いです。
なまえ | あたい |
---|---|
パソコン | Windows 10 Pro |
Rust 開発 | WSL 2 ( Ubuntu ) |
Android 端末 | Pixel 8 Pro / Xperia 1 V |
Rust | rustc 1.85.0 (4d91de4e4 2025-02-17) |
UniFFI | 0.29 |
Android NDK | 27 ( WSL2 でRust をビルドする場合はWindows 側は不要 ) |
DiskInfo3
のお陰で、Cドライブ
を少し開放できたのでWSL 2
を入れます。ちょいまって
古いAndroid Studio
の残骸を消したら空いた。
Rust
ほとんどやったこと無いので、まずはチュートリアルをこなしてみる。
Rust
のマスコットキャラクター?のカニが挨拶をしてくれるプログラムを作るらしいです。
Linux
なのでこっち。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
そのままエンターでいいはず。
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
https://www.rust-lang.org/ja/learn/get-started
これをやってRust
の感覚を掴んでみる。
カニのマスコットが挨拶してくれるプログラムを作るらしい。
新しいプロジェクトを作って
cargo new hello-rust
移動して実行
cd hello-rust
cargo build
Hello World
できた
さすがにWSL2
上でvim
を使うのは厳しいので、VSCode
とWSL2
を接続してVSCode
で開発できるようにします。
https://code.visualstudio.com/docs/remote/wsl
VSCode
側に拡張機能をあらかじめインストールしておいて、
WSL2
上のUbuntu
で、code .
コマンドを叩くと、WSL2
と接続したVSCode
が開きます。左下にWSL: Ubuntu
って出てる!!!
すごく統合されていてこの手の開発者は嬉しそう、わたしはAndroid
なんで,,,
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
を叩きます。
カニさんが出てきたら成功です。
アスキーアート、かわいい。
バックスラッシュが¥マークになってるけど気にしないことに。
Android
でRust
を呼び出すためには、冒頭の通りC++
時代のようにJNI
を使うか、UniFFI
でKotlin
バインディングを自動生成するかの2択っぽいです。
両方試してみますが、速度が必要ない場合はUniFFI
が簡単で良かったです。
ビルドするためにはAndroid NDK
をRust
をコンパイルするマシンに入れておく必要があります。
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
の行はスキップできます。
https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-21-rust-on-android.html
少し古いですが、この記事通りにやることが出来ます。
C++
をAndroid
でやったことある方ならわかるかもしれませんが、JNI
の関数はクソ長いです。
JNI
の関数の命名規則は、パッケージ名、クラス名、関数名を知っている必要があるので、先にAndroid
プロジェクトを作ります。ちなみに後者のUniFFI
ならもっと簡単です。
今回はWSL2
でRust
をコンパイルするため、Native
を選択する必要はありません。
Windows
でRust
をコンパイルする場合もSDK Manager
からAndroid NDK
をインストールすればいいはずな気がするので、Native
を選ぶ必要はないと思います。
次に、Kotlin
でexternal 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
コードを書きましょう。
まずはそれ用のRust
プロジェクトを作ります。VSCode
を開きます。
cargo new android-rust-jni
cd android-rust-jni/
code .
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 }
次に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
とか言うのになる。
そしたらビルドします。
と、その前にAndroid
をターゲットに追加します。多分この4つ?
上からARM 64ビット
、ARM 32 ビット
、x64
、x86
。のハズ?
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
次に、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"
そしたら各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
みたいなのが出ればいいはず。
あともうちょっと!
.so
ファイルを回収します。target
フォルダ内に各 CPU
向けのフォルダがあるので開いて、release
の中のlib から始まる 拡張子 so
のファイルを取り出します。
WSL2
ならエクスプローラーから見れるのでラクラクです。
app
→src
→main
フォルダへ進み、jniLibs
フォルダを作成します。
また、その中に以下4つの名前でフォルダを作ってください。
ファイルツリー
の表示をProject
にした時に、この場所にフォルダが作られていれば大丈夫です。
そしたら今作った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
ファイルといった感じです。
カニさんに挨拶されたいことを忘れかけてた。もう見れますよ。
MainActivity.kt
で共有ライブラリ
をロードするようにすれば完了です。あとはexternal fun greeting()
を呼び出して使いましょう。
System.loadLibrary()
ですが、共有ライブラリの名前から先頭のlib
と.so
を消した名前を渡す必要があります。
ちなみに、Rust
とKotlin (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")
}
}
}
これで完了です。早速実行してみましょう。
カニさん出た。
試しに32ビット
のAndroid
端末でも動かしてみた。(Xperia Z3 Compact
)
動いてます。
画像送るほうが大変、、、Android ビーム (懐かしい)
を経由してQuickShare
。
x86_64
の.so
ファイルもちゃんと同梱したので、Windows
でエミュレーターを使ったときの開発でも特に問題なく実行できるはずです。
これが嫌な場合はUniFFI
を採用するべきです、少しはラクできるはず。
https://mozilla.github.io/uniffi-rs/latest/
こちらはRust
とKotlin
を繋ぐ部分を自動で作ってくれます(Kotlin バインディング
)。
しかも簡単。
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" ] }
詳しくは本家
https://mozilla.github.io/uniffi-rs/latest/tutorial/Prerequisites.html
src
の中のmain.rs
をlib.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();
}
今回は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();
}
https://mozilla.github.io/uniffi-rs/latest/tutorial/foreign_language_bindings.html
もうゴールは近い。
次はKotlin
でRust
コードを呼び出すバインディングを生成するために使うファイルを作ります。
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"
バインディングのために使う.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)コードが入ってるはず!
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"
これで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
Android Studio
でプロジェクトを作ってください。Jetpack Compose
のやつを選んでいいです。
Native
である必要もないです。ビルド済みRust
を使うので。
次に、ビルドした.so
ファイルを置くためのフォルダを作ります。
これも JNI AndroidStudio のプロジェクトに共有ライブラリを入れる と同じです。
詳しくはそっちに譲ります。ざっくり。スクショのとおりにフォルダを作ります。
そしたら今作った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
ファイルを置けばいいです。
と、その前に、
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
と同じ階層に貼り付ける。
package 名
を直してもいいはず。
JNA
はJNI
のときとは違い、パッケージ名が違ったとしても動くはず。
バインディングがあるので、対応する関数が見つかるはず。
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
でもカニさんが挨拶してくれました。良かった。
また、使いたい関数にに少し書き出すだけでKotlin (JNI)
で呼び出せることに気付きましたか?
JNI
のときはJava
のプリミティブ型jstring
とかを使う必要があったのに対し、UniFFI
は気にする必要がない。。
また、JNI
の長い関数名のルールもありません。が、ほんとはJNI
でも長い関数名を回避できる、、、
というわけで、今回はひたすら計算する処理をRust
で書いて、更に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
で書いて使うことにします。
そう思ってたのですが、、、、
実は単純な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]);
}
}
これをJNI
とUniFFI
で試して速いか確かめます。
そのためには、アセンブリコードを読む必要があるのですが、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
に聞いた。
Gemini
さん曰く、ARM NEON
(ARM の SIMD の名前)の場合は、アセンブリを見てq0
みたいな名前の付くレジスタを使っているらしい。
これを見るとldp q0
ってところでq0
レジスタを使ってるので確かにそうなのかもしれない?
cargo
はRUSTFLAGS="-C target-feature=+hogehoge"
でCPU
の機能が有効にできますが、逆にマイナス-hogehoge
すると無効にできるらしい。
というわけでこの場合のアセンブリコードもみてみます。SIMD 命令
、ARM アーキテクチャ
だとNEON
って名前なのでneon
。Intel
だとまた別。
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
で。
というわけで、今回は結構前にやったボーカルあり曲とカラオケ曲を使ってボーカルのみ曲を作る記事の、
ボーカル曲からカラオケ曲を引き算し、ボーカルだけにしてる計算部分だけをRust
で書きます。
https://takusan.negitoro.dev/posts/summer_vacation_music_vocal_only/
音は波なので、足したり引いたり出来ます。
これを応用して、aac
をPCM
にデコードして、引き算して、aac
にエンコードすれば出来るはず。
試す内容としてはJNI SIMD あり
、UniFFI SIMD あり
、Kotlin リリースビルド
、JNI SIMD無効
で試します。
あと、そもそもRust
やC++
を使ったネイティブコードが、本当に速いのか確かめるためにKotlin
も。あとSIMD
の有り無しを見たい。
使う端末はXperia 1 V
とGoogle Pixel 8 Pro
。
バイト配列の大きさは47290528 バイト (45MB)
。これを引き算していく。
というわけで作りました。
音声のエンコード、デコードは前回の記事のままなので、そちらを見るか、後述するGitHub
でソースコードを見てください。
新しいのほちい
Rust + JNI | Rust + UniFFI | Kotlin | Rust + JNI ( SIMD 未使用 ) |
---|---|---|---|
66 ms | 446 ms | 128 ms | 94 ms |
59 ms | 459 ms | 22 ms | 103 ms |
72 ms | 440 ms | 20 ms | 96 ms |
61 ms | 444 ms | 21 ms | 100 ms |
57 ms | 440 ms | 21 ms | 94 ms |
同期のSoC
よりも性能がちょっと低い。
Rust + JNI | Rust + UniFFI | Kotlin | Rust + JNI ( SIMD 未使用 ) |
---|---|---|---|
131 ms | 572 ms | 213 ms | 172 ms |
121 ms | 567 ms | 78 ms | 192 ms |
121 ms | 563 ms | 70 ms | 181 ms |
151 ms | 572 ms | 58 ms | 177 ms |
122 ms | 561 ms | 53 ms | 199 ms |
この規模だとKotlin (JVM)
が一番速かった。(というか題材にこれを選んだのが良くなかった?)
JVM
が初回を除いてなぜか速い。もっと大規模だと違うのかもしれない。
C++
で試せてないのであれですが、別にネイティブコードを書いても速くなるわけじゃないのか、、、
Android の JVM
すごい。
UniFFI
はJava Native Access (JNA)
都合で遅くなってる?
JNI
独自の型(jstring
)とかを使わずに呼び出せるので、それで時間がかかってるのかも?
.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:~$
今回作った.so
が軒並みUNALIGNED
。。。
Java / Kotlin
で十分に速かった。
Android NDK
を入れて、Java Native Interface
のバインディングを書いて、各 CPU アーキテクチャ
向けにコンパイルして、、、って面倒だしクラッシュした時に直せる自信がない。
GitHub
のコードでNDK
が必要って言われた瞬間に目を逸らしてる(偏見)
そーいえば昔というか、32bit
時代の、ネイティブコードを使っててarm64-v8a
(ARM 64bit
)が入ってないアプリ、すでに動かなくなってそう。
Google Tensor
はG2 ( Pixel 7 )
から32 ビット
ネイティブコードのサポート無し、
Snapdragon
も8 Gen 3
で32 ビット
ネイティブコードのサポートが無くなった。
もっとも、targetSdk
が古すぎてインストールが弾かれてしまいそうなので、こんなことにはならないと思います、が。