WorkPad 用プログラムの組み方(3)

Prev 1 , 2 , 3, 4 , 5 , 6 , 7 , Next


このページの目次


さて、「何もしない」プログラムを作ったら、 次は「何か痕跡を残せる」プログラムが良かろう。 しかし単に「痕跡を残すだけ」では、 それが本当に正しい動きなのかどうか判らない。

そこで、「起動コード」をある特定の「データベース」に保存し、 PC がそれを参照できるようにしてみよう。 これは突き詰めていくと Real Time Clock State reporter になる。 が、今回は hardware に関する情報などについては、 考えないことにする。 今回のサンプルと モトローラ社の WebPage からの情報を使うと、 Real Time Clock State reporter は実に簡単に作ることができるので、 それは応用問題としておこう。

このプログラムを組むに当たっては、 「起動コード(Boot Code)」と 「データベース(Database)」について理解する必要がある。 まずはその2つを順に説明していこう。


Boot Code

前のプログラムの PilotMain() 関数の宣言部を見てみよう。

DWord
PilotMain( Word cmd, Ptr cmdPBP, Word launchFlags )

起動コードは cmd という、第一引数である。

PalmOS には「コマンドライン」は存在しない。 Drag & Drop という概念もない。 そのため、通常の c program のように

int
main( int argc, char **argv )

のような「コマンドラインから与えられるオプション」を持たない。 そこで、PilotMain() という独自の関数名と、 独自の引数を用意したのだろう。 そうすれば多少のメモリをセーブすることができる。


これは良い発想ではない。

PilotMain() と同じ情報を main() の形式で引き渡すことは簡単な話だ。 main() のフォーマット以上に汎用性の高い形式は存在しないからだ。 にもかかわらず、ここで多少の(たかが数十バイトの)メモリをけちり、 無意味な非互換性を持たせることは、 プログラマーに対する大きな負担となっている。 数十バイトごとき、 コンパイラーが賢ければどうとでもスタック領域から捻出できる。

その一方で、拡張性は間違いなく失われている。 たとえ「起動コードが 何通りもある」と言っても、所詮は有限数値。 乱暴に消費すれば簡単にフィールドは不足してしまう。 これもまた、PalmOS の悪い点である。

また、Drag & Drop という発想がない、というのも問題だ。

これはようするにデフォルトで起動しているアイコンの一覧を表示してる プログラムが、 Application Launcher であって、 Folder List ではないからだ。 しかしそれはすなわち、
「様々なデータに対して、同一のアプリケーションを使う」
という発想がない事を示している。 それはそのまま 応用性のないプログラム, 同一性質の粗悪なプログラムの乱造 を意味する。 ハンドヘルドのような小さなデバイスにおいて、 似たり寄ったりの、しかし微妙に異る、プログラムをいくつも入れ、 何種類ものアプリケーションを使わなくてはいけない、 という事実はそのまま「使い勝手の悪さ」を意味する。

簡単に間違えないようにできる操作数を1ステップ減らしたために、 応用性がなくなり結果として使えないものになる、 というのは過去何百例となく現れているにも関わらず、 このデザインだ、というのは極めて示唆的である。

つまり「考えているようで、下手な考えしかしていない」ことを示唆している。 しかもそれを「良く考えられている」といわれる事自体、 どの程度の使われ方をしているのかが良く判る。


cmd が取りうる値の範囲は PalmOS のバージョンによって異るが、 PalmOS SDK の中に定数が全て記述されている。
<SDK install directory>/Incs/System/SystemMgr.h
に定義されている sysAppLaunchCmd シリーズがそれである。


はっきり言うと PalmOS のドキュメントのどこにも、 全ての起動コードとそれに対する説明は存在しない。 どのコードで起動されるのかによって cmdPBP, launchFlags と言った、PilotMain の第2、第3引数の意味が変化するにも関わらず、である。 こんな「Reference Manual」が許される段階ですでにレベルが判る、 というものだ


PalmSDK 3.1a に記録されている起動コード、 並びにそのの意味はそれぞれ次の通りである。 番号も記述してはあるが、シンボル名の方で使うことをお勧めする。 また、SDK のバージョンによって、 どのコードが何と呼ばれているのかは微妙に変化している可能性もある。 注意されたい。

sysAppLaunchCmdNormalLaunch
起動番号 0
Normal Launch、 つまり通常 Application Launcher から起動された場合、 この起動コードになる。
デフォルトでは cmdPBP は NULL になる。 ただし、他のアプリケーションがこの起動コードで起動した場合、 cmdPBP が NULL 以外の値を取る可能性はある。 そのように起動してもcmdPBP引数の意味が解釈できなくて、 無意味だろうが。
この起動コードの場合だけ、Global 変数が使える、 などの制約がある。
sysAppLaunchCmdFind
起動番号 1

Find string つまり、文字列検索の場合に使われる起動コード。

Palm デバイスは Graffity エリアの右下側に「検索(Find)」を 起動するための領域があるが、ここで文字列検索を行うと、 実は登録されている全ての Application を順番に このsysAppLaunchCmdFindという起動コードで 起動している。

cmdPBP には <SDK install directory>/Incs/Find.h に定義されている FindParamsType 構造体へのポインターが渡されてくる。

残念ながらこの「検索」は正規表現も使えない無能な検索であることを 余儀なくされる。 他の Application とバランスをとる必要があるからだ。 これは結果的に「使えない」状態であることを意味する。

sysAppLaunchCmdGoTo
起動番号 2
sysAppLaunchCmdFind あるいは sysAppLaunchCmdExgReceiveData の後 (つまり検索が行われた後)、 そのレコードの内容をユーザーが実際に確認できるように、 「検索できたそのアプリケーションに」 送られる起動コード。
アプリケーションは通常の起動処理を実行後、 要求された項目を表示する必要がある。
cmdPBPとして UI/Find.hGoToParamsType型の 構造体へのポインターが渡される。

ここには PalmOS の思想が隠れている。

PalmOS は「あるデータベースに対して検索できるアプリケーション」 と「表示できるアプリケーション」は同一のものだと思っている。 だから「発見できたアプリケーション」に「表示」を依頼する。

これは一般的に極めて無能なやり方だ。 同一のデータベースを、複数のアプリケーションが、 それぞれの興味関心の範囲で用いる、 と言うモデルが存在しないことを意味するからだ。 また、このモデルでは、1つのアプリケーションが複数のデータベースを管理し、 他のアプリケーションも複数のデータベースを管理し、 各々が「少しづつ」重複したデータベースを管理できる場合も考えていない。

この事実は、複数のアプリケーションが同じようなことをするために、 同じような機能を持たなくてはならず、 なおかつその中の1つだけが検索の時に答えなくてはいけない、 と言うことを意味する (そうでなければユーザーは同じ情報を何度も見せられることになる)。 では一体どのアプリケーションが答えるべきなのか、 どうやって判定するというのだ?


sysAppLaunchCmdSyncNotify
起動番号 3
アプリケーション自身が HotSync でインストールされた場合、 あるいはアプリケーションが HotSync で何らかの書き換えを受けた場合に、 HotSync 終了後、 この起動コードでアプリケーションは一度起動される。
sysAppLaunchCmdTimeChange
起動番号 4
現在時刻設定を変更すると、 全てのアプリケーションをこの起動コードで呼び直す。 このコードで起動されたら Alarm などの設定を 適切なものに設定し直すべきだ。
sysAppLaunchCmdSystemReset
起動番号 5

ソフトリセット、あるいはハードリセットが実行された直後に、 存在する全てのアプリケーションとプリファレンスパネルは この起動コードで起動される。

cmdPBPとしてSysAppLaunchCmdSystemResetType 型の構造体へのポインターが渡される。

sysAppLaunchCmdAlarmTriggered
起動番号 6

各 Application は特定の日付、時刻になったらプログラムを起動する、 「アラーム」という機能を PalmOS に要求することができる。

「アラーム」に従って、特定の時刻になったら、PalmOS は この起動番号でアプリケーションを起動する。 実は「アラーム」が発生したら「即座に応答する」ための sysAppLaunchCmdAlarmTriggeredと その後、じっくりと GUI を表示するなどができる sysAppLaunchCmdDisplayAlarm の2つの起動コードがある。 sysAppLaunchCmdDisplayAlarm でも起動して欲しい場合は、この起動コードで起動されたときに その旨を PalmOS に返す必要がある。

cmdPBP には SysAlarmTriggeredParamType (AlarmMgr.h に記載されている) 型の構造体へのポインターが渡される。

この起動コードで起動された Application をブロック (たとえば何かの入力を待つなど)してはいけない。 GUI などを叩くのもいけない。 GUI を叩いても良いのは sysAppLaunchCmdDisplayAlarm だけ、ということになっている。

sysAppLaunchCmdDisplayAlarm
起動番号 7

sysAppLaunchCmdAlarmTriggeredで、 起動要求が出された Application については、 その後、この起動コードで Application が再度コールされる。

この起動コードは、主に GUI を必要とする Application 用に存在する。 つまり、特定の時刻に対して複数のアプリケーションが登録されていたなら、 とりあえず sysAppLaunchCmdDisplayAlarm で必要な Application を全部起動し、 その後 GUI (たとえば「時間だよっ!」と表示するとか…この場合、 ユーザーが見たことの確認をとるために、 OK button などの tap を待たなくてはいけない) を必要とする 処理を順次 sysAppLaunchCmdDisplayAlarm で起動されたときに処理していく…というシナリオの元に作られていた、 らしい。 このため、複数の Applicationはこの段階では、順次、 GUI に対する応答1つづつに反応する形で、 実行が進められる。

cmdPBPには SysDisplayAlarmParamType(AlarmMgr.h に定義されている) 型の構造体へのポインターが入る。


PalmOS は、この処理にバグがある。

どうやら、PalmOS はけちけちと処理をする余り、 このsysAppLaunchCmdDisplayAlarm 処理を、 「その処理をしなくてはいけないときに起動している Palm Application の イベントハンドラー内部で」 処理しようとしているらしい。

この構造は大抵の場合、「イベントの限定」という重要な能力を要求する。 Alarm 用のウィンドウが出ている最中に Application Launcher への 切り替え要求が発生してもそれを無視する、等の処理が必要になるからだ。 このためか、実はこのイベント処理中に、Real Time Clock などの割り込みの 一部に対するマスクが設定される。 このマスクには「日付が変った」割り込みなどが含まれる。

極めて奇異なことだが、AutoOff 機能は健在である (何が「奇異」かというと、Real Time Clock をマスクしておきながら AutoOff 機能を止めない、という状態が、だ。 普通、AutoOff 機能を止めることはあり得ても、 Real Time Clock を止める必要性はどこにもない)。 結果、Real Time Clock の機能の一部がマスクされたまま Sleep してしまう。 その後、日付がさらにもう1日変化してから電源ボタンを入れられると、 日付が1日狂った状態になる。

ここでさらに奇異なことが起る。 Real Time Clock に対する割り込み処理マスクが元に戻らない場合があるのだ。 どうしてそうなるのかは未だに判っていない。 日付変更割り込みに対する割り込みハンドラーと、 秒変更割り込みに対する割り込みハンドラーは同じものなので、 PalmOS を24時間以内ごとに1度起動してやれば、 たとえこうなっても「秒変更割り込み」の際に日付変更割り込み処理も行われる。 が、PalmOS を24時間以上電源 off 状態に放置すると、 その間秒変更割り込みはかからないので、 日付が変化しても内部の「年月日」を管理しているデータ領域が 適切に処理されない、という現象が起る。

これは、そもそもが PalmOS が Multi thread を歌いながら 十分な thread resource がないこと、 thread に priority の概念がなく それゆえに「DisplayAlarm」用の 優先度の高い thread が実行できないこと、 GUI に Window の概念がなく、それゆえに Window owner の概念もなく、 それゆえに適切な event queue 処理ができないこと、 に起因している。

が、もう一歩押し進めて、 Palm Computing にこれらに関して解答を迫ると、 実はこれは『PlamOS は Palm Computing のものとは言えないから』 である、と言う主張を聞くことになる。

実は、PalmOS はもともとは KADAK 社 という会社が作り上げた Real Time Operating System(RTOS) だという ことに行き着く。 この会社の WebPage に行って、FAQ を見てみるがいい。 PalmOS で thread を使うプログラムを作る場合、 KADAK と契約を結ぶ必要がある理由が書いてある。 結局 US Robotics だった頃なのか、そのさらに前なのか… Palm をつくった人たちは、ここから OS を買い、 自分の目的に合わせて変更し、それを PalmOS としたのだ。 だから、thread や mutex, condition など、thread 制御の基本となる リソースの数などを制御したり、 ダイナミックに生成したりする書き換えがほとんどできないのだ、 という Palm Computing の主張を耳にすることになる。

実はこの主張も不明瞭なところがある。 RTOS における最も重要なキーポイントは、 割り込み制御 priority 制御実行時間の適切な推定である。 であるならば、このような事は起らないはずなのだ。

RTOS にとって Real Time Clock は最も重要な規範であり、 それゆえに最優先で管理されるべきものである。 一方でその処理は極めて単純であり、 割り込みハンドラー内部だけで処理できないはずがない。 結果、今のような Real Time Clock の制御方法になるはずはない。 KADAK 社の RTOS が腐っていたのでなければ(まぁ多少は腐っていようが)、 PalmOS へと変更する際に、バグを埋め込んだ 場合しかあり得ないのだ。

優先度の高い thread を最優先で実行させることなしに RTOS はつくれない。 全て同じ priority では Real Time もへったくれもないからだ。 ならば priority の異る Alarm thread を用意できないわけはない。 現状のような状態に陥るのは、 KADAK 社の RTOS が腐っていたのでなければ(まぁ多少は腐っていようが)、 PalmOS へと変更する際に、priority の概念を切り捨てた 場合だけだ。

RTOS は「実行時間の適切な推定」が重要な事項になる。 つまり system call は
「早いときは 1usec で処理できるが、遅いと 1sec かかる」
よりも
「早いときも、遅いときも 10msec かかる」
方が(system call にかかる時間が正確に推測できるので)、 各 priority の thread の実行時間を正確に見積もれるからだ。 判ると思うが、これは人間相手を主な仕事とする OS に 要求されている機能とは大幅に異る。 人間相手の場合は
「大抵の場合は早く動くが、たまに時間がかかる」
方がありがたい。 ということは PalmOS のベース OS に RTOS を使うのは明らかに間違っている。 にもかかわらず KADAK社の RTOS を使ったというのは、 PalmOS をつくる上での根本的な戦略ミス だということがわかる。

結局、PalmOS の問題点に対する excuse にはならず、 単に Palm がおかしたミスだけがひたすらに判る、と言うことだ。 そもそも、AMX に問題があるかどうかは、 API 一覧を見ただけで解るレベルではないか。

さて、この問題をプログラム上で完璧に回避する方法は1つだけ。 sysAppLaunchCmdDisplayAlarm 中に GUI を使わないことだ。 beep 音を発する程度であればこれだけでも構わない。

一般ユーザーとしての自衛手段は、 スケジューラーなどでアラーム機能を使わないこと。 少くとも「日付がもうすぐ変る、ような時刻にアラームを設定してはいけない」。 あるいは「絶対に毎日使う」というのも手ではあるが、 この場合は「病気を持っているが発病していない」状態であり、 他の問題が発生する危険性を孕んでいるのであまりお勧めはできない。


sysAppLaunchCmdCountryChange
起動番号 8
国情報が変わった場合に、 全てのアプリケーションはこの起動コードで起動される。
sysAppLaunchCmdSyncRequestLocal (あるいは、古い PalmOS では sysAppLaunchCmdSyncRequest)
起動番号 9
Cradle 上の HotSync ボタンを押すと、 HotSync Application をこの起動コードで立ち上げる。 HotSync Application をすり替えたい場合などには重要だろう。
sysAppLaunchCmdSaveData
起動番号 10
sysAppLaunchCmdFind のような「他のアプリケーション」を起動する前に、
『現在動いている Application は』
この起動コードを送られ再起動される。 検索などの結果、動作が暴走しても構わないように、 作業内容のうち保存しておいた方が良いものを保存する、 などの作業のためである。 このコードで再起動されたアプリケーションが終了したら 検索等の処理が行われる。

これも PalmOS のデザインのおかしいところである。

「検索」のような処理があるのは判るし、 現在実行中の Application が「検索」の実行の knowtice をうける、 というのも判る。 データを保存しろと言うのも良く判る。 しかし、普通はそう言う情報は「ソフトウェア割り込み」か 「event」を使うものだ。

「ソフトウェア割り込み」を使う方法は unix では一般的な方法だ。 外部プロセスで何か起ったときには OS が関連するプロセスに 割り込みをかける。 アプリケーションは何かやるべき事があるならば、 割り込み処理にてそれを実行し、 割り込みをかけてきたソフトウェアに「準備は整った」旨を返す。

それは event を使う場合でも一緒だ。 (ソフトウェア割り込みを使って event queue に特殊な event を載せ)、 新しい event を獲得する、というタイミングは大抵の場合、 最小の critical risk 状態にあるので、「準備」は最小限で済むことが多い。 そこで、特殊な event を受信したら必要なセーブを行い、 相手 thread に「準備完了」を通達する。

が、今回はそのどちらでもなく、動いている thread をブロックし、 『1から起動しなおす』 というとんでもない手段を用いている。 これでは、global 変数で管理されているデータは保存できても、 stack からしか辿れない所においてある、 重要な一時データは一切保存できない。

その一方で、Application の起動の多くは global 変数の利用を禁止し、 結果、さまざまな起動コードで共通して使えるルーチンは、 global 変数なしで組まざるを得ない構造になっている。 つまり、できの良いソフトほど、重要な一時データを保管できないのだ。

これならば、このような起動コードはなくても全く問題はない。 どうせまともな「復旧」作業ができないのだから。


sysAppLaunchCmdInitDatabase
起動番号 11
HotSync にてデータベースが作成された場合に、 同じ Creator ID を持つ Application はこの起動コードで 起動される。
これはたとえばデータベースの初期化などに使える。 というのは、HotSync に使われる通信 Protocol は貧弱で、 必要な処理を全て HotSync 上で処理することもできないし、 全てを PC 上で処理して database を丸々 WorkPad に転送するのは 通信速度が遅すぎるから,である。
cmdPBPSysAppLaunchCmdInitDatabaseType 型へのポインターである。
sysAppLaunchCmdSyncCallApplicationV10
起動番号 12
PalmOS 1.0 でのみ発生する起動コードで、 DesktopLink Server からの "call application" コマンドで 起動されたアプリケーションはこの起動コードになる。 PalmOS 1.0 用のプログラムを作ろうとしない方が速いと思う。 具体的な情報がもはやどこにも残っていないからだ。

これ以降は、PalmOS 2.0 以降でのみ発生する起動コードである。


sysAppLaunchCmdPanelCalledFromApp
起動番号 13
Application から Command Panel を呼び出すと、このコードで起動されるらしい。 panel は pick list の代わりに done button を表示するべきだ、とある。 どうやら環境設定パネルをアプリケーションから起動するときに使う コードらしい。 逆に環境設定パネルに自前のパネルを追加した場合は(追加できるのだ)、 このコードに対応し、設定が終了したら Done ボタンを表示すること、 「パネル切り替えポップトリガー」を表示しないこと、 を守る必要がある。
sysAppLaunchCmdReturnFromPanel
起動番号 14
sysAppLaunchCmdPanelCalledFromApp が終了すると、この起動コードでアプリケーションは起動される。 実は、このコードで起動されてもアプリケーションは global 変数を 使えないらしい。 それだけでなく、一体どの Panel から戻ってきたのかも判らないらしい (らしい、というのはこの起動コードを私は使ったことがないからだ)。
ぬうぅ…だとすると、一体このコードによる起動には 何の意味があるというのだ? こんなコードで起動されても、何もできないではないか。
そんなことをするよりもちゃんと global が使える状態で再起動してくれ。
sysAppLaunchCmdLookup
起動番号 15
システムやアプリケーションが Target アプリケーションから情報を 検索するときに、 この起動コードで Target アプリケーションを起動します。 こちらの「検索」は各アプリケーションが、 cmdPBP 引数の型を規定することが可能です。
たとえば、現在このコードをサポートしている Addres Book の場合は、 SDK に含まれているソースコードの Address.c と AppLaunchCmd.h に定義されています。
sysAppLaunchCmdSystemLock
起動番号 16
システム内部のセキュリティーアプリケーションに送られる 起動コード。 これで起動されると、 セキュリティーアプリケーションはデバイスをロックしにかかる。
セキュリティーアプリケーションの代価物を作ろうと言うのでない限り、 この起動コードに応答する必要はありません。
sysAppLaunchCmdSyncRequestRemote
起動番号 17
Remote Hotsync ボタンが押されたときに、 HotSync Application に対して送られる起動コード。
sysAppLaunchCmdHandleSyncCallApp
起動番号 18
PalmOS 2.0 以降において、 PalmOS 1.0 における sysAppLaunchCmdSyncCallApplication 起動コードに代わるものとして用意された。
HotSync Protocol における、call application リクエストに答えて Palm 上のアプリケーションが起動されるときは、 このコードで起動される。
ちなみに DlkControl を『dlkCtlSendCallAppReply』を使って 呼び出すことで、reply を返すことができる。
sysAppLaunchCmdAddRecord
起動番号 19
用途不明。Header file には
「Add a record to an applications's database」
とあるが、自分で自分の database に record を追加変更したらどうなる?
sysSvcLaunchCmdSetServiceID
起動番号 20
sysSvcLaunchCmdGetServiceID
起動番号 21
sysSvcLaunchCmdGetServiceList
起動番号 22
sysSvcLaunchCmdGetServiceInfo
起動番号 23
標準サービスパネル起動コード。 実はそれしか書いてないので良く判らない。
sysAppLaunchCmdFailedAppNotify
起動番号 24
「An app just switched to failed.」
と書いてあるだけ。意味不明。
sysAppLaunchCmdEventHook
起動番号 25
「Application event hook callback」
と書いてあるだけ。意味不明。 cron のごとくイベントで callback をかけられるといいたいのか…。 (多分そうだろうが、global 変数は使えないのだろうなぁ…)
sysAppLaunchCmdExgReceiveData
起動番号 26
「Exg command for app to receive data.」
と書いてあるだけ。意味不明。
sysAppLaunchCmdExgAskUser
起動番号 27
「Exg command sent before asking user.」
と書いてあるだけ。意味不明。

起動番号の30番から39番までは 標準 Dialer Service Launch Code 用に予約されている。

ちなみに、ヘッダーファイルには 「cmdPBPに関することを書くこと」 という DOLATOR(do lator 後でやること)が書いてあるぐらいだ。 場当たりであることよの。


sysDialLaunchCmdDial
起動番号 30
「modem をダイアルする」とある。
こう書くと「このアプリケーションが」という主語が明確であるかのようだが、 実際には「特定のアプリケーションをこのコードで起動すると」 かもしれないし、そうじゃなくて 「このコードで起動されたら、Modem Mgr を叩いて」 という意味かも知れない。 多分、Header file を書いた人はどちらなのか判っていて、 他の人も常識で判ると思い込んでいるのだろう。
思い込みが生じやすいのは 「特定のアプリケーションを叩く」 方だろうからそういう意味なのだろうが、馬鹿丸出しである。
sysDialLaunchCmdHangUp
起動番号 31
一切の説明はない。不明。
sysDialLaunchCmdLast
起動番号 39
一切の説明はない。 おそらくその名前からするにこれは「sysDialLaunchCmd」シリーズの 最後の値を指定するためだけのものだろう。

起動番号の40番から49番までは 追加の標準 service panel 用起動コード用に予約されている。


sysSvcLaunchCmdGetQuickEditLabel
起動番号 40
「SvcQuickEditLabelInfoType」 とだけ書いてある。不明だ。
sysSvcLaunchCmdLast
起動番号 49
追加の標準 service panel 用起動コード用の最後の値。
sysAppLaunchCmdNotify
起動番号 51
Notify Manager というものがある。 こいつが SysNotifyBroadcast を使って Application を 起動してきた場合の起動コードである。
cmdPBP は SysNotifyParamType 構造体へのポインターを持っている。

ううっぷ。ご苦労様。 一杯あってゲンナリしたことだろう。

この他に user custome な起動コードがある。 sysAppLaunchCmdCustomBase という定数が定義されており、 この値以上の値の場合にはユーザー定義起動コードと言うことになっている。 今現在、この値は 0x8000 と定義されている。


Pilotmain() には第3引数としてlaunchFlagsがある。 普通、ユーザプログラムが、 別のアプリケーションを起動する場合には 0 を指定する。 しかし、一応このコードにも意味がある。 また、このコードは 16bit のビットフラグ状態になっている。

意味はあるのだが、相変わらずこれも「そうなる」のか 「そうするような Application を書かなくてはいけない」 のかさっぱり判らない。

sysAppLaunchFlagNewThread
コード 0x01
アプリケーション用に新スレッドを作成する。 sysAppLaunchFlagNewStackコードも 続いて実行される。
sysAppLaunchFlagNewStack
コード 0x02
アプリケーションのために新しく Stack 領域を確保する。
sysAppLaunchFlagNewGlobals
コード 0x04
アプリケーション用の global 領域を新しく作成する。 Memory Chunk 用の新しい owner ID も獲得する。
sysAppLaunchFlagUIApp
コード 0x08
起動されたのは user Interface application であることを表す。
sysAppLaunchFlagSubCall
コード 0x10
起動ルーチンに、Application は PilotMain() を サブルーチンコールしているだけだ、と言うことを告げている。 つまり、global 変数を管理している A5 レジスターは変化しておらず、 global 変数は有効なままであることを伝えている。
このフラグは SysAppLaunch によってしか使われてはならない、 と書かれている。呼び出し側がこのビットを設定してはならないそうだ。
sysAppLaunchFlagDataRelocated
コード 0x80
SysAppStartup あるいは StartupCode.c (つまり PilotMain を起動する、 起動ルーチン)が global 変数領域を再配置した、と言うことを意味する。
このフラグは SysAppLaunch によってしか使われてはならない、 と書かれている。呼び出し側がこのビットを設定してはならないそうだ。

大抵の起動コードにおいて、global 変数が使えない、という制限がある。 この意味を理解するには、PilotMain() がどのように起動されているのか 理解する必要がある。

Application を起動する方法は2種類ある。 SysAppLaunch() API を使う方法と、 SysUIAppSwitch() API を使う方法である。 どちらも、起動する Application を指定する。 が、その後が違う。

SysUIAppSwitch() API は指定された Application の 'code' 領域の 先頭アドレスから実行を開始する。 結果、ユーザーが Application を作成したときの起動ルーチンが実行される。 起動ルーチンは global 変数用領域を設定し (これは単なる malloc() と同じ作業をしているだけだ)、 必要な初期化(global 領域に対する 0 クリア、 exit() 処理に対する挙動や、 C++ であれば method table の設定など)を行ってから PilotMain() を呼び出す。

一方 SysAppLaunch() API は、'code' 領域から、 『PilotMain()』という名前の関数を探し出す。 そして、その関数を『普通の 関数へのポインターを呼び出すように』 呼び出す。 この場合、global 変数領域は「呼び出した側」のそれのままである。 stack も「呼び出した側」のそれのままである。 従って、呼び出された側が global 変数領域をアクセスしようとすると、 そこは「呼び出し側の」別の global 変数が存在する。 その領域に対する上書きを行うことになってしまうので、 呼び出し側へ戻った後に破綻が生じてしまう。 だから global 変数を使うことができない。

『PilotMain()』という名前の関数を探し出す と書いた。 これができるためには、prc ファイルには PilotMain() という 名前を持つ関数がどこにあるのか判らなくてはいけない。 そして、実際それはできる。 CW も gcc も全ての global 関数はその関数の最後に関数名を保持している。 global 関数の entry ポイントとサイズは別のところに記録されているので、 関数名を探し出すことも可能なのだ。

…だがもうしばらく、この事実は忘れていて構わない。


Database

「記録するべきデータ」が判ったら、次は「記録する対象」だ。 これを理解するにはまず、WorkPad の基本的構造を理解するところから 始めた方が良いだろう。

WorkPad が用いることができる記録メディアは、 基本的に ROM 並びに RAM だけである。 もちろん Visor のように外部デバイスを用意できるものもあるが、 全ての WorkPad に共通している記録メディアは ROM と RAM だけである。 WorkPad の場合は RAM 領域はバッテリーバックアップされているので、 どちらに記録されているデータも通常の状態では消滅することはない。 RAM 領域を消去するにはハードリセットをかけなくてはいけない。

ROM も RAM も CPU から直接アクセスできるアドレス空間上に存在する。 よって、ROM も RAM も CPU から直接読み込むことができる。 WorkPad の CPU は MC68000 ベースなので (というか MPU と言わなくてはいけないのか?) 仮想アドレスは存在せず、supervisor モードであっても user モードであっても、 基本的にはあらゆる所をアクセスすることができる。

WorkPad が採用している Dragonball(68328) 並びにその後継機種である EZ Dragonball(68EZ328) には Chip Selector という機能がある。 これは具体的には特定のアドレスを CPU core が発行すると、 そこに対するアクセスをブロックする bus error を発生させるチップである。 WorkPad ではこの機能を使って、ROM 領域だけでなく RAM 領域の一部も デフォルトでは書き込み不可能にすることで、 プログラムの暴走などによる破壊から RAM 領域内のデータを保護している。 もちろん、全ての空域を保護してしまうと stack 領域などが作れないので、 一部保護されていない RAM 領域もある。

直接読み込むことができる、 ということは ROM 領域並びに保護された RAM 領域上に プログラムを用意した場合、 その領域をそのまま実行できるということだ。 また、RAM 領域上にデータ領域をつくった場合、 そこにあるデータを書き込まない場合は自由にアクセスできる、 ということでもある。

そこで、PalmOS はこの ROM 領域並びに RAM 領域上に 『storage』領域を用意する。

Storage 領域には複数の Database を作成・用意できる。 Database は 0 個以上 64k 個(65535個まで、と言われているが、厳密には OS のバージョンによって異るようだ)までの record から成り立っている。 各 record は 1byte 以上、64kbyte(と言っても 65535 という意味ではなく、 若干少いサイズが最大サイズになる。 最大サイズは OS のバージョンで異るが、65500byte までと仮定すれば、 現在存在するどのバージョンでも問題なく使える) までのメモリチャンク(単なる連続したバイト列空間)である。

「あぁ、ようするに Database というのは record へのポインタの配列なんだな」 と思ったあなた、80% 正解である。 Database は「特別な record」で、 その内容は Database 名をはじめとする管理情報が格納されている。

ただし、単純な pointer ではない。 record は生成したり削除したりできるが、 それによって RAM 領域は利用中の領域とそうじゃない領域がどんどん 細かく分割されていく、fragmentation という問題が発生する。 この問題を回避するために、 どこからも参照されていない record は自由に移動できることになっている。 必要なサイズの record を作成できない場合は Garbage Collector が動きだし、 誰からも参照されていない record を移動して空き領域を作成する。 この管理構造は単に Database 領域だけでなく、 普通の heap 領域も同じ管理構造をとっている。


実はこれは諸刃の構造だった。

そもそも、Palm device は最初、RAM 128kbyte で、 そのうち 64kbyte が Database 領域として利用可能だった。 のこり 64kbyte は普通に言う heap 領域で、 kernel や Application stack などが確保されていた。

保管するデータには「常識的な最低サイズ」というのがあるので、 このころの Database の数や record の数には「たかが知れて」いた。 このような世界では 64kbyte の上限値も、 それしかメモリがないのだから十分だった。 record の数 n に対して O( n ) かかる garbage collection も n が小さいのでたかが知れた時間しかかからない。 だいたい、garbage collection なしにはようよう動かない。

ところが、その後、RAM のサイズがどんどん大きくなるにしたがって、 64k という record の限定は使いづらく、 しかも record の数を増大させる要因となった。 record の数の増大は garbage collection の実行時間を増大させる。 さらには「ディレクトリ」のような管理階層構造を持たないため、 Database 自身も数の増大と共に検索に時間がかかるようになった。 だからといって、garbage collection で移動してしまうものである以上、 Database の位置は決め打ちにはできない。 結果、RAM のサイズを 8Mbyte まで増大させた段階で、 これ以上の RAM の増大はシステムが遅すぎて使い物にならなくなった。 結局スケーラビリティーがなさ過ぎたのだ。

Directory 構造がサポートされていれば、 Database の検索にかかる時間は現状よりも短くて済む。 Database の record の数に上限を設ける必要は実際にはないはずだし、 record のサイズも 32bit で用意しておけば record 数は少くて済む。 現在風の malloc のような heap management を利用すれば garbage collector が動く必要はほとんどないし、 record サイズの変更に際して配置を最適化する機構を用意すれば garbage collector phase は必要なくなる。 さらに Chip Selector の性能から言って、 あと数ヵ所分「書き込み可能領域」を作ることは可能だ。 record への書き込みを専用 API ではなく、 「書き込み許可」モデルにすれば IO にかかる時間も減り、 高速化するだろう。


もし、あなたが昔、MC68000 のプログラムを組んだことがある人なら、 そろそろ次の疑問が出てくるはずだ。
「ちょっとまて。24bit しかアドレス空間がない MC68000 では、 RAM 領域は8Mbyte 以上など用意できないだろう? 全部合わせて 16Mbyte しか空間がないんだから。 ROM 領域と、Hardware 制御用の Memory Mapped IO 分を計算したら もうイッパイイッパイじゃないか」

んー。近いな。

68328 というチップには(他の線と共有しつつ)31本のアドレス線が出ている。 MC68000 は偶数番地のアドレスから、16bit 単位でしかデータをIO できないので、 A0 が省略されているだけで、32bit アドレスできるようになっている。 MC68000 のマスクを使っているとは言え、その辺りは改造してあるようだ。

ただ、アドレス線を他の線と共有していることからも判るように、 32bit のどこへアクセスするのも同じ速度、と言うわけには行かない。 MSB 8bit に対するアクセスは実際には遅くなる。 じゃぁ、どうするのかというと、MSB8bit が変化しない場合は、 MSB8bit を出力しない…一種のバンク切り替えのような構造になっている。

WorkPad の物理機構はこの構造を利用して、 アドレス線に繋がる全てのリソースを 255枚の「カード」として 管理している。 16Mbyte のアドレス空間を持った「カード」を 255 枚 (アドレスの MSB 8bitが 0x00 な部分は 0x01 のものと同じカードを 指している)まで管理できることになっている。 もともと MC68000 はインテルの CPU と異り IOPort という概念がないことも手伝って、 この「カード」には ROM/RAM だけでなく特別なハードウェアを つないで制御するのは極めて自然な構造となっている。 PalmVII などは無線機をカードで接続している。

PalmPilot までは、SIMM Card Slot が1つだけあり、 そこに OS の入った ROM と RAM が乗ったカードが刺さっていた。 PalmIII 以降は ROM と RAM はメインボードに移動し、 SIMM Card Slot が1つ空いた状態になっている。 ここにカードをさすと Card 1 になる。 PalmV のような薄型のものは…はて中にカードスロットはあるのだろうか? ないような気がするが記憶が定かでない。

PalmOS はこの物理機構と完璧に対応している。 PalmOS にも「カード」の概念がある。 カード番号は 0 からはじまり、254 までである。 カード番号 0 はアドレス 0x01000000 からはじまる領域に、 カード番号 1 はアドレス 0x02000000 からはじまる領域に、 それぞれ存在することになっている。 PalmOS 自身はカード番号 0 上にある ROM に存在しなくてはいけない。 つまり、物理構造そのまんま、というわけだ。

各カードの RAM ならびに ROM 領域は、 それぞれ1つ以上の heap 領域として登録される。 各 heap 領域には ID が割り振られている。 この heap 領域は1カード辺りいくつまで、と言う数は決まっていない。 ただ、決まっていることがあって、 heap 0 はカード 0 の RAM 領域の内、Read/Write が自在にできるところ、 heap 1 はカード 0 の ROM 領域、 heap 2 はカード 0 の RAM 領域の内、Chip Selector によって read only 領域に定められているところ、 heap 3 以降はカード1以降の領域と言うことになっている。

heap 0 は別名 dynamic heap と言われる。 program が通常演算に利用する chunk, stack 等は全てこの dynamic heap にとられる。 heap 1 以降は全て storage heap で、ここに database が格納される。 ただし、現在のところ Card 1 以降を「メモリカード」として、 RAM heap 領域が割り当てられるように利用しているモデルは存在しない。 実質的に database が用意できるのは card 0, heap 2 だけである。


現在有効な card を調べる方法が実は PalmOS には存在しない。 しかし、database を作成、指定する際には card 番号をよこせという。 無茶を言うものである。

現在存在するプログラムは、 この問題を「全部 card 0 決め打ち」にすることで対処している。 おそらくこの決め打ちは妥当なものだろう。 現行の OS デザインでは、複数カードを同時に管理して、 高いパフォーマンスを得ることはできにないだろう。


heap 0 とそれ以外の heap は read/write 可能か、read/only か、 という違い以外に dynamic heap 管理型なのか、 storage heap 管理型なのかという点が異る。 このため、memory chunk ( storage heap 管理型の場合は record と呼ばれる) を指定する「ハンドル」を獲得する方法は大幅に異り、 それぞれについて専用の API がある。 しかし、一旦「ハンドル」を獲得した後は、 その「ハンドル」を Lock してポインターを獲得し、 アクセスする方法は基本的同じである。 (ただし、heap 1 以降は書き込めないので、書き込みに関しては また専用の API が必要になるが)。


ここでは dynamic heap に関するこれ以上の説明はやめよう。 話を storage heap の管理、database の管理に集中させる。

Database は実際には「Record」の配列型と、 「Resource」の配列型の2種類が存在する。 どちらも基本性質は同じものだが、 Resource は Record よりもより厳密にフォーマットが定まっている。 これは 「アプリケーションプログラム」と その関連情報(表示するビットマップイメージとか)を格納するのが Resource で、 アプリケーションプログラムが利用する単なるデータを格納するのが Record である、という違いから来ている。

これらを PC に送ると、HotSync Manager は Record のデータベースを .pdb という拡張子のついたファイルに、 Resource のデータベースを .prc という拡張子のついたファイルに書き出す。 ただし、HotSync マネージャー自身はファイルの拡張子を利用して そのファイルがどちらを表しているのか調べているわけではない。 これらのファイルは 内部に resource database を表すファイルなのか、 record データベースを表すファイルなのかを示すフラグを持っているのだ。 さらに言えば、このファイルの構造そのままの形で WorkPad の中に 入っているわけでもないし、 メモリ空間の連続性や順序だって異っているかも知れない。

しかし、とりあえず無意味にファイル拡張子を変更するのでなければ、 なぜプログラムが .prc で、データが .pdb なのか、 HotSync マネージャーが .pdb ファイルをバックアップするが、 .prc ファイルをなかなかバックアップしないのかも判っただろう。


データベースを管理する API で最初に重要になるのは、 自分が必要とするデータベースが存在するかどうかを確認する手段である。 これには DmFindDatabase() という API を使う。

Database には全て、Database 名というものがついている。 pilot-xfer であれば -l あるいは -L オプション付きで起動すれば、 WorkPad 内部にある Database の一覧を見ることができるが、 そのとき見える「一覧」は この Database 名である。 Database 名は(名前の最後にデフォルトでつく '\0' も含めて) 32byte までである。

DmFindDatabase() は

LocalID	DmFindDatabase(UInt cardNo, const CharPtr nameP)

という定義になっている。 nameP には Database 名を格納した文字列へのポインターを、 cardNo には検索する Database の格納されている Card 番号を指定する。 この関数は LocalID 型という「データベースのID」を返してきます。 LocalID 型は値が 0 の場合に NULL に相当する。 つまり Database が発見できなかった場合は 0 が返される。

cardNo を指定する、 ということは同じ Database 名を持つ複数の Database を それぞれ異る Card における、と言うことだろうか? 答は Yes である。それどころか、 同じ Database 名を持つデータベースは heap 単位で登録できる。 つまり、heap 1 と heap 2 にそれぞれ「afo」という Database を置ける、 と言うことだ。

しかし、heap 1 と heap 2 は同じ Card 上にある。 どちらを指定するのかどうやって区別するのか? 実は区別できない。PalmOS は同一 Card 上の Database を探すときは、 heap 番号の数字の大きい方から検索する。 つまり RAM→ROM の順で検索するのだ。

この検索順序は Patch などに使える。 実際、PalmOS はそのずいぶん多くの部分を ROM 上に置いた プログラムによって実装されている。 しかしそれらにバグは付き物だ。 そのため、PalmOS にはパッチ用の prc ファイルが Palm Computing から入手できる。 これをインストールすると、 ROM 上にあるプログラムではなく RAM 上のプログラムが発見され、 そちらが起動される。

DmFindDatabase() の実行速度は本質的に早くない。 1からデータベース名をサーチしている関係上、 サーチ速度はデータベースの総数 n に対し O(n) の速度になる。 データベースの総数は、Application Launcher から見ることができる アプリケーションの総数よりも多い上に、 Database は flat model で保存されているので、 Directory モデルと異り全ての database を検索するのに比例した状態が 発生するのだ。


必要な Database を探したのに無かった場合、 Database を作成する必要があるだろう。 これには DmCreateDatabase()DmCreateDatabaseFromImage() を使う。

DmCreateDatabase()は

Err     DmCreateDatabase(UInt cardNo, const Char * const nameP,
                         ULong creator, ULong type, Boolean resDB)

という定義になっている。

cardNo は Database を作成する Card 番号を指定する。

nameP で Database の名前を指定する。

resDB には true(1) あるいは false(0)を指定する。 1 の場合作成するのは resource Database になる。 0 の場合は通常の record Database である。

type に指定するのは 32bit の database type というデータである。 この「database type」というのは、 Windows で言うところの「拡張子」に相当する。 そのデータベースが何を表すのか、 ということを示すために使われる。 どこにも type に関する一覧は存在しないが、 慣用的に使われている type はかなりある。 たとえばアプリケーションプログラムは 'appl' 型である。

creator ID は、実はその名とは異り「作成者 ID」ではない。 これはプログラムの ID である。 Palm のデータベースには全てに creator ID という番号がついている。 Application Launcher 上で「削除」する際には、 同じ creator ID を持っているものは全て一括で削除される、 など同一 creator ID のデータベースをセットで操作することが多い。

creator ID は Palm Computing のページ で登録することができ、 登録された creator ID は他のアプリケーションとは重複することはない。 基本的にはアルファベット4文字からなり、 全部小文字の場合だけは Palm Computing が 自前のプログラム用に予約している。


Palm が出しているプログラムの書き方に関するドキュメントでは、 次のような注意書がしてある:

あほか。

1つめの条件は Application が database と密着しているモデルしか 考えていないことを表している。

2つ目の条件はまぁ、フラットモデルだから許すとしよう。

しかし、3つ目のその条件は何だ。 database 名に creatorID をつける必要があるなら、 database は独立している必要性が全くない。 creatorID なんぞではなく、application 内部に database 名を resource などの形で埋め込んでおいて、 それを全部自動的に削除するようにすれば良い。 creatorID を必要とするなら、 同じ database 名でも互いに区別できるはずだ。

結局、ここにも PalmOS が何も考えられていないことが露呈している。 従って、私はここに次のようにすることを推薦する:


creator ID や type を指定する際に、 'DATA' のようにまるで文字列のようなものを指定する形が Palm 用のプログラムには良く出てくる。

これはちゃんと動く。 ちゃんと動くが、実は ANSI C strict に見て正しい記法ではない。 この記法では 各文字データ が 8bit であることを仮定しなくてはいけないし、 順に address の小さい方から記載した結果を ULong で呼んだ場合の 数値イメージとして処理すると言うことは、 big endian と little endian で値が異ることをも意味するが、 そのような定義を C 言語の定義は許していない。

困ったことに、Palm Computing はこのことを知らないらしい。 彼らは登録として ' で囲った4文字の値を使え、と主張する。

Database の type はとくにどこにも登録されていない。 のでどのような type があるのか判らない。 さらに困ったことに、type の値が判っても、 そのフォーマットが公開になっていないものも多い。 公開になってないのなら Database 名で処理して欲しいものだが、 なぜか同一の type を持っている異る名前の Database を検索したり、 混乱が見られるようだ。 文句を言うぐらいなら内容を公開し、 Yahoo などで検索できるよう登録してくれればいいのに。


Database を作るもう一方の方法は DmCreateDatabaseFromImage() を使う方法だ。

Err     DmCreateDatabaseFromImage(Ptr bufferP)

この関数は、bufferP に prc ファイルや pdb ファイルと全く同じ データイメージを与える。 すると、WorkPad の内部で HotSync を行ったかのごとく、 データベースを構成してくれる。

複雑なデータで Database を初期化する必要があるプログラムをつくった場合は 予め初期化データのイメージを pdb ファイルで作っておき、 プログラムに埋め込んでおくと、初期化がしやすいだろう。 もっとも私は使わないが。

どちらの API の場合も、Database の作成には時間がかかる。 まず、与えられた Database 名が妥当なのか、 既存のデータベースではないのか、 などが最初にチェックされる。 仮に妥当であった場合、他の引数の妥当性もチェックされる。 さらに全てが妥当であった場合には Database を新規登録する必要があるが、 このために必要なメモリ領域を確保するために空き領域を検索する。 Database リストを管理する領域に対する packing、 新しい chunk の確保のための空き領域の確保並びに必要に応じて garbage collection が行われる。


必要なデータベースが作成でき、 Find によって Database の ID を手にいれたら、 次にやることは Database に対するアクセスのための handle を 獲得することである。

データベースをアクセスするための handle を獲得するには、基本的には DmOpenDatabase() を使う。 今回の例でもこれを使う。

ちなみに creator ID と type が決まっていて、 それが1種類しかないと判っている場合は DmOpenDatabaseByTypeCreator() を使って、検索と handle 獲得を一括して行う、という手もある。 今回はこれについてはこれ以上言及しない。 これには問題がありすぎて使うべきではないからだ。


これ以外にも、record database や resource database を 順次 open していく、などの API がいくつもある。 はっきり言って、API が多いというのは、
「頻繁にそのようなタイプのアクセスがある」
というよりは、 システムのデザインが悪くて DmOpenDatabase() が 遅いことを示しているようだ。

DmOpenDatabase() を実行すると、 Database を管理するための memory chunk を lock して、 garbage collection target から外すようにしている。 これは、こうしなければ record access ができないので 当たり前と言えば当たり前なのだが、 この過程で「本当に lock してよいものなのか」等のチェックが延々はいる。 これが極めて遅い。

さらに、この「遅さ」にもかかわらず PalmOS の garbage collection の 効率はあまり良くない。一度 garbage collector が動き出したら、 領域確保に失敗する可能性はかなり高い。

これはある意味当然の話だ。 garbage collection が必要となったのはまさに「今使っている領域群」 にたいする操作が原因であり、 その「原因となっている領域群」をこの garbage collector は動かせないのだ。 Application プログラム自身が入っている database、 data を格納してある database などを格納した chunk は lock されているのだから。

lisp や smalltalk で効率的に動く garbage collection が、 C++ では余り効率よく動かず、苦労する理由はここにも原因がある。 また、RT-Java などが苦労し、フォーカスを当てているのもこの点である。 昔から存在する領域は時間があるときにじっくりと最適化し、 目先の忙しいときには「まさに今 Hot な領域」を中心に garbage collection をかけることで gc にかかる時間を減らそうと言うわけだ。

こう考えると、PalmOS の gc 戦略がいかに「間違って」いるかわかる。 「今まさに lock している」Database や「今まさに lock している」record を最適化するのが早道なのに、それらは動かせないのだから。 こういう場合は「gc を使うという発想が間違っている」のだ。

もちろん、gc なしでは動かなかった 128kbyte しか RAM がなかった昔は やむをえまい。 だが、いつまでもそれに拘泥する必要はないはずだ。 通常 512kbyte を越えたら、Knuth & Standish による boundary tag 法(これも結構古典になってしまったが) を使った方が効率は良くなる。


関数は

   DmOpenRef DmOpenDatabase( UInt cardNo, LocalID dbID, UInt mode );

と定義されている。

dbID は DmFindDatabase() で得られた database ID で、 0 ではないことを確認してあるもの。 cardNo には DmFindDatabase() で私たものと同じ値を渡す。

mode 用の定数は大きく分けて6種類あり、 (open() に対する mode 引数と同様) bitwize or で結合して使える。 その値によって Database に対する open のしかたが変わる。

dmModeReadWrite
database を読み書き両用で開く。
dmModeReadOnly
database を読み込み専用で開く。書き込むことはできない。
dmModeWrite
database を書き込み専用で開く。
dmModeLeaveOpen
application 終了後も database を open したままにする。
dmModeExclusive
Database を閉じるまで、他の thread によるその database の open を禁止する。
dmModeShowSecret
これは説明が必要だ。
database は record の配列からなるが、 各 record にも attribute が存在する。 その中に private record というマークが存在する。 通常、このレコードは参照されないが、 このオプションをつけて database を open すると、 それらの record を参照できるようになる。

DmOpenDatabase()の戻り値は Database に対する handle である。 ただし、0 だった場合は open に失敗している。


DmOpenDatabase() を使って open したものは、 終了前に close しなくてはいけない。

dmModeLeaveOpen などという mode があるぐらいな事からも判るように、 Application が終了する際に一応解放を忘れた dynamic heap 領域、 close を忘れた Database や record は OS によって検索され、 適宜解放される。 しかし、これに頼るのはお世辞にも良い習慣ではない。


こういっておいてなんだが、次のようなことも言える。

まず最初に、この機能に頼っても特に実行時間が遅くなるわけではない。 PalmOS は Application 終了時に dynamic 並びに storage heap を検索して 終了したはずの Application に属しているとマークされている chunk を 解放し、unlock して歩く。 この場合「検索」にかかる時間はたとえ全て解放していたとしても、 同じである。 結局存在する全ての chunk を検索する必要があるのだから。 単に検索の一部が Application 実行時にクリアされるか、 Application が終了してから全てが行われるのか、の違いでしかない。 また、解放に至っては「開放されるべきものを全て解放する」ので 全く同じ時間しかかからない。

次に、PalmOS のこの機能に期待するのは無謀である。 PalmOS のデザインはしみじみと酷いできである。 ということはどこにどのようなバグが潜んでいても不思議はない。 「全ての chunk を検索」すれば発見できるものを、 全て発見するとは限らないじゃないか。 今のところ、「大きなリーク」は報告されてこそいないが、 ないとは言えない。 その場合、data を書いた record や database がちゃんと close されておらず、 問題が生じる可能性は高い。

結局のところ、「着実な」ソフトを作るのであれば、 まじめに自力で全て close するべきだ、ということだ。 PalmOS も補助はしてくれるが、所詮は補助に過ぎず、あてにすべきではない。


database への handle を閉じるには、 DmCloseDatabase()を使う。これは

   Err DmCloseDatabase (DmOpenRef dbR)

と宣言されている。

dbR には DmOpenDatabase() で得られた 0 ではない database への ハンドルを渡す。

戻り値としてはシステムエラーの値が帰ってくる。

システムエラーの値のうち、 Database 操作関連のエラーだけでもかなりの数がある。 DmCloseDatabase() 自身に関しては、 大半のエラーは返されても「どうしろと言うのだ」と言いたくなるような エラーばかりなので、 ここではリストは設けない。


一旦 Database への handle を獲得したら、 Database の中の record を作成・参照・変更したいはずだ。 そのためにはまず、いくつrecord があるのか調べなくてはいけない。 これを可能にする方法は2つある。

DmNumRecords() という関数は

UInt DmNumRecords (DmOpenRef dbR)

と宣言されている関数で、 すでに open されている Database へのハンドルを利用して その Database に存在する record の数を返す。

DmDatabaseSize()という関数は

Err DmDatabaseSize (UInt cardNo, LocalID dbID, ULongPtr numRecordsP, ULongPtr totalBytesP, ULongPtr dataBytesP )

と宣言されており、 open されている、いないに関わらず、 cardNo とdbID で指定された database に関して、 record の数(numRecordsP)、 管理用オーバヘッドも含めた database 全体のサイズ(totalBytesP)、 各 record のサイズの合計(dataBytesP) を得ることができる。

ここには重要な条件がある。

Database の record の数、というのは正確には Database が管理している record への handle の数だ。 各 record への handle には index 番号がふられていて、 0 から (numRecords-1) までの index 値を取ることができる。 しかし、全ての record への handle が record を指しているわけではない。 実際には NULL を指している handle を保持しているものも含まれる。

これは当然そうなる。 その内容が互いに互いをポイントする構造になっているタイプの 膨大な数の record を管理しなくては行けない場合、 各 record の index を使う以外の方法は、普通、ない。 ということは、record の index 値を保存したまま record を 追加、消去しなくてはいけない。 これを可能にするには、 ある record よりも小さな index 値を持つ record を消去しても、 そこには NULL handle を格納して、 全体の record 数を変化させない、という処理をするしかない。

NULL handle を除いた、record の総数を調べる方法は DmRecordInfo()を全ての record index に対して実行し、 record が実体を持っていることを確認して回るしかない。 これは各 record について延々とチェックを繰り返すので 物凄く時間がかかる処理である。

今回は、record を消すことはしないことにしよう。 そうすれば DmNumRecords() で返ってきた値はそのまま有効な record の総数と することができる。


新しい record を作る方法が必要である。 これには DmNewRecord()を使う。

DmNewRecord() は次のように宣言されている。

VoidHand DmNewRecord (DmOpenRef dbR, UIntPtr atP, ULong size);

dbR はすでに open した database へのnンドルである。 指定された database に新しい record を追加する。

size は新しくつくる record の大きさを指定する。 record は基本的には size を 0 にはできない。 また、OS のバージョン依存ではあるが、64k 弱のサイズよりも大きな サイズにすることもできない。 推奨最大値は(PalmOS に依存しない形にしたいなら) 65500 byte である。

atP は record を作成して欲しい index 番号を指定するための引数であり、 同時に実際に作成された record の index 番号を表している。 ユーザーは UInt な 変数 at (いや、名前は本当はどうでもいいんだが)を作り、 そこに自分が希望する record の index 番号を入力して DmNewRecord() を呼ぶと、 実際に record が作られた index 番号が返ってくるのだ。

もし、新しい record が作られた場合はその record への「ハンドル」 が返ってくる。 同時に atP の値が更新されている。 もし、何らかの理由で record がつくられなかった場合は、 戻り値は 0 になる。この場合の atP が返す値は不定である。

仮に record を作ること自体は問題がなかったとしよう。 すでに n 個の record を持っている database に1つ record を追加する場合、 &at で渡す引数にどのような値を与えるとどのように record が database 全体の中で配置されるのか説明する。

at に 0 から (n-1) までの値を与えた場合、 record は与えられた index 番号 at に『挿入』される。 仮に 3 を指定したとするならば、旧来 index 3 にあった record は index 4 に、 旧来 4 であったものは 5 に…と1つづつ増加した index を持つようになる。

at に n から dmMaxRecordIndexまでの値を与えた場合、 at には n が返る。 そして、record 配列の n のところに新しい record が『追加』される。 ようするに、record は database 上では chain 構造を持っていると 考えて構わない。


実際には、database 上で record はただの1次元 chain 構造で 管理されているわけではないらしい。 具体的な数字は判らないが(おおよそ 1k個だと言われている)、 ある一定の数の record を管理するための1次元配列があり、 その1次元配列が chain 構造で繋がっているのだという。 この 1次元配列構造は多少すきまがあっても構わない形で管理されており、 これによって record に対する追加、 消去処理が膨大な「配列コピーと chunk 領域の reallocation/resize 処理」にならないよう、 同時に record の index 指定に対して record の実体を検索する 速度が遅くならないよう、 配慮されている、と言う。

これがどの PalmOS についても言えることなのか、 確認することはできていない。 が、PalmOS 3.0 ならびに 3.1 においては、 index による record の検索が比較的 database 上の record 数に依存しない速度で動いているのは事実である。 ただし、PalmOS API の応答速度はどれも「常識的に考えた場合の速度」 よりもはるかに遅い。 このため、API がデフォルトで持っている overhead が大きすぎて、 実は O(n) の処理なのだが係数が小さすぎて 問題とならないだけなのかもしれない。


既存の record に対する handle を獲得しよう。

先にも少し言ったが、memory に対するアクセス function は 全ての heap 領域で共通になるように作られている。 つまり、chunk に対する handle を獲得し、 その handle を lock して pointer を獲得し、 pointer を使って参照し、 unlock し、handle を解放する、というのが memory アクセスの 基本パターンになる。

Database 領域は chip selector によって、 通常時の書き込みは禁止になっている。 このため、pointer を獲得してもそれだけでは書き込みを行うことはできず、 書き込みは専用 API を使うことになるが、 複数の thread で同時に同じ chunk に書き込みを行おうとすると racing 状態が発生する危険性がある。 この状態を回避するために書き込み時には排他制御を行う必要があるが、 この排他制御も handle から pointer を獲得するという作業で行う。

つまり、PalmOS では、memory chunk を特定する方法として handle を使い、 その領域に対する access を可能にするために handle から pointer を生成するわけだ。

handle を獲得するにはDmGetRecord()を使う。 これ以外に DmQueryRecord() のような API も存在するが、 今回は使わないのでとりあえず DmGetRecord() に説明を集中する。

VoidHand DmGetRecord( DmOpenRef dbR, UInt index );

dbR は DmOpenDatabase() で獲得した 0 ではない database への handle である。

index は目的とする record の index である。

この関数の戻り値として、record の handle が返る。 ただし、handle が 0 の場合は handle 獲得に失敗したことを意味する。

この関数の動作は実は見てくれよりもはるかにややこしい。 index で指定した record がすでに一度同一 application によって DmGetRecord() で open されている場合、 handle は正常に獲得されることはなく、 0 が返される。 ただし、DmQueryRecord() で open されているばあいは エラーとはならない。

一方 index で指定する値が DmNumRecords() で返される値の範囲外を 指していたり、index で指定している record が実は存在しない (管理上 handle が 0 になっている場合)、 Fatal Error になって、エラーウィンドウが表示され、 ソフトウェア reset されてしまう。

DmGetRecord() は handle を正当に獲得できた場合、 record を管理する領域に「busyビット」がたてられる。 この busy ビット は handle の解放時にクリアされるが、 万が一この busy ビットが記録されたままプログラムが終了してしまうと busy ビットが立ったままになる。 この状態で DmGetRecord() が呼び出されると、 やはり Fatal Error になる。


DmGetRecord() に対する操作はかなり注意する必要がある。

まず Fatal Error が出る。 エラーを返すのでも、detector を指定できるのでもなく、 いきなりエラーウィンドウが開いて reset されてしまう。 DmGetRecord() のような API に対して、 このような interface にするというのは非常識なデザインであるが、 そうなっている。

2つ目は、「Fatal Error が出る条件が不安定だ」という点である。 例えば database の index としては有効だが、 record の存在しない index を指定した場合、 1度目は 0 が返るだけなのだが 2度目になると Fatal Error が出る、 という場合がある。

おそらくは、handle として NULL が返っているにも関わらず、 busy bit が立っているのだろう。 存在しない record 本体に対する busy bit を立てないで欲しいもんだが…。

この性質はさらに OS のバージョンによって変わる。 3.1 と 3.0 ではこの性質が違うため、 3.0 だと動くものが 3.1 では動かないコードというのがかなりあり、 デバッグに苦労した記憶がある。

DmGetRecord() は動作速度が以外と遅い。これにも注意が必要である。 index から record handler を検索する事自体は遅くないはずなので、 遅くなる理由として考えられるのは、 busy bit を立てる処理は排他制御が必要になる、 という点だけである。 排他制御を必要とする API は一般に 10msec(1/100sec) ほどかかることが多い (それ以上になるものもあるが)。 thread を切り替える context switch 用の tick が 100Hz で かかっていることを考えると、 排他制御の実装か context switch 回りのコードがおかしいのではないか? と推察される。


DmGetRecord() で強調したように、 獲得した record handle は解放しなくてはいけない。

DmReleaseRecord() がこの解放用の API である。

Err DmReleaseRecord (DmOpenRef dbR, UInt index, Boolean dirty)

と定義されている。

dbR と index は DmGetRecord() で handle を確保したときと同じ引数を渡す。

dirty としては true(1) あるいは false(0) を渡す。 dirty を true にした場合、 database 上の record の attribute の dirty bit が立つ。 後で HotSync をするときに quick HotSync を行う場合は、 dirty bit の立っている record だけが変更されているものとして、 dirty bit が立っている record だけを PC に転送する、 などの使い方をする。

戻り値としては、エラーがなければ 0 が返る。 エラーが出た場合はエラーが返る。


Database は close し忘れても、比較的大丈夫である。 では record は?

実は record は必ず release しなくてはいけない。 これを忘れると、HotSync でデータベースを持ってくることもできなくなる。

record を DmGetRecord() で確保すると、 busy ビットが立つ。 そして、これは各種 Fatal Error を発生させる元になる。 プログラムを強制終了させると database は close されるが、 record は release されない。 このため、record を release し忘れると永久に復帰不能な状態に陥る。

結局 database だけ自動的に閉じてもらっても何もうれしくない。 全く後先の考えられていない API もあったものだ。


record に対する handle が正常に獲得できた場合、 この handle から pointer を獲得する必要がある。 pointer を獲得しないと値を読み書きできないからだ。

handle から pointer を獲得するには MemHandleLock() という API を使う。

VoidPtr MemHandleLock (VoidHand h)

というプロトタイプ宣言がなされている。

h は DmGetRecord() などで獲得できた 0 以外の handle である。

handle が適切なものであればこの関数は handle に対応する chunk への pointer を返す。

この関数は pointer を返すと同時に handle に対して lock を実行する。 lock された chunk は garbage collection 処理対象からはずされる。 これはこの chunk が unlock されるまで続く。

DmGetRecord() と異り、 handle に対して 14回まで重複 lock することが許されている。 これは内部に reference counter が存在するためである。 reference counter があるということは、 逆に言えば MemHandleLock() を呼んだのと同じ回数だけ handle を unlock する必要がある、ということだ。


garbage collection の事を考えると、handle の lock 期間は 必要最小限に抑えるべきだ、ということになる。 また、解放できるタイミング全てで一旦 unlock してから再 lock したほうが 良い、と言うことになる。 が、現実問題として、それはやるべきではない。

MemHandleLock() API と MemHandleUnlock() API(これは unlock 用の APIだ) は かなりの遅さを誇る。 排他制御 overhead が4重ぐらいにかかっているのではなかろうか? と疑問視したくなるほど遅いのだ。 実際、record を操作するには chip selector を操作して、 通常 read only な領域を read write に書き換えなくてはいけない。 従って、確かに排他制御はそれぐらい必要になるかも知れない。

しかし、通常、排他制御というのは「本当に衝突するのでなければ」 数十 usec ぐらいで終了する処理のはずの処理だ。 ところが PalmOS は 10msec 単位でかかることがある。 これははっきり言って PalmOS の謎の1つであり、 おそらくはかなり重大なバグの在処を示していると想像される。 なぜかというと、PalmOS では「context switch」用のタイマー割り込みの 発生頻度も 10msec 単位なのだ。 排他制御は thread scheduling 機構とタイトに結び付いている。 そこに「context switch」のタイミングと同じ時間がかかる、 というのは「何かある」と見るべきだろう。

結果、gc のことを考えてなどいられないような状況が発生する。 gc 処理をしたければ、Application を終了するまさにその直前か、 起動直後でまだなにも open していないタイミングを狙った方が良さそうだ。


lock した handle は unlock しなくてはいけない。 MemHandleUnlock()でそれを行う。 lock と unlock は厳密に対応づけられている必要がある。 unlock, unlock, lock, lock のような「数だけあっている」ような 処理は許されない。

Err MemHandleUnlock (VoidHand h)

とプロトタイプ宣言されている。

h に渡すのは、MemHandleLock() の時に渡したのと同じ handle である。

戻り値としては、エラーがなければ 0 が、 エラーが生じた場合はエラーコードが返る。


record の pointer を獲得した後、 record に記述されているデータの参照は通常のメモリ空間に対するそれと 全く同じに行って構わない。 注意するべき事があるとすれば、 MC68000 という CPU は short や long などの型に対するアクセスは 偶数アドレス単位でしかできない、という点に注意することぐらいである。 この条件を満たすために、 byte aligned な構造体は C コンパイラーの方で作れないように 排除してくれている。 ユーザーが無理やり奇数アドレスからの複数バイト読み出しさえ要求しなければ 全く問題は生じない。 逆に奇数アドレスからの multi byte アクセスを行ったら、 その瞬間に Fatal Error が発生する。 注意するように。

問題となるのは書き込みの方である。

Chip Selector によって通常書き込み禁止になっている領域に対する 書き込みは「レンジチェック」などの必要性から、 DmSet()DmWrite() という2つの API を使うことになる。

DmSet() は memset() のようなものである。 指定された領域を一定の値で埋め尽くす。

Err DmSet (VoidPtr recordP, ULong offset, ULong bytes, Byte value)

とプロトタイプ宣言されている。

recordP には MemHandleLock() で渡された、 record の先頭へのポインターを渡す。 ここに record の先頭以外へのポインターを渡した場合、 動作は不定である。 私が経験した最悪な状態は Fatal Error だった。

offset は書き込みを開始する領域の、 recordP からのオフセットのバイト数である。

bytes は書き込むバイト数である。

value は書き込みイメージの値である。

この関数の戻り値としては、 エラーがなければ 0 を、エラーがあった場合はエラーコードを返す。

DmWrite() は memmove() に相当する動作を行う。

Err DmWrite (VoidPtr recordP, ULong offset, const void * const srcP, ULong bytes)

とプロトタイプ宣言されている。

recordP は DmSet() の時と同様、record の先頭への pointer しか 渡してはならない。それ以外の pointer は無効であり、Fatal Error になる。

offset は record の先頭からの offset 値(byte) で、 recordP と offset を使って書き込み開始ポイントを指す。

srcP は書き込みたいイメージへのポインターを指定する。 srcP 自身は record だろうが通常の dynamic heap 領域だろうが構わない。

bytes は書き込みたいイメージのバイト数を指定する。

DmWriteCheck() という API がある。

Err DmWriteCheck (VoidPtr recordP, ULong offset, ULong bytes )

と宣言されている。

DmWriteCheck() は(srcP を除いて) DmWrite() と同じ引数をとる。 この関数の目的は、DmWrite() を本当に実行できるか、 同じ引数を用いてチェックすることにある。

DmWrite() 自身を実行するのはかなり危険なので、 その前にチェックできるものはチェックしろ、 ということなのだろうが現実問題としては DmWriteCheck() で検出できる 程度のエラーであればどうせ Fatal Error にはならないので、 チェックするだけ無駄である。


DmWrite() と DmSet() の遅さこそが、PalmOS 最大の欠点だろう。 recordP から record handle を復元しさらにその有効性を調べる、 というのが最初に行われているようだが、 この pointer → handle 復元自体が極めて遅い。 MemPtrRecoverHandle() という API があって これを使っているのだと思われるのだが、これが著しく遅いのだ。

これはある意味当然である。 user から渡されてきたポインターは信頼できるものではない。 PalmOS は「得体の知れないポインター」は使えない。 アクセスした瞬間に Bus Error を発生して Fatal Error になるのでは 何の意味もない。 従って、 やむなく「自分が管理している memory chunk handle 一覧」を参照して、 その中で lock されている memory chunk を見付けだし、 該当する pointer がないか検索しているのだろう。 わざと間違ったポインターを渡したときにかかる時間から推定して、 これ以外の手段をとっているとは思えない。

しかし、同時にこれはどえらい作業である。 渡された pointer が存在する card 番号は簡単に判る。 しかしその中のどの heap なのかを割だし、 そこにある全ての chunk に対して検索する必要がある。 storage heap の場合、database と record の両方について 検索する羽目に陥るだろう。 64kbyte という record サイズの制限は record の数の増大となって 表出するのでこの検索はさらに大変なものになる。

recordP の妥当性が確認されたらそのサイズも判るので、 offset と bytes で指定された範囲が妥当かどうかも判る。 これらのチェックが終了したらあとは memmove() や memset() と同じ事をやる。

PalmOS に実装されている memmove() の動作速度もお世辞にも速くない。 MC68000 用の memmove() については tips が大量に存在し、 それらをどれぐらい良く知っているかによって動作速度は何倍も異なる。 どうやら PalmOS に入っている memmove() は余り賢くないらしい。

ただし、CodeWarrior に gcc などの memmove() コードを持っていって コンパイルすると良いパフォーマンスが得られない。 理由はいくつかあるらしい。

この memmove() の際に、主に srcP 側が理由で、 Fatal Error が発生することがあり得る。 DmWriteCheck() が無意味なのはこれが最大の理由だ。 「read するだけならどこでもいいのではないか?」 と思うかも知れないが、 何のデバイスも繋がっていないアドレスに対してはさすがに bus エラーが発生する。 これはそのまま Fatal Error となって表出する。

結局、DmWriteCheck() を行っても Fatal Error を回避できない、 というのはそういうことだ。


RAM 領域に対する read-only 機能は chip selector という unit が 実装している。 もし、PalmOS API を一切叩かず、NetLib のような background で動いている thread も存在しない状態でならば、 chip selector を直接操作すれば、DmWrite() がかかえる遅さの問題は 解消できる。 全ての責任はプログラマーが持つ、ということと、 万が一失敗したらシステム全体が崩壊する、 という危険性をはらんではいるが。

というわけで、実は現在、この方法を実装する、 「良い方法」を募集中である。
「こんな方法で実装してみましたぁ」
何て言うのがあったら是非 奥山 までご一報いただきたい。


最後の最後に、 DmGetLastErr() という関数を紹介して API の説明を終わりたい。

今まで説明してきた Dm なんちゃら… という API 関数の中には、 実行時のエラーコードを返す物と、返さない物があった。 返さないものの場合、エラーが発生したことは判っても 何が発生したのを知る方法がなかった。 この関数はそのエラーを調べるためのものである。

Err DmGetLastErr (void)

が prototype 宣言になっている。

この API を使う際には重要な注意点がある。

この API はその名前と異なり、 「本当の意味での最後のエラー」を返すわけではない。 たとえば DmWrite() という API はエラーを返すが、 DmWrite() でエラーが返ってきた直後に DmGetLastErr() を呼んでも DmWrite() と同じエラーが返るわけではない。

この API は基本的に「エラーを返さない API のエラーを獲得するためのもの」 という事になっている。 たとえば、DmOpenDatabase() はエラーコードを返さない。 ここで 0 が返ってきた場合は、DmGetLastErr() を使ってなぜ失敗したのかを 調べるわけだ。

しかし、「エラーを返さない API のエラー」しか反映しないのかというと そういうわけでもない。

   DmOpenDatabase(....);   // This call will cause error.
   DmCloseDatabase(....);  // This call will cause error too.
   error   = DmGetLastErr();

というような例の場合、 DmGetLastErr() は DmCloseDatabase() のエラーの内容によっては、 DmCloseDatabase() のエラーの影響をうけてしまうのだ。

これはおそらくこういうことなのだと思う。

DmCloseDatabase() のような API は、 呼び出された直後のチェックの段階でエラーが検出できる場合、 単純にエラーを返すだけである。 この場合は DmGetLastErr() は DmCloseDatabase() の影響をうけない。

一方、DmCloseDatabase() の最初のチェックは通過したのだが、 その後ずーーーっと処理を続けていく過程で、 DmOpenDatabase() 等と同じ内部 API を使いそこでエラーが発生したとしよう。 この場合、この「内部 API」がエラーを保存する変数に直接作用してしまい、 結果、DmGetLastErr() は DmCloseDatabase() の影響をうけてしまう。

結局、エラーが発生したらとっととなになのか調べろ、ということらしい。


これで、必要な API に対する説明は全て終了した。 エラーコードについては、ここで説明しない。 では、次がプログラムだ。


Program Code

最初に述べたように、 このプログラムはプログラムが起動された際の起動コードを保存する、 というものである。

まず、エラーチェックを最小限にすることにしよう。 Fatal Error は出されるとやっかいだが出ちゃったらしょうがないし、 一方でまじめにエラーチェックをやってたら日がくれる。 Database の根性がネジ曲がっているのはすでに述べた通りだ。

次に、データを文字列で保管することにしよう。 なぜその方が良いのかは、論より証拠。 一発実行した方が判りやすい。

プログラムコードは次のようになる。

#include <Pilot.h>
#include <stdio.h>
#include <string.h>


#define testDBCreator   'OKKY'
#define testDBType      'Data'
#define testDBname      "testlog"


DWord                   PilotMain( Word cmd, Ptr cmdPBP, Word launchFlags );
static void             Log( DmOpenRef logDB, char *strP );


DWord
PilotMain( Word cmd, Ptr cmdPBP, Word launchFlags )
{
    char                buffer[128];
    LocalID             dbID;
    DmOpenRef           logDB;

    dbID        = DmFindDatabase( 0, testDBname );
    if ( dbID != 0 ) {
        logDB       = DmOpenDatabase( 0, dbID, dmModeReadWrite );
        if ( logDB == 0 ) {
            return 1;
        }
    } else {
        if ( DmCreateDatabase( 0,
                               testDBname,
                               testDBCreator,
                               testDBType, false )) {
            return 1;
        }

        dbID        = DmFindDatabase( 0, testDBname );
        if ( dbID == 0 ) {
            return 1;
        }
        logDB       = DmOpenDatabase( 0, dbID, dmModeReadWrite );

        sprintf( buffer, "\n\nOKKY\n\n" );
        Log( logDB, buffer );
    }

    sprintf( buffer, "cmd = %d\n", cmd );
    Log( logDB, buffer );

    DmCloseDatabase( logDB );
    return 0;
}


static void
Log( DmOpenRef logDB, char *strP )
{
    VoidHand    recH;
    char        *recP;
    int         len;
    UInt        index;

    len         = strlen( strP );
    index       = DmNumRecords( logDB );
    recH        = DmNewRecord( logDB, &index, len );
    if ( recH != 0 ) {
        recP    = MemHandleLock( recH );
        DmWrite( recP, 0, strP, len );
        MemHandleUnlock( recH );
        DmReleaseRecord( logDB, index, true );
    }
}

他のファイルは前出の 何もしないプログラム と基本的に同じで構わない。 あ、ただ MakefileCFLAGS 変数を

CFLAGS			= $(PalmCFlags) -O3 -g

に設定して欲しい。 -O3 にした方が出力コードは良いものになるし、 gcc の出力は -g によって品質が悪くなることはない。 prc ファイルには -g オプションが出力したデバッグ情報は 記録されないので特に理由がない限り -g はつけっぱなしで構わない。

プログラムの説明をしよう。

プログラムは PilotMain() と Log() の2つの関数からなる。 Log() を別関数にしたのは、 単に record への記録が面倒な作業なので1つにしたかったからに過ぎない。 この機能の分け方が良いなどとは思わないで欲しい。

最初に行うのは、記録用の database の検索である。 このプログラムは HotSync を実行した直後に一度起動されるが、 多分その最初の起動の時以外は database は発見されるだろう。 発見されたら database を open して、 logDBというハンドルを設定する。 この作業のどこかでおかしいことがあったら、 プログラムを終了する。 ちなみにこの終了コード 1 は「でたらめ」である。 0 以外の何かを返す、事しか考えていない。 正しいエラーコードを調べるのはうっとうしいし、 だいたい調べて返せたからこの場合どう事態が改善されるというのだろう? (結構乱暴なもののいいようである)

database が発見できなかった場合、まず database を作成しなくてはいけない。 作成に成功したら、その「今作成したばかりの database を」探し直す。 発見できたらその database を open するところまでは database を 無事発見できた場合と一緒である。

database を新規作成した場合、"\n\nOKKY\n\n" という文字列を 作成し、Log() に送りつけている。こいつの効能はあとで説明する。

あとは簡単である。 sprintf() を使って、cmd を表現した文字列を発生させ、 Log() に送りつける。 で、database を close して終了する。

Log() という関数の中でやっていることは単純で、 新しい record を与えられた文字列帳に合わせて作成し、 その文字列を record に記録しているだけである。 エラーチェックをほとんど何もしていないので、 実はこれだと結構危険なのだが、まぁ、テストだ、かまうまい。


make を実行して prc ファイルを作成し、pilot-xfer を使って WorkPad に転送する。 そして 2,3 度実行してみる。 相変わらず一瞬画面を白くするだけですぐ終了する。

ただ、前回と違い、 pilot-xfer -l を実行してみると、 testlogという database が新しく 作られていることに気がつくだろう。 もちろん、このプログラムが作成したものだ。 こいつを取り出して testlog.pdb ファイルを PC 上にコピーする。

testlog.pdb をテキストエディターで開いて欲しい。 ただし、この「テキストエディター」にはいくつか条件がある。

私が普段使っている Meadow はこの条件を満たすので開いてみよう。

testlog^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@...........
(中略)
.....^@^@

OKKY

cmd = 3
cmd = 0
cmd = 0

さ、これでなぜ「最初に変な文字列を入れたのか」と「文字列で記録したのか」 が判っただろう。 ようするに目印だ。

pdb ファイルのフォーマットは管理情報がファイルの先頭の方に集中している。 実際のデータは連続領域に存在しており、 データ的には別 record だろうが同一 record だろうが関係なく、 ただひたすら record の記録順に並んでいるだけなのだ。 従って、"\n\nOKKY\n\n"のように 最初に改行コードをいくつか連続させてから、 典型的な文字列を放り込み、さらに改行をいくつか続けた文字列を データとして先頭に埋め込んでおき、 ファイルの先頭から 『OKKY』行までを切り捨てると、 PC 上では通常のファイルに記録したのと同じイメージが得られるのだ。

database 操作は時間がかかる作業なので、 高速で処理しなくてはいけないプログラムに対しては バッファリングなどを考えなくてはいけないが、 この方法を使えばプログラムを停止させることなく実行ログを保存できる。 かなり応用の効く方法なので覚えておいて損はないだろう。

さて。これでめでたくデータを作成するプログラムも組めるようになった。 よかったよかった。


Prev 1 , 2 , 3, 4 , 5 , 6 , 7 , Next


change log

2000/01/30
sysAppLaunchCmdReturnFromPanel という起動コードでは global は使えないことを教えてもらう。 しかもどの Panel から返ってきたのか判らないというじゃありませんか。 なんですかそれは?ますます意味がないじゃあーりませんか?と思う。 そもそも Panel を使う意味がないっす。
さらに、pilot-tech-ml でみずかみさんが流してくれた POSE のエラーを 見て
「あぁ、global は sysAppLaunchCmdNormalLaunch 以外では まじで使えないんだ」
と悟り、そう言う風に書き直す。
さらにてにおはをいくつか直す。

このページに関する コメント、情報、誤りなどの情報がありましたら、 奥山 まで。