Android NdkBinderについて
Android NdkBinderについて
※本稿は2019年6月に書いて放置していたものを今になって発掘して公開しているので、もしかしたら古い記述がまだ残っているかもしれない(一応ひと通り読み直して確認したつもり)。何かおかしそうなところがあったらコメント等で教えてください。
NdkBinderとは、Androidのサービスでクライアントと接続するためのBinderをAndroid NDKを使ってCで実装するためのAPIだ。今回はこのAPIを紹介していこうと思う。
Android API Level 29で初めて追加されたライブラリで、Android NDK r19には存在しない。r20で追加されたようだ。libndk_binder.soにリンクするにはcompileSdkVersion 29にする必要がある(ndk_binderの利用が必須ならminSdkVersion 29である)。2019年6月の本稿執筆時点でまだ正式版がリリースされていないAndroid Qの最新機能ということになる。
Binderのおさらい
Binderは、android.app.Serviceクラスで実装されるサービスと、そこに繋がるクライアントを接続するチャンネルとして機能するものだ。クライアントはサービスと同じアプリケーションにあるかもしれないし、別のアプリケーションにあるかもしれない。
Binderは一般的にはServiceのonBind()を実装する時にインスタンス化されるもので、多くの場合は
(1)Binderクラスを拡張するか、(2)Messageクラスを使うか、(3)AIDLツールを使用して生成されたクラスを実装してそのインスタンスを返す。なお、(1)はインプロセスでクライアントとサーバが動作していなければならない。
これらの方式では、いずれもJavaのインスタンスがJavaのコードとして実装されて返されている。サービスとして提供するメソッドの実装も呼び出しもJavaで行われることが暗黙的な前提となっている。これでは厳しい処理速度が求められるクライアント・サービスを実装することができない。
しかし、AndroidのBinder IPCは、アプリケーション同士のやり取りを実現するためにかなり低レベルで実装され最適化されており、低レイヤーの部分だけでクライアント/サーバーの仕組みを実現できれば、アプリケーションでも高度にリアルタイムな処理も可能になるかもしれない。NDK Binderは、こういった期待に答えるものである。
参考サイト
2019年6月の本稿執筆時点でここしか見当たらなかった。(と当時書いて今もざっくり探してやっぱり見当たらないようだったのだけど、もしかしたらちゃんと探せばあるかもしれない。)
Binder jobjectの生成
Androidのサービスフレームワークは、AndroidManifest.xmlを通じてJavaレベルで指定されている必要があり、少なくともBinderを通じてonBind()やonServiceConnected()で接続が完了するまでは、Javaのレベルで接続可能になっている必要がある。NdkBinderには、JNIを通じてJavaコードとやり取りできる関数が用意されている。
AIBinder_fromJavaBinder(JNIEnv *env, jobject binder)-
AIBinder_toJavaBinder(JNIEnv *env, AIBinder *binder)を使えば、ServiceのonBind()で返すBinderオブジェクトをNDKで実装して返すことができる
筆者が2019年6月の本稿執筆のためにGoogleでAIBinder_toJavaBinderをGoogleで検索してみたところ、NDKのリファレンスドキュメント以外は全く引っかからない状況だった。現状ほぼ誰も調べていないAPIであるようだ。
AIBinderオブジェクトのネイティブコードからの生成
AIBinder_toJavaBinder()でNDKの生成したオブジェクトをjobjectに変換するには、AIBinderのインスタンスを生成する必要がある。このためには、まずAIBinder_Classという「Binderのクラス」を表すインスタンスを生成して、そのクラスのインスタンスをAIBinderとして生成する。
AIBinder_ClassのオブジェクトはAIBinder_Class_define()で生成できる。
AIBinder_Class_define(
const char *interfaceDescriptor,
AIBinder_Class_onCreate onCreate,
AIBinder_Class_onDestroy onDestroy,
AIBinder_Class_onTransact onTransact)
interfaceDescriptor引数の文字列にどのような内容が想定されるのかは定かではないが、Android CTSに含まれるndkbinderのテストでは "this-is-arbitrary" のような雑な文字列が指定されている。アプリケーションが一意に識別できれば十分ではないかと思われる。
AIBinder_Class_onXxx型の引数はどれも関数ポインタで、それぞれonCreate()、onDestroy()、onTransact()の際に呼び出される。
このAIBinder_ClassオブジェクトをもとにAIBinder_new()でAIBinderオブジェクトを生成する。
AIBinder* AIBinder_new(const AIBinder_Class *clazz, void *args)
AIBinder構造体を引数に取る関数には、JavaのBinderクラスのメソッドに対応するものが多数ある。
AParcel
JavaのParcelに対応するのはAParcel構造体だ。これ自体はopaque pointerとして使うことが想定されていて、メンバー定義は公開されていない。
NdkBinderのAPIにはAParcel_で始まる関数が多数含まれており、Parcelの読み書き等の操作はこれらを使用して行われる。
aidlツールのC++サポート
JavaでServiceとBinderを利用したメッセージングについては、aidlはAndroid SDKのbuild-toolsに含まれるaidlツールを使用して、AIDLによるインターフェース定義からJavaコードをクライアントとサーバ用に自動生成できる。これと同じことが、build-tools 29.0.0に含まれるaidlから、C++についても可能になった。
おそらく、これを利用せずに手作業でNdkBinderを使用するのは膨大な肉体労働になるので、aidlで自動生成するようにしたほうが良い。
aidlツールのC++サポートについては、このドキュメントである程度把握することができる。
( https://android.googlesource.com/platform/system/tools/aidl/+/master/docs/aidl-cpp.md )
2020年10月現在は、AOSPのドキュメンテーションページが正となったようだ。
ただ、すべてを鵜呑みにしてはいけない。というのは、このaidlツールにはC++コード生成モードが2つあるからだ。ひとつはコマンドラインヘルプにも出現する--lang=cppというオプションを指定すると生成できるC++コードで、これはNdkBinder APIと互換性がない。NdkBinderと互換性があるのは、コマンドラインに登場しない--lang=ndkというオプションで生成されるC++コードである。
--lang=cppで生成されるコードは、おそらくAOSP内部で使用されるコードを生成するために使われており、Android NDKの公開APIに含まれていないものが多数使用されている(Googleプロジェクトでよく使用されているらしいStringPointer.hなど)。このオプションを使用する場合、通常アプリケーション開発者がJavaコード生成のために記述きたようなAIDLファイルでは、期待通りにコードが生成されないようだ(たとえばListなどが処理してもらえない)。
NdkBinderのAPIを使用している限り、有用なオプションは--lang=ndkのみである。このオプションはAOSPのaidlのソースを見れば存在が確認できる。
--lang=ndkオプションを指定する場合、cppソースの生成先を-oで指定するのと同様、ヘッダファイルを生成する-hオプションも必須になる。
ちなみに、build-tools r29.0.0から利用可能になっていると書いたが、r29.0.0ではなくr30.0.0以降のaidl(.exe)を使うべきだ。というのは、Android API Level 30でSharedRefBaseクラスまわりのAPIが変更されており、派生クラスのインスタンスの生成はSharedRefBase::make<T>()で行われるようになり、targetSdkVersionを30以上にしている場合はnew()による生成はできなくなった(詳しくはandroid/ndk/issues/1307を参照)。
Discussion