高度な検索
Developer Connection
Member Login ログイン | ご入会 ADC連絡先

Technote 1120

Opening Resource Files Twice Considered Hard?


目次

問題の所在

FSpOpenResFile の動作に関する詳細な説明

解決方法のレシピ

要約
部分の Mac OS プログラマは FSpOpenResFile (あるいはこのルーチンの古いバージョンである OpenResFileOpenRFPerm、および HOpenResFile) を呼び出すことが難しいとは考えていません。しかし、リソースファイルが既にオープンしているときに、これらの呼び出しがどのように動作するかは今まで適切にドキュメントに記述されていませんでした。

この TECHNOTE では、リソースファイルが既にオープンしているときの FSpOpenResFile の動作を正確に説明し、このときに発生する問題を回避するために解決方法を提案します。

Resource Manager を使用するすべての Mac OS プログラマは、この TECHNOTE の「要約」を読んで、このような問題があることを正しく認識してください。また、非アプリケーションコードを書いているプログラマは、この TECHNOTE 全体をよく読んで、できるだけ互換性を確保するようにコードを記述してください。


問題の所在

『Inside Macintosh: More Macintosh Toolbox』(p1-60) には、リソースファイルが既にオープンしているときの FSpOpenResFile の動作について次のような説明があります。

FSpOpenResFile を使って既にオープンしているリソースフォークをオープンしようとすると、既存のアクセスパスに対するパーミッションにしたがって、FSpOpenResFile から既存のファイルリファレンス番号または新しいファイルリファレンス番号のいずれかが返されます。たとえば、それ以前に書き込みアクセスを使ってオープンされているファイルに読み込み専用アクセスのリクエストが正常に実行されると、アプリケーションには新しいファイルリファレンス番号が返されます。一方、同じファイルを対象に書き込みアクセスのリクエストを再度実行すると、前と同じファイルリファレンス番号が返されます。この場合、FSpOpenResFile はそのファイルを現在のリソースファイルにしません。

この説明はほとんど正しいのですが、完全に正しいとはいえません。正確な状況はもっと複雑です。

  • ファイルをオープンするために使用する正確な呼び出し (つまり、OpenResFileOpenRFPermHOpenResFile、および FSpOpenResFile)。
  • 呼び出しに与えられているパーミッション。
  • リソースファイルが既にオープンされているかどうか。
  • リソースファイルが既にオープンされているかどうか (現在のプロセスまたは別のプロセスによって)。
  • 別のプロセスによってリソースファイルが既にオープンされているかどうか (このプロセスはローカルマシン上または別のマシン上にあり、ファイル共有によりファイルにアクセスしています)。
  • リソースファイルが既にオープンされているかどうか (それ以前にファイルをオープンしたときに使用したパーミッションで)。
これらの複雑な要因がからみ合うことで、リソースファイルのオープンは考えていたよりも潜在的にもっと複雑なものになります。

問題の簡略化

幸いなことに、前述したすべての要因が問題を現実に複雑化するわけではありません。次の説明にしたがって分析を簡略化することができます。
  • OpenResFileOpenRFPermHOpenResFile、および FSpOpenResFile はすべて同じように動作します。このことは一見明らかなように思えますが、DTS エンジニアたちはこのことが当然だとは考えていません。ただし、実際にテストした結果そうであることを知れば安心できると思います。

注意:
OpenResFileはパーミッションのパラメータをとりません。OpenResFile を呼び出すと、fsCurPerm パーミッションを指定したかのように動作します。

  • FSpOpenResFile を呼び出すと、次のような 3 つになります。
    1. 正常終了 -- 新しいリソースマップが作成され、リソースチェーンのいちばん上に追加されます。このルーチンは新しいリソースマップを現在のリソースマップにして、そのリソースファイルリファレンス番号を返します。
    2. 部分的に正常終了 -- 既存のリソースマップのリソースファイルリファレンス番号が返されます。その既存のマップが現在のリソースマップになります。
    3. エラー -- FSpOpenResFile は -1 を返し、リソースチェーンに変更は加えられません。ResError を呼び出して、実際のエラー番号を取得することができます。

注意:
前述の「部分的な正常終了」の結果、CurResFile が変更されます。これは、『Inside Macintosh: More Macintosh Toolbox』からの引用の最後の文と矛盾します。この TECHNOTE が正しく、『Inside Macintosh』の説明は誤りです。

  • すべての場合で、fsWrPermfsRdWrPerm のどちらを指定しても結果は同じになります。

これらの点を組み合わせると、正確な状況の説明がかなり容易になります。残されているのは、これ以外の場合での動作を説明することです。

FSpOpenResFile の動作に関する詳細な説明

次の一覧表は、さまざまな場合に FSpOpenResFile がどのように動作するかを正確に示しています。

最初のオープンで使用したパーミッション 2 回目のオープンで使用するパーミッション 同じプロセス 同じマシン 異なるマシン
fsCurPerm fsCurPerm 正常終了
読み込み専用
正常終了
読み込み専用
エラー -54
fsCurPerm fsRdPerm 正常終了
読み込み専用
正常終了
読み込み専用
エラー -54
fsCurPerm fsWrPerm
fsRdWrPerm
部分的に正常終了
読み書き可能
エラー -49 エラー -54
fsRdPerm fsCurPerm 正常終了
読み込み専用
正常終了
読み込み専用
正常終了
読み込み専用
fsRdPerm fsRdPerm 正常終了
読み込み専用
正常終了
読み込み専用
正常終了
読み込み専用
fsRdPerm fsWrPerm
fsRdWrPerm
部分的に正常終了
読み込み専用
エラー -49 正常終了
読み込み専用
fsWrPerm
fsRdWrPerm
fsCurPerm 正常終了
読み込み専用
正常終了
読み込み専用
エラー -54
fsWrPerm
fsRdWrPerm
fsRdPerm 正常終了
読み込み専用
正常終了
読み込み専用
エラー -54
fsWrPerm
fsRdWrPerm
fsWrPerm
fsRdWrPerm
部分的に正常終了
読み書き可能
エラー -49 エラー -54

先頭の列は、ファイルを最初にオープンしたときに使用したパーミッションの値を示しています。2 番目の列は今回 FSpOpenResFile を呼び出すために使用するパーミッションの値を示しています。

残りの 3 つの列は、次の 3 つの場合それぞれで実行結果がどのようになるかを説明しています。

  • 同じプロセス -- ファイルは同じプロセスによって既にオープンされています。
  • 同じマシン -- ファイルは同じマシン上の別のプロセスによって既にオープンされています。
  • 異なるマシン -- ファイルは、ファイル共有によって接続されている別のマシン上の別のプロセスによって既にオープンされています。

それぞれのセルには 2 つの項目が示されています。第 1 の項目は前述した呼び出しの結果です。第 2 の項目は返されたリソースファイルリファレンス番号に関連するパーミッション (実行結果が「正常終了」または「部分的に正常終了」の場合) または返されたエラーコード (実行結果が「エラー」の場合) です。

これではっきりわかること

前述の一覧表から重要な問題がいくつか明らかになります。
  • ファイルが同じプロセスによって既にオープンされている場合、そのファイルに対して書き込みパーミッションを要求すると、実行結果は「部分的に正常終了」と評価されます。つまり、FSpOpenResFile は既存のリソースマップのリソースファイルリファレンス番号を返すことになります。この状況に対する準備をあらかじめ行っていないと (しかも、使用後にリソースマップをクローズしていると) 面倒な状況になります。たとえば、デベロッパが開発中のアプリケーションのリソースマップ、あるいはシステムリソースマップを誤ってクローズしてしまうというのはよくあることです。
  • 要求した通りのパーミッションを取得できないこともあります。たとえば、一覧表の「同じプロセス」の列で、ファイルが fsRdPerm を使って既にオープンされている場合に、fsRdWrPerm を使ってファイルを再度オープンしようとすると、返されるリソースファイルリファレンス番号は読み込み専用のパーミッションを持ちます。後述するサンプルコードを使用すると、この状況をチェックすることができます。
  • 既にオープンしているファイルに対して、別のマシンが fsRdPerm を使って fsRdWrPerm パーミッションを要求すると、ルーチンは正常に実行されますが、読み込み専用のリソースファイルリファレンス番号が返されることになります。
  • 別のプロセスによってリソースファイルが既にオープンされているときに返される正確なエラーコードは、そのプロセスがローカルマシンで実行されているかどうかによって異なります。

最後にわかること

同じリソースファイルを 2 度オープンするときに発生する最後の問題は Resource Manager の設計そのものに内在しており、それを回避することは非常に困難です。リソースファイルをオープンするとき、Resource Manager はすべてのリソースのカタログ (リソースマップ) をヒープにロードし、そのマップを使って、リソースファイル内に常駐するそれぞれのリソースのデータを検索します。

リソースマップを変更しても、Resource Manager はリソースファイルをオープンした複数のプロセス間の調整を行いません。Resource Manager は、同じリソースファイルに対して 2 つの読み書き可能なリソースファイルリファレンス番号をオープンできないようにしますが、読み込み専用のリソースファイルリファレンス番号と読み書き可能なリソースファイルリファレンス番号を同時に取得する操作を停止することはありません。

その結果、次のような状況で重大な問題が発生することになります。

    1. プロセス A がリソースファイルを読み書き可能でオープンします。Resource Manager はそのファイルに対応するリソースマップをプロセス A のヒープに読み込みます。
    2. プロセス B がそのリソースを読み込み専用でオープンします。Resource Manager はそのファイルに対応するリソースマップをプロセス B のヒープに読み込みます。
    3. この後、プロセス A はリソースファイルを変更し、UpdateResFile を呼び出して、それらの変更内容をディスクに書き戻します。この処理により、リソースファイル内のさまざまなリソースの位置は動的に変更されることになります。
    4. この後、プロセス B は Resource Manager を呼び出して、リソースファイルからリソースを読み込みます。プロセス B が持っているリソースマップのコピーはもはや実際のファイルとは一致しないため、Resource Manager は偽のリソースデータを返すことになります。

    このような制限は、『Inside Macintosh: More Macintosh Toolbox』にある「FSpOpenResFile」の説明の「Special Considerations」の節で確かに説明されていますが、その説明は、リソースファイルを 2 度オープンするときの問題と同様に、正しくない内容が繰り返されています。

    解決方法のレシピ

    ここでは、FSpOpenResFile の奇妙な動作がソフトウェアを台無しにしてしまうのを防ぐために役立つテクニックをいくつか紹介します。これらのテクニックは、最も推奨できるものから順に示します (しかも実装しやすい順に)。

    リソースファイルを一度だけオープンする

    通常のアプリケーションレベルのコードを書いている場合は、プロセスが既にリソースファイルをオープンしていることを記憶しておき、それを 2 度オープンするのを防ぐのが最も簡単な方法です。次のコードは、このような処理を行う単純なサンプルを示しています。

     static SInt16 gResourceResFile = 0;
     static UInt32 gResourceUsageCount = 0;
    
     static OSErr StartUsingResources(ConstFSSpecPtr fss)
     {
         OSErr  err;
         SInt16 tmpResFile;
    
         err = noErr;
         if (gResourceUsageCount == 0) {
             tmpResFile = FSpOpenResFile(fss, fsRdWrPerm);
             err = ResError();
             if (err == noErr) {
                 gResourceResFile = tmpResFile;
             }
         }
         if (err == noErr) {
             gResourceUsageCount += 1;
         }
    
         return err;
     }
    
     static void StopUsingResources(void)
     {
         gResourceUsageCount -= 1;
         if (gResourceUsageCount == 0) {
             CloseResFile(gResourceResFile);
             gResourceResFile = 0;
         }
     }
    

    このテクニックを複数のリソースファイルに拡張する方法は、デベロッパ自身の練習としておきます。

    リソースファイルを読み込み専用でオープンする

    リソースファイルが現在のプロセス (たとえば、システム機能拡張のコードで) によって既にオープンされているかどうかはっきりわからない状況では、最も簡単な解決方法は常にリソースファイルを読み込み専用で (つまり、fsRdPerm を使って) オープンすることです。この方法を使用すると、安全にクローズすることのできる新しいリソースリファレンス番号が常に返されることになります。

    この解決方法をさらに洗練されたものにするには、リソースファイルのオープン、読み込み、およびクローズをすばやく行い、その間に他のプロセスに時間が引き渡されないようにします。こうすれば、ファイルを読み込んでいる間に別のプロセスがそのファイルを変更することもなくなり、前述したような面倒な問題による障害を最小限に抑えることができます。この方法は一定の状況だけで役立ちますが、レシピの 1 つとして覚えておくと確かに便利です。

    TopMapHndl をチェックする

    リソースファイルが現在のプロセスによって既にオープンされているかどうかがはっきりわからない状況で、リソースを読み書き可能でオープンする必要がある場合、TopMapHndl 下位メモリグローバルをモニタして、FSpOpenResFile を呼び出した後で変更が加えられていないかどうかを確認することが最善のテクニックです。このグローバルに変更が加えられている場合は、新しいリソースマップがリソースチェーンのいちばん上に追加されており、それをきちんとクローズする責任があります。このグローバルに変更が加えられていない場合、既存のリソースマップリファレンス番号が返されており、それをクローズする必要はありません。

    次のコードはこのテクニックを具体的に示しています。

    
     static void SafeOpenResFileReadWrite(ConstFSSpecPtr fss)
     {
         OSErr   err;
         SInt16  oldResFile;
         Handle  oldTopMap;
         SInt16  resFile;
         Boolean shouldClose;
    
         oldResFile = CurResFile();
    
         oldTopMap = LMGetTopMapHndl();
         resFile = FSpOpenResFile(fss, fsRdWrPerm);
         err = ResError();
    
         if (err == noErr) {
             shouldClose = (LMGetTopMapHndl() != oldTopMap);
    
             // do the stuff with the resource file
    
             if (shouldClose) {
                 CloseResFile(resFile);
             }
         }
    
         UseResFile(oldResFile);
     }
    

    なお、このテクニックは、ファイルを読み書き可能でオープンする必要があり、そのファイルが現在のプロセスによって既にオープンされているかどうかはっきりわからない場合のみ有効である点に注意してください。このような場合、このテクニックは非アプリケーションコード (システム機能拡張、共有ライブラリ、アプリケーションのプラグインなど) によって必要とされますが、標準ファイルフィルタ関数などの未知の環境で実行されているアプリケーションコードでも役立つことがあります。

    常に CurResFile を保持する

    前述のどのテクニックを使用するかに関係なく、FSpOpenResFile の呼び出しと、CurResFile および UseResFile の呼び出しをひとまとめに扱い、コードが誤って現在のリソースマップを変更してしまわないようにするのもよい考えです。前述のコードでは、このテクニックも具体的に示しています。

    パーミッションをチェックする リソースファイルに書き込みを行う必要があり、ファイルが既にオープンされているかどうかはっきりしない場合は、リソースファイルリファレンス番号を調べて、それが読み書きアクセスをサポートしているかどうかを確認します。読み書き可能なリソースファイルリファレンス番号があってもファイルへの書き込みが正常に実行できるかどうかは保証されていませんが、少なくとも最初のステップでこのことをチェックするのはよい考えです。

    File Manager のルーチンである PBGetFCBInfoSync を呼び出し、ioFCBFlags のビット 8 の値を取得することで、リソースファイルリファレンス番号が読み書き可能であるかどうかをチェックすることができます。次のコードはこのテクニックを具体的に示しています。

     static Boolean IsResourceFileRefNumWritable(SInt16 rsrcRefNum)
     {
         Boolean result;
         FCBPBRec fcbPB;
    
         fcbPB.ioNamePtr = nil;
         fcbPB.ioVRefNum = 0;
         fcbPB.ioRefNum = rsrcRefNum;
         fcbPB.ioFCBIndx = 0;
         if ( PBGetFCBInfoSync(&fcbPB) == noErr ) {
             result = ((fcbPB.ioFCBFlags & (1 << 8)) != 0);
         } else {
             result = false;
         }
    
         return result;
     }
    
    要約

    同じリソースファイルを 2 度オープンする場合、Resource Manager により、次のような奇妙な動作が発生します。

    • FSpOpenResFile が新しいリソースファイルリファレンス番号をオープンする代わりに、既存のリソースファイルリファレンス番号を返す。
    • 読み書きアクセスを明示的に要求した場合でも、FSpOpenResFile が読み込み専用のリソースファイルリファレンス番号を返す。
    • 読み込み専用のリソースファイルリファレンス番号によってリソースファイルを読み込む場合でも、同時に別の読み書き可能なリソースファイルリファレンス番号によってリソースファイルに変更が加えられていると、リソースデータが壊れてしまう可能性がある。
    これらの問題を回避する最善の方法はとにかくリソースファイルを 2 度オープンしないことです。これを避けることができない場合、この TECHNOTE に記述されている、障害を最小限に抑えるために利用できるアプローチを参照してください。

    参考文献