FSSpecs vs. FSRefs

本稿では、FSSpec を使ったコードを FSRef に置き換えようという時の一助となる情報を扱います。

Files.h

まずは Files.h を一通り読んでみよう。そこにはたくさんの記述があって、新しい API だって全部載っている。存在すら知らない API は使うことだってできやしないのだから。読むのにはちょっとばかり時間がかかるけど、価値あるものになることでしょう。

FSSpec と FSRef はどう違う?

Files.h ではそれぞれ次のように定義されています。

struct FSSpec { 
    short       vRefNum; 
    long        parID;
    StrFileName name; /* a Str63 on MacOS*/ 
}; 

struct FSRef { 
    UInt8 hidden[80]; /* private to File Manager; */ 
}; 

最も大きな違いは、FSRef は存在しないファイルを指すことができないことですね。それと FSRef は 80バイトの領域ということがわかっているだけで、中身の構造については Apple は明らかにしていないという点。さらに、FSRef は内部にファイル名を保持していないという点。Mac OS X はファイル名に最大 255文字の UniChar を使えるんだから当たり前だけど。("FSRefs と Unicode ロングファイル名" を参照)

それぞれの相互変換はどうしたらいい?

FSSpec → FSRef なら FSpMakeFSRef()。

err = FSpMakeFSRef( &fsSpec, &fsRef );

FSRef → FSSpec なら FSGetCatalogInfo()。

err = FSGetCatalogInfo( &fsRef, kFSCatInfoNone, nil, nil, &fsSpec, nil );

FSRef が有効なものであるかを調べるには?

inline Boolean FSRefIsValid( const FSRef &fsr ) 
{
    return FSGetCatalogInfo( &fsr, kFSCatInfoNone, nil, nil, nil, nil ) == noErr; 
}

2つの FSRef が同じものを指しているかどうかを調べるには?

inline Boolean operator == ( const FSRef &lhs, const FSRef &rhs ) 
{
    return FSCompareFSRefs( &lhs, &rhs ) == noErr; 
}

FSCompareFSRefs() は 2つの FSRef が同じボリュームの違うファイルを指しているときには errFSRefDifferent を返し、違うボリュームにあるときには diffVolErr を返すことも覚えておくと良いでしょう。

FSRef の親ディレクトリを取得するには?

err = FSGetCatalogInfo( &fsRef, kFSCatInfoNone, nil, nil, nil, &parentFSRef ); 

存在しないアイテムを表現するにはどうしたらいい? (例えばこれから作ろうとしているファイルとか)

Apple は CFURLRef を推奨しているけど、私の経験では Mac OS 9 で問題が生じることがある。なので、その代わりに私はそのアイテムの親フォルダ FSRef と、アイテム名を HFSUniStr255 で保持したクラスを使ってます。

class CExtFSRef {
    public:// No lectures on why these should be private. ;-)
        FSRef parentFSRef;
        THFSUniStr255 name;
    ...
    // Some useful member functions.
};

THFSUniStr255 というのは HFSUniStr255 をラップした自作クラスです。CExtFSRef クラスは NavCreatePutFileDialog() の結果で得られたものを保持するのに使っています。Unicode-savvy なファイル生成 API は、親 FSRef と HFSUniStr255 を引数にするので、CFURLRef にして保持せずに、そのままそれを保持しているわけです。もちろん、次に示すようにファイル名を CFStringRef で保持しておいて、必要に応じて HFSUniStr255 に変換して使う、という方法でもいいでしょう。

class CExtFSRef2 { 
    public: 
        FSRef parentFSRef;
        CFStringRef name;
    ... 
    // Some useful member functions. 
};

CFURLRef を使う方法もあるけれど、ファイル保存ダイアログと一緒に使う場合には上に示したやり方の方が簡単だと思います。

AppleEvents

AppleEvent 経由で FSRef を渡してはいけません。例えば Finder への AppleEvent などですね。Mac OS 9 ではうまく動くかもしれませんが、Mac OS X において FSRef はプロセス内でのみ有効なものなのです。AppleEvent は真っ黒な絵みたいなもので、様々な人がそれぞれ異なったアプローチでファイル参照を渡そうとするし、受け取る側のアプリケーションでの処理方法もまた、ものによって違います。したがってここで明言できるのは FSRef を直接渡しても意図通りには動かないだろう、ということだけなのです。George Warner 氏の MoreFinderEvents の中にこのワナにはまるコードをいくつか見てとれます。

永続的ストレージへの保存

SSpec と同様、FSRef は Mac OS 9 / Mac OS X の同じブート内でのみ有効であるだけでなく、Mac OS X では同一プロセス内でのみ有効なのです。(たとえ同じアプリケーションを別々に起動した場合でも、それぞれのプロセス内でのみ有効になります)したがって、この値をストレージに記録しておくようなことはしてはいけないです。そういった場合には alias を使いましょう。({{|Alias Manager|http://developer.apple.com/techpubs/macosx/Carbon/Files/AliasManager/aliasmanager.html}})

FSSpec を使い続けることはできますか?

私の知る限り、答えは Yes です。FSSpec はこれからも正しいファイル参照方法であり続けるでしょう。けれども、FSSpec の name メンバは有効な値にならないことがあるから、記録用/表示用のいずれにも使うべきではありません。本当の名前が Pascal String で表現できない場合や、31文字以上の場合には name メンバは有効な値にならないのです。後者の場合「A really, really longfile#23A4」のような文字列が取得されます。FSSpec は現在も機能しますが、本当の名前を含まないだけです。

FSSpec を全て FSRef に置き換えることはできますか?

おそらく出来ませんが、それはアプリケーション毎にケース・バイ・ケースです。

QuickTime API 群は現在も FSSpec を要求します。私は Toolbox の全てを使っているわけではないので、他にも FSRef を使用するバージョンのものが用意されていない API が残っているかもしれません。

Drag Manager が直接 FSRef をサポートしていないようなので、私は Drag を作ったり受け取ったりする場面では、いまだに FSSpec を使っています。

どうやったら Open/Save ダイアログで FSRef と Unicode のロングファイル名をサポートできますか?

Navigation Service 3.0 で搭載された NavCreateXXX 系 API を使う必要があります。(Navigation Services)これらのダイアログをシートとして実装する場合にも同様です。(*1)

(*1)
新しい NavCreateXXX API で kWindowModalityAppModal を指定した場合、NavDialogRun() はユーザがダイアログを閉じるまで処理を戻しません。シートを実装するために kWindowModalityWindowModal を使った場合には、Mac OS X の NavDialogRun() はすぐに処理を戻します。つまり NavDialogRun は処理を続けているけど、通常の Window のように振る舞うということです。これは Navigation.h に記述されてはいますが、十分には浸透してないようです

自アプリケーションの FSRef を得る方法は?

CFM バイナリの場合、FSSpec を取得できます。

OSErr GetCurrentProcessFSSpec( FSSpec &outFSSpec ) 
{
    ProcessSerialNumber currentProcess = { 0, kCurrentProcess };
    ProcessInfoRec processInfo; 

    processInfo.processInfoLength = sizeof(ProcessInfoRec); 
    processInfo.processName = NULL; /* we don't need the process name */ 
    processInfo.processAppSpec = &outFSSpec; 

    return GetProcessInformation( &currentProcess, &processInfo ); 
}

そしてそれを FSRef に変換します。バンドル形式の場合、この方法だとバンドルフォルダの FSRef ではなく、実行ファイルの FSRef が取れてしまいます。したがって次の方法を使います。

OSErr GetMyBundleFSRef( FSRef &outFSRef ) 
{
    ProcessSerialNumber currentProcess = { 0, kCurrentProcess }; 

    return GetProcessBundleLocation( &currentProcess, &outFSRef ); 
}

この方法は CFM バイナリでも使えるでしょう。

LaunchServices

LaunchServices は Mac OS X のみで使える API 群です。Mac OS X のファイル処理での最新情報を知りたかったら、LaunchServices.h を読むことを断然おすすめします。例えば、バンドルアプリケーションに関することや、表示名のこと、書類とアプリケーションの結びつけの新しいルールのこと、などなどです。

パッケージはファイルとフォルダの両方の特性を持っています。フォルダがパッケージかどうかは LSCopyItemInfoForRef() を使えば調べられます。

OSStatus LSIsApplication ( const FSRef &inRef, Boolean &outIsApplication, Boolean &outIsBundled ) 
{
    LSItemInfoRecord info;
    OSStatus err = LSCopyItemInfoForRef( &inRef, kLSRequestBasicFlagsOnly, &info ); 

    if ( err == noErr ) 
    {
        outIsApplication = ( kLSItemInfoIsApplication & info.flags ) != 0; 
        outIsBundled = ( kLSItemInfoIsPackage & info.flags ) != 0; 
    }

    return err; 
}

kLSRequestTypeCreator マスクフラグとともに LSCopyItemInfoForRef() を使うと、バンドルアプリケーションからタイプとクリエータを取得できます。FSGetCatalogInfo() はバンドルアプリケーションを普通のフォルダとして扱うので、タイプやクリエータ情報の為に使うことはできません。

LSGetApplicationForItem() を使うと、Finder がファイルをを開くのに使われるアプリケーションを調べることができます。

LSGetApplicationForInfo() を使うと、特定の拡張子/ファイルタイプ/クリエータをもつファイルを開くのに使われるアプリケーションを調べることができます。Mac OS X では書類とアプリケーションを結びつけるのに複雑なルールが存在し、さらにユーザがデフォルト設定を変更することもできるので、こういった機能をもつ API が必要になるのです。

ファイルパスの取得

理論的には、CFURLRef を作成し、それを経由してパスを生成するのは、ファイルパスを取得する方法の一つです。

CFURLRef url = CFURLCreateFromFSRef( kCFAllocatorDefault, &fsRef ); 
CFStringRef cfString = NULL; 

if ( url ) 
{
    cfString = CFURLCopyFileSystemPath( iCFURLRef, inPathStyle ); 
    CFRelease( url ); 
}

ここで「理論的には」という言い回しを使ったのにはワケがあって、Mac OS X(少なくとも v10.2.4 時点では)にはこれがうまく動かないことがあるバグが知られているからです。特に、パス中に Unicode サロゲートペアが含まれている場合には処理に失敗することが分かっています。

その代わりに FSRefMakePath() を使えます。ただし、私はこの API を使ったことがないので、読者に対して例を示したり、CFURLRef を使った方法と比べてどうか、といったことに言及するのはやめておきます。Files.h にある、この API のコメントをよく読んでみると、Mac OS 9 ではこの API は有意な名前を返さないことがあり得ることがわかります。Files.h にはさらに、FSRefMakePath() が HFS ファイルパスを返す時(Mac OS 9 の時のみ)と、UTF-8 形式の POSIX パスを返す時(おそらく Mac OS X の時のみ)があることが述べられています。

Mac OS X で HFS 形式のパスを取得したい場合や、FSRefMakePath() が常に期待通り動くわけではない事が判明した場合には、次のような方法が使えます。

Boolean GetPathManually( const FSRef &inFSRef, CFMutableStringRef ioPath, UniChar inSepChar ) 
{
    // ioPath should already be created with CFStringCreateMutablexxx before calling this. 

    FSRef localRef = inFSRef;
    OSStatus err = noErr;
    FSCatalogInfo info = {};
    std::vector<HFSUniStr255> names;

    CFStringDelete( ioPath, CFRangeMake( 0, CFStringGetLength( ioPath ) ) ); 

    do { 
        HFSUniStr255 name; 

        err = FSGetCatalogInfo( &localRef, kFSCatInfoNodeID, &info, &name, nil, &localRef ); 
        if ( err == noErr ) 
            names.push_back( name ); 
    } while ( err == noErr && info.nodeID != fsRtDirID ); 

    for ( int i = names.size() - 1; i >= 0; --i ) 
    {
        CFStringAppendCharacters( ioPath, names[ i ].unicode, names[ i ].length ); 
        if ( i > 0 ) 
            CFStringAppendCharacters( ioPath, &inSepChar, 1 ); 
    }

    return err == noErr; 
}

その他の注意点

Apple は FSRef の構造を公開していないので推測の域ではあるけれど、おそらく指しているアイテムの存在するボリュームフォーマットに依存した内容になっていると思う。現在のところ、HFS/HFS+ ボリュームに対する FSRef は移動やリネーム操作が起こっても追従して正しいファイルを指す。おそらく、VRefNum と file/directry ID を保持しているためです。この点では、FSRef は FSSpec よりも有益です。ただ、FSRef の構造が公開されていないことを勘案すると、この特徴はあくまで「現時点における」ものであることに留意しておくべきです。つまりいつ変更されるともしれないので、これに依存したコードは避けるべきでしょう。こういった追従が必要なら alias を使いましょう。(Alias Manager)

Tip

もし C++ で書かれているコードを FSRef を利用するものに更新していく場合には、FSSpec と FSRef を相互変換するキャスト演算子をオーバーロードするという方法が使えます。これにより移行のコストを小さくできます。

CarbonLib

CarbonLib を使ったプロジェクトの場合には、FSRef が HFS+ API として MacOS 9 で新たに追加されたということに注意。CarbonLib は FSRef 関連のラッパを用意しているけど、中身は実装されていないから、実際には OS 8 で FSRef を使った CarbonLib API を使えないのです。


(C) 2003 SkyTag Software, Inc.
訳文に対してのフィードバックは amn@mugiwara.jp まで

[index]