(経緯)
Mind for Android に必須のパッケージローダ(MindPkgLoader)アプリの中のサービスが使用中にたまに停止したり再起動する問題があり長いこと調べていたが根本的に解決したのでいざ新版をGoogle Playに登録しようとしたら別の理由でGooglePlayへの登録が拒否された。8月から64bit対応でないコードを含むアプリを登録できなくなったからである。(誤解のないように補足すると、既登録のGooglePlay上の64bit非対応アプリはダウンロードは可能で問題なく使うことができるが、GooglePlay側のルール変更としてアプリの新規登録とアップデートが制限されるいうもの)
アプリの64bit対応とは、正確にはC言語で書いた部分(Mindで言うとカーネルと呼ぶ部分)を64bit化するという意味である。ちなみにMindコンパイラ自体は64bit対応の必要はない。AndroidのアプリのパッケージにはMindコンパイラは含まれないからだ。
(方針1)
Windows版のMindに比べ、Mind for Android は64bit対応に違いが出る。
前者では32bit版と64bit版で別のパッケージを用意することができるが後者では開発環境はクラウド上なので一つであり、コンパイル結果を一つの apkファイルとして生成するので、利用者から見ると開発環境をことさら64bitで動作させるようなモードだとか利用上の認識は無い(となるはず)。
生成されたアプリをインストールする先の端末が64bitだったら64bitで動作するだけであり、コンパイル段階ではアプリを誰がどの端末(32bit/64bit)で利用するかはコンパイル時には予想できない。
先にAndroidの標準言語のJavaの話をすると、Javaソース上では32bit/64bitの区別は無いし、それをコンパイルした結果のクラスファイル(.classとか.dex)も1種類しか生成されない。Javaの仮想マシン(インタプリタ)がその端末向けに64bitでセッティングされていれば64bitで仮想マシンが動作する・・というもののようだ。端末の「設定」メニューからアプリの情報をいくら眺めても32bit/64bitの別が表示されないのはそのためである。
次にMindだが、「Mコード」と呼ぶ中間コードを生成するタイプのコンパイラで、この点は「バイトコード」(今現在はそう呼ばないかも‥)と呼ばれる中間コードを生成する Javaと似ている。
Mコードは当然だがCPU命令は含まず抽象化されているので64bit向けに特別に変更の必要は無い・・というよりも、先に書いたように、アプリの生成段階では32bit/64bitのどちらの端末で動作させられるのかが不明であるため、基本的な考え方としてはこの中間コードを64bit動作のためにことさら変更できない。
したがって今回の64bit対応はMコード自体には手を付けず、そのMコードを実行する役割の部分(Mindではディスパッチャとかカーネルと呼んでいる)の改良で対応することになる。
Cで書かれているディスパッチャ(カーネル)は以下の方針で改造することになる。
まず、Androidの規約で、C記述部は32bitコードと64bitコードの両方を生成する必要がある。make時にCPUのアーキテクチャを指定するオプションがあり、32bitと64bitを併記することで自動的に32bitのコード、64bitのコードが両方生成される。正確には .so という拡張子を持つ共有ライブラリが複数生成される。こんな感じ、
(例)CPU指定「armeabi-v7a + arm64-v8a」の場合
/ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\
○○.apk
└lib/
├armeabi-v7a/
| libJniTest64-jni.so
└arm64-v8a/
libJniTest64-jni.so
↑Cのmake時に2種類が生成される
\______________________/
上記で先にある .so が32bit用のオブジェクト(共有ライブラリ)、次の .so が64bit用のオブジェクトである。両者はファイル名は同じだが格納フォルダが異なる。
この2種類の共有ライブラリはそのまま .apk ファイル内に埋め込まれ、端末上でアプリが起動されたとき・・正確には、JavaのプログラムからMindの共有ライブラリをロードするメソッドを実行したととき・・自動判別で32bit用または64bit用のどちらかの共有ライブラリがロードされる。32bitと64bitの共有ライブラリのいずれか選択的にロードするようなコードを明に書く必要はなくOSがやってくれるので少しは楽だが、他の改造の大変さから見ればたいした話ではない。
実はこの仕組みはCPUの違いについても同様で、ARM系CPUとIntel系CPUについても、すべてC記述部のmake時にCPUアーキテクチャをオプションで併記しておけば全部生成される。32bit/64bit、ARM/Intelすべて用意するなら4個の共有ライブラを抱える必要があるが、現在のMind for AndroidではIntel系は出力しておらずARM系端末のみをサポートしているので先のように全部で2種類となる。
次に、カーネル内部の改造について。
先に書いたように、32bit/64bitの共有ライブラリはmake段階で別生成であるため、Cのコードとして32bit処理と64bit処理を動的に分岐する必要はなく条件コンパイルで使い分けることができる。この点はありがたい。ちなみに、Androidうんぬん以前に、C言語で一般的に32bitコンパイル環境と64bit環境を識別する変数があるはずだが・・と調べたら"_LP64"という変数があるらしく、AndroidのJNIでもこれが参照できた。
カーネル内で変更が必要になる部分は、一般論としてはデータ幅とポインタ情報(=アドレス)のビット数であるが、今回は先に書いたような理由でデータスタックの幅は64bit(文字列情報で言うとアドレス部が32bitでカウント部が32bit)という現状から変えない。大きな変更が必要になるのはポインタを64bitで扱うための処理となる。
Mindのスタック操作のC側の処理だが現在はたとえば次のようなマクロを用意している。
/ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\
#define PUSH_L(value) *(--DstackPointer)=(LONG)(value)
\_____________________/
上にある PUSH_L マクロは引数で与えた32ビット整数をデータスタック(仮想スタック)にPUSHする。
まず「DstackPointer」はスタックポインタの役割の大域変数だが引用側はこのままで良いはず。
次に「(LONG)(value)」でのキャスト LONG の部分、これはマクロで、
typedef long LONG;
のように定義されているが、ここを
typedef int LONG;
とすれば良いようだ(intは どちらの環境でも4バイト長)。このほかマクロは山のように数が多いがこんな感じで行けそうだ。
次に問題の文字列操作、
/ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄\
#define PUSH_S(addr,count) PUSH_A(addr),PUSH_C(count)
\_____________________/
上記の PUSH_S というマクロは文字列情報(アドレス+カウントで合計64bit)をデータスタックにプッシュするものである。マクロの右辺ではさらに下位マクロを使い、まずアドレスをプッシュ(32bit)し、ついでカウントをプッシュ(32bit)している。
この「アドレス」が問題。
先に書いたような理由で64bit環境であっても同じデータ幅で扱いたいため、Mindで言う「アドレス」は32bitのままで行きたい。さもないと、文字列情報は128bit長となり大改造になるだけでなく、32bitと64bitのどちらでも同じMコードが走るようにするのも困難になる。
昔、Intel 386 のあたりだったか、32bitのCPUで16bitのアプリケーションを走らせるためにセグメントとかセレクタと呼ばれる機構を使って実メモリにマッピンングする手法が考え出され、今もWIndowsやLinuxで使われているが、それと似たようなことをすればMindでも32bitのアドレス扱いを維持できそうだ。ただ、CPUやOSはMind特有のサービスなどやってくれないので自前で実装することになる。
整理すると、MindのC記述部(カーネル)は64bitのmake時には当然64bitのコードとして生成する。しかしMindのアドレス操作(たとえば文字列操作語)ではアドレスを仮想32bitで扱い、スタックに文字列情報を積む際もアドレス部は32bitとする。そしてメモリに実際にアクセスする段階で実アドレスとの変換をおこなうような変換ルーチンを自前で持つという話である。当然だがアドレス変換のオーバーヘッドが増えるが、もともとJavaに比べてMindのプログラムははるかに速く動くことと、CはMindよりもさらに速いわけだからあまり気にしないことにする。
もう一つ言えば、Mindの2種のスタックポインタは64bit環境では素直に実64bitにしておけば、スタック操作・整数演算・実数演算などは速度は落ちず、アドレス変換が入るのは文字列操作やファイルやソケットなどのI/Oに限られ、スタック上に演算対象のデータが置かれることが多いMindではその点でもあまり速度低下にはならないのではと思っている。
(続く)