Macプログラミングとの遭遇

2012/02/07 GCCのバグ(多分)

YSFLIGHTのバージョンアップに向けて、テスト版を公開しようかな、と、思って、Mac OSX上でコンパイルしようと思って、どうせなら、32-bitと64-bitと同じMakefileでコンパイルできるようにしよう、と、思って、よくよく調べたら、Mac OS X Lionは64-bitバイナリしかサポートしてないし、Intel Macは二代目から64-bit CPUになったとかで、YSFLIGHTはどうせMac OSX 10.5.8以降しか対応してないから、それじゃ、以後YSFLIGHT for Macは64-bitだけにしよう、と、思った。一応、Makefile書く練習の一環で、環境変数で切り替えられるようにしたけど。

Windows用だと、まだ32-bitバイナリにしておく意義がある。YSFLIGHTを実行するのに十分高速な32-bit Atom CPU搭載のPCがごろごろしているから。

僕はどうもIDEってやつが好きじゃない。バージョンが変わるたびに使い方が変わるし。毎回違う使い方を覚えるために浪費する時間があまりにももったいない。Makefileの使い方を極めれば、多分、僕がプログラマーのキャリアを続けてる間ぐらいはMakeはどこでも使えるだろう。Visual C++付属のNMAKEが貧弱で、Windows用だけ別のMakefile書かなきゃならないけど、Linux用とMac用ならほとんどの部分を共通にできる。そう思って、最近、gmakeの本を買った。

感想は、あんまりよくまとまってない。というか、通して読まないとわかんない気がする。いや、リファレンスブックというよりはそういう通して読む本なのかもしれない。だとしても、読むと「こんなこともできるのか」とわかることがあるんで、役に立っている。日本にいたときにmakeってタイトルの本を買って持ってきたと思うんだけど、研究室で行方不明になった。日本語の本だから誰かが持って行っちゃったってことは無さそうなんだけど。

cmake使うといいと聞いたこともあったんだけど、仕事でcmake使って、やっぱりやめた。YSFLIGHTにはcmakeは使わない方針。

IDE使わなかったらデバッガどうするの?ってよく聞かれるけど、いや、デバッガで取れるようなバグなんてデバッガ使わなくてもすぐ取れるんだって。本当に追跡が難しいバグは、実行途中の経過をどんどん出力してどこで最初に何が狂ったのか調べる方が早く取れる、と、思う。第一、デバッグモードでコンパイルすると動くのに、リリースモードだと動作が変になるようなやつだと本当にデバッガは役に立たない。マルチスレッドプログラムで、大きなデータ食わせたとき、10回に1回変になる、なんていうのは最悪だけど、そうなったら、途中経過を全部セーブするように仕掛けて、変な結果が出たら止まるようにして、変な結果が出るまで何度もバッチで回し続けて、変な結果が出たところで、どの時点で変になったかを見る、ていうようようなデバッグの仕方をすることにしている。

それは、本題ではなくて、Mac OSX用64ビットYSFLIGHTをコンパイルしようと思ったら、変なリンクエラーが出た。↓こんなエラーメッセージ。

ld: in /Users/soji/ysbin/macosx64/lib/libfsguicore.a(fsgui.o), in section __TEXT,__text reloc 334: local relocation for address 0x0001C802 in section __text does not target section __const

Relocationなんて、6809CPUを思い出させるようなキーワードに、どうも、セクションがどうこう言ってるのは、8086のセグメントを思い出させるような、なんともレトロなエラーメッセージ、と、思った。が、、fsgui.oで何かが良くない、という以外まったく意味がわからない。334という数字はソースの行では無さそうだし、アドレス0x0001C802と言われても、そこに何が書いてあるのかさっぱりわからない。仕方ないんで、Googleで「ld local relocation does not target section」とかで検索してみたところ、他にも困ってる人がいるようだった。ところが、そういう人たちは、どこからからダウンロードしてきたソースをコンパイルしようとしてこのエラーに直面しているようで、それに対する解決は、それぞれのプログラム特有の解決みたいで、僕の問題の解決は見つからなかった。

ちなみに、このエラーは32-bitバイナリでコンパイルしたときは出ない。Linux上で64-bitバイナリをコンパイルしたときも、やっぱり出なかった。Mac OSX上で64-bitバイナリをコンパイルしてリンクしようとしたときだけ出るようだ。

それで、その検索して出てきた内容を読んだ限りでは、どうも、Mac OSXの64-bitバイナリは、データが決まったアドレスからのオフセットに無いと困るとか、そのような説明が書いてあった。決まったオフセットって、64-bitバイナリなんだから(4ギガx4ギガ)バイト、リニアに使えるんじゃないの?と、思ったけど、どうも上のエラーは、その所定の位置にデータを配置できない、というエラーのようだった。このエラーを出すオブジェクトファイルのうち、数本は結構大きなオブジェクトだった。そういうバイナリだと、中でStaticに持ってるデータも大きいのかも、と、想像できた。ところが、一本は、結構短いファイルで、内部のStaticデータが大きいはずが無い。せいぜいStaticに持ってるデータなんて、Wide Characterの文字列がいくつかある程度だった。

ひょっとして、このWide Characterの文字列が悪いのか?

そう思って、試しに、Wide Characterの文字列を使ってるところを探して、

wStr.Set(L"X");

こんな感じに書いてある箇所を、全部次のように書き換えてみた。

const wchar_t x[]={'X',0};
wStr.Set(x);

もちろん、実質的な意味は変わらない。コンパイラが、xの置き場所をスタティックなメモリにしようと思えばそうもできるし、スタック上に取って、この位置に来たときに内用をセットする(文字列はデータセグメントからコードセグメントに移動したようなもん)こともできる。ただ、前者の書き方だと、L"X"は間違いなくスタティックなメモリ上に置かれるはずだから、後者だとスタティックなデータの量が減る可能性がある。

で、試しにコンパイルしてみたら、なんと成功!こりゃ、多分GCCのバグだね。Mac OSX用バイナリを出力するところは、Appleが多分勝手に何か手を入れてるはずだから、そのときなんか失敗したんじゃないのかな。

ちなみに、このGCCは、XCode 3と一緒にインストールされたものだから、XCode 4のGCCだと解決しているかもしれない。けど、ひょっとしてこれと同じエラーで困ってる人がいたら、ソースの中のWide Character 文字列の書き方を変えてみると直るかもしれない。

2010/03/03 ヘルプファイルを開く & 言語の判定

YSFLIGHT for Mac OS Xで、メニューからヘルプファイルを開けるようにするのを忘れていたのに気がついたので、なんとかすることにした。YSFLIGHTは、ヘルプをHTMLファイルで作ってあるので、メニューからヘルプを開くには、Webブラウザを起動して、そのHTMLファイルを開かせる必要があった。WindowsだとShellExecute関数一発でできる。これまでの感触だと、Mac OSのCocoa APIを使った方が簡潔にできることの方が多いように思うので(いくつか例外を除いて)、多分、これも関数一発とかいうレベルでできるだろうと思って調べたところ、"Launch Services"というのがあるらしいことがわかった。これも、Launch Servicesというキーワードを知ってれば良かったんだけど、"execute"とか"command"とか"application"とか"external program"とか、いろいろキーワードで検索してやっと、最終的にこのLaunch Servicesにたどりつくまで結構しんどかった。30分か一時間ぐらいは探してた気がする。最後は、"open web site cocoa"とかで調べてやっとたどりついたんだったかな?こういうの、なんとか頭良く検索する方法って無いもんなんだろうか。

上のページから、Launch Servicesというものを使えばいいということがわかって、今度は、それを使った例題を調べて見つけたのがこれ。ここには、デフォルトのブラウザで特定のURLを開く方法が出ていた。というわけで、コマンドを実行する関数を書いてみた。

void FsLaunchCommandC(const char cmdLine[])
{
    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
    NSString *bundlePath;
    bundlePath=[[[NSBundle mainBundle] bundlePath] stringByAppendingString:@"/"];
    NSString *fullPath;
    fullPath=[bundlePath stringByAppendingString:[NSString stringWithUTF8String:cmdLine]];
    printf("Open File> %s\n",[fullPath UTF8String]);
    [[NSWorkspace sharedWorkspace] openFile:fullPath];
    [pool release];
}
void FsOpenWebSiteC(const char cmdLine[])
{
     NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
    printf("Open Web Site> %s\n",cmdLine);
    NSURL *url;
    NSString *urlString;
    urlString=[NSString stringWithUTF8String:cmdLine];
    url=[NSURL URLWithString:urlString];
    [[NSWorkspace sharedWorkspace] openURL:url];
    [pool release];
}

YSFLIGHTのヘルプは、バンドルディレクトリからの相対パスで入ってるので、FsLaunchCommandCでは、バンドルパスにcmdLineを接続してフルパスにしている。FsOpenWebSiteCは、入ってくるのはWeb siteのURLであることが前提なので、この関数では、何も頭に追加していない。

それで、この関数は、Objective-Cなので、C++で書いてあるYSFLIGHTの本体のプログラムから呼び出せるように、橋渡しの関数も必要で、それは↓。

extern "C" void FsLaunchCommandC(const char cmdLine[]);
extern "C" void FsOpenWebSiteC(const char url[]);
void FsLaunchCommand(const char cmdline[])
{
    if(strncmp(cmdline,"http",4)==0)
    {
        FsOpenWebSiteC(cmdline);
    }
    else
    {
        FsLaunchCommandC(cmdline);
    }
}

cmdLineの最初の4文字でFsOpenWebSiteCかFsLaunchCommandCか選ぶようにした。

それから、ときどき重要なメッセージとか出すとき、日本語環境か英語環境か判定していたんだけど、この判定方法も調べてみた。WindowsだとGetLocaleInfoという関数で、どうやら帰ってくる文字列が0411だと日本語らしい。Macだと、NSLocaleクラスのpreferredLanguage関数(これ、Objective-Cだとメソッドってことになってんのかな)が返す値で判定するらしいことがわかった。こっちの方は"locale"で探したら案外簡単に見つかった。書いてみたのが、次の関数で、

const char *FsGetLanguageIdC(char buf[])
{
    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
    NSArray *lang=[NSLocale preferredLanguages];
    buf[0]=0;
    if(NULL!=lang)
    {
        NSString *langId=[lang objectAtIndex:0];
        if(NULL!=langId)
        {
            static int first=1;
            if(1==first)
            {
                printf("Language Tag: %s\n",[langId UTF8String]);
                first=0;
            }
            strncpy(buf,[langId UTF8String],16);
            buf[16]=0;
        }
        else
        {
            printf("Primary Language Not Available.\n");
        }
    }
    else
    {
        printf("Preferred Language Not Available.\n");
    }
    [pool release];
    return buf;
}

帰ってきた文字列が"ja"だと日本語らしい。ただ、日本語環境のMacがまわりに無いので確認できなかった。一時的にWean HallのMac部屋のiMacを日本語環境にできないかと思ったけど、切り替えできないようにしてあるみたいで失敗。多分大丈夫だと思うんだけど。YSFLIGHTのページにも書いたけど、15インチMacbook ProにeSATAポートが追加されるかExpress Cardスロットが復活してくれればすぐにも発注するんだけど。

2010/01/27 ジョイスティックの逆襲

ジョイスティックリーダー+ジョイスティック調整プログラム」に書いた通り、Mac OSでジョイスティックを読み取る方法は、既に調べてわかっていた。というか、それでもう大丈夫だと思っていた。ところが、YSFLIGHT for Mac OS Xユーザから、Mac OS 10.6でジョイスティックを接続するとプログラムが動かないという報告があった。ラッキーなことに、彼は、現在カーネギー・メロンの学生で、親切にも、彼のMac Bookと問題のジョイスティックを持ってきて問題の現象を見せてくれて、また、僕が書いたテストプログラムをその場で実行して、結果を見せてくれた。

その結果わかったのは、どうもIOHIDDeviceInterface122::queryElementValueという関数が使えないらしいということがわかった。また、問題のジョイスティックではX軸とボタン0が二回検出されていることもわかった。どうも、ふたつのうちのひとつは、何か別の情報のことを言っているようだった。さらに調べると、Element Typeというのをチェックすると、片方はCollectionというタイプで、他の要素は、Misc、Button、または、Axisというタイプであることがわかった。ひょっとすると、このCollectionタイプの要素を読み取ろうとしたのが悪かったかな?と、思って、Element Typeをチェックして、問題の要素の値を読み取らないようにしてみたが、これもだめ。

再度Apple Developer Connectionに戻って、いろいろ調べていたところ、次のページ

http://developer.apple.com/mac/library/documentation/DeviceDrivers/Conceptual
/HID/new_api_10_5/tn2187.html#//apple_ref/doc/uid/TP40000970-CH214-SW

ここに衝撃の記述が。

以下抜粋:

Note: For binary compatibility, HID code written for Mac OS X version 10.4 ("Tiger") will continue to work for the lifetime of Mac OS X version 10.5 ("Leopard"). New development targeting Leopard should use the new HID Manager APIs.

つまり、旧式のやりかた(IOHIDDeviceInterfaceを使うやりかた)は、Mac OS X 10.5までは保障するけど、それ以後は保障の限りではない。保障の限りではなくても、普通動くようにするだろ、と、突っ込みを入れたくなったのだが、目の前で起きていることを見る限りでは、Appleは、旧式のやり方を本当に動かなくしたらしい。ということは、10.4当時に書かれたMac用ゲームは 仮に新しい環境でコンパイルしても、10.6で実行するとジョイスティックは読めないってことか。

なぜ旧式の方法を使ってしまったかというと、ジョイスティック読み取りプログラムを書いていたときにApple Developer Connectionを検索したら最初に出てきたのがそっちだったというだけなんだけど。そういう情報は削除するか、別の場所に移すか、ページトップにでっかく「この方法はもう使えません!」と警告しておいてもらいたい。

そのようなわけで、10.5以降で使われるようになった関数でジョイスティック読み取り部分を書きなおすことにした。とりあえず、Accessing a HID Deviceを読んでみるが、いつもの通り、一発目、読んでもわけがわからない。なので、HID_Explorerという例題をダウンロードしてくる。

で、例題を見ていつも思うんだけど、というか、なんで例題を書く人たちは、いつまでたっても何年経っても学習しないのかと思うんだけど、欲しいのは、HIDのアクセスだけするプログラムなのだ。ダイアログ作ったり、メニュー作ったり、そういう枝葉を全部ばっさり落とした例題が欲しいのだ。コンソールから起動して、ジョイスティック状態を表示するだけでいい。というか、それ以外余計なものは無い方がいい。実際に使用するプログラムじゃないんだから、main関数ひとつだけ あれば、関数に分かれてなくてもいい。ジョイスティックにアクセスするには、どういうシーケンスで何をやってけばいいのかがわかるように書いてあるのがいいのだ。が、HID_Explorerは、余計なものがたくさんついているので、仕方ないので中をちょっとずつ読む。このページの下の方から、僕が書いた、 簡潔な例題がダウンロードできるんで、そっちを見た方が100倍わかりやすい。

例によって、自分のPCでコードをできるだけ書いておいて、それをCMUに転送して、Mac部屋でコンパイルしてテスト、という手順でやったのだが、案外あっさり動いた。 とりあえず、これで、Mac OS 10.6でもジョイスティックの読み取りができるようになったらしい。新しい仕組みは、COMオブジェクトを使わなくなったのはいい点だ。COMオブジェクトの大半は、COMである必要が無い。C++のクラスライブラリとして提供 すればいいのに。ところが、無理にCとかVisual Basicとかからも利用できるようにするとかするから、あんなにややこしいことになる。

ちなみに、下は、コンソールから実行して、マウスとジョイスティックの状態を一秒ごとにプリントするプログラム。デバイス読み取りの例題って、こういう例題であるべきなんだけど、いつになったら、例題書く人たちは学習するのかね。どこに例題を探しに行っても不必要に長い例題ばっかしだもんね。要点だけ書いてある例題って滅多に行き当たらない。

JoystickSample.cpp

なお、「ジョイスティックリーダー+ジョイスティック調整プログラム」の方も更新したので、今はMac OS 10.6でも使えるようになっているはずだ。

2010/01/07 NSSoundを使って、ギャップ無しでWAVサウンドをループで鳴らす方法

YSFLIGHT for Mac OS Xで、もうひとつ問題になっていたのが、エンジン音だった。エンジン音は、単にWAVファイルをループで鳴らしているだけなのだが、サウンドが最後に達して、先頭に戻るとき、微妙に空白というか無音のギャップが入ってしまう。Googleで検索したところ、いくつかのサイトがヒットして、

http://minikidsgames.sourceforge.net/mini-game-tutorial/let-it-sound/

http://www.idevapps.com/forum/archive/index.php/t-3223.html

どうやら、単純にNSSoundを使うだけだと、ギャップを避ける方法は無いらしい。Appleに好意的に解釈すれば、NSSoundの仕様だけど、正直言って、こりゃ、欠陥だ。Appleがこの欠陥を早いとこなんとかしてくれるといいんだけど、当面、プログラム側でなんとかギャップを回避する方法を探さなきゃならない。

数秒に一度無音が聞こえて(?)しまうのは、どうも気持ち悪い。なので、なんとかしようと思っていて、やっとなんとか、解決できた。基本的に、

[NSSound setLoops:YES];
[NSSound play];

こう書けば、サウンドをループさせて鳴らすことができるが、このままだとギャップが入ってしまう。そのギャップは、サウンドが末尾に達したときに入るんだから、末尾になるちょっと前に強制的に先頭に戻してしまったらどうだろうか?YSFLIGHT for Mac OS Xは楽に20fpsは出るだろうから、最低でも一秒間に20回はサウンドの位置をチェックするチャンスがある。だとすれば、サウンドが末尾から0.05秒に達した時点で、再生位置を先頭に強制的に移動してしまうことで、ギャップは避けることができるんじゃないだろうか。と、思って書いてみた実験プログラムが↓。

// Filename: nssound.m
#include <stdio.h>
#include <time.h>
#import <Cocoa/Cocoa.h>

int main(int ac,char *av[])
{
    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];

    if(ac<2)
    {
        printf("Usage nssound filename.wav\n");
        exit(1);
    }

    NSSound *sound=nil;
    sound=[[NSSound alloc] initWithContentsOfFile:[NSString stringWithUTF8String:av[1]] byReference:NO];

    [sound setLoops:YES];
    [sound play];

    printf("Duration %lf\n",[sound duration]);

    time_t curTime,time0;
    curTime=time(NULL);
    time0=curTime;
    while(1)
    {
        [NSThread sleepForTimeInterval:0.01];

        if(0.05>[sound duration]-[sound currentTime])
        {
            [sound setCurrentTime:0.0];
        }

        if(curTime!=time(NULL))
        {
            printf("* %d\n",(int)(time(NULL)-time0));
            curTime=time(NULL);
        }
    }

    [pool release];

    return 0;
}

このプログラムは、ターミナルから、次のようにタイプしてコンパイルできる。

g++ nssound.m -framework Cocoa -o nssound

実行するには、↓。

./nssound wavefile.wav

ハイライトした部分が、再生位置をチェックしてる部分で、残り0.05秒を超えてたら、強制的に先頭に再生位置を戻してやる。必ずしもスムーズにつながらないけど、音の空白ができるよりずっと良くなった。多分、別スレッドにして、もっと頻繁に位置をチェックしてやればもっとスムーズになるかも。

2010/01/04 Alt (Command), Shift, Controlキー押下の検出方法

YSFLIGHTのMacへの移植で困っていたのが、Alt, Command, Shift, Controlなどのキー押下の検出方法が無いことだった。と、思っていた。Cocoa APIでは、ESCキーとかFunctionキーには、特別なキーコード(0xf700〜)が割り当てられていて、keyDownイベントでそのコードが来たら検出できる。

ところが、Alt, Command, Shift, Controlなどのキーには、 キーコードが割り当てられていない。なので、一見、これらの特殊キーの押下、キー状態の検出はできないのかと思っていた。とくに、YSFLIGHTでは、デフォルトで、Controlキーを押すと、飛行中に設定の変更ができるようになっているので、Controlキーが検出できないと、Mac版だけ別のキーを割り当てるか、あるいは、使いたい人は、別のキーを割り当てて使ってもらうしかないか、と、思っていた。

でも、それもいまいちパッとしない。なので、別の手を考えていた。各種イベントkeyDownイベントとかmouseMovedイベントとか、イベントが発生するたびに、Shiftキーなどの情報は、イベントのパラメータ、NSEvent *theEventのメンバー変数modifierFlagsを参照することで知ることができる。だから、タイマーイベントとか発生させて、定期的にmodifierFlagsをチェックすることで、特殊キーを押した、放したを検出できないか、ということを考えていた。

というようなことを考えて、NSEventクラスのリファレンスを見ていたところ、イベントタイプNSFlagsChangedというのが目に留まった。Cocoaのネーミングルールから考えると、NSViewクラスに、flagsChangedという関数が存在するはず。これってひょっとして、特殊キー状態の変化のイベント?と、思ってflagsChangedを検索してみたところ、

Technical Q&A QA1519: Detecting the Caps Lock Key
Mac Dev Center: Cocoa Event-Handling Guide: Handling Key Events

まさにその通りだった。これこそ、僕が探していたイベントハンドリング関数だった。早速、今日から開いたWean HallのMac部屋に行って実験。Control、Shift、Alt (Command)キーすべて検出できることを確認した。

ただ、この方法だと、直前の特殊キーの状態を覚えておかないと、どのキーが押された、あるいは放されたことによってflagsChangedイベントが発生したのかわからないのが難点だけど、これは、ひとつ変数を用意すればいいだけなので、大した問題にはならない。

なお、既に、「最小主義者のためのMac OS Xフレームワーク」もバージョンアップして、FsInkey、FsGetKeyState関数で、ShiftやControlなどの特殊キーを検出できるようにした。

2009/12/25 最小主義者のためのMac OS Xフレームワーク

前から予告していた、「最小主義者のためのMac OS Xフレームワーク」を公開した。既に、Sword Edge, Planet Attacker 2009, YS FLIGHT SIMULATOR for Mac OS X (現在テスト版)、で動作を確認しているので、結構安定して使えると思う。

プログラミング環境は、簡単なことをするなら簡単なプログラミングで、難しいことをするならそれなりのプログラミングでできるのが理想だ。しかし、最近の傾向として、簡単なことをしようと思っても、結構長いプログラムを書かなきゃならなくて、プログラムの勉強を始めようって人がなかなか入ってこれない。このフレームワークを使えば、簡単なプログラムなら、昔のBASIC言語のような手軽さで作ることができる。さすがにYSFLIGHT用には、少し手を加えたバージョンを使っているけど、基本は↑からダウンロードできるフレームワークを使っている。ソースコードも公開しているので、必要に応じて手を加えて使えっていただければ良いと思う。

2009/10/17 Directory ListingとUNICODEファイル名

YSFLIGHTでは、航空機、地上物、マップのファイルのリストを、air*.lst, gro*.lst, sce*.lstというファイルにそれぞれ記録している。なので、YSFLIGHTを移植するには、これにマッチするファイルを探してくる関数を書かなくてはならない。Apple Developer Connectionを少し検索したところ、NSFileManagerを使えばいいらしい、ということがわかった。そのメソッドcontentsOfDirectoryAtPath を使うと指定したディレクトリ内のファイルのリストが取れるらしい。そういうわけで、次のコードを書いてみた。

#include <Cocoa/Cocoa.h>

int main(void)
{
    NSAutoreleasePool *pool;
    pool=[[NSAutoreleasePool alloc] init];

    NSFileManager *fileMngr;
    fileMngr=[[NSFileManager alloc] init];

    NSArray *fileList;
    fileList=[fileMngr contentsOfDirectoryAtPath:@"." error:NULL];

    printf("%d files are found.\n",[fileList count]);

    int i,j;
    for(i=0; i<[fileList count]; i++)
    {
        NSString *str;
        char utf8[256];
        str=[fileList objectAtIndex:i];
        strncpy(utf8,[str UTF8String],255);
        utf8[255]=0;
        printf("[%d] %s\n",i,utf8);
        for(j=0; utf8[j]!=0; j++)
        {
            printf("%02x ",(unsigned char)utf8[j]);
        }
        printf("\n");
    }

    [pool release];

    return 0;
}

このコードは、

g++ filelist.m -framework Cocoa

と、タイプしてコンパイルできる。

それで、試行錯誤無しで、一発で成功。結構Objective-Cにも慣れてきたっぽい。もっとも、目的は、Objective-Cで書かなくちゃならない部分を最小にして、大半をC++で書けるようにすることなんだけど。

ちなみに、

        for(j=0; utf8[j]!=0; j++)
        {
            printf("%02x ",(unsigned char)utf8[j]);
        }

この3行を追加したのは、UNICODEの文字がどうUTF8の文字コードに変換されるのかを確認するため。実を言うと、YSFLIGHTをMac OS Xに移植する上でひとつ困りそうだったのは、UNICODEファイル名だった。Mac OS Xでは、ファイル名はデフォルトではUNICODEでストアされているらしい。もしも、ユーザーがファイル名にUNICODEが混じったファイルを開こうとしたとき、ファイル名がうまいことUTF8の文字コードに変換できないかもしれない。そうすると、ファイルをfopen関数で開くことができなくて、ファイル読み書きの部分を全面的に書き直さなくてはならなくなるかもしれない。そういう危惧があった。

Wean HallのMac部屋のMacには、当然日本語のIMEなんか入っているわけもなく(入っていても使い方がわからん)、仕方ないので、日本語のWebサイトを開いて、カット&ペーストを使って、テキストファイルのファイル名を「日本語.txt」に変更した。そして、上記のコードをふたたび実行。「日本語」の部分がUTF8に変換された段階で、変なコードになってしまって、文字化けするであろうと予想していた。ところが、正常に日本語って出るじゃん。そのときのコンソール出力が↓これ。

どうも、NSStringクラスのUTF8string関数は、UNICODE一文字を3バイトに変換して返すらしい。だから、変換しても情報は失われていない。と、いうと、次の疑問は、このNSStringで変換後のUTF8文字列のファイル名でfopenはできるのか?そこで、次のコードを書いてみた。

#include <stdio.h>

unsigned char nihongo[]=
{
    0xe6,0x97,0xa5,0xe6,0x9c,0xac,0xe8,0xaa,0x9e,0
};

int main(void)
{
    FILE *fp;
    fp=fopen((char *)nihongo,"w");
    fclose(fp);
    return 0;
}

で、実行したところ、無事「日本語」というファイル名のファイルができた。

結論としては、UNICODEファイル名をcharの配列に変換しても、情報は失われない。また、変換後のcharの配列をfopenに渡すと、普通にファイルを開いたり生成したりできる。ということがわかった。YSFLIGHT移植に向けて、もう一歩前進した。

2009/09/21 アプリケーションアイコンの設定

なんと、昔まだ学部生だったころに書いたレトロ風ゲームSword EdgeをMac OS Xに移植できてしまった。間もなく、Mac版もアップロードする予定。

必要なことは既にわかっていたので、コードの移植は大した手間ではなかった。が、それにアイコンをひっつけようと思ったら、予想外に手間がかかってしまった。まあ、XcodeのIDEを使えばできる話なんだけど、僕は抵抗して、なんとかUnix Terminalでできる限り全部済ませようとしているので、手間がかかった。

Apple Developer Connectionによると、アプリケーションアイコンを設定するには、まずIcon Composerというプログラムで、アイコンファイルを作って、それを、アプリケーションフォルダ下のContents/Resourcesフォルダにコピーする。そして、ContentsサブフォルダのInfo.plistファイルにそのアイコンファイルへのリンクを書けばいいらしい。

それなら簡単、と、思って、その手順を追ってみた。まずアイコンのPNGをWindowsから持ってきて、Icon ComposerでSwordEdge.icnsというファイルを作った。そのファイルを、SwordEdge.app/Contents/Resourcesにコピーした。それで、次のような内容のSwordEdge.app/Info.plistファイルを作った。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIconFile</key>
<string>SwordEdge.icns</string>
</dict>
</plist>

これで、アイコンが出るかな?と、思ったら出ない。

Info.plistの記述が間違っていたのかと思って、Xcode IDEを使って、空のプロジェクトを作った。そして、Info.plistを空のプロジェクトからコピーしてきて、アイコンへのリンク(CFBundleIconFile)を追加した。が、それでもアイコンは出ない。ひょっとして、ファイル名がまずいのかと思って、Apple Developer Connectionの手順にあったのと同じようにアイコンファイルをAPPL.icnsというファイル名にして、Info.plistも変更した。が、それでも出ない。仕方ないので、Apple Developer Connectionの手順を上から下までやってみたところ、今度はアイコンが出た。が、当然プログラムはSword Edgeではない。またXcodeが中でなんかやってるらしい、と思った。

その時点で時間切れ。Wean HallのMacintosh部屋を出なくてはならなかったので、次は、Xcodeが作ったプログラムから始めて、アイコンが見えなくなるまで、ファイル、ディレクトリ、Info.plistのエントリをひとつずつ削除していけば、アイコンの表示に必要な最小の構成がわかるはずだ。

次の日、またMac部屋に来てログインして、自分のプログラムのフォルダを開いてみた。そしたら、なんと、前日は見えていなかったSword Edgeのアイコンが出ている。Xcodeで作った方ではなく、間違いなく、自作のSword Edgeの方にアイコンが出ている。どうなってんの?試しに、SwordEdge.app/Contents/Resourcesを削除してみた。予想通り、アイコンは消えてデフォルトのアイコンになった。再度Contents/Resourcesサブディレクトリを作って、APPL.icnsをコピーすると、ふたたびアイコン復活。今度は、APPL.icnsをSwordEdge.icnsに変更して、Info.plistもそれに合わせて修正。またアイコン消滅。今度は、SwordEdge.icnsをAPPL.icnsに変更して、Info.plistは変更しなかった。なので、Info.plist内のアイコンファイル名は正しくない。が、アイコン復活。Info.plistって本当に必要なんだろうか?試しに、Info.plistをInfoplistに名前変更してみたが、アイコンは消滅しなかった。まあ、一応、Info.plistも作っておいた方がよさそうだけど、無くても良さそうだった。それより、アイコンファイル名が問題のようだった。APPL.icnsにするだけでよかったのか。

それじゃ、せっかくだから、公開用のパッケージを作ろうかと思って、SwordEdge.appを別ディレクトリにコピーして、readme.txt, readmej.txt, readmee.txtを同じディレクトリにコピー(ターミナルで)して、Finderで開いてみたら、今度はまたアイコンがデフォルトアイコンになっている。ひょっとして、昨日アイコンが出なかったのは、単にFinderのアイコンイメージのキャッシュのせいなのでは?と、思って、ためしにログアウトしてログインしてみたら、なんのことはない、アイコン復活。どうやら、アイコンを設定した後は、いったんログアウトしてログインしなおさなきゃならない。多分Xcodeでコンパイルしているときは、内部でFinderとなんかやりとりしてアイコンを更新させているのだろう。果たしてこれが正しい方法なのか、100%確認できたわけではないけれど、とりあえず、APPL.icnsをContents/Resourcesに置いて、Contents/Info.plistからリンクすればアイコンが出るらしいということがわかった。

それから、OpenGLでビットマップフォントを表示する関数をアップロードした。MacOS X上でもコンパイルして動作を確認した。これを使うと、字体はそんなに綺麗じゃないけど、とりあえず、異なるプラットフォーム間でも、同じ関数で同じ文字が書ける。ただ、これをやってて、ひとつ気がついたのは、どうやらNSOpenGLViewクラスは、できた直後は、まだOpenGL Contextがカレントになってないらしい。NSOpenGLViewのprepareOpenGL関数が呼ばれた段階で、初めてContextがカレントになってるようだ。なので、OpenGLの初期化コードを書きたかったら、この関数をオーバーライドして、その中に書かなくてはならないらしい。

prepareOpenGLが呼ばれるタイミングは、NSOpenGLViewができた直後ではないらしく、イベントループに突入してからのようだった。この点はMacOS Xにコードを移植するときに注意しなくてはならないらしい。多分、Window開いて、NSOpenGLViewを作って、prepareOpenGLが呼ばれるまでの間、イベントループを空回りさせればいいのかな。

2009/09/17

結構MacOS Xプログラミングもわかってきたような気がする。まだわからない点もいくつもあるけど。ただ、MacOS Xはよくできているように思った。OSがよくできてるのか、APIがよくできてるのかという問題はあるものの、一本のシンプルなコードで、OpenGLで描画可能なWindowを開くことができる。同じことをMFCでやろうと思ったら、、、、考えただけで嫌になる。一応、過去にトライしてできたにはできたんだけど、それをベースに何か作ろうって気になるようなものにはならなかった。Windowsでプログラムを書こうっていうなら、必要が無ければMFCは使わないほうがいい。できればWin32 APIを直接叩いた方がいいと思う。MFCはリソースファイルに依存するという弱点がある。リソースファイルがあるおかげで、ダイアログクラスを一本作って、同じクラスを別のMFCプログラムに持っていこうとすると、うんざりするような手間がかかり、途中のステップを間違うと、バージョンコントロールシステムがうまいことソースを復旧してくれることを祈るしかなくなる。COMという手はあるかもしれないけど、いつまでもMicrosoftの天下が続くとは限らないので、あんまり面倒な奥深いところまでWindowsプログラミングに突っ込んでいく気はない。そもそも、過去10年ぐらいが異常な状態だった。なんで企業の製品設計だのシミュレーションだのに使うのと同じだけのパワーを持ったPCが各家庭に一台以上存在する必要があったのか。いずれ、PCのパワーが家庭で必要なレベルを超えたら、ふたたび、ハイエンドなPCは必要なところにだけ置かれるようになり、各家庭にはそこそこのパワーのPCが入るようになるだろう。NetBookなんてのが台頭してきてるのは、その兆候だと思う。この10年ぐらいの異常な状態が、異常であるということに気づかなかったMicrosoftはWindows Vistaで大失敗した。というのは、話の本筋ではないけど、とにかく、Objective-Cであるという点を除けば、MacOS Xはよくできている。

また、Objective-Cの感じも結構わかってきたと思う。C++だと、基本的にコンパイラが頭のいいことは全部やってしまう。それぞれのクラスがどういう関数を持ってるか、というのは、コンパイル時に決めておかなくてならない。悪名高いImplicit Templateを使ってしまうと、リンカがもう少し頭のいい処理をしなくてはならない。だとしても、どのクラスでどの関数が使えるのか、実行時の前にすべて決まっている必要がある。が、Objective-Cだと、実行時まで、あるクラスが、必要なメソッドを持っているか、決まっていなくてもいい。もちろん、プログラムを書いておく必要はあるけれど、そのメソッドを記述してさえあれば、実行時にプログラム中で勝手に探し出してそのメソッドが使われる。

多分、この柔軟性は、結構なパフォーマンスのロスと引き換えに実装されている。メソッドを探し出してくるというのは、C++のバーチャル関数に相当する機能だけど、メソッドをキーワードなりハッシュキーなりから探してくる手間は、バーチャル関数を呼び出す手間にくらべて、かなり大きい。多分Objective-Cの方がC++よりも、よりオブジェクト指向の目指すところに近いのかもしれない。ただ、C++で大概のことは間に合ってしまうし、パフォーマンスもいい。C++の方がObjective-Cより実用的だ。アップル社は、僕から見ると、技術者集団というよりも、芸術家集団に見える。実用性より見てくれを重視してるように見えるというか。多分、NeXT STEPを作ったとき、彼らにはObjective-Cの方がC++より芸術的に見えたんだろう。NeXT STEPの敗北から、10年以上が経過しているというのに、もしも、この日が来ることを、NeXT STEPに関わった誰かが知っていれば、同じアーキテクチャをC++で実現するために、十分すぎる時間があった。

とはいえ、今学期教えているクラスようのMac用Wrapperは無事完成。多分、YSFLIGHTもMacOS Xに移植できるような感触になってきたんで、興味としては、Cocoa APIの他に移植に必要な機能を知ることに移ってきた。それと、いくつか、まだやり残しがあった。ひとつは、UNIXTerminalからコンパイルしたプログラムでメインウィンドウがキー入力を受け付けない件。これは、多分、そのプログラムがメニューを持ってないとかいう理由で、完成したプログラムとして扱われないからではないか、と、思ってメニューを作ってひっつけてみたものの、敗北。

その後、Apple Developer ConnectionはあきらめてGoogleで数時間検索したところ、次のポストに行き着いた。

http://forums.macnn.com/79/developer-center/209595/cocoa-without-nib-file-need-help/

これによると、MacOS Xのプログラムは、

Something.app/Contents/MacOS

というディレクトリ構造を持っていなくてはならなくて、例えば、プログラム名がYsProgramだったら、実行ファイルは、

YsProgram.app/Contents/MacOS/YsProgram

みたいなファイル名でなければならないという。本当かよ?と、疑った。でも、試してみた。そしたら動いた。なんと、これによって、もうひとつのやり残し問題が解決した。Unix TerminalからコンパイルしたプログラムがFinderから実行できないという問題があったのだが、こういうディレクトリ構造にしただけで実行できるようになった。これにより、Xcode IDEから完全に自由になった。とにかく、プログラム書いてる最中にマウスに手を伸ばさなきゃならないのが耐え難い苦痛なので、テキストエディタとターミナルで全部できるようになったのは大きい。

もうひとつのやり残しは、Application Delegateを使って、ウィンドウをすべて閉じたらプログラムを終了するように指示しているにも関わらず、自分の関数で作ったウィンドウだと閉じてもプログラムが終了してくれないという問題だった。が、前回書いたとおり、NSNotificationCenterを使うことで、Window closeのイベントとかが取れるらしいので、Window closeを検出したら、その場でプログラムを終了させればいい。問題は、どこでNSNotificationCenterにレスポンだーを登録するかだけど、それは、どうやらNSWindowのinitWithContentRectをオーバーライドしてその中に書けばいいらしい。というわけで、次のようなWindowクラスを作ってみた。

@interface YsOpenGLWindow : NSWindow
{
}

@end

@implementation YsOpenGLWindow
- (void) keyDown:(NSEvent *)theEvent
{
    NSString *chrs;
    chrs=[theEvent characters];
    if([chrs length]>0)
    {
        int unicode;
        unicode=[chrs characterAtIndex:0];
        printf("keyDown %d\n",unicode);
    }
}

- (void) keyUp:(NSEvent *)theEvent
{
    NSString *chrs;
    chrs=[theEvent characters];
    if([chrs length]>0)
    {
        int unicode;
        unicode=[chrs characterAtIndex:0];
        printf("keyUp %d\n",unicode);
    }
}

- (void) mouseDown:(NSEvent *)theEvent
{
    printf("%s %d %d\n",__FUNCTION__,(int)[theEvent locationInWindow].x,(int)[theEvent locationInWindow].y);
}

- (void) mouseUp:(NSEvent *)theEvent
{
    printf("%s %d %d\n",__FUNCTION__,(int)[theEvent locationInWindow].x,(int)[theEvent locationInWindow].y);
}

- (void) rightMouseDown:(NSEvent *)theEvent
{
    printf("%s %d %d\n",__FUNCTION__,(int)[theEvent locationInWindow].x,(int)[theEvent locationInWindow].y);
}

- (void) rightMouseUp:(NSEvent *)theEvent
{
    printf("%s %d %d\n",__FUNCTION__,(int)[theEvent locationInWindow].x,(int)[theEvent locationInWindow].y);
}

- (void) otherMouseDown:(NSEvent *)theEvent
{
    printf("%s %d %d\n",__FUNCTION__,(int)[theEvent locationInWindow].x,(int)[theEvent locationInWindow].y);
}

- (void) otherMouseUp:(NSEvent *)theEvent
{
    printf("%s %d %d\n",__FUNCTION__,(int)[theEvent locationInWindow].x,(int)[theEvent locationInWindow].y);
}

- (void) mouseMoved:(NSEvent *)theEvent
{
    printf("%s %d %d\n",__FUNCTION__,(int)[theEvent locationInWindow].x,(int)[theEvent locationInWindow].y);
}

- (id) initWithContentRect: (NSRect)rect styleMask:(NSUInteger)wndStyle backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferFlg
{
    [super initWithContentRect:rect styleMask:wndStyle backing:bufferingType defer:deferFlg];

    [[NSNotificationCenter defaultCenter]
        addObserver:self
        selector:@selector(windowDidResize:)
        name:NSWindowDidResizeNotification
        object:self];

    [[NSNotificationCenter defaultCenter]
      addObserver:self
      selector:@selector(windowWillClose:)
      name:NSWindowWillCloseNotification
      object:self];

    [self setAcceptsMouseMovedEvents:YES];

    printf("%s\n",__FUNCTION__);
    return self;
}

- (void) windowDidResize: (NSNotification *)notification
{
    printf("%s\n",__FUNCTION__);
}

- (void) windowWillClose: (NSNotification *)notification
{
    printf("%s\n",__FUNCTION__);
    [NSApp terminate:nil];    // This can also be exit(0);
}

@end

これも成功。ウィンドウを閉じるとうまいことプログラムが終了するようになった。あと、キー入力とかも受け付けるようにしてみたんだけど、Apple Developer ConnectionのNSWindowクラスを見たら、keyDownイベントを受け付ける関数の記述はあるんだけど、keyUpとかマウスボタンに対応する関数の記述がない。お前はFM-7か!と、思ったんだけど、単にNSWindowの説明に記述が無いだけど、普通にkeyUpとかmouseDownとかmouseMoveとか検出することができた。とりあえず、今のところの実験プログラムが↓これ。

[実験コード]

コンパイルするには、Unix Terminalで、

gcc minimum-04-KeyMouse.m -framework Cocoa -framework OpenGL

と、タイプすればOK。面倒なIDEを開いてプロジェクトを作ってファイルを追加して、とかしなくてもいい。実行ファイルは、minimum.app/Contents/MacOSの下にコピーすれば実行可能。

2009/09/09

授業に行く前、一時間ぐらいWean HallのMac ClusterでMac OS Xと格闘した。結構、授業用のコードができてきた。

ところで、ひとつ重要なことを発見。もっと前に発見していたけど、多分、まだここには書いてなかったと思う。もしも、Xcode上でプロジェクトを作って、Xcode上でコンパイル、実行した場合、コンソールが出てこないので、当然、コンソールの出力が見えない。が、Xcodeでコンパイルした同じプログラムを、Unixターミナル上から実行すると、コンソール出力がターミナルに出てくるので、見える。例えば、XcodeでCocoaプロジェクトを/User/soji/MyProgram というディレクトリに作ったとすると、コンパイルしたプログラムは、多分次のディレクトリにできる。

/User/soji/MyProgram/build/Debug/MyProgram.app/Contents/MacOS

実行ファイル名はプロジェクト名と多分同じ。Unixターミナルで、このディレクトリに移動して、

./MyProgram

とタイプすると、プログラムは実行できて、すべてのコンソール出力が見える。コンソール出力は、結構、デバッガよりも使える。とくに、プログラムが複雑になってくると、コンソール出力を見てる方が、デバッガでたどるより効率よくデバッグできることが多い。デバッガは、プログラムを止めて、その瞬間のスナップショットを見せてくれるけど、有効なコンソール出力は、プログラムがたどってきた軌跡を見せてくれる。プログラムがクラッシュしちゃったときは、デバッガでまずどこで落ちたか確認するけれど、その後は、ほとんどコンソール出力を見ながらデバッグすることが多い。

それで、話を元に戻すと、これまでのところ、次の3点問題があった。

  1. ウィンドウを全部閉じたときプログラムが終了して欲しいんだけど、終わってくれない。
  2. 自分の関数でウィンドウを開きたいんだけど、リソースファイルに記述されたメインウィンドウが勝手に開いてしまう。
  3. UnixターミナルからコンパイルしたプログラムをFinderから実行すると、"You can't open the application "(プログラム名)" because the Classic environment is no longer supported."と言われて実行できない。あと、キー入力もウィンドウで受けることができない。

最初の問題については、どうやらApplication Delegateというのを使うといいらしいので、前回次のように書いてみたのだが、うまくいかなかった。

(うまくいかなかった例)

@interface YsAppDelegate : NSObject <NSApplicationDelegate>

- (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApp

@end

@implementation YsAppDelegate

- (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApp
{
return YES;
}
@end

こう書くと、NSApplicationDelegateが定義されてないというエラーが出る。ちなみに、Apple Developer Connectionからダウンロードできる例題の中で、このような記述があるのだが、その例題も同様にコンパイルできなかった。(そういうの出さないでおいてほしいよね)。エラーが出てるのは、<NSApplicationDelegate>の部分なので、これを削除するとエラーは出なくなるけど、ウィンドウを閉じてもプログラムは終了してくれなかった。

仕方ないので、他の例題も引き続き見たところ、どうも、このapplicationShouldTerminateAfterLastWindowClosedという関数は、@implementation〜@endのブロックに書いても、@interface〜@endのブロックには書かないものらしい。そこで、main.mを次のように書いてみた。

(これは成功)

#import <Cocoa/Cocoa.h>
@interface YsAppDelegate : NSObject

@end

@implementation YsAppDelegate

- (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApp
{
return YES;
}
@end

int main(int argc, char *argv[])
{
[NSApplication sharedApplication];
[NSBundle loadNibNamed:@"MainMenu" owner:NSApp];

YsAppDelegate *delegate;
delegate=[YsAppDelegate alloc];
[delegate init];
[NSApp setDelegate: delegate];

[NSApp run];
}

見ればわかるけど、YsAppDelegateの@interface〜@endは空にした。が、applicationShouldTerminateAfterLastWindowClosedを@implementation〜@endのブロックに書いてみた。そして、Xcodeでコンパイルして実行して、メインウィンドウを閉じると、、、、プログラムも同時に終了!大成功!

調子に乗って、自分の関数で別のウィンドウも開いてみたところ、そうしたら、メインウィンドウ、別ウィンドウの両方を閉じてもプログラムは終了してくれなかった。近いんだけど、まだなんか足らんらしい。

Application Delegateがうまくいったので、試しにWindow Delegateというのもやってみた。これまで調べた感じだと、NSWindowクラスには、リサイズとかウィンドウクローズとかを受ける関数が無くて、そういうイベントを検出するには、Window Delegateというのを使わざるを得ない、と思っていた。(後で間違いだったとわかったけど)。というわけで、次のようなコードを書いてみた。

(Window Delegateの実験失敗)

#import <Cocoa/Cocoa.h>

@interface YsAppDelegate : NSObject

@end

@implementation YsAppDelegate

- (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApp
{
printf("%s\n",__FUNCTION__);
return YES;
}

@end

@interface YsWndDelegate : NSObject

@end

@implementation YsWndDelegate

- (void)windowDidResize:(NSNotification *)notification
{
printf("*\n");
}

@end


int main(int argc, char *argv[])
{
[NSApplication sharedApplication];
[NSBundle loadNibNamed:@"MainMenu" owner:NSApp];

YsAppDelegate *delegate;
delegate=[YsAppDelegate alloc];
[delegate init];
[NSApp setDelegate: delegate];

YsWndDelegate *wndDelegate;
wndDelegate=[YsWndDelegate alloc];
[wndDelegate init];

NSWindow *mainWnd;
mainWnd=[NSApp mainWindow];
[mainWnd setDelegate: wndDelegate];

printf("%08x\n",mainWnd);

[NSApp run];
}

このプログラムをXcodeでコンパイルして、Unixターミナルから実行してみた。期待していたのは、ウィンドウのサイズを変更するたびに * がコンソールに出力される、だったのだが、そんなに甘くなくて、だめだった。なぜだめだったかというと、[NSApp run];の前では、まだ[NSApp mainWindow]がNULLのままだった。どうやら、Main Windowはできていないらしい。

どうも、この状態は、鶏と卵の典型的なパターンのようだった。多分、Window Creationとかのイベントを捕らえるには、Window Delegateを使わなくてはならないような雰囲気なんだけど、Windowを作らないことにはDelegateを設定できない。

そもそも、NSWindowクラスの説明の中に多量の「NSなんとかNotification」という通知の記述がたくさんあるにも関わらず、NSWindowにはそれを受ける関数が無い。いったいどういうことだ?と、思った。それで、Apple Developer Connectionで、「Window programming」で検索するとWindowを開くとかViewをひっつけるとかやり方がわかったんだから「Notification programming」とかで検索したらなんか出たりして?と、適当に見当をつけてやってみたら大成功。このページに行き当たった。それによると、Windowのリサイズとかを検出するには、通知を知らせるクラス(Observer)と関数(Action)を[NSNotificationCenter defaultCenter]に登録するといいらしい。が、問題はどこで?WindowのCreationを迎撃して、その場で登録したいんだけど。どうも、正しい場所は、NSWindowのinitWithContentRect関数をオーバーライドして、そこに書くのがよさそうだった、が、今日は時間切れ。続きはまた次回。

2009/09/07

今日こそクリーブランド航空ショーに行くはずだったのに、計器飛行でもぎりぎりの悪天候。航空ショーはあきらめた。午後になってちょっと天候が回復してきたんで、(それでも悪天候)、計器飛行の訓練にでも行こうかと思って、フライトスクールに電話を入れて空港に来てみたら、再度天候が悪化。ILSなら降りられたかもしらんけど、使用中の滑走路10のグライドスロープ(縦方向の誘導)がメンテナンス中か、使用不能で、これじゃあとても飛べたもんじゃない。計器飛行の訓練もあきらめた。今年のレイバーデイの連休は終わっちゃった。

仕方ないんで、結局CMUに来てMacプログラミングの続きをする。まず、YsPollEvent()関数を作る。

static void YsPollEvent(void)
{
    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];

    while(1)
    {
        [pool release];
        pool=[[NSAutoreleasePool alloc] init];

        NSEvent *event;
        event=[NSApp
            nextEventMatchingMask:NSAnyEventMask
            untilDate: [NSDate distantPast]
            inMode: NSDefaultRunLoopMode
            dequeue:YES];
        if(event!=nil)
        {
            [NSApp sendEvent:event];
            [NSApp updateWindows];
        }
        else
        {
            break;
        }
    }

    [pool release];
}

この関数は、イベントキューにイベントがある限りその処理をするが、キューが空になったらすぐ戻ってくる。これを使って、main関数を次のように書ける。

int main(int argc, char *argv[])
{
    [NSApplication sharedApplication];
    [NSBundle loadNibNamed:@"MainMenu" owner:NSApp];

    [NSApp finishLaunching];

    while(1)
    {
        YsPollEvent();
 
   }
}

このwhileループの中では、なんか繰り返し処理させたいような処理(アニメーションとか)を書くことができる。続いて、NIBファイルを使わずにWindowを開けるか実験。

@interface YsOpenGLWindow : NSWindow
{
}
- (void) keyDown:(NSEvent *)theEvent;
@end

@implementation YsOpenGLWindow
- (void) keyDown:(NSEvent *)theEvent
{
    exit(1);
}

@end

@interface YsOpenGLView : NSOpenGLView 
{
}
- (void) drawRect: (NSRect) bounds;
@end

@implementation YsOpenGLView
-(void) drawRect: (NSRect) bounds
{
     static float top=0.0;

    glClearColor(0,0,0,0);
    glClear(GL_COLOR_BUFFER_BIT);

    glColor3ub(0,255,0);

    glBegin(GL_POLYGON);
    glVertex2f(top,0.1);
    glVertex2f(0.3,0.8);
    glVertex2f(0.7,0.8);
    glEnd();

    top+=0.1;
    if(top>1.0)
    {
        top=0.0;
    }
    glFlush();

    [[self openGLContext] flushBuffer];
}
@end

static YsOpenGLWindow *ysWnd=nil;
static YsOpenGLView *ysView=nil;

static void YsOpenWindow(void)
{
    NSRect contRect;
    contRect=NSMakeRect(32, 32, 800, 600);  // Top-Y coordinate is neglected.  Why?

    unsigned int winStyle=
        NSTitledWindowMask|
        NSClosableWindowMask|
        NSMiniaturizableWindowMask|
        NSResizableWindowMask;

    ysWnd=[YsOpenGLWindow alloc];
    [ysWnd
        initWithContentRect:contRect
        styleMask:winStyle
        backing:NSBackingStoreBuffered 
        defer:NO];

    NSOpenGLPixelFormat *format;
    NSOpenGLPixelFormatAttribute formatAttrib[]=
    {
        NSOpenGLPFAWindow,
        NSOpenGLPFADepthSize,(NSOpenGLPixelFormatAttribute)32,
        NSOpenGLPFADoubleBuffer,
        0
    };
    format=[NSOpenGLPixelFormat alloc];
    [format initWithAttributes: formatAttrib];    

    ysView=[YsOpenGLView alloc];
    contRect=NSMakeRect(0,0,800,600);
    [ysView
        initWithFrame:contRect
        pixelFormat:format];    

    [ysWnd setContentView:ysView];
    [ysWnd makeFirstResponder:ysView];    

    [ysWnd makeKeyAndOrderFront:nil];
    [ysWnd makeMainWindow];
}

このYsOpenWindowは成功。ウィンドウの左上の角が画面の(32,32)に来て欲しいのに、なぜかずいぶん下の方に出てくるが、それ以外はうまいこと行った。これでNIBファイルを使わずにウィンドウを開くことができる。

今日は、もうひとつ実験してみた。ウィンドウをすべて閉じたら自動的にプログラムを終了するようにしたい。そのためには、Delegateとかいうものを使うらしい。例題プログラムを参考にして、次のようなクラスを書いてみた。

@interface YsMacDelegate : NSObject <NSApplicationDelegate>
{
}
- (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication;
@end

@implementation YsMacDelegate
- (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication
{
    return YES;
}
@end

が、なぜかNSApplicationDelegateが定義されていないというエラーが出てコンパイルできなかった。ちなみに、参考にした例題ファイルもコンパイルしようとしたら同じエラーが出た。この点は、とりあえず、どうすりゃいいのか今日は解明できなかった。

ちなみに、今日書いたプログラムがこれ。XcodeでCocoaフレームワークのテンプレートを作って、main.mを置き換えるとコンパイルできるはず。ちなみに、このソースは、UNIXターミナルからコンパイルすることもできて、

gcc main.m -framework Cocoa -framework OpenGL

とやると、実行ファイルもできて、ターミナルからは実行できるのだが、なぜかFinderから実行はできなかった。

現時点でも宿題:

  1. Delegateはどうやったら作れるのか。
  2. なんで作ったWindowの角が(32,32)に来ないのか。
  3. なんでターミナルでコンパイルしたプログラムがFinderから実行できないか。

2009/09/05

本当は、今日はクリーブランド航空ショーに行っているはずだったんだけど、都合により行けなくなった。月曜までやってるから、代わりに月曜に行く。なので、今日は、Shenley Parkにハチドリの写真を撮りに行き、その後CMUに来てちょっとMacプログラミングの続きをやってみた。ちなみに、ハチドリはいたけど、いい写真は撮れなかった。フォーカスがこの倍ぐらい速くないとチャンスは無さそうだな。

実は、前の晩にGoogleで検索していたら、大きな発見があった。

http://cocoawithlove.com/2009/01/demystifying-nsapplication-by.html

このポストに、イベントループを自分で書く方法が説明してあった。なんだ、やっぱり書けるんじゃん。これで、イベントループをテンプレートに奪われずに済む。というわけで、本当にできるか試してみた。XcodeでまっさらなCocoaアプリケーションを作って、main関数のNSApplicationMainをコメントアウトして、それを自前のイベントループに置き換えてみる。

#import <Cocoa/Cocoa.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    //return NSApplicationMain(argc,  (const char **) argv);

    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];

    FILE *fp;
    fp=fopen("/Users/soji/MyAFS/src/MacOS/test.txt","w");
    fprintf(fp,"!\n");
    fclose(fp);

    [NSApplication sharedApplication];
    [NSBundle loadNibNamed:@"MainMenu" owner:NSApp];
    // [NSApp run];

    [NSApp finishLaunching];
    while(1)
    {
        [pool release];
        pool=[[NSAutoreleasePool alloc] init];

        NSEvent *event;
        event=[NSApp
            nextEventMatchingMask:NSAnyEventMask
            untilDate: [NSDate distantPast]
            inMode: NSDefaultRunLoopMode
            dequeue:YES];
        [NSApp sendEvent:event];
        [NSApp updateWindows];
       
        static int x=0;
        fp=fopen("/Users/soji/MyAFS/src/MacOS/test.txt","w");
        fprintf(fp,"%d\n",x++);
        
fclose(fp);
    }
 
    [pool release];
    return 0;
}

上のポストのコードからはちょっと修正してあって、まずmain関数に書けるようにしたのと、NIBファイル名をmyMainからMainMenuに変更した。あと、イベント待ちがブロッキングにならないように、[NSDate distantFuture]を[NSDate distantPast]に変更した。

NSAutoreleasePool *pool が何をしてるんだかさっぱりわからないのだが、とりあえず動いた!

そして、OpenGLのプロジェクトでも試してみて、drawRect関数を次のように変更。(描くたびに頂点のひとつがスルスルと移動する)

-(void) drawRect: (NSRect) bounds
{
    static float top=0.0;
           
    glClearColor(0,0,0,0);
    glClear(GL_COLOR_BUFFER_BIT);
 
    glColor3ub(0,255,0);
           
    glBegin(GL_POLYGON);
    glVertex2f(top,0.1);
    glVertex2f(0.3,0.8);
    glVertex2f(0.7,0.8);
    glEnd();
           
    top+=0.1;
    if(top>1.0)
    {
        top=0.0;
    }
    glFlush();
}

そして、main関数のイベントループ内から、無理やり書き換えさせるようにした。

int main(int argc, char *argv[])
{
//    return NSApplicationMain(argc,  (const char **) argv);
 
    NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init];
           
    FILE *fp;
    fp=fopen("/Users/soji/MyAFS/src/MacOS/test.txt","w");
    fprintf(fp,"!\n");
    fclose(fp);
           
    [NSApplication sharedApplication];
    [NSBundle loadNibNamed:@"MainMenu" owner:NSApp];
    // [NSApp run];
           
    [NSApp finishLaunching];

    while(1)
    {
        [pool release];
        pool=[[NSAutoreleasePool alloc] init];
                       
        NSEvent *event;
        event=[NSApp
            nextEventMatchingMask:NSAnyEventMask
            untilDate: [NSDate distantPast]
            inMode: NSDefaultRunLoopMode
            dequeue:YES];
        [NSApp sendEvent:event];
        [NSApp updateWindows];
 
        [[NSApp mainWindow] display];
                       
        static int x=0;
        fp=fopen("/Users/soji/MyAFS/src/MacOS/test.txt","w");
        fprintf(fp,"%d\n",x++);
       
fclose(fp);
    }
           
    [pool release];
    return 0;
}
 

このコードで、タイマーイベントを使わずにアニメーションさせることに成功。

もうひとつ、大きな発見があって、次のポストによると、Unixターミナルから、Cocoaフレームワークを使ったプログラムをコンパイルする方法があるらしい。

http://www.cocoalab.com/?q=node/11

なんでも、

gcc mytest.m -framework cocoa

こうタイプするといいらしい。あとは、NIBファイルを使わずにウィンドウを開く方法がわかれば、Xcode IDEに依存せずにプログラム書けるんでないの?

あと、 “Mac Dev Center: Window Programming Guide”によると、NIBファイルを使わずにウィンドウを開く方法があるらしい。Apple Developer Connectionでは、前にも“Open Close Windows”とかいうキーワードで検索したと思ったんだけど、見落としていたらしい。

まあ、一歩ずつ進んでいこうと思うんで、次回は、Xcodeが作ったプロジェクト上からウィンドウが開けるかどうか試してみようと思う。

2009/09/04

これまでにわかったことは、Mac OSのメインAPIは、もう終わってる言語 Objective-C で書かれてるってことだった。そして、どうも、簡単に800x600のウィンドウを開くことはできなさそうだってことだった。一枚ウィンドウを開くために、でっかい太ったテンプレートが必要になるらしい。このテンプレートを使わない方法もありそうなのだが、それは、なかなか見つからない。ただ、ひとついいこともわかって、Objective-Cの関数は、extern "C"を使えば、C++コードから呼び出すことができる。

今日は、短い時間しかMacの前で使うことができなかったけど、Objective-Cコードで書かれているmain関数をC++コード側でハイジャックできないか、トライしてみることにした。まず、XcodeのテンプレートからCocoaアプリケーションを作る。そして、main.mの中のmain関数の名前をsubmainに変更。プロジェクトに新しいC++ソースを追加して、

extern "C" int submain(int ac,char **av);

と書いて、C++コードにmain関数を書き、その中からsubmainを呼び出してみた。すると、うまいことウィンドウが開いて、メニューも使えるようだった。

次に、マルチスレッドにして、submainをサブスレッドとして呼び出してみる。これができれば、ウィンドウとかメニューとかは、Objective-Cに任せておいて、メインのプログラムをC++で書いて、必要に応じてextern "C"を使ってObjective-Cのコードと交信すればいい。pthread.hが使えるみたいだったので、submain関数をスレッドで呼び出せるように書き換えて、C++のmain関数からスレッドとしてsubmainを呼び出すようにしてみた。

そして、コンパイルして実行、してみたのだが、残念ながらうまく動かなかった。Cocoaはメインスレッドで走ることを前提にしてるらしい。まあ、それでもいいや。それなら、Cocoaがメインで、自分の本物のメインのプログラムをサブスレッドで走らせればいい。少なくとも重要なのは、プログラムのエントリーポイントはC++側で持てるということだ。

次回は、自分のスレッドからアニメーションできないかやってみよう。来週試す時間があればいいけど。最近ミーティングばっかり多くてなかなか時間が取れないんだけど。

2009/09/01

この秋学期、機械工学科で初級プログラミングを教えている。それで、来週から簡単な2次元のプログラミングを始めようと思っている。この授業は、初級プログラミングなので、システム特有の関数とか、ウィンドウを開くとか、コールバック関数とか、そういう話には突入したくない。だから、MFCとかWin 32 APIなんてのは教えない。グラフィックスと言っても、簡単にウィンドウを開いて、直線とか円とか四角形とかプリミティブが描けて、キーが押されるまで待って、プログラムを閉じるぐらいのことから始めたい。学生がWin32APIとかMFCとか使わずに、それができるように、Windows用に簡単なコードを書いた。多分、こういうウィンドウを開くとか、線を引くとかいう関数のセットは、各地の大学の授業ごとに無数に作られては消えているに違いない。

が、少なくとも一人の学生が、C++の課題を自分のiMacでやりたいとのことだった。実を言うと、僕も機会があれば、Mac OS Xのプログラミングを勉強してみたいと思っていた。けど、それに時間を使う理由が無かったもんで、まだやってなかった。

最後にMacプログラミングをやったのは、もうずいぶん前のことになる。そのときは、Macプログラミングが得意な友達が助けてくれた。けど、僕の印象としては、Windows上でのプログラミングに比べてMac OS用のプログラミングはかなり難しく、その友達がいなかったら多分僕はさじを投げていたと思う。それは、昔のことで、今はMac OSもバージョン10になり、APIも整理されて、良くなっているだろう。と、勝手に思っていた。

そういうわけで、Carnegie MelloのSchool of Computer ScienceのMac部屋に行ってみた。ログインして、Unixターミナルを開く。Mac OS XはFree BSDベースと聞いていたので、gccとか使えるのだろうか。試しに g++ とだけタイプしてEnterキーを押す。よしよし、g++は使えるようになってる。とりあえず、最初に、まず800x600のウィンドウを開く関数を作ってみよう。そのぐらい簡単にできて、然るべきだ。

その時点で、Mac OS Xプログラミングに関して、僕は、とってもまったくほんとに素人だった。まず、どういうライブラリを使えばいいのか、から始めないといかん。とりあえず、Safariを開いて、Apple Developer Connectionを見てみる。それによると、Mac OS Xでプログラムを書くには、CocoaとCarbonというAPIがあるらしい。なんでふたつあるの?と、思ったが、どうも読んだ感じではCocoaを勧めているようだ。どこにヘッダーがあるのかわからんかったけど、適当に見当つけて、多分 Cocoa/Cocoa.h とかincludeすりゃなんとかなるんじゃないか、と、思って次のプログラムを書いてみた。

#include <stdio.h>
#include <Cocoa/Cocoa.h>

int main(void)
{
            return 0;
}

当然、何もしないプログラムだが、これによって、とりあえずMac部屋でCocoaが使える状態にあって、Unixターミナルからコンパイルできるかどうか確認できる。

そして、g++ test.cppとタイプしてみたのだが、変なエラーが出た。とりあえず、Cocoa.hは存在するらしいけど、ヘッダの中に、"stray @"があるって?仕方ないので、findコマンドでエラーメッセージにあるFramework.hの所在を探してemacsで開いてみたところ、見たことも無い変な記述があるし。

仕方ないんで、ふたたびApple Developer Connectionを見る。それによると、Cocoaってのは、Objective-C用で、CarbonというのがC++用とのこと。ああ、そういうことか。Cocoaってのは、古くからのMac用のプログラムをコンパイルするためのライブラリで、Carbonってのがネイティブのライブラリなのか。それにしても、いまどきObjective-Cを使ってる人がいるのかね。それじゃあ、Carbonの例題を見てみようか、と、思ったらなんか例題が少ない。それで、よくよくもう少しApple Developer Connectionを読んでみたところ、Cocoaが標準で、よってObjective-CがMac OS Xの標準開発言語だって?しかも、CarbonはWrapperだけど、いろいろCocoaに対応が追いついてないところがあるって?嘘だろう。。。。

「嘘だろう。。。。」ってのは、他のOSで開発してた人がこの状況に直面したら普通に思う感覚なんじゃないだろうか。何がどうやったら、鳴り物入りで出てきた最新OSのMac OS Xが死語のプログラミング言語を標準に採用してるとは、想像もできんかった。僕も割と頑固なプログラマーで、自分で作ってきたプログラミングスタイルを変えない方だけど、Appleには頑固を通り越して、懐古主義なプログラマーがいるのかな。だから、いつまで経ってもマイナーなんだよ。。。。

僕は、Appleの懐古主義に付き合うつもりはまったくなくて、やりたいのは、まず簡単にウィンドウが一枚開ける関数をことだった。が、30分ばかり、Apple Developer Connectionを調べて結局あきらめた。どうも、どの例題を見ても、なにやら.NIBと呼ばれる、Windowsのリソースファイルに相当するファイルが無いとウィンドウが開けない。(というか、.NIBファイルを使わずにウィンドウを開いてる例題を発見できなかった)。画像ビューアとかどうすんだろ?画像にフィットする大きさのウィンドウとか開けないと困ると思うんだけど。方法はあるんだろうけど、それが発見できなかった。

多分、ひとつの理由は、Objective-Cにあるだろう。C++のコードのつもりで検索キーワードを選んでもヒットしないに違いない。もうCocoaはいいや。とにかく、Apple内では邪険に扱われているらしいがCarbonを見てみよう。が、さらに30分ばかり検索を続行したが、Carbonの例題の中にも、.NIBファイルを使わずにウィンドウを開いている例題は発見できなかった。どの例題もCreateWindowFromNibという関数でウィンドウを開けている。方法はあると思うんだけど。けど、それを発見できず。

僕も、他の仕事があるんで、あんまりこれに時間を浪費するわけにいかないんで、とりあえず、ゼロから書くのはあきらめて、XcodeなるIDEを使ってみることにした。僕は、テンプレートが与えられて、その中身をろくに理解もせずに使うのはどうも嫌だ。それに、テンプレートがC/C++のソースコード以外のファイルを含んでいる場合、同じプログラムが将来のバージョンのコンパイラでもコンパイルできると誰が保障してくれるのだろう?テンプレート依存のプログラムは、そのテンプレートのサポートが終わると同時にそのプログラムも大規模な書き換えが必要になる。だから、MFCもできる限り使わない。仕事で必要なときは使うけど。ただ、Macプログラミングを始めるには、どうやらXcode IDEを使ってテンプレートから作ってみるしかないらしい。

というわけで、次の例題をやってみた。

http://developer.apple.com/mac/library/documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_intro/opengl_intro.html

これはうまくいって、うまいことOpenGLが使えるウィンドウが開いた。なので、このプログラムを修正して、初級プログラミングコースにつきものの、次のようなプログラムが書けるようにできれば、とりあえず僕の目標は達成。

#include <stdio.h>
#include "graphics.h"

int main(void)
{
    OpenWindow(800,600);
    SetColor(255,255,255);
    DrawLine(0,0,799,599);
    while(Inkey()==0)
    {
        PollEvents();
    }
    return 0;
}

が、また壁に突き当った。テンプレートが出してきたmain関数の中で、NSApplicationMainという関数を読んでいるけど、イベントループがこの関数内にあるらしく、メインループをプログラム側に持たせてくれないらしい。WindowsだとPeekMessage/WaitMessageを使って、自分でメッセージループを回せるんだけど。検索したけど、似たような関数は発見できなくて、何かループさせたかったら、Appleはタイマーイベントを使って欲しいらしい。

イベント・ドリブンってのは重要な概念だ。テキストエディタを作るにはこれほどいい仕組みはない。エディタ系ならいいよね。けど、コールバック関数ってのは、初級プログラミングをやってる学生にはちょっと難しい。まあ、難しいことをやらせてもいいんだけど、そういうことをすると「ここはこういうもんだから、中身は後から理解することにして、このように使ってくれ」的な説明が多くなる。それがあんまり多くなると授業の中身がスカスカになる。いろんなプログラミングの授業で #include <stdio.h> ってのは、呪文だと思って覚えるように、とか教えているけど、なんでこれをはっきりと、#includeというのは、使用する道具のセットを指定するもので、stdio.hには、コンソール入力、出力の関数が記述されている、と教えないもんだから、いつまで立っても #include が何をするものかよくわからないまま先に突き進む学生が発生する。だから、まだ学生にはコールバック関数とかは使わずにやってもらいたい。やっぱり基本は、メインループ。

まあ、人々がオブジェクト指向プログラミングの威力を理解してきたころ、なんでもかんでもイベントでやるべきなんじゃないかと思ってしまった人たちがいた。僕はまったく同意できないんだけど。僕は、シンプルなことはシンプルなプログラミングでできるべきだと思う。複雑な仕組みは、やることが複雑になったときに取り入れればいい。単純な仕組みから、複雑な仕組みへの移行ができるような枠組みを作っておくのはいいけれど、あるプログラミングスタイルをプログラマーに押し付けるようなAPIの設計は、設計者の傲慢だ。

ここまで時間を浪費して、どうもCocoaもCarbonも僕も目的には役に立たないようだ、という印象を持った。Apple Developer Connectionによると、Mac OS XはX-WindowとGLXをサポートするとのことなので、じゃあ、GLXでいいや。というわけで、簡単なGLXのプログラムを持ってきてコンパイルしようとしたのだが、どこにもglx.hが無い。findコマンドで探したけど無い。対応してないじゃん。

というわけで、数時間浪費してたかが800x600のウィンドウ一枚も開くことができずに、初日は終了。