PalmOS 5.x Hack開発講座 第6回 「メニューの文字化けを直す」その2

 前回からの続きです 肝心要のARMに関係のあるところを見てゆきましょう

3.3.ARMに関係のあるところを... [MenuAddItem-03e9]、[ResLoadMenu-03e8]ターゲット

3.3.1.APIコール [APICall.h]、[APICall.c]

 今回のプロジェクトの構成を見て、「あれ?[libarmboot.a]や[libarmui.a]がくっついていないよ」と気が付きましたか?

 まず、OS5ではどのようにAPIをコールしているのかということからお話しましょうね いきなりアセンブリレベルの話になりますが目を通してみてください ARMアーキテクチャに関するドキュメントはhttp://www.arm.comから入手できますので参考に...

 まず、OS5ではAPI関数の個々のアドレスを羅列したトラップベクターテーブルというものを複数持っています 例えば、[WinDrawChars]であれば、SystemAPIに関するトラップベクターテーブルの先頭アドレスから0xB18バイトオフセットしたアドレスに[WinDrawChars]関数の先頭アドレスが記録されています

 つまり、何らかの方法で[WinDrawChars]の先頭アドレスを取得して、プログラムカウンタをそのアドレスにセットしてあげれば、[WinDrawChars]関数が実行されるということになります

 では、この個々のAPI関数のアドレスを取得するにはどうしているのでしょうかというお話を続けてゆきます

 まず、[r9]というレジスタにシステムグローバル領域の末尾アドレスが格納されていて(という決まりでPalmOS5は書かれています)、このシステムグローバル領域のアドレスから-4バイトオフセットしたアドレスにハードウエアレベルのAPI関数のトラップベクターテーブルの先頭アドレスが記されています
 -8バイトオフセットしたところにはシステムレベルの、-12バイトオフセットしたところにはUIレベルのAPI関数のトラップベクターテーブルの先頭アドレスが記されているということになっています

 例えば、[WinDrawChars]関数を呼び出すには

1.[r9]レジスタに格納されているアドレス値を読み出し
2.その読み出したアドレス値から-8引いたアドレスに格納されているアドレス値を読み出す
3.この読み出したアドレス値がシステムレベルのトラップベクターテーブルの先頭アドレスなので、このアドレス値に0xB18を足して
4.そのアドレスに格納されているアドレス値を読み出してプログラムカウンタをセット

 はあ、言葉にすると難しいですね

 例えば、こんなコードがあったとします


void myFnc(void)
{
	Char str[10] = "abcdefghi"
	WinDrawChars(str, 9, 10, 20);
}

 このコードと同じことをアセンブリコードで書いてみましょう


void myFnc(void)
{
	Char str[10] = "abcdefghi"
	//WinDrawChars(str, 9, 10, 20);
	asm{
		mov r3, #20
			//引数の4番目にy座標をセット
		mov r2, #10
			//引数の3番目にx座標をセット
		mov r1, #9
			//引数の2番目に描画する文字数をセット
		mov r0, str
			//引数の1番目に描画する文字列のポインタをセット
		ldr r12, [r9, #-8]
			//r9レジスタに格納されている値から-8オフセットしたアドレスに
					  格納されている値を読み出してr12レジスタに格納
		mov lr, pc
			//この呼び出し元に戻ってくるためにリターンアドレスをセット
		ldr pc, [r12, #0xb18]
			//r12レジスタに格納されている値から0xb18オフセットしたアドレスに
					  格納されている値を読み出してプログラムカウンタにセット
	}
}

 こんな感じです

※コード中で[asm{ ... }]とくくっているところはインラインアセンブラといって、大概のコンパイラに実装されている機能で、cもしくはc++で記述しているコード中にアセンブリコードを埋め込めるというものを利用しています このインラインアセンブラの書き方は特に何かに定められているわけではありませんので、コンパイラによって異なります お使いの環境に合わせて修正してください

 個々のAPI関数がどのレベルに属するのか、またトラップベクターテーブルのどこにアドレスが格納されているのかという情報は、[YAHM Dev Info.]からダウンロードしたSDKの中に含まれている[SysTables.txt]に書かれています

 なんとなく解れば、あとは簡単 今回お話しているプロジェクトの中に含めている[ArmApiCall.c]からひとつ抜粋しましょう


// ArmApiCall.c

#include "ArmApiCall.h"

#pragma thumb off
...

// SystemTable 0x424 FtrGet
asm Err FtrGet(UInt32 creator, UInt16 featureNum,UInt32 *valueP)
{
	ldr r12,[r9,#-8]
	ldr pc,[r12,#0x424]
}

...
#pragma thumb reset

 ここでは、[FtrGet]関数を引っ張ってきました [FtrGet]関数のプロトタイプ宣言は<PalmOS.h>をインクルードしてあればすでに済んでいるので、ここではその関数の実体を書くだけでOKです ここでは、呼び出し元にダイレクトにリターンしてもらうために、リターンアドレスのセットはしていませんが、行っていることは先の例と同じです 留意点は、ARMコードで書いているために、先頭と末尾にプラグマ文を入れてARMコードでコンパイルするように指示していることくらいでしょうか

 ...っと、大事なことがありました OS5では本体メモリに[cardNo]という概念がなくなっているので、データベース操作がらみの関数に変更が加わっています [ArmApiCall.c]の中では、変更されたAPI関数のひとつである[DmGetNextDatabaseByTypeCreator]について実体を記述してありますので、このあたりを参考にしてください
 基本的に、引数の中に[UInt16 cardNo]が含まれている関数は変更されています といっても、引数から[UInt16 cardNo]を単純に取り除いただけなので難しくはありません

 そうそう、[ArmApiCall.c]には、今回使用しているAPI+αしか記述していませんので、必要に応じて追加していってくださいね あと、[APICall.h]のほうは、OS5で変更が加わったために<PalmOS.h>をインクルードしてもプロトタイプ宣言がなされないもについての宣言を行っているだけですので簡単です

 このあたりの記述が面倒な場合には、素直にIgorさんが用意してくれているライブラリを利用しましょう (なぜかは解りませんが、Igorさんが用意してくれているライブラリには個々の関数に8バイトのパディングが入っていますので若干生成されるコードが大きくなります)
 ただし、ARMコードとThumbコードを混在させる場合には、ここでお話したように自前でAPIコールの実体を記述しなければいけませんので、その場合はあきらめてこつこつと書いていってくださいな

 さて、Hackコードのお話に移りましょう

3.3.2.[MenuAddItem.cpp]

 こちらのほうが簡単なので、先にお話させてくださいね もう一個のほうは、スタック領域を自前で確保するように書いていますのでちょっと面倒なのであとまわし...

 まず、コードを見てみましょう


// MenuAddItem.cpp

#include <PalmOS.h>
#include "ArmApiCall.h"

#define RESID	0x03e9
#define CRID	'To-f'

#define myApiMenuAddItem __myApiEntry__ 

Err myApiMenuAddItem(UInt16 positionId, UInt16 id, Char cmd, const Char *textP);
typedef Err (*osApi)(UInt16 positionId, UInt16 id, Char cmd, const Char *textP);

void prepTof(UInt8* from, UInt8* to);

Err myApiMenuAddItem(UInt16 positionId, UInt16 id, Char cmd, const Char *textP)
{
	UInt8* from = (UInt8*)textP;
	UInt8* to = (UInt8*)((UInt32)from + StrLen(textP));

	prepTof(from, to);

	osApi osApiMenuAddItem;
	FtrGet(CRID, RESID, (UInt32*)&osApiMenuAddItem);

	return osApiMenuAddItem(positionId, id, cmd, textP);
}

#pragma thumb on
void prepTof(UInt8* from, UInt8* to)
{
	#include <CharLatin.h>
	for(UInt8* i = from; i < to; i++)
	{	if(i[0] == chrHorizontalEllipsis)
		{	if(i[1] == chrNull)
				i[0] = chrEllipsis;
		}
		else if(i[0] == chrFullStop)
		{	if((i[1] == chrFullStop) && (i[2] == chrFullStop) && (i[3] == chrNull))
			{	i[0] = chrEllipsis;	i[1] = chrNull; i += 3;
			}
		}
	}
}
#pragma thumb reset

// Don't care below -----------------------
// Simple Startp code ---------------------
// Always compile as ARM instruction set --

extern "C" void __ARMlet_Startup__(void);

#pragma thumb off
asm void __ARMlet_Startup__(void)
{
	b __myApiEntry__
}
#pragma thumb reset

 まず、インクルードするヘッダが異なります 今回はAPIコールに関する部分を自前で用意しているので、<PalmOS.h>と"APICall.h"になります

 [RESID]と[CRID]のディファイン文は同じ

 次は若干異なります


#define myApiMenuAddItem __myApiEntry__ 

 今回は、コードサイズのダイエットを目指して、エントリーポイントのみARMコード、その他の部分はThumbコードでコンパイルするようにしていますので、コード末尾の[__ARMlet_Startup__]の中で呼んでいる[__myApiEntry__]を[myApiMenuAddItem]にすげ替えています

※ARMコードでコンパイルするのかThumbコードでコンパイルするのかはターゲットの設定パネルで変更できます [PNO Application]のデフォルトはThumbコードになっています

 あとは、関数のプロトタイプ宣言をして、[myApiMenuAddItem]と[prepTof]関数の実体を記述しています この関数の中身は見ていただければお分かりになるでしょうから細かな解説は省きますね

 今回はThumbコードでコンパイルしてもらうようにしていますが、このThumbコードで次の記述を行う場合には[Thumb_interworking.c]というファイルをターゲットに含めておく必要があります この記述はオリジナルのAPIを呼び出すのに使っていますよね [Thumb_interworking.c]は[C:\Program Files\Metrowerks\CodeWarrior\CW for Palm OS Support\(ARMlet Support)\(Source)]に置かれていますので、ここからひっぱってきてください


typedef Err (*osApi)(UInt16 positionId, UInt16 id, Char cmd, const Char *textP);
...
return osApiMenuAddItem(positionId, id, cmd, textP);

 あとは、しいて留意点を挙げるとすると、検索する文字列から一つ一つの文字を取り出すときには[Char]型ではなくて[UInt8]型を使うということでしょうか [Char]型の実体は[signed int]型なので、文字コードで0x80以上の文字をあつかおうとするときには[UInt8]型を使うということを頭の片隅にでも置いておくと良いでしょう

 さて、肝心のエントリーポイントの記述です

 まず、プロトタイプ宣言をして、


extern "C" void __ARMlet_Startup__(void);

※今回はファイル名が[*.cpp]としてあるので、コンパイラはc++で記述されていると認識するので[extern "C"]を入れています ファイル名が[*.c]としてある場合には不要ですので省いてください

 次に、プラグマ文でARMコードとしてコンパイルするように指示を行っています


#pragma thumb off

 このプラグマ文の宣言は、ターゲットの設定に優先されますので、コード中でARMコード、Thumbコードを切り替えたいときに、このプラグマ文を記述します

 強制的にARMコードにする場合は


#pragma thumb off

 逆に、強制的にThumbコードにするには


#pragma thumb on

 ターゲットの設定に戻すには


#pragma thumb reset

 と記述すればOKです 一番最後に[#pragma thumb reset]を入れてありますよね

さて、エントリーポイントの実体はインラインアセンブラで記述しています といっても、単に[myApiMenuAddItem]に飛ばしているだけですが..


asm void __ARMlet_Startup__(void)
{
	b __myApiEntry__
}

 アセンブリコードの[b]は、無条件ブランチです 簡単ですね

 アプリケーションで[MenuAddItem]がコールされると、この[__ARMlet_Startup__]に突入し、そのまま[myApiMenuAddItem]に飛ばされて、[__ARMlet_Startup__]には戻らずに、ダイレクトにアプリケーションに戻る という流れになります

 cやc++ではこの[b]に相当する動作を記述することはできませんが、なつかしのBASICで表現すると[gosub]文ではなくて[goto]文に相当する命令となっています

 どうでしょうか、見慣れないアセンブリコードが出てきたので少し難しく感じたかもしれませんね 先にも書きましたが、ARMアーキテクチャに関するドキュメントはhttp://www.arm.comから入手できますので参考になさってください ARM、Thumbのオペコードの早見表もここから入手できます また、日本語のドキュメントも少し用意されていますし、cに関するプログラミングのテクニックなんかも紹介されていますので面白いですよ

 ちょっと長くなってしまったので、続きは次回へもちこします 次回はスタック領域を自前で確保するという処理を「必要も無いのに」行っている[ResLoadMenu]についてのお話です OS5での関数呼び出しについても少し触れようと考えていますのでもう少しおつきあいくださいませ

 第7回「メニューの文字化けを直す」その3へ

質問、感想などはPalmHackersSalonのBBSへ

T-Pilot sekino