背景
- C/C++ がメインの Android アプリを開発している(GLES/Vulkan グラフィックス, レイトレーシング, 画像処理, 機械学習など)
- 実機で動かして実際のパフォーマンスを知る必要がある, エミュレーターでは実行が対応していない or 実行が遅すぎる
- PC で NDK でクロスコンパイル -> apk 転送 -> 実行がめんどい
- グラフィックや機械学習系だと lldb デバッグはなかなかフィットしないし, デバッグとしては画像などを出したい
その上で, C/C++ メインアプリに UI や, Android のカメラ画像などの取得が欲しい(カメラ画像に対するリアルタイム StyleTransfer アプリ開発ととか)
- imgui や SDL で GUI だとスマホの画面だと操作しずらい. カメラからの画像とか使いたいときに連携がめんどい
- Kotlin + Jetpack UI でもいいが, iOS, PC とも統一できて, hot reload ができ, 最近(?) native module のロードに対応した Flutter を使いたい.
- Flutter Desktop + native code アプリを作る準備メモ(2020 年 1 月 7 日時点) https://qiita.com/syoyo/items/7e2c17261089e0027c30
- これにより, ある程度 PC での開発とコードを合わせたりができる
Termux で native コンパイル(clang)して, その .so を Flutter(dart ffi)で呼ぶという仕組みを作って, 開発の効率化をはかってみます.
Known issue
.so のリロードで問題がありました. 手間をかければいけますが... 詳細は後述します.
必要なもの
- non-rooted な Android(つまり普通に市販されている Android スマホ)
- Cortex-a72 以降あたりのアーキテクチャで out-of-order 実行でメモリ 4GB or more で性能のよいスマホかタブレット(最近はノート PC よりもスマホのほうが高性能ですよね)
- 今回は Pixel4(Snapdragon 855) を使いました.
- Termux + clang など開発環境を pkg install
- 2020/01/05 時点では clang 7 でした.
- PC 開発環境
- Android 実機(スマホ)とは usb でつなげている
前提
Flutter のアプリは, 実機では arm64 で動いているものとします.
(Flutter デフォルトでは, x86 32bit 64bit, arm 32bit 64bit の四種類全部対応の apk を作っている)
Pixel4(Android 10) ではデフォルト arm64 のようでした.
(Flutter(Dart)上で 32bit か 64bit か確認する API はあるのかな?)
を参考に, Flutter アプリ側で Cmake で native module をビルドしておく仕組みを作っておきます.
(その後, アプリをいったん起動したのち実行時に .so を入れ替えるため)
C/C++ コンパイル
Termux 上で .so をつくる
pkg で入る clang を使って, -fPIC -c -std=c++14 -stdlib=c++
あたりでコンパイルして, .so を作ります.
(clang 7 では C++17 も対応しています)
Termux での clang では ANDROID
などは定義されませんので, 将来的にアプリを APK で publish する場合, Android 依存のものについてはいくらか注意してコードを書く必要があります.
たとえば, printf や pthread(bionic では pthread cacnel など一部実装されていない) やファイルアクセス周りなど(Android では APK assert のファイル書き込みなどは C++ レイヤーではできない. NDK clang では -std=c++17
でも std::filesytem は対応していない)
リリース間際になってきたら, PC で ndk-build してコンパイル確認したほうがいいかもしれません.
Android で .so を Flutter app に転送するしくみをつくる
Optional: Flutter アプリの storage 設定
storage にアクセスするために, STORAGE permission が必要かもしれません.
デフォルトでは提供されていないようで, permission_handler などの plugin を使う必要があります.
このあたりは web にいろいろ資料がありますので, それらを参考にして設定しておきましょう.
Termux で /sdcard にアクセスできるようにする
/sdcard
or /storage/emulated/0
(基本どちらもおなじ) に .so を配置するため,
Termux で storage アクセスのセットアップをしておきます.
セットアップしますと, ~/storage
が /storage/emulatated/0
への symlink として生成されます.
Android での .so のアクセスについて
Using Dynamic linked libraries in android
https://hero.handmade.network/forums/code-discussion/t/750-using_dynamic_linked_libraries_in_android
Android 7 からセキュリティが厳しくなり, .so は /data/data/<app>/
(アプリ固有のストレージ領域)からしか dlopen できません(もしくは, マルチユーザーを考慮すると /data/user/<uid>/<appid>
. 実態はどちらもおなじようです. Flutter の path_provider で, アプリのデータ領域を取得すると /data/user/<uid>
を返します).
Termux でコンパイルした .so を /sdcard
などに配置して, そこから dlopen("/sdcard/mylib.so");
などとするとエラーになります(ファイルは storage permission を付ければ open はできるが, symbol の取得でエラーになる). /data/local/tmp
もダメでした
adb run-as で .so をアプリストレージに転送
Android: /data/data配下にadb push
http://yuki312.blogspot.com/2019/01/android-datadataadb-push.html
PC から, 実機に adb ログインして run-as で Flutter app の id になればいけます.
(Flutter app を debug モードでビルドしている必要があるかと思います)
いったん最初に Flutter アプリを Android で動かし, /data/data/<appid>
を作っておく必要があると思います.
<appid>
は AndroidManifest.xml でわかります.(Flutter の場合は android/app/src/main/AndroidManifest.xml
にあります).
adb exec-in
で, PC から /data/data
に直接ファイル転送はうまくいきませんでした(Android 10). exec-in は run-as と組み合わせての実行ができませんでした(exec-out はできる).
.so 転送の手順
- Termux で .so をビルドする.
- Termux で .so を
/sdcard/
にコピーする(/data/local/tmp
は後述の cp/cat が Pixel4 ではうまくいかなかった) - PC から adb shell して adb run-as する
- adb の shell で
cat /sdcard/mylib.so > /data/data/<appid>/files/
orcp /sdcard/mylib.so /data/data/<appid>/files/
でfiles
ディレクトリ(files
フォルダは必須ではないが, デフォルトで用意されているので使っています)
ちょっとめんどいですね. 何かしらスクリプトとか(rsync or file watch スクリプトとか), vscode 拡張とか書けば効率化できるでしょうか?
adb exec-in で run-as で PC から /data/data/<appid>/
に直接ファイル転送もいけるはず?
C++ コードが安定してきたら, 普通の Android C++ 開発のように, PC 側で cross-compile というのも手かもしれません.
libc/libm, libc++
Termux で printf や sin() など, libc/libm 関数を使うと, Flutter(Android bionic)で動かすと ABI が合わないのかクラッシュしますので注意です.
Gradle cmake では, C++ STL はデフォルトでは c++-static です.
Termux でコンパイルした .so は libc++_shared.so
に依存するので,
gradle 設定で arguments "-DANDROID_STL=c++_shared"
を指定し, c++_shared にして libc++_shared.so が Flutter の apk に含まれるようにしておきます.
(Termux 側で頑張って STL static リンクするのもできるのかな?...(試したかぎりではいくつか .a などが足りなかった))
android/app/build.gradle
android {
compileSdkVersion 28
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.flutter_native_vulkan_experiment"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared"
}
}
}
...
ちなみに, これにより Apk に含まれる arm64-v8a の libc++_shared.so
は 300 kb くらいとそんなにファイルサイズは大きくありませんでした.
NanoSTL
std::vector は動くようですが, いろいろ C++ コード書いていったら Termux(Linux) と Flutter(Android) で STL の ABI とかが合わないことになるかもしれません.
使っている STL が vector, string, map くらいであったら, NanoSTL https://github.com/lighttransport/nanostl の利用も検討してみましょう. libm の実装も少しあるよ(ただし近似関数での実装. それなりに精度はあるとおもうが correctly rounded で完全(i.e, 0.5 ulp 以下)ではない)
Flutter(Dart) 側のコード
DynamiLibrary.open("/data/data/<appid>/files/mylib.so")
という感じで .so を指定します.
.so リロードやっぱり簡単にはできない
しかし, Android SELinux がらみで, .so 差し替えて flutter hot reload or C レイヤーで dlclose
, dlopen
し直すとセグフォしました.
F/libc (27288): Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x9d0 in tid 27330 (Thread-2), pid 27288 (lkan_experiment)
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/flame/flame:10/QQ1B.200105.004/6031802:user/release-keys'
Revision: 'MP1.0'
ABI: 'arm64'
Linux(Android) 側で, アプリが実行中に保持している .so と, ファイルが挿し変わっていたらエラーになるようです.
Flutter/Dart -> [DynamicLibrary.open] -> stub.so -> [dlopen] -> module.so
という二段構えも試しましたが(stub.so
はアプリ実行中に挿し変わらない), やはり stub.so での dlopen でエラーになりました.
解決策としては,
- 関数を呼び終わったあとに stub.so から, module.so を毎回 dlclose する
- .so を書き換えるときに, まずは Flutter に通知を出して, dlclose() を読んでから, .so ファイル差し替え
毎回 dlclose は対応自体は楽ですが, グラフィックスで毎フレーム呼び出す関数などでは dlopen/dlclose のコストがかかりそうです.
必要なときだけ dlclose したいですが, 残念なことに, Flutter(Dart) では, 現状 DynamicLibrary.close をサポートしていません.
いちおう, dlclose
のシンボル自体を lookup として使う方法もあります. ただ, github issue のやりとりを考慮すると, これはいずれは NG になるかもしれません.
多段 .so ロード
.so 内で dlopen, dlclose する方法です
- stub.so
- 各種エントリポイント
- module.so を unload(dlclose) する機能を追加
.so hot reload するには,
- C++ コード編集し, module.so ビルドする
- Flutter へ Unload させるコマンドを出す(Flutter 側で hot reload 検知 https://stackoverflow.com/questions/55281077/how-to-detect-hot-reload-inside-the-code-of-a-flutter-app, or JSON-RPC あたりで通信?)
- Flutter が Unload メソッド呼ぶ
- adb で module.so 転送
- Flutter hot reload して module.so を再度読み込み
という仕組みを作る必要があります.
dlclose を Flutter(Dart)で行う.
.so ロード中に, .so を書き換え dlclose を呼ぶと seg fault するので,
data 領域にロックファイルを作り, ダブルバッファリングのように .so を切り替え(module0.so
, module1.so
) する方法があります.
- C++ コード編集し, module.so ビルドする
- adb exec-out あたりでロックファイルを読む.
-
moduleN.so
にリネームして Flutter data フォルダに .so 転送. - Flutter 側で hot reload 検知 https://stackoverflow.com/questions/55281077/how-to-detect-hot-reload-inside-the-code-of-a-flutter-app 時に unload(dlclose) する
- Flutter が, .so ロードするファイルを, ロックファイルを読み込んでどの .so を読むか決める
これですと, Flutter の hot reload の仕組みの中で対応できるので, なにかアプリと通信する必要はありません.
その他
Android 10 では, Vulkan validation layers(.so)を別の APK からロードすることができます.
この方法でもしかしたら別 APK の .so をロードできるかもしれません...
(現状ではシステム設定の gpu_debug 限定っぽい?)
感想
とりあえず Termux 連携 + .so リロードできる仕組みを作りましたが, adb で .so 転送で PC を介する必要があったり, printf 使えないとか STL 絡みの不都合が将来的にあるかも, というのを考えると, PC で Android cross compile してビルド + .so 転送のほうが開発楽かもという気がしてきました...
参考までに, PC(Linux) で Cmake で NDK toolchain 使って C/C++ アプリをコンパイルする設定サンプルです.
.so リロードも現状手間がかかることがわかりました.
ここまでやっておいてなんですが, PC で C/C++ 開発 + API を先にある程度決めておき, あとはデータを JSON-RPC などでやりとりなどして, なるべく .so リロードの必要性をなくすほうがいいかもしれません.
VSCode 経由では Flutter アプリの再起動は 15 秒くらいかかりますが,
Dart コードに変更がなければ, コマンドラインから flutter run --machine
だと 4~5 秒くらいで立ち上げできます.
(build.gradle で abiFilter で arm64-v8a
だけにしておくとより高速に apk ビルドできる)
.cc の watch
PC 側で作業するとき, Flutter の hot-reload では, native code(.cc ファイル) までは watch してくれません.
Linux 限定であれば entr(1) http://eradman.com/entrproject/ (Ubuntu 18.04 なら apt で入ります), クロスプラットフォーム重視なら node.js + file watch するアプリで, .cc が変更されたら build & adb run-as して .so 転送するしくみを構築するとよいかなと思います.
Flutter に hot reload リクエストを送るのは, VSCode でダミーの .dart を touch するのが楽です.
手動で Flutter Android アプリに hot reload 命令を送るメモ(2020 年 1 月 14 日時点)
https://qiita.com/syoyo/items/37d5f22689bb50217858
TODO
-
.so を fread or mmap して, elf バイナリを解析して直接エントリポイント叩く方法も考えてみる(
dlopen
しない) -
non-rooted 実機だとセキュリティ周りでいろいろ制限あり面倒なので, Vulkan 1.1 対応の開発ボードで ASOP で開発環境整えたい
- 既存のもとして hikey960 が Android 標準(?)の開発ボードとしてあるが, GPU の Vulkan 対応が 1.0 まで. あとは Mali なので GPU 遅い
-
Termux で動く adb が使えるかためす https://github.com/MasterDevX/Termux-ADB
- とりあえず試してみたところ, 自分自身(localhost:5555) に adb すると, ポート関連でエラーになった...
- C/C++ のコードが大きいと, termux ではコンパイル遅かったりメモリ足らずなどでコンパイルがこけたりするかもなので(Pixel4 だとメモリ 6GB ありそれなりに快適であった), Jetson AGX などパワフルな Arm 開発ボード で .so をクロスコンパイルし, adb or JSON-RPC あたりでネットワーク転送して .so リロードの仕組みを作る
- termux への ssh ログインは, wifi 接続だと termux が不安定な気がするので, USB イーサネットを使ってみる
- 優秀な Flutter desktop + C++ 若人さまが, 人類史上最速で優秀な Flutter Android + C++ 若人さまへと昇華なされるスキームを確立する旅に出たい.