MacOSX 上での開発について勉強したことについてちょっと書いてみる

対象読者は不明。

まず全体の印象だけど、ソース無いようわあん! というのがデカいです。 Web にある情報はすごくたくさんの情報がまとまってるところと、それほど多くない ML と、っていうイメージ。あと開発者はあんまりソフト以外の開発情報(日記書くとかそういう)とか出してないなぁというイメージ。本とかで調べるとわかるのかもしれないけど、なんも買ってないから不明。

Ruby/SDL on MacOSX

on MacOSX とか in MacOSX とか悩みますよね。私は気にしない境地に達しています。

初日は Ruby/SDL を見てた。色々考えたけど、やっぱ SDL がイカンだろうという感は強い。要は SDL はユーザーの main をマクロで SDL_main に書きかえていて、 libSDLmain.a に main を忍ばせて、そこで Cocoa の起動をしてから Cocoa のイベントループに入ったら SDL_main を呼ぶ、と。

これのうっとうしいところは、一旦 main が動いちゃうと Cocoa の初期化をしてまた戻ってくる…ってことが簡単にできないことです。でまぁこいう苦労の記録がある、と。

http://www2s.biglobe.ne.jp/~nunokawa/wiki.cgi?page=Ruby%2FSDL+on+Mac+OS+X

http://eto.com/d/RubySDLonMacOSX.html

今はずいぶん簡単になってるみたいで、動かしたいだけなら

#include <SDL.h>
void ruby_init();
void ruby_options(int argc, char* argv[]);
void ruby_run();
int main(int argc, char* argv[]) {
    ruby_init();
    ruby_options(argc, argv);
    ruby_run();
    return 0;
}

こんな感じのファイルを `sdl-config --cflags --libs` -lruby つきでコンパイルしてやれば動きます。

んでまぁ根本的な対策としては、私が考えてたのは、 Cocoa の初期化は別スレッド、とか初期化前に継続作っておいて舞い戻ってくる、とかですが、前者はどうもうまくいかなくて(少なくともイベント取れないと思う)、後者はめんどくなってやめました。

D/SDL on MacOSX

んで次は D のゲームのコンパイルしてたわけですな。

http://shinh.skr.jp/osxbin/

D のコンパイラは gdcmac がすばらしいのでありがたく使う。

http://gdcmac.sourceforge.net/

まずは普通に D の非互換によるエラーを潰す。そんなにたいした量なかった。

リンクは大変。まず昔も書いたような話。

http://zinnia.dyndns.org/~hiki/SDLKB/?MacOSX%2BSDL%2BD%B8%C0%B8%EC%A4%C7%A4%CE%A5%A2%A5%D7%A5%EA%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3%A5%B3%A5%F3%A5%D1%A5%A4%A5%EB%CB%A1

要は Ruby/SDL と同じで、 libphobos.a も main を奪いたがるのでここでも競合する。しゃーないなーということで main の無い libgphobos.a を作ることにする。めんどくさいから shell script をはる。

#!/bin/sh -x

rm -fr t
mkdir t
cd t
cp /usr/lib/libgphobos.a .
for i in i386 ppc ; do
    mkdir $i
    cd $i
    lipo ../libgphobos.a -extract $i -output libgphobos.a
    libtool -static libgphobos.a -o gp.a
    ruby ../../arext.rb gp.a
    rm cmain.o || exit
    ar crus libgphobosnm.a *.o
    cd ..
done

lipo -create -arch i386 i386/libgphobosnm.a -arch ppc ppc/libgphobosnm.a -output ../libgphobosnm.a

何してるかっていうと libgphobos.a が Universal Binary なので、 i386ppc でそれぞれ libgphobos.a を取り出して、 .a を展開してから cmain.o を削除、そんでから .a にまとめて、もう一度 Universal Binary にしている。 lipo は Universal Binary を操作するツールで、 i386 だけを取り出したりできる。この段階ではまだ新しいフォーマットなので、古き良き ar アーカイブにするために libtool -static を使い、そんでから展開している。展開は ar でできるのだけど、うっといことに同じ名前オブジェクトファイルが入ってたので ar 展開ツールをサクっと作った。こんなの:

if ARGF.read(8) != "!<arch>\n"
  abort 'it is not ar'
end

m=[]
while x=ARGF.gets
  x = x.split
  abort 'wrong format?' if x[6] != '`'
  x = [x[0], Time.at(x[1].to_i), x[2], x[3], x[4], x[5].to_i, x[6]]

  if /\#(\d+)\/(\d+)/ =~ fn=x[0]
    fn = ARGF.read(s=$2.to_i)[/[^\0]+/]
    x[5] -= s
  end
  c = ARGF.read(x[5])

  on = fn
  while m.include?(fn)
    en = File.extname(fn)
    bn = File.basename(fn, '.*')
    fn = bn + '_' + en
  end
  if on != fn
    puts "filename changed: #{on} => #{fn}"
  end
  m << fn
  File.open(fn, 'w') do |o|
    o.print(c)
  end
end

これで libgphobos からは main を消せた。後は D の初期化をする SDL_main 相当を書いてやればいい。以下 boot.d

import std.c.stdio;
extern (C) int chdir(char* s);
private extern (C) char* rindex(char* s, int c);
private extern (C) int _d_run_main(int argc, char **argv, void * p);
int main();
extern (C) int SDL_main(int argc, char** argv) {
    char buf[256];
    char* p = rindex(argv[0], '/')+1;
    sprintf(buf.ptr, "%s.app/Contents/Resources", p);
    if (0 != chdir(buf.ptr)) {
        printf("cannot change dir\n");
    }
    return _d_run_main(argc, argv, & main);
}

こんなのを一緒にリンクしてやると、

  • libSDLmain.a の main から起動
    • Cocoa 初期化
    • SDL_main を呼び出し
  • boot.d の SDL_main
    • .app 形式に対応するため chdir (この時まだ D の GC は初期化されてないので使えないので注意)
    • _d_run_main で _Dmain を呼ぶ
  • ユーザーコード

という流れになる。

で、 Universal Binary を作る話だけど、基本的には gdc -arch i386 -arch ppc などとすればよろしくやってくれる。ところがリンクは、 libgphobos.a を変えちゃってる都合で自前でやることになった。 gcc のオプションに -v をつけて ld のオプションを調べて、それを適当に編集して -lgphobos を自作のに変えればいい。それぞれリンクできたら lipo でひっつける。うちだとリンクオプションはこんな感じになった。 crt が微妙に違うんだなーと思った。

LIBS=../libgphobosnm.a -lstdc++ ../bulletml/libbulletml_d.a -F$(FPATH) -framework Cocoa $(FOPTS) -framework OpenGL ../SDLMain.o
LD_386=/usr/libexec/gcc/i686-apple-darwin8/4.0.1/collect2 -dynamic -arch i386 -arch_multiple -macosx_version_min 10.3 -multiply_defined suppress -weak_reference_mismatches non-weak -lcrt1.o /usr/lib/gcc/i686-apple-darwin8/4.0.1/crt3.o -L/usr/lib/gcc/i686-apple-darwin8/4.0.1 -L/usr/lib/gcc/i686-apple-darwin8/4.0.1 -L/usr/lib/gcc/i686-apple-darwin8/4.0.1/../../.. -lgcc_s.10.4 -lgcc -lm -lSystem
LD_PPC=/usr/libexec/gcc/powerpc-apple-darwin8/4.0.1/collect2 -dynamic -arch ppc -arch_multiple -macosx_version_min 10.3 -multiply_defined suppress -weak_reference_mismatches non-weak -lcrt1.o /usr/lib/gcc/powerpc-apple-darwin8/4.0.1/crt2.o -L/usr/lib/gcc/powerpc-apple-darwin8/4.0.1 -L/usr/lib/gcc/powerpc-apple-darwin8/4.0.1 -L/usr/lib/gcc/powerpc-apple-darwin8/4.0.1/../../.. -lgcc_s.10.4 -lgcc -lm -lSystemStubs -lSystem

あとはここに昔書いたような方法でパッケージを作りました。要は hdiutils ってコマンドを使うと良い。

http://zinnia.dyndns.org/~hiki/SDLKB/?MacOSX%2BSDL%A4%C7%A4%CE%C7%DB%C9%DB%CA%AA%BA%EE%C0%AE%CB%A1

MacOSX で Window マネージャー的なことをやりたい using CGS

次。 w3mimg と sevil を作ったわけだけど、これらは割と似たようなことがやりたいのだったりする。つまり既に動いてるプロセスのウィンドウの情報が欲しい。 sevil の場合はウィンドウを書き換えたい、と。ちなみにこのへんから特にあやしいのでウソがあったらすんません。あとこの内容は Apple 的に激しく非推奨らしいのでそのへん適当に。

まず情報取得の方法ですが、2種類あるかなと。一つは Core Graphics Services という API を使うこと。これはなんか Apple の ML でその API は使うな! とか言ってるのを見かけたようなものですが(リンク失念)、高速に動いて、特に設定を変えることなく動きます。欠点はいつまでサポートされるかとか昔の OS で動くかとか不透明すぎること。あとドキュメントが存在しないこと。

まぁそれさえ妥協すれば割と簡単に使える、例えば画面にあるウィンドウ一覧はこんな感じ。

#include <Carbon/Carbon.h>

typedef void* CGSValue;

int _CGSDefaultConnection();
void CGSGetOnScreenWindowCount(int cid, int wid, int* cnt);
void CGSGetOnScreenWindowList(int cid, int wid, int cnt, int* ids, int* acnt);
void CGSGetWindowProperty(int cid, int wid, CGSValue key, CGSValue* val);
CGSValue CGSCreateCStringNoCopy(char* str);
void CGSGetScreenRectForWindow(int cid, int wid, CGRect* rect);
char* CGSCStringValue(CGSValue str);

int main() {
    int cid, cnt, *ids;
    int i;

    cid = _CGSDefaultConnection();
    CGSGetOnScreenWindowCount(cid, 0, &cnt);
    ids = (int*)malloc(sizeof(int)*cnt);
    CGSGetOnScreenWindowList(cid, 0, cnt, ids, &cnt);
    for (i = 0; i < cnt; i++) {
        CGSValue windowTitle;
        char* title;
        CGSGetWindowProperty(cid, ids[i],
                             CGSCreateCStringNoCopy("kCGSWindowTitle"),
                             &windowTitle);
        title = CGSCStringValue(windowTitle);

        if (title && *title) {
            CGRect rect;
            CGSGetScreenRectForWindow(cid, ids[i], &rect);
            int x = (int)rect.origin.x;
            int y = (int)rect.origin.y;
            int w = (int)rect.size.width;
            int h = (int)rect.size.height;

            printf("%s\t(%d,%d) (%d,%d)\n", title, x, y, w, h);
        }
    }
    free(ids);
    return 0;
}

CGSGetOnScreenWindowList を CGSGetWindowList にすれば画面に無いのも取れるし、ウィンドウがどのプロセスに属しているか、とか、あとウィンドウのレベル(背景だとか普通だとかメニューだとか)とかの属性も取れる。詳細は適当にヘッダでも探すと良い。

ただまぁ、この API は書き換え系の命令が全部エラーで帰ってくる。っていうのは X でもそうだけど、 Window Manager みたいにヨソの Window を自在に操れる存在は一つのプロセスしか許されてないみたい。そして OSX は Dock がその役割を担っているので、ヤツを殺して自分で完全な Window Manager を書く気が無いと手が出しにくい。

ただ、 VirtueDesktop は Apple Event で Dock と通信することで機能を一部実現しているみたい。実際ソースの中の DocCommunicator を適当にリンクしてやると、 Dock を殺さずにウィンドウの上下を入れ替える関数 (CGSExt*) とかが使えたりする。

ただあんまり追ってないのでよくわかってない。暇な時に深追いしてみるテーマとしてはなかなか面白そう。 VirtueDesktop も DesktopManager も mach_inject.c とかそいう萌え萌えなタイトルのコードが入ってるんですよね。

MacOSX で Window マネージャー的なことをやりたい using Accessibility API

でもちょいマトモな方法として Accessibility API というのがあると。これはそもそも機能全体がデフォルトではオフになっていて、環境設定からユニバーサルアクセスかなんかをオンにしないと使えない。まぁ便利なのでオンにしたらいいんじゃないのと思う。

これ何する API かっていうと、要はウィンドウの情報取得や、ユーザーがマウスやキーボードで行えることをエミュレートする、っていう API らしい。たぶん Apple Event とかいうので通信する。んでたぶんかなり重い。 Apple Script っていうのはたぶんこの API のフロントエンド。 Apple Script 重いって言われてるけどたぶんこの層がそもそも重いんだと思う。ていうかなんでこんなに重いかは不明。使いかた間違ってるかも。特になんか取れない情報取ろうとするとすごいことになる。

でこれもまぁ全然情報無いんだよなー。便利なのに。そろそろ文章書くの飽きてきたので win.cc 参照! とかでいい気もする。んと、 C でこいう汎用的なデータのやりとりするの大変ね、という。 libdwarf とかのめんどくささに似てるか。まぁ少し書こう。

まず、

 AXUIElementRef appRef = AXUIElementCreateApplication(pid);

なり

 AXUIElementRef sysRef = AXUIElementCreateSystemWide();

なりで根っこのオブジェクトが取ってくる。目的のプロセスが定まってる場合は前者で pid 使って取ってくる。んであとは UI Inspector で適当に調べたいウィンドウとかがどの情報を取れるか調べて、ずるずると拾ってくる。んー例えば

 AXUIElementRef winRef;
 AXUIElementCopyAttributeValue(app, kAXFocusedWindowAttribute, &winRef);

とかこんな感じで(castいるかも)、さっきのアプリの中でフォーカスのあたってるウィンドウを拾ってくる、とかができる。似たような感じで Window の中のタイトルを拾ってきたら Core Foundation の String がとってこれるし、 Window の中の最大化ボタンを拾ってきて、そのボタンを押したシミュレートをさせる、とかもできる。例えば今拾ってきた Window を上に持ってくるなら

  AXUIElementPerformAction(winRef, kAXRaiseAction);

とかでマウスクリックした時のシミュレートになる。そんな感じ。たぶん。

このへんの API は Witch は知ってるはずだ…とか思って witchdaemon を nm してて気付いた。ていうか今回書いたような内容はほとんどばいなりー的な知識で仕入れた気がする。 ktrace&kdump で CoreGraphics.framework という名前を知ったし。まぁオブジェクトファイルがあったらとりあえず挨拶がてら nm しとけっていう話。あと ADC のドキュメントはなんか古いのかあってないので基本的な使い方わかったらヘッダ見るといい。

あと、

http://shinh.skr.jp/pbh/

OSX の内容もちょこっと書き足した。あと PBH からもリンクはったけど、 OSX でばいなりーな内容はこちらの方の記述が色々面白かったです。

http://d.hatena.ne.jp/mteramoto/20070114/p1

Cocoa

そいや前のに書き忘れてたけど、 Accessibility APICocoa バージョンもあるはず。私は Objective-C より better-C としての C++ というよくわからない気分だったので、というか要はハマリポイントを増やしたくなくて Carbon の方の API を使いました。

でその Cocoa だけど、 w3mimg のイメージを表示する部分で少し使った。まぁよくあるイベントドリブンの GUI フレームワークだと思うんだけど、ちょっとびっくりしたのはこういう文かな。

    [image compositeToPoint: NSMakePoint(0, 0)
           operation: NSCompositeSourceOver];

えーと image を (0,0) にはりつける、えーとえーと、どこに?っていう。なんかフォーカスのある Window みたいな概念があるらしくて、自動的にそこに書かれるらしい。へえ OO 的にそれはアリなのかなぁという。

あと w3mimg は本当は他の実装はたぶん端末の window に直接描画してるんだけど、 w3mimg は透明な薄皮かぶせてそこに描画している。なんかピクセル直接書き換えはえらいめんどくさいんだけど、本当にこんな方法しか無いのかなぁと謎。今これとか参考にして 32/24bpp 決め打ちっていうのをやめようと思っているんだけど。

http://homepage.mac.com/mkino2/cocoaProg/Carbon/CarbonGraphics/CarbonGraphics.html

Objective-C はそれなりに書いたことがあるしまぁそんなに問題無いかんじ。 GC 無しで使う言語じゃないなーとは思う。なんか次あたり GC 入るらしいですね。 Xcode にはもうそいうオプションあるらしいし、 libobjc とか見ても gc_init とか普通にあるから、前のバージョンにこっそり入れて流通させておいて、次から公開 API でそれ以前のバージョンは知らね、とかそいう感じなのかなーとか妄想しましたが知らん。

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h