Hackではグローバル変数は使えないの?

第一回 ちょっとした思いつきに至った経緯と試してみたお話

こんにちは
この*******での講座、連載にできるかどうか...
役に立つ内容は他の方々にお任せして、できるだけ雑多なお話を、できるだけTipsを盛り込んで紹介してゆきますね

第一回は、Hackアプリでグローバル変数を扱う方法について、ちょっとした思いつきを試してみたお話しです
この話題を展開しようと思いついたときは、失敗談で終わるつもりでしたが、一応うまくいきそうだというところまでお話しできそうです
(ここでは、Notificationにより呼び出されるコードを含んだアプリケーションもHackの仲間に入れてあげましょうね)

-まえふり-

Hackを書かれたことのある方はご存知と思いますが、Hackでは、一般的なグローバル変数領域(アプリケーション実行中のみ確保される領域)では物足りず、システム稼動中はずっと確保されるシステムグローバル変数領域のような扱いができる領域が欲しくなります

例えば、最低でもオーバリッドしたシステムAPIのTrapアドレスはどこかに残しておかなければいけませんよね
(Hackを無効にする際にはソフトリセットをかけてしまうという場合は別にして...)

で、こんなシステムグローバル変数領域のような扱いができる領域を確保する方法は無いかなとずっと考えていました
ばかですねぇ...

グローバル変数を多用することの善し悪しについてはいろいろな議論がなされていますが、グローバル変数が使えるとコーディングが圧倒的に楽、もしくはコードサイズを小さくすることができることがある、というのは事実だと思います

先にお話しておきますが、Hackと称される類のアプリケーション(以下Hack)で、一般に呼ばれる意味でのグローバル変数を扱う方法はありません
なぜなら、システムがグローバル変数に使用する領域を確保してくれないからです

システムがアプリケーションのためにグローバル変数領域を確保してくれるのは、唯一、アプリケーションをsysAppLaunchCmdNormalLaunchで呼びだしてあげるときだけ
言い方を変えると、PalmOSは、「sysAppLaunchCmdNormalLaunchでアプリケーションを呼ぶ時にのみグローバル変数領域を確保する」ように組まれていますし、普通にアプリケーションをビルドすると、この「sysAppLaunchCmdNormalLaunchで呼ばれたときは、システムがグローバル変数領域を確保してくれている」という前提で実行コードが生成されます

では、一般的なHackでは、設定を保存したり、前回の状態を保存する為にどのような手段をとっているのでしょうか

例えば、システムAPIをトラップするHackの場合、先にお話したように、最低でもオーバリッドしたシステムAPIのTrapアドレスはどこかに残しておかなければいけません

この場合、記録しておかなければいけない値はシステムAPI関数のTrapアドレス(ポインタ)ですので、32bitの領域を一つ格納できれば良いということで、FeatureManagerにお願いすることが多いようです

記録するときは (システムAPIを自前のTrap関数でオーバリッドする場合)

UInt32 orgTrapAddr = (UInt32)SysGetTrapAddress (sysTrapFntGetFont);
FtrSet ('zatu', 0, orgTrapAddr);
SysSetTrapAddress (sysTrapFntGetFont, &myTrapFntGetFont);

こんな感じですね

読み出すときは (元のシステムAPIに戻す場合)

UInt32 orgTrapAaddr;
FtrGet ('zatu', 0, &orgTrapAaddr);
SysSetTrapAddress (sysTrapFntGetFont, (UInt32*)orgTrapAaddr);
FtrUnregister ('zatu', 0);

こうでしょうか

他にも、FtrPtrNewでストレージヒープに領域を確保してFeatureManagerとDataManagerにお願いして読み書きする(この場合、書き込みにはDmWriteを使う必要あり)とか、preferenceを使ってPreferenceManagerに読み書きをお願いするうという方法もあります
他には、MemHandleNewを使ってダイナミックヒープに領域を確保してMemoryManagerにお願いするという方法も使えるはずですが、MemHandleのオーナをきちんとシステムに設定してあげないと勝手に領域が開放されてしまう場合もありますので使い方には注意が必要です

そうそう、自前で用意したTrap関数からプライベート関数を呼ぶ際には、グローバル変数が使えませんので、変数を関数の引数として受け渡しすることになります
この場合、プライベート関数呼び出しのたびに「スタック領域を確保して引数の値をセット」という処理が入りますので、グローバル変数が使える場合に比べてコードサイズが多くなってしまうことになります

さて、上記の例のように、もともとのシステムAPIのアドレスを記録しておくだけならば、コールされるたびの値の読み出しは必要ありませんので、特に問題はありません
ですが、前回コールされた時の情況に応じて動作を変更させたいとか、いろいろなオプション設定を用意しておいて動作を変更させたいという場合はどうでしょうか
コールされるたびに設定を読み出してくる必要がでてきます

ということは、一般的な方法を使うと、コールされるたびにFeatureManager、DataManagerなどの方々にお願いすることになります

さて、各Managerにお願いするということは、何らかのオーバヘッドがかかってしまうということを意味しています
FeatureManagerにお願いする場合は、Featureテーブルを検索する時間がかかりますし、PreferenceManagerにお願いする場合はSavedPreferencesもしくはUnsavedPreferencesを探す時間と、さらにその中身の目指すリソースを探す時間がオーバヘッドとして確実にレスポンスの低下を招きます

FntGetFontのような頻繁に呼ばれることのない関数をオーバリッドする場合は、各Managerにお願いする際に発生するオーバヘッドが体感できるほどのレスポンス低下を招くことはないでしょう
ですが、例えばEvtGetEvent(OS5ではsysNotifyEventDequeuedEventによるコール)をオーバリッドする場合はどうなりますか?

オーバヘッドがイベント発生のたびにかかるとしたら...

レスポンスの低下が一番よく判るのは、NotePadのような手書き入力アプリでしょう
下手な書き方をされているHackを有効にしていると、書き始めを取りこぼしたりしていませんか?

ちょっと理由は異なりますが、[T-Suite]あたりを試してみてください

-思いつきに至った経緯-

やっと本題にたどりつきました

Hackの中でシステムグローバル変数領域のような扱いができる領域を確保する方法...

思いついたのです!

その思いつきとは...

[実行コード中に領域を確保する]

です (このお話を読んで、判らないことが少しでもあったら、絶対にまねしないで下さいね)

この思いつきに至った経緯は以下のよう

そもそも

アドレスを決め打ちできる場所に領域を確保する方法は無いかな...
いちいち誰かにお願いするのは、コードを書くのがめんどうくさいし...

こういうのはどう?

システムが使用しているグローバル変数領域にすき間を見つけるか?
でも、PalmOS5になったらどこか判らなくなってしまった...

では、こういうのは?

変数格納用のリソースを用意したら?
でも、毎回リソースを探すAPIを呼ばなくてはhandleを取得できないし、毎回探さない為にhandleをFeatureに格納したら同じことになってしまう...

ん?

実行コード中に領域を確保したら?
コードが確定したら相対位置は決まるのでアドレスを決め打ちできるはず!

ということでした

思いついたら即実行
あとはどうやってコードに載せるかです

領域をコード中に確保するには、誰からも呼ばれないダミーの関数を書いておけばいいはず
領域のアドレスを得るのは、Hackを書く上でおなじみの「関数へのポインタ」を取得すればできるはず

この方法なら、一切API関数を呼ばなくて済むはずなので、オーバヘッドはかからないはずです
また、関数へのポインタを取得するのは、アセンブリコードで書けば、lea [+相対アドレス],A2 という感じになるはずで、命令長は32bit
ということは、FtrGetやPrefSetAppPreferencesなどのAPIコールを多用するのに比べれば十数バイト程度の節約になるはず
さらに、プライベート変数に引数を渡さなくて良いので、さらなるコードサイズの節約もできるはずです

...と、「はず」ばかりではなにもわからないので、まずは簡単なコードを書いて見てみました

-試してみたお話-

まず、関数へのポインタが望むとおりに得られるかを確認しましょう
これが第一歩

#include <PalmOS.h>

static void TestGlobalVal (void) 
//ダミー関数
{
    UInt16 dumVal1 = 1;
    UInt16 dumVal2 = 2;
}

static void GetGlobalValPtr (void)
{
    UInt32* GlobalPtr = (UInt32*)&TestGlobalVal; 
//ダミー関数の相対アドレス取得
}

UInt32 PilotMain (UInt16 cmd, MemPtr cmdPBP, UInt16 launchFlags)
{
    switch (cmd)
        {
        case sysAppLaunchCmdNormalLaunch:
            GetGlobalValPtr ();

        default:
        break;
        }
    return true;
}

たったこれだけのコードですが、これで十分
適当にブレークポイントをセットしてGlobalPtrにどんな値が入ってくるのか、GlobalPtrに格納されるアドレスにはどんな値が書き込まれているのか、デバッガで見てみましょう

きちんと予想通りに行けば、GlobalPtrにはTestGlobalVal()と定義したダミー関数の先頭アドレスが格納され、そのアドレスの内容を見ると、TestGlobalVal関数の中に記述したコードが入っているはずです

具体的には
UInt16のサイズの変数を二つ格納できる領域(この場合は4バイト)をシステムスタック上に確保して、それぞれの値をセット、確保した領域を解放してreturnとなっているはずです

さて、デバッガを起動してみてみましょう
おっ、GlobalPtrになんだかそれらしい値が入ってきましたね

GlobalPtrを右クリックして[View Memory]を選んでメモリの中身を見てみましょう

ん、なんだかわけがわからない?

まあ、だれでもそうですよね

では、Disasembleしたコードをみてみましょうか

こんどはどうですか?

アセンブリコードは知らなくても、なんとなくおわかりでしょう

UInt16のサイズの変数を二つ格納できる領域(この場合4バイト)をシステムスタック上に確保して、それぞれの値をセット、確保した領域を解放してreturnとなっていますね

さあ、ちゃんとダミー関数の先頭アドレスが取得できることが確認できたので、こんどは、値の読み出しです

先ほどのコードに一行付け加えて...

static void GetGlobalValPtr (void)
{
    UInt32* GlobalPtr = (UInt32*)&TestGlobalVal;
    UInt32 val4read = *GlobalPtr;  
//←追加
}

さあ、もう一度

*GlobalPtrとval4readの値が同じになっていることを確認してください
きちんと読み出せましたね

もう一息です
これで、あとは書き込みができれば万事OK

あれっ?
そういえば、エミュレータを動かしています?それともシミュレータですか?
ここからは、話の都合上エミュレータを動かしてくださいませ

もう一行追加しましょう

static void GetGlobalValPtr (void)
{
    UInt32* GlobalPtr = (UInt32*)&TestGlobalVal;
    UInt32 val4read = *GlobalPtr;
    *GlobalPtr = 'zatu';  
//←追加
}

どうでした?

ありゃ、エラーが出てしまいました?
では、今度はシミュレータを使ってみて下さい(って何の解決にもなっていませんがものは試しです)

*GlobalPtrの値が書き込もうとした'zatu'になっています
うまく書き込めましたね

さて、なぜエラーが出てしまったのでしょうか
もう一度エミュレータに戻って、なんと言って怒っているのか考えてみましょう

どうやら、勝手にストレージヒープに書きむなと怒っているようです
「そこはストレージヒープで、プロテクトされているのだから、きちんとOSが用意している関数を使え」と申しているようです...

実はわかっていたことなのですが...
もしかしたら、システムが感知できないポインタ直接指定の書き込みならうまくいくかなと...
甘かったみたいですね

これは、PalmOSをターゲットにしたプログラミングではお約束ごとで、リファレンスを見ると「ストレージヒープへの書き込みはDmWriteを使わなくてはいけない」ということになっています

ちょっと考えてみましょう

ダイナミックヒープへの書き込みミスは、大概の場合ソフトリセットで復旧できる程度のダメージしか与えません

一方、ストレージヒープへの書き込みはどうでしょうか
PCで言うところのハードディスクに相当する領域への書き込みになります
つまり、アプリケーションそのものが書き換えられてしまうこともあって、最悪ハードリセットの憂き目に...

こんなことが起こるのを未然に防止するために、ストレージヒープへの書き込みは意図的に行う場合以外は許されていないのでしょう
これはOSで書き込みを監視しているレベルではなく、MPUの動作レベルでのメモリ管理によって書き込みを禁止しているようです

はあ、あきらめましょうか...

でも、DmWriteというAPIでストレージヒープへの書き込みができるということは、何らかの手段が用意されているはずです
(アプリケーション開発者が使えるかどうかは別にして)

もうちょっとがんばってみました

-すこしがんばってみたお話-

PalmSource殿が公開しているPalmOSのソースコードを覗いてみました

DmWriteはDataManagerの管轄ですので、DataMgr.cを覗きます

なんだか見慣れないAPIを呼んでいますね

書き込む前に

// Prepare for writing
MemSemaphoreReserve(true);

終わる時に

MemSemaphoreRelease(true);

この間では、単に「引数として渡された値の妥当性をチェックしてOKであればMemMoveでデータを転送する」というなんていうことのない処理をしています
ということは、MemSemaphoreReserve(true)とMemSemaphoreRelease(true)ではさんであげれば、ストレージヒープへの書き込みが許されるはず

ところが、この両者のAPIはPalmOSReferenceの中のDataManagerの章にはリストアップされていません
Appendixの中のSystemUseOnlyFunctionsの中に書かれているだけです

ただ、MemoryMgr.cの中のMemSemaphoreReserveのところを見るとこう書かれています

"enables writes to the data storage area"

んっ、使っちゃいけないといわれても使ってみたくなるようなことが書いてありますね

試してみましょう...

static void GetGlobalValPtr (void)
{
    UInt32* GlobalPtr = (UInt32*)&TestGlobalVal;
    UInt32 val4read = *GlobalPtr;
    MemSemaphoreReserve(true);  
//←追加
    *GlobalPtr = 'zatu';
    MemSemaphoreRelease(true);  
//←追加
}

どうです?

おっ、うまくエラーも出ずに流れました
結果オーライ

調子に乗ってもう一行確認のために追加しましょう

static void GetGlobalValPtr (void)
{
    UInt32* GlobalPtr = (UInt32*)&TestGlobalVal;
    UInt32 val4read = *GlobalPtr;
    MemSemaphoreReserve(true);
    *GlobalPtr = 'zatu';
    MemSemaphoreRelease(true);
    UInt32 val4read2 = *GlobalPtr;  
//←追加
}

もともとの*GlobalPtrの値がval4readに、新しく書き込んだ値が*GlobalPtrに、さらに読み出したval4read2に新しく書き込んだ値が入っていますね

うまくいきました
じゃあ、止めずにそのまま流して、もう一度起動してみましょう

今度は、全部同じ値(前回セットした値)が入っていますね

FeatureManagerにもPreferenceManagerにもお願いすることなく済んでしまいました
しかも、アプリケーション再起動時には前の値を保持してくれています

んー、意外とすんなりとうまくいきましたね

さてさて、具体的に何か作ってみましょうか

アセンブリコード解説

LINK (Link)

ソースオペランドで指定されるアドレスを基点とし、デスティネーションオペランドで指定される大きさの領域を確保
・関数内で使用するプライベート変数用の領域を確保するということ

UNLK (Unlink)

LINK命令で確保された領域を開放
デスティネーションオペランドにはLINK命令でソースオペランドに指定したアドレスをセット
・呼び出し元に戻る前の後処理ですね

MOVE (Move)

ソースオペランドのデータをデスティネーションオペランドへ転送する
・単純なデータ転送命令です

LEA (Load Effective Address)

ソースオペランドの内容に従って実効アドレス値を計算し、その結果をデスティネーションオペランドで指定されるアドレスレジスタにセットする
・「僕は今どこにいるの?」とか「あれはどこにいるの?」なんていうときに使います

RTS (Return from Subroutine)

システムスタックからコンディションコードレジスタとプログラムカウンタの内容を回復する
・用は呼び出し元に戻るということ

-何か作ってみたお話-

ちょっとしたHackを組んでみましょう

OS4.x以前の機種と、OS5.xの機種、両方で試して見られるように、Notificationを利用したHackにしてみます

んー、何を使って何をさせるかですが...

ざっとNotificationにはどんなものがあるかを見てみると、sysNotifyMenuCmdBarOpenEventが手ごろそうです

コマンドツールバーを表示させようとした時に発行されるNotificationですね

これでいきましょう

次は何をさせるか...

では、こうしましょうか

コマンドツールバーを表示させようとした時のアプリケーションの「クリエータID」と、そのアプリケーションで「何回目」の呼び出しかを画面左上に表示

前に呼び出されたときと異なるアプリケーションから呼ばれたときは、前に呼び出されたときのアプリケーションのクリエータIDと、そのアプリケーションで呼び出された回数を画面左上に表示して、記録する回数をリセット

何の役にも立ちませんね

まあいいでしょう

有効・無効の切替の処理は本題ではないの省きます(面倒くさいですし、ソフトリセットに頼ればすみますから)

さて、記録の必要な値は、呼ばれた回数と呼んだアプリケーションのクリエータIDの二つです
あっ、動作中かどうかを判断するためのフラグもいりますね

カウントとフラグはUInt8で1バイトずつ、クリエータIDはUInt32で4バイトの領域を確保しましょう

まず、グローバルに使いたい変数を構造体で定義してしまいます(あとあと楽なので)

typedef struct
{
    UInt8 count; 
//回数格納用
    UInt8 isActive; 
//動作中のフラグ格納用
    UInt32 prevAppCreatorID; 
//呼ばれたアプリケーションのクリエータID格納用
} GlobalValType;

次は、領域の確保ですね
先にお話したように、ダミーの関数を書きましょう

ただ、適当にサイズを食いそうなAPI関数を並べてもよいのですが、この場合コンパイルしてみないと確保できた領域のサイズがわからないとか、オプティマイズのレベルによってもサイズが変わってしまうということになります

これでは、ちょっと気持ち悪いので、サイズを確定できるようにいきなりアセンブリコードで書いてしまいましょう

関数名の前に[asm]というキーワードを入れておくと、「関数の中はアセンブリコードで記述されている」とコンパイラがみなしてくれる仕組みになっていますので、次のように簡単なアセンブラ制御命令だけを使って書いてみました

static void asm GlobalVal (void)
{
    DC.B 0
//count用の領域
    DC.B false
//isActive用の領域
    DC.L 'strt'
//prevAppCreatorID用の領域
}

一応説明しておきますと、[DC]というのはDeclareConstantの意味で、定数の定義を行っていることになります
[.B]は[バイト]ですので1バイト、[.W]は[ワード]で2バイト、[.L]は[ロング]で4バイト

なんていうことはないですね
初期設定も済ませてしまいました

次はNotificationManagerから呼んでもらうための関数を書いておきましょう

static Err callbackFunc (SysNotifyParamType *notifyParamsP)
{
    GlobalValType* GlobalValP = (GlobalValType*)&GlobalVal;
//グローバル領域のアドレス取得

    WinDrawChars ((Char*)&GlobalValP->prevAppCreatorID, 4, 10, 10);
//クリエータIDの描画

    UInt16 cardNo;
    LocalID dbID;

    SysCurAppDatabase (&cardNo, &dbID);
//今動作しているアプリケーションの取得

    UInt32 curAppCreatorID;

    DmDatabaseInfo (cardNo, dbID, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
            &curAppCreatorID);
//クリエータIDの取得

    Boolean isSame = (GlobalValP->prevAppCreatorID == curAppCreatorID);
//前回呼ばれたときとの比較

    UInt8 tmpCount = GlobalValP->count;
    Char tmpCountA[5];

    if (isSame)
    {
        StrIToA (tmpCountA, ++tmpCount);
//前回と同じ時はカウントアップ
    }
    else
    {
        StrIToA (tmpCountA, tmpCount);
//前回と異なるのでそのまま数字文字に変換
        tmpCount = 1;
//回数をリセット
    }

    WinDrawChars (tmpCountA, StrLen (tmpCountA), 10, 25);
//回数の描画

    
//グローバル領域への書き込み
    MemSemaphoreReserve (true);
    GlobalValP->prevAppCreatorID = curAppCreatorID;
    GlobalValP->count = tmpCount;
    MemSemaphoreRelease (true);

    return errNone;
}

さて、次は有効にする処理を行う関数の記述です

static void activate (void)
{
    GlobalValType* GlobalValP = (GlobalValType*)&GlobalVal;
//グローバル領域のアドレス取得

    if(!GlobalValP->isActive)
    {

        UInt16 cardNo;
        LocalID dbID;

        SysCurAppDatabase (&cardNo, &dbID);
//今動作しているアプリケーションの取得

        SysNotifyRegister (cardNo, dbID, sysNotifyMenuCmdBarOpenEvent,
              &callbackFunc, sysNotifyNormalPriority, NULL);

        DmDatabaseProtect (cardNo, dbID, true);
//データベースのロック

        
//グローバル領域への書き込み
        MemSemaphoreReserve (true);
        GlobalValP->isActive = true;
//動作中のフラグをセット
        MemSemaphoreRelease (true);
    }
}

このへんの処理は「Palm OS 5 講座:第4回 Hackの作成」を参照してください

あとは、お約束 PilotMain の記述です
sysAppLaunchCmdNormalLaunchの時だけ動作するように簡単に書いておきましょう
あと、ソフトリセット時は動作中のフラグをリセットする処理も

UInt32 PilotMain (UInt16 cmd, MemPtr cmdPBP, UInt16 launchFlags)
{
    switch (cmd)
        {
        case sysAppLaunchCmdNormalLaunch:
            activate (); 
//アクティブに
            break;

        case sysAppLaunchCmdSystemReset:
            GlobalValType* GlobalValP = (GlobalValType*)&GlobalVal;
//グローバル領域のアドレス取得
            
//グローバル領域への書き込み
            MemSemaphoreReserve (true);
            GlobalValP->isActive = true;
//動作中のフラグをリセット
            MemSemaphoreRelease (true);
        }
    return true;
}

あとは、一番最初に

#include <PalmOS.h>

でおしまい Starter.cpp

インストールして実行してみましょう Starter.prc

何も起こりませんでした?

そうでしょうか...

コマンドツールバーを呼び出してみてください

へへ、うまくいきました!
画面左上に控えめな文字列が...

今回のお話はこれでおしまい

いかがでしたか?

普通にアプリケーションを開発する過程では、あまり触れることが無い部分、
アセンブリコードの見方、デバッグのやりかた、などなどに興味を持ってもらえたとしたら幸いです
(僕の目論見どおり!)

あっ、コードサイズが本当に小さくなるのか、実行速度の向上につながっているのか確認していませんでした...

これは次回ですね

また、アセンブリコードに登場してもらうことになりそうです