Hello, carbon.


はじめに

背景

 10年前にC言語で書いたMacintoshのプログラムをMac OS Xに移植することを思い立ちました。プログラムは科学技術計算を行い、とりあえずMacのインターフェイスで動けば良いと考え作成しました。筆者は典型的な「サンデイ・プログラマ」で、「THINK C プログラミング講座」を主な参考としました [THINK]。
 MacOSの時代にはThink CやSymantec Cを購入する必要がありましたが、Mac OS Xにはプログラミング環境が標準で付属しており、環境を整備する面での敷居は低くなっているようです。また、OS XのプログラムをC言語で記述する場合にはCarbonを利用することになりますが、重要なAPIが利用できるので移植は容易だとされています。
 しかしながら、Carbonに少し触れてみると、Cを開発言語として選択する初心者にとってはMacintoshでのプログラミングはむしろハードルが高いように感じられます。これは、Carbonはその自由度の高さ故に「Inside Macintosh」に相当する明確なガイドラインがなく、技術情報が限定されていることによるものと考えられます。
 Carbonのガイドラインは変化し続けており、旧来のToolBox APIは推奨されないものがあります。グラフィックの主なAPIであったQuickDrawはTigerの登場において、退場の時を迎えました [QDREF]。適切なガイドラインに基づいたプログラムを作成するためには、Appleから提供される最新の情報を押さえなければなりませんが、ADC (Apple Developer Connection)やMOSAの会員でない者には情報の入手は容易ではありません。また、非会員はそもそも専門家でない場合が多く、最新の情報を得ることは本質的に困難であるといえましょう。
 Appleは詳細な技術情報を公開していますが、それらのほとんどが英文です。また、Carbonに関する書籍は数が少なく、そのほとんどが経験者を対象としているようです。Appleが著者である「入門Carbon」はその典型であり [CARBON]、和文の図書、例えば「Mac OS X Carbonプログラミングガイド(小池邦人, 中野洋一著)」、は入手が困難です。
 以下に記す内容は、このような背景の元に、筆者の経験をメモとして残すことを主な目的としたものです。

内容

 "Hello, world" プログラムは C プログラミングの第一歩としてしばしば紹介されます。この文字列をいくつかのやり方で表示する方法を考えました。
 MacOSでのプログラミングからCarbon環境への移行には複数の経路があります。Mac OS Xの機能を利用するために必要な変更方法のいくつかを整理しました。
 上記のためのプログラム、関連する記述においては、推奨されているプログラムの方法を踏襲していません。また、筆者の未熟さに起因する不適切な内容が含まれている可能性があります。

プログラミングの環境

 筆者の環境は次に示す通りです。プログラムは同環境での動作を確認しています。
・PowerBook G3 333MHz, 192 MB
・Mac OS X 10.3.9
・Xcode 1.1
・Interface Builder 2.4


"Hello, world"

 この文字列を表示する5つの方法を提示します。イベント処理はCarbon event manager、リソースはNibベース, グラフィックはQuickDrawにそれぞれ基づいています。
 これらのプログラムは、メニュー、aboutウィンドウなど標準的なアプリケーションが備えるべき機能が欠如している、「実験」的なものです。

(1) ウインドウに表示、文字列をmain関数に記述
 新規プロジェクトを"carbon application"として作成すると、"main.c"と"main.nib "が生成されます。"main.nib "にはからのウインドウが含まれるが、変更しません。
 "main.c"にはデフォルトで骨格となるコードが埋め込まれていますが、これは下記のリストに差し替えます。
 始めに"carbon.h"をインクルードし、イベントハンドラの関数を宣言します。
 main関数は、nibファイルからウインドウを作成し、このウインドウにイベントハンドラを設定します。ウインドウを表示し、"Hello, world"を描きます。ここで、制御をシステムに渡します。
 イベントハンドラでは、クローズボタンが押された時に制御をmain関数に戻します。main関数ではウインドウを破棄し、プログラムを終了します。

"main.c"のリスト

#include <Carbon/Carbon.h> pascal OSStatus MainWindow_EventHandler (EventHandlerCallRef myHandler, EventRef event, void *userData); int main(int argc, char* argv[]) { WindowRef aWindowRef; IBNibRef nibRef; OSStatus err; EventTypeSpec eventSpec = {kEventClassWindow, kEventWindowClose}; err = CreateNibReference (CFSTR("main"), &nibRef); require_noerr (err, CantGetNibRef); err = CreateWindowFromNib (nibRef, CFSTR("MainWindow"), &aWindowRef); require_noerr (err, CantCreateWindow); DisposeNibReference (nibRef); err = InstallWindowEventHandler (aWindowRef, NewEventHandlerUPP(MainWindow_EventHandler), 1, &eventSpec, (void *)aWindowRef, NULL); ShowWindow (aWindowRef); SelectWindow (aWindowRef); SetPortWindowPort(aWindowRef); MoveTo(20,20); DrawString("¥pHello, world"); RunAppModalLoopForWindow (aWindowRef); HideWindow (aWindowRef); DisposeWindow (aWindowRef); CantGetNibRef: CantCreateWindow: return err; } pascal OSStatus MainWindow_EventHandler (EventHandlerCallRef myHandler, EventRef event, void *userData) { short ret = eventNotHandledErr; WindowRef aWRef; UInt32 eventKind; aWRef = (WindowRef)userData; SetPortWindowPort(aWRef); if (GetEventClass (event) == kEventClassWindow) { eventKind = GetEventKind (event); if (eventKind == kEventWindowClose) { QuitAppModalLoopForWindow(aWRef); ret = noErr; } } return(ret); }

 ユーザーのアクションに対する対応はウィンドウのクローズしか定義していませんが、ドックへの格納、ウインドウの最大化、ウインドウの移動、アプリケーションの切り替え時のウインドウ内部の描画が可能です。アプリケーションを隠すこともできますが、再描画されません。

(2) ウインドウに表示、文字列をNibファイルに記述
 "main.nib"の"MainWindow"にテキストフィールドを加えます。ここでは、EditTextを入れました。SignatureとIDをそれぞれセットし、これをソースプログラムで定義します。
 main関数では、テキストフィールドのコントロールIDを取得し、データとして"Hello, world"をセットします。

"main.c"のリスト

#include <Carbon/Carbon.h> #define HELLO2_SIGNATURE 'hll2' #define HELLO2_TEXT_ID 1 pascal OSStatus MainWindow_EventHandler (EventHandlerCallRef myHandler, EventRef event, void *userData); int main(int argc, char* argv[]) { WindowRef aWindowRef; IBNibRef nibRef; OSStatus err; EventTypeSpec eventSpec = {kEventClassWindow, kEventWindowClose}; ControlID aCntrlID; ControlRef aCntrlRef; Str255 aTextBuff = {"¥pHello, world"}; err = CreateNibReference (CFSTR("main"), &nibRef); require_noerr (err, CantGetNibRef); err = CreateWindowFromNib (nibRef, CFSTR("MainWindow"), &aWindowRef); require_noerr (err, CantCreateWindow); DisposeNibReference (nibRef); err = InstallWindowEventHandler (aWindowRef, NewEventHandlerUPP(MainWindow_EventHandler), 1, &eventSpec, (void *)aWindowRef, NULL); ShowWindow (aWindowRef); SelectWindow (aWindowRef); aCntrlID.signature = HELLO2_SIGNATURE; aCntrlID.id = HELLO2_TEXT_ID; GetControlByID(aWindowRef, &aCntrlID, &aCntrlRef); SetControlData(aCntrlRef, kControlNoPart, kControlEditTextTextTag, *aTextBuff, aTextBuff+1 ); RunAppModalLoopForWindow (aWindowRef); HideWindow (aWindowRef); DisposeWindow (aWindowRef); CantGetNibRef: CantCreateWindow: return err; } pascal OSStatus MainWindow_EventHandler (EventHandlerCallRef myHandler, EventRef event, void *userData) { short ret = eventNotHandledErr; WindowRef aWRef; UInt32 eventKind; aWRef = (WindowRef)userData; SetPortWindowPort(aWRef); if (GetEventClass (event) == kEventClassWindow) { eventKind = GetEventKind (event); if (eventKind == kEventWindowClose) { QuitAppModalLoopForWindow(aWRef); ret = noErr; } } return(ret); }

 テキストフィールド内のテキストを選択するとハイライト表示されますが、編集はできません。

(3) ウインドウに表示、文字列を文字列ファイルに記述
 文字列をファイル"Localizable.strings"に記述します。
 "main.c"のmain関数ではこの文字列を読み込み、テキストフィールドにセットします。

"Localizable.strings"のリスト

{ "HELLO_WORLD" = "Hello, world."; }


"main.c"のリスト

#include <Carbon/Carbon.h> #define TEXT_KEY "HELLO_WORLD" #define HELLO3_SIGNATURE 'hll3' #define HELLO3_TEXT_ID 1 pascal OSStatus MainWindow_EventHandler (EventHandlerCallRef myHandler, EventRef event, void *userData); int main(int argc, char* argv[]) { WindowRef aWindowRef; IBNibRef nibRef; OSStatus err; EventTypeSpec eventSpec = {kEventClassWindow, kEventWindowClose}; ControlID aCntrlID; ControlRef aCntrlRef; CFStringRef aText; err = CreateNibReference (CFSTR("main"), &nibRef); require_noerr (err, CantGetNibRef); err = CreateWindowFromNib (nibRef, CFSTR("MainWindow"), &aWindowRef); require_noerr (err, CantCreateWindow); DisposeNibReference (nibRef); err = InstallWindowEventHandler (aWindowRef, NewEventHandlerUPP(MainWindow_EventHandler), 1, &eventSpec, (void *)aWindowRef, NULL); aCntrlID.signature = HELLO3_SIGNATURE; aCntrlID.id = HELLO3_TEXT_ID; GetControlByID(aWindowRef, &aCntrlID, &aCntrlRef); ShowWindow (aWindowRef); SelectWindow (aWindowRef); aText = CFCopyLocalizedString (CFSTR(TEXT_KEY), NULL); SetControlData (aCntrlRef, 0, kControlEditTextCFStringTag, sizeof(CFStringRef), &aText); CFRelease (aText); DrawOneControl(aCntrlRef); RunAppModalLoopForWindow (aWindowRef); HideWindow (aWindowRef); DisposeWindow (aWindowRef); CantGetNibRef: CantCreateWindow: return err; } pascal OSStatus MainWindow_EventHandler (EventHandlerCallRef myHandler, EventRef event, void *userData) { short ret = eventNotHandledErr; WindowRef aWRef; UInt32 eventKind; aWRef = (WindowRef)userData; SetPortWindowPort(aWRef); if (GetEventClass (event) == kEventClassWindow) { eventKind = GetEventKind (event); if (eventKind == kEventWindowClose) { QuitAppModalLoopForWindow(aWRef); ret = noErr; } } return(ret); }

(4) 文字列をファイルに記述
 Navigation Servicesによりファイルを作成、文字列"Hello, world"を書き出します。
 "main.nib"は使いません。
 main関数では、ナビゲーションイベントハンドラを登録し、制御をシステムに移します。イベントハンドラでは、書き出すファイルが選択された場合に、ファイルのFSRefを得ます。
 ここでmain関数に戻り、ファイルを開き、文字列"Hello, world"を書き出し、ファイルを閉じます。


"main.c"のリスト

#include <Carbon/Carbon.h> #define OUTPUT_FILE_SIGNATURE 'SepP' #define OUTPUT_FILE_TYPE 'helo' pascal void FileNavEventCallback (NavEventCallbackMessage callBackSelector, NavCBRecPtr callBackParms, void *callbackUD); OSErr PrepareSaveFile (NavReplyRecord *reply); NavEventUPP gEventProc; FSRef gFileRef; int main(int argc, char* argv[]) { extern NavEventUPP gEventProc; NavDialogRef aNavRef; NavDialogCreationOptions aNavOptions; char aText[] = "Hello, world.¥0"; HFSUniStr255 aForkName; SInt16 aForkRefNum; long aSize; OSStatus err; err = NavGetDefaultDialogCreationOptions (&aNavOptions); if (err == noErr) { gEventProc = NewNavEventUPP (FileNavEventCallback); aNavOptions.modality = kWindowModalityAppModal; err = NavCreatePutFileDialog (&aNavOptions, OUTPUT_FILE_TYPE, OUTPUT_FILE_SIGNATURE, gEventProc, NULL, &aNavRef); if (err == noErr) { if (aNavRef != NULL) { err = NavDialogRun(aNavRef); if (err != noErr) { NavDialogDispose(aNavRef); DisposeNavEventUPP(gEventProc); return err; } } } } err = FSGetDataForkName (&aForkName); err = FSOpenFork (&gFileRef, (UniCharCount) aForkName.length, aForkName.unicode, fsRdWrPerm, &aForkRefNum); if (err == noErr) { aSize = strlen (aText); err = FSWriteFork (aForkRefNum, fsAtMark, 0, aSize, (void *)aText, NULL); err = FSCloseFork (aForkRefNum); } return err; } pascal void FileNavEventCallback (NavEventCallbackMessage callBackSelector, NavCBRecPtr callBackParms, void *callbackUD) { extern NavEventUPP gEventProc; NavReplyRecord reply; NavUserAction userAction = 0; OSStatus err = noErr; switch (callBackSelector) { case kNavCBUserAction: err = NavDialogGetReply (callBackParms->context, &reply); if (err == noErr) { userAction = NavDialogGetUserAction(callBackParms->context); switch (userAction) { case kNavUserActionSaveAs: err = PrepareSaveFile (&reply); break; } err = NavDisposeReply (&reply); } break; case kNavCBTerminate: NavDialogDispose (callBackParms->context); DisposeNavEventUPP (gEventProc); break; } } OSErr PrepareSaveFile (NavReplyRecord *reply) { OSErr err = noErr; FSRef fileRefParent; AEDesc actualDesc; err = AECoerceDesc (&reply->selection, typeFSRef, &actualDesc); if (err == noErr) { err = AEGetDescData (&actualDesc, (void *)&fileRefParent, sizeof(FSRef)); if (err == noErr) { UniChar *nameBuffer = NULL; UniCharCount sourceLength = 0; sourceLength = (UniCharCount) CFStringGetLength (reply->saveFileName); nameBuffer = (UniChar *) NewPtr (sourceLength * 2); CFStringGetCharacters (reply->saveFileName, CFRangeMake(0, sourceLength), &nameBuffer[0]); if (nameBuffer != NULL) { if (reply->replacing) { FSRef fileToDelete; err = FSMakeFSRefUnicode (&fileRefParent, sourceLength, nameBuffer, kTextEncodingUnicodeDefault, &fileToDelete); if (err == noErr) { err = FSDeleteObject (&fileToDelete); } } if (err == noErr) { extern FSRef gFileRef; FileInfo *fileInfo; FSCatalogInfo catalogInfo; fileInfo = (FileInfo *) &catalogInfo.finderInfo[0]; BlockZero (fileInfo, sizeof(FileInfo)); fileInfo->fileType = OUTPUT_FILE_TYPE; fileInfo->fileCreator = OUTPUT_FILE_SIGNATURE; err = FSCreateFileUnicode (&fileRefParent, sourceLength, nameBuffer, kFSCatInfoFinderInfo, &catalogInfo, &gFileRef, NULL); } } DisposePtr ((Ptr)nameBuffer); } AEDisposeDesc (&actualDesc); } return err; }

(5) 文字列を印刷出力
 Printing managerにより文字列"Hello, world"を印刷します。印刷に対応すると、pdfファイルを作成することが可能となります。印刷設定は省略します。また、印刷ダイアログをシートとすることが可能ですが、この機能は利用しません。
 "main.nib"は使いません。
 main関数では、印刷セッションを作成し、ページ書式、ページ設定を作成します。ページ数の最小と最大値はいずれも1とします。次に印刷ダイアログを呼び、印刷を開始します。
 このプログラムだけmain関数のみで構成しています。

"main.c"のリスト

#include <Carbon/Carbon.h> int main(int argc, char* argv[]) { PMPrintSession aPrintSession = NULL; PMPageFormat aPageFormat = kPMNoPageFormat; PMPrintSettings aPrintSettings = kPMNoPrintSettings; UInt32 aMinPage = 1; UInt32 aMaxPage = 1; Boolean aAccepted; GrafPtr aPrintingPort; PMRect aPageRect; OSStatus aStatus = noErr; // Create Print Session aStatus = PMCreateSession (&aPrintSession); // Prepare Page Format if (aStatus == noErr) { aStatus = PMCreatePageFormat (&aPageFormat); if (aStatus == noErr) { aStatus = PMSessionDefaultPageFormat (aPrintSession, aPageFormat); } } if (aStatus != noErr) { PMRelease(aPageFormat); PMRelease(aPrintSession); goto CantSetupPrinting; } // Create Print Settings if (aStatus == noErr) { aStatus = PMCreatePrintSettings (&aPrintSettings); if (aStatus == noErr) { aStatus = PMSessionDefaultPrintSettings (aPrintSession, aPrintSettings); } } // Set Page Range aStatus = PMSetPageRange(aPrintSettings, aMinPage, aMaxPage); if (aStatus != noErr) goto CantSetupPrinting; // Print Dialog aStatus = PMSessionPrintDialog (aPrintSession, aPrintSettings, aPageFormat, &aAccepted); aStatus = PMSessionBeginDocument(aPrintSession, aPrintSettings, aPageFormat); if (aStatus == noErr) { aStatus = PMSessionBeginPage(aPrintSession, aPageFormat, NULL); if (aStatus == noErr) { aStatus = PMSessionGetGraphicsContext(aPrintSession, kPMGraphicsContextQuickdraw, (void**)&aPrintingPort); if (aStatus == noErr) { SetPort(aPrintingPort); aStatus = PMGetAdjustedPageRect(aPageFormat, &aPageRect); MoveTo(20,20); DrawString("¥pHello, world"); } aStatus = PMSessionEndPage (aPrintSession); } aStatus = PMSessionEndDocument (aPrintSession); } CantSetupPrinting: return aStatus; }

Carbonへの移行


 Classic環境を前提としたプログラムをOS Xの機能を活かすように移植する場合には、コードのかなりの部分を書き換える必要が生じます。違いのいくつかをまとめます。

(1) イベント処理
 イベント処理はウインドウシステムのプログラムの根幹ですので、Carbon Event Managerに則ることが推奨されます。根幹であるが故に、移植する時にはmain関数を大きく変えることになるでしょう。
 メニューイベントハンドラのインストールはマクロで定義されています [CARBON]。メニュー・バーの設定とイベントハンドラのインストールは次のように書けます。


メニュー・バーの設定とイベントハンドラのインストール部分のリスト

// Set the menu bar err = SetMenuBarFromNib(nibRef, CFSTR("MenuBar")); require_noerr( err, CantSetMenuBar ); // Install event handlers
menuRef = GetMenuHandle(FILE_MENU_ID); err = InstallMenuEventHandler (menuRef, NewEventHandlerUPP(SP_SeprepEventHandler), 1, &eventSpec, NULL, NULL);

(2) リソース
 Classic環境ではResEditでリソースを作成しましたが、これがnibファイルに替えられました。nibファイルはInterface Builderにより作成し、メニュー、ウインドウ、様々なコントロールを配置できます。
 従来、文字列は一つのファイルに含まれていましたが、これはローカライズのために別のファイル"Localizable.strings"に移されています。

(3) Modal Dialog、Alert
 Classic環境のダイアログとアラートはウインドウ・クラスに統合されました。いずれも取り扱いがウインドウと同様になり、コーディングの量はかなり増えることになりました。

(4) 文字列
 元々、Macintoshのプログラミング言語はpascalでしたので、Classic環境においてはpascal文字列とC文字列を使い分ける必要がありました。OS Xのシステムはユニコード文字列であるCFStringをサポートするので、扱うべき文字列の種類が増えることになります。

(5) ファイル処理
 「入門Carbon」では、参照するファイルを表現するデータ型としてFSRefを推奨しています。また、ファイルの選択などにはNavigation Servicesを利用することになります。これらから、ファイル処理はClassic版を書き換える必要が生じます。
 データ型として、従来のFSSpec、FSRefとともに、URLによる表現も可能ですので、上手に使い分けることが求められます。

(6) プリント処理
 Printing Managerにより処理しますが、スレッド化されている、pdfベースのレンダリングが行われる、描画はContextベースに行う、ことなどがClassic環境と異なります。

(7) ヘルプ
 ヘルプはヘルプビューアを起動して閲覧します。内容はhtmlで記述できるので、簡易なものも作成できると思います。

(8) About
 アプリケーションのプロパティを記述したファイル"Info.plist"の内容を表示します。所定の情報を"Info.plist"にまとめておくことになります。

(9) Quartz
 QuickDrawはQuartzに代替されます。描画の対象となるContextはウインドウ、印刷、pdfなどの対象に応じて作成、利用します。このシステムを理解することが第一歩となるでしょう。

(9) 標準ライブラリ
 Cの標準ライブラリはヘッダファイルをインクルードしなくとも使えるようです。

(10) opaque
 「不透明な、不明瞭な」という意味ですが、重要なデータ型はその実態を隠蔽されて、特定のアクセサ関数を通じてのみ操作できるようになっています。
 システムがオブジェクト指向化されていることによるものと想像しますが、手続き型言語のCに馴染んでくると違和感のようなものを感じます。


引用文献

[QDREF] QuickDraw Reference, 2005-11-09, Apple Computer, Inc. (2005)
[CARBON] 入門Carbon, Apple Computer, Inc.(著), 長瀬 嘉秀(監訳), (株)テクノロジックアート(訳), (株)オライリー・ジャパン, ISBN 4-87311-069-6 (2001)
[THINK] THINK C プログラミング講座, 藤本 裕之 著, 技術評論社 (1991)


参考資料

・Apple Developer Connection, http://developer.apple.com/ja/
・Macプロ裏ミング日記, http://www.ottimo.co.jp/koike/


経歴

N-88 Basic / MS-DOS / NEC PC-9801 CV
Basic98 / MS-DOS / NEC PC-9801 CV
TURBO C / MS-DOS / NEC PC-9801 CV
THINK C / Mac OS / PowerBook 140
Symantec C / Mac OS / PowerBook 140
Xcode / Max OS X Panther / PowerBook G3

busaho
December 1, 2005