LoginSignup
10
3

More than 3 years have passed since last update.

Termux + Flutter + dart ffi で .so リロード対応し Android C/C++ + GUI アプリ開発を効率化するメモ

Last updated at Posted at 2020-01-13

背景

  • 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 を使いたい.

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 もダメでした :cry:

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/ or cp /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 するには,

という仕組みを作る必要があります.

dlclose を Flutter(Dart)で行う.

.so ロード中に, .so を書き換え dlclose を呼ぶと seg fault するので,
data 領域にロックファイルを作り, ダブルバッファリングのように .so を切り替え(module0.so, module1.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++ 若人さまへと昇華なされるスキームを確立する旅に出たい.
10
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
3