Palm Programmer's Laboratory
【C++】 ARMlet と 68K におけるコード共有
[開発情報]
概要
このページでは、ARMlet と 68K におけるソースコード共有について説明します。
説明
Palm OS 5 向けに ARMlet を実装する場合、それ以前のデバイスもサポートするのであれば同じ処理を行うコードを 68K 用にも実装しなければなりません。また、Palm OS Simulator で動作するコードを用意する場合、さらに Windows DLL を追加で用意する必要があります。ARMlet 化するロジックが少量の場合には問題になりませんが、コード量が増加すると同期の問題が発生します。このドキュメントでは、ソースコードレベルでの共有を行うことで管理を容易にするデザインと実装を紹介します。
まず、「【C++】ARMlet 対応に関するデザインメモ 」 で紹介している方法で ARMlet 化する処理を抽象化します。ここではクラス Foo で対象のロジックを抽象化し、派生クラスとして Foo68K、FooARM、FooPalmSim の3クラスを用意しました。
単純な実装では、各派生クラスで Foo が抽象化する処理を実装することになります。Foo68K ではクラス内に直接、FooARM と FooPalmSim ではそれぞれが呼び出す ARMlet や Windows DLL 関数にそれらの処理を実装することになるでしょう。
しかし、カプセル化する処理が大規模になるほどこれら3つの処理が 「 同じ動作をする 」 ことを保証するのは面倒になっていきます。そこで、実際の処理をするクラスを独立させて共有することを考えます。このクラスを FooImp と名づけましょう。実際にどの環境で動作するかということを度外視すれば、ひとまずは以下のようなイメージになります。
しかし、話はそれほど簡単ではありません。なぜなら、FooImp は 68K、ARM、Win32 という3つの環境で動作しなければならないからです。おさらいを兼ねて順番に見ていきましょう。まずは Foo68K です。これは簡単ですね。Foo68K も FooImp も 68K 環境内で動作します。処理の委譲は直接的なメソッド呼び出しで事足ります。
では、次に FooARM ではどうでしょうか。これは少しばかりやっかいです。FooARM は実際の処理( すなわち FooImp )を ARM コードで動作させなければなりません。FooARM から( PceNativeCall 経由で )呼び出せるのは ARMlet のエントリポイントである ARMlet_Main 関数だけで、そこから FooImp を呼び出すことになります。また、この呼び出しは 68K - ARM 境界をまたぐわけですから、パラメータの endian をひっくり返す作業もしなくてはなりません。さらに、FooImp から Palm OS API を呼び出す必要がある場合はそれも考慮に入れる必要があります。
最後に FooPalmSim です。図をわざと FooARM の場合に似せて書いてあるのでお分かりだと思いますが、注意しなければならない点は FooARM の場合と同じです。すなわち、DLL 関数( ここでは DLLFunc_Foo と名付けます )を経由する必要があること、endian をひっくり返す必要があること、そして Palm OS API の呼び出しを考慮する必要があるということです。
では、ここまでお絵かきしてきた内容を1つにまとめてみましょう。スペースの問題があるので少々苦しいですが、以下のようになります。
では、この図をベースとして FooImp のインタフェイスと実装について考えていきます。1つのソースコードで3つの環境をカバーするため、以下のポイントを確認する必要があります。
- endian の変換を FooImp 自身で行うわけにはいかない。
- 統一的なインタフェイスで Palm OS API を呼び出す必要がある。
さて、では endian の変換から見ていきましょう。ARM と Palm OS Simulator の場合は FooImp に渡すパラメータをすべて処理する必要があるわけですが、先にも書いたとおり、FooImp でこれを行うわけにはいきません。となれば FooARM か ARMlet_Main ( あるいは FooPalmSim か DLLFunc_Foo )で処理する必要があります。
...が、ちょっと待ってください。その前に、ARMlet 呼び出しではパラメータとして ARMlet 側に渡せるのは 32ビット値1つだけであることを思い出しましょう。そのために、一般的な ARMlet 呼び出しのデザインでは構造体にパラメータを収め、そのポインタだけを渡すのでしたね。ここではパラメータ数に特定の前提をしないため、一般的なデザインとしてパラメータを集約するための構造体(≒クラス)を導入することにします。これを FooParam としましょう。
となると、パラメータに関する情報は FooParam が責任を持つことになるわけですから、endian の変換も FooParam に担当させるのがいいでしょう。これにより、FooImp と FooParam の関係が以下のようになります。
FooParam は保有するパラメータの endian を変換するための ReverseEndian メソッドを提供し、FooImp はこの FooParam のポインタをパラメータとして受け取ります。図が誤解を招きそうなので補足しておきますが、FooImp 自身が FooParam::ReverseEndian を呼び出すわけではありません。
ところで、FooParam オブジェクトを用意するのは誰の責任でしょうか。そもそも Foo のインタフェイスに FooParam を指定する必要はない(もちろんそうすることも可能ですが)わけですから、これは Foo 派生クラスの仕事でしょう。つまり、Foo のクライアントから受け取ったパラメータ群を FooParam オブジェクトに収め、そのアドレスを指定して FooImp を呼び出すわけです。その際、FooARM ならば FooParam は 68K-ARM 境界をまたぐことになりますし、FooPalmSim ならば FooParam は Windows DLL に渡されることになります。
話を戻します。endian の変換を誰がやるべきかですが、処理そのものは FooParam::ReverseEndian に実装されたわけですから、後は誰が呼び出すかだけの問題です。これに関しては Foo 派生クラスでも ARMlet_Main ( あるいは DLLFunc_Foo )のどちらでもいいでしょう。ここではひとまず ARMlet_Main で呼び出すということにしておきます。
さて、endian の話が片付いたので次は Palm OS API 呼び出しの問題です。FooImp は同じソースコードで 68K、ARM、Palm OS Simulator のどの環境でも動作しなければなりません。68K の世界で Palm OS API を呼び出すには、普通に API をコールするだけですが、ARM と Palm OS Simulator ではエントリポイントに渡される Call68KFuncType 型の関数ポインタである call68KFuncP を経由してコールバックしなければなりません。このような異なる API 呼び出し方式を統一的に扱う方法を考える必要があります。
ここで、「 【C/C++】 ARMlet での Palm OS API の呼出し方 」 の最後で紹介した PalmOSAPI クラスを思い出してください。このクラスはコールバックに必要な emulStateP と call68KFuncP をカプセル化し、Palm OS API と同じ関数インタフェイスを提供することでコールバックによる API 呼び出しを簡略化していました。仮に ARMlet や Windows DLL でこの方式を利用するのであれば、インタフェイスレベルで互換性のある PalmOSAPI クラスを 68K 用に提供することにより、FooImp の実装を PalmOSAPI クラスへの依存に統一することができます。API をクラスメソッドでラップする呼び出しコストは、メソッドを inline 指定することで事実上なしにすることができます。
...ここまでで基本的なデザインは出来上がりました。あとは ARMlet_Main と DLLFunc_Foo 関数です。これらは基本的に同じことをするもののはずです。実際、それぞれのソースコードは以下のようになります( DLL に関しては Visual C++ 6.0 前提です )。これなら単純なプリプロセッサマクロをいくつか定義するだけでソースコードを共有することができるでしょう。
extern "C"{ UInt32 ARMlet_Main( const void* pEmulState, void* pUserData68K, Call68KFuncType* pCall68KFunc ) { PalmOSAPI api( pEmulState, pCall68KFunc ); FooParam* pData = static_cast<FooParam*>( pUserData68K ); pData->ReverseEndian( ); UInt32 ret = FooImp::Execute( *pData, &api ); pData->ReverseEndian( ); return ret; } }
extern "C"{ __declspec(dllexport) UInt32 DLLFunc_Foo( const void* pEmulState, void* pUserData68K, Call68KFuncType* pCall68KFunc ) { PalmOSAPI api( pEmulState, pCall68KFunc ); FooParam* pData = static_cast<FooParam*>( pUserData68K ); pData->ReverseEndian( ); UInt32 ret = FooImp::Execute( *pData, &api ); pData->ReverseEndian( ); return ret; } }
...ひとまずはこんなところでしょうか。その他にも環境毎にインクルードするヘッダファイルを切り分けるなどの仕組みが必要ですが、それらは一般常識の範疇なので割愛します。詳細はサンプルプロジェクトのソースコードを参照してください。
サンプルプロジェクト
上記のデザインで実際に動作するサンプルプロジェクトを用意しました。動作といっても単純に Alert を表示するだけのものです。CodeWarrior 9J でビルドできるセットで、Palm OS Simulator 向けの Windows DLL は Visual C++ 6.0 用のプロジェクトになっています。
ARMletSample.zip(557)
上記サンプルのクリエータIDはデフォルトの 'STRT' のままになっていますのでご注意ください。
参考情報