ホームページ開発ツール>Xojo / Real Studio Trial and Error・レガシーAPIを延命する

 Xojo / Real Studio Trial and Error

レガシーAPIを延命する

目次
 はじめに

 以下は、Xojo Cocoa&Carbonビルドについての話題です。

 OS9時代から利用されてきたテクノロジー(以下、レガシーAPI)は、さすがに時代とともに非推奨(deprecated)となってきました。
 今後新規に作成するアプリケーションで利用することはないとしても、既に利用中のもので多少なりとも延命したい、というニーズはあるかと思い、少し調べてみました。

 今回対象としたレガシーAPIは、以下の二つです。
 1. リソースフォーク
 2. エイリアスパス

 なお検証には、Xojo 2014 Release 2を用いています。(Mac mini + OS X 10.10.1 Yosemite)


 現状確認

 リソースフォーク
 エイリアスパス
 共通

 方針

 いずれも、Xojoの標準機能では対応できないため、考えられるのは、(1)プラグイン、(2)コマンドラインツール(以下、ツール)、あたりでしょう。
 アプリケーションの開発のし易さではプラグインの方に分がありますが、ツールの方が(ツール自体の)作り易さでは上なので、まずは後者で作ってみることにしました。
作成にはXcode 6.1を使用したが、標準ではコマンドラインツールが含まれていないので、別途導入しておく。
 仕様は、以下の通りとしました。

 リソースフォーク
 エイリアスパス

 リソースフォークの実装
  1. Xcode6.1を起動し、File > New > Project...メニューを選択
  2. ダイアログのリストから「OS X > Application」と選択し、「Command Line Tool」をクリックしてNextボタンを押す
  3. プロジェクト名を「rsrcCommand」として保存
  4. プロジェクトの設定を変更
    OS X Deployment Target > OS X 10.7
    Objective-C Automatic Reference Counting > No
  5. rsrcCommand.mを開き、中身を全文削除後に、以下をペースト
    #import <Foundation/Foundation.h>
    #import <AppKit/AppKit.h>
    
    static int ExtractResources(NSString *sourceFile, NSString *kind, NSString *destFolder);
    
    int main (int argc, const char * argv[]) {
    	NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    	
    	// insert code here...
    	
    	NSString *pathF = [NSString stringWithUTF8String:argv[1]];
    	NSString *pathD = [NSString stringWithUTF8String:argv[2]];
    	NSString *kind  = [NSString stringWithUTF8String:argv[3]];
    	
    	// Get ResourcesFork and Put File
    	int ret = ExtractResources(pathF, kind, pathD);
    	
    	[pool drain];
    	return ret;
    }
    
    int ExtractResources(NSString *sourceFile, NSString *kind, NSString *destFolder) {
    	NSError *error;
    	OSStatus err;
    	int refnum;
    	int ret = 0;
    	
    	OSType type = 0;
    	if ([kind isEqualToString:@"PICT"]) type = 0x50494354;
    	if ([kind isEqualToString:@"TEXT"]) type = 0x54455854;
    	
    	// Verify the existance of destFolder
    	BOOL isFolder = true;
    	if (![[NSFileManager defaultManager] fileExistsAtPath:destFolder isDirectory:&isFolder] || !isFolder) return -1;
    	
    	// Open the source resource file
    	FSRef resFile;
    	err = FSPathMakeRef((UInt8 *)[sourceFile fileSystemRepresentation], &resFile, false);
    	if (err != noErr) return -2;
    	
    	HFSUniStr255 forkName;
    	FSGetResourceForkName(&forkName);
    	err = FSOpenResourceFile(&resFile, forkName.length, forkName.unicode, fsRdPerm, &refnum);
    	if (err == eofErr) return -3;
    	else if (err != noErr) return -4;
    	
    	UseResFile(refnum);
    	
    	// Extract all resources of the given type
    	Str255 resName;
    	Handle resHand;
    	short theID;
    	ResType theType;
    	int i;
    	int max = Count1Resources(type);
    	for (i=1; i<=max; i++) {
    		
    		// Get ID
    		resHand = Get1IndResource(type, i);
    		GetResInfo(resHand, &theID, &theType, resName);
    		
    		// Load the resource from ID
    		Handle resource = Get1Resource(type, theID);
    		if (resource == nil) {
    			ret = -5;
    			continue;
    		}
    		HLock(resource);
    		NSData *data = [[NSData alloc] initWithBytesNoCopy:*resource length:GetHandleSize(resource) freeWhenDone:NO];
    		
    		// Get SubFolder ( Create if not Exists )
    		NSString *destSubFolder = [destFolder stringByAppendingPathComponent:kind];
    		if ( ![[NSFileManager defaultManager] fileExistsAtPath:destSubFolder] )
    		[[NSFileManager defaultManager] createDirectoryAtPath:destSubFolder withIntermediateDirectories:YES attributes:nil error:&error];
    		
    		// Write the resource in a new file
    		NSString *destFile, *destPath;
    		if ([kind isEqualToString:@"TEXT"]) {
    			destFile = [NSString stringWithFormat:@"%d", theID];
    			destPath = [destSubFolder stringByAppendingPathComponent:destFile];
    			if( ![[NSFileManager defaultManager] createFileAtPath:destPath contents:data attributes:nil] ) ret = -6;
    		}
    		if ([kind isEqualToString:@"PICT"]) {
    			destFile = [NSString stringWithFormat:@"%d.tiff", theID];
    			destPath = [destSubFolder stringByAppendingPathComponent:destFile];
    			NSImage *img = [[[NSImage alloc] initWithData: data] autorelease];
    			[[img TIFFRepresentation] writeToFile:destPath atomically:YES];
    		}
    		
    		// Release the resource
    		[data release];
    		HUnlock(resource);
    		ReleaseResource(resource);
    	}
    	return ret;
    }
    
    注1)作者の慣れの関係でCocoa APIを利用しているが、Carbonで統一することも可能かもしれない。
    注2)独自のリソースタイプを使用したい場合は、適宜追加してください。
    注3)当初、NSApplicationLoad()を記述していたが、不要だったので削除した。(2014.12.02)

  6. ビルド
  7. 出来上がったツールを、(ひとまず)Xojoプロジェクトと同じ場所に置く
  8. Xojo側の、ツールを起動して結果を受け取るメソッドは、以下の通り
    メソッド名: GetResourceFork
    引数: f As FolderItem, kind As String
    戻り値型: FolderItem
    
    Dim f2 As FolderItem
    
    // rsrcCommandの取得(アプリケーションパッケージ内)
    f2=App.ExecutableFile.Parent.Parent.Child("Applications").Child("rsrcCommand")
    if f2=nil or f2.Exists=false then
      // なければ、プロジェクトファイルの場所から取得
      f2=App.ExecutableFile.Parent.Parent.Parent.Parent.Child("rsrcCommand")
      if f2=nil or f2.Exists=false then
        return nil
      end if
    end if
    Dim appPath As String = f2.NativePath
        
    // テンポラリフォルダ内に一時フォルダを作成
    Dim dt As New Date
    Dim fname As String
    fname=Format(dt.Year,"0000")+Format(dt.Month,"00")+Format(dt.Day,"00") _
        +Format(dt.Hour,"00")+Format(dt.Minute,"00")+Format(dt.Second,"00")  // 現在日時の取得(フォルダ名を固有化するために用いる)
    f2=SpecialFolder.Temporary.Child(fname)  // テンポラリフォルダ内にフォルダを取得
    f2.CreateAsFolder  // フォルダの生成
    
    // rsrcCommand実行
    Dim s As Shell
    s = New Shell
    s.Execute(""""+appPath+""" """+f.NativePath+""" """+f2.NativePath+""" "+kind)
    if s.ErrorCode <> 0 Then
      MsgBox("Error code: " + Str(s.ErrorCode))
      return nil
    end if
    
    // Sleep 0.1 seconds
    Dim t As Integer = ticks
    do
    loop until ticks-t > 60*0.1
    
    // 一時フォルダを返す
    return f2
    
    注1)一時フォルダはそのままにしておいても問題ないと思われるが、必要ならリソース取得後に削除してください。(中身の詰まったフォルダの削除の仕方は、Xojoリファレンスのサンプルが参考になります。)

  9. Xojo側の、リソースフォークにアクセスするメソッドは以下の通り
    TEXT:TextInputStreamで開き、.ReadAllで読み込む
    PICT:通常の画像ファイルと同じ方法で開く
 結果、リソースフォークを読み込めることを確認しました。


 エイリアスパスの実装
  1. Xcode6.1を起動し、File > New > Project...メニューを選択
  2. ダイアログのリストから「OS X > Application」と選択し、「Command Line Tool」をクリックしてNextボタンを押す
  3. プロジェクト名を「aliasCommand」として保存
  4. プロジェクトの設定を変更
    OS X Deployment Target > OS X 10.7
    Objective-C Automatic Reference Counting > No
  5. aliasCommand.mを開き、中身を全文削除後に、以下をペースト
    #import <Foundation/Foundation.h>
    
    static int GetAliasPath(const char* sourceFile);
    static void HandleToString(AliasHandle ahd);
    
    int main (int argc, const char * argv[]) {
    	NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    	
    	// insert code here...
    	
    	// Get Alias
    	int ret = GetAliasPath(argv[1]);
    	
    	[pool drain];
    	return ret;
    }
    
    int GetAliasPath(const char *sourcePath) {
    	OSStatus err;
    	FSRef theRef;
    	err = FSPathMakeRef((unsigned char *)sourcePath, &theRef, NULL);
    	if (err != 0) return err;
    	
    	AliasHandle ahd;
    	FSNewAlias(nil, &theRef, &ahd);
    	
    	HandleToString(ahd);
    	
    	DisposeHandle((Handle)ahd);
    	return err;
    }
    
    void HandleToString(AliasHandle ahd) {
    	long size = GetHandleSize((Handle)ahd);
    	int state = HGetState((Handle)ahd);
    	HLock((Handle)ahd);
    	unsigned char *ahdc = (unsigned char *)*ahd;
    	unsigned char *strbin = (unsigned char *)malloc(size);
    	for(int i=0; i<size; i++) {
    		strbin[i] = ahdc[i];
    	}
    	HSetState((Handle)ahd, state);
    	
    	for(int i=0; i<size; i++) {
    		printf("%02x", strbin[i]);
    	}
    }
    
  6. ビルド
  7. 出来上がったツールを、(ひとまず)Xojoプロジェクトと同じ場所に置く
  8. Xojo側の、ツールを起動して結果を受け取るメソッドは、以下の通り
    メソッド名: GetAliasPath
    引数: filePath As String
    戻り値型: String
    
    // パスが空なら戻る
    if filePath="" then
      return ""
    end if
    
    // aliasCommandの取得(アプリケーションパッケージ内)
    Dim f2 As FolderItem
    f2=App.ExecutableFile.Parent.Parent.Child("Applications").Child("aliasCommand")
    if f2=nil or f2.Exists=false then
      // なければ、プロジェクトファイルの場所から取得
      f2=App.ExecutableFile.Parent.Parent.Parent.Parent.Child("aliasCommand")
      if f2=nil or f2.Exists=false then
        msgBox "not found."
        return ""
      end if
    end if
    Dim appPath As String = f2.NativePath
    
    // 入力ファイルの取得(NativePathを取得するため)
    f2=GetFolderItem(filePath)
    
    // AliasPath実行
    Dim s As Shell
    s = New Shell
    s.Execute(""""+appPath+""" """+f2.NativePath)+""""
    if s.ErrorCode <> 0 Then
      MsgBox("Error code: " + Str(s.ErrorCode))
      return ""
    end if
    Dim st As String
    st=s.Result
    s.Close
    
    // 返ってきた値はバイナリーを文字で表現したものなので、バイナリーに戻す(ただし、結果は文字列として返す)
    Dim i, cnt As Integer
    Dim mb As MemoryBlock
    
    cnt=st.LenB/2
    mb=new MemoryBlock(cnt)
    for i=0 to cnt-1
      mb.Byte(i)=Val("&h"+MidB(st,i*2+1,2))
    next
    
    return mb.StringValue(0,cnt)
    
    注1)行きがかり上、パスを引数にしているが、FolderItemでもよい。(再取得のステップを省略できる。)
    注2)バイナリーに戻すステップが、試行錯誤時の未整理状態のままだったので、クリンアップした。(2014.12.02)

  9. Xojo側で、CarbonのDeclareメソッドコールと上記メソッドコールを差し替え
 結果、(保存した)エイリアスパスを、絶対パスに復元できることを確認しました。


 おわりに

 開発時はいいとして、ビルド時はその都度手作業で、パッケージ内のApplicationsフォルダにコマンドラインツールをコピーする必要があり、面倒ではあります。
 やはりプラグイン化した方がいいかも。


 お世話になったサイト

 貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)

 参考サイト(1):XonTC[05] リソースを扱う
 参考サイト(2):FolderItem.Delete - Xojo Documentation
 参考サイト(3):cocoa - How i can modernize deprecated FSPathMakeRef, FSIsAliasFile, FSResolveAliasFileWithMountFlags, FSRefMakePath - Stack Overflow
 参考サイト(4):Alias Storage | Cocoabuilder


 更新履歴

 2014.12.02 ソースコードを一部改変
 2014.11.28 新規作成


[Home]  [MacSoft]  [Donation]  [History]  [Privacy Policy]  [Affiliate Policy]