引き続き(っていつの「続き」だ?)、Ruby の組み込みに奮闘中。いろいろわかってきた。
- Ruby を組み込むには、
ruby_init(); ruby_init_loadpath();
を適当なところで呼べばよい。applicationWillFinishLaunching
の中で呼んでいるが、問題ないようだ。 NSTextView
から対話的に Ruby スクリプトを実行するには、NSTextView
のデリゲートメソッドtextView:doCommandBySelector:
を実装して、insertNewline:
セレクタが来たらスクリプトを読み込んでrb_eval_string_protect()
で評価すればよい。- さらに、Ruby からの出力を同じ
NSTextView
に出すには、文字列をNSTextView
に挿入するメソッドwrite
を持つカスタムクラスを定義して、そのインスタンスをグローバル変数$stdout
,$stderr
に入れておけばよい。最初、NSPipe
やらNSThread
を使ってごちゃごちゃ書いていたのだが、そんなことをする必要はなかった。
これまでのところで一番厄介だったのは、スクリプトを command-ピリオドで止める処理。Cocoa アプリのキー入力はすべて run loop で処理されるが、スクリプト実行中は run loop が走っていないため、うっかり while 1; end
みたいなスクリプトを実行してしまうと止める方法がなくなる。
自前でキースキャンをやるには NSApplication
の nextEventMatchingMask:untilDate:inMode:dequeue:
メソッドを使えばよいのだが、これをどこから呼び出すかが問題。最初、Ruby の set_trace_func
機能を使ってみたのだが、スクリプトの速度低下があまりにも激しかった。1.8.6 なら C レベルで rb_set_event_hook()
というのが使えるのだが、Mac OS 10.4 の Ruby は 1.8.2 なのでだめ。
いろいろ試行錯誤した末、Ruby の Thread
クラスを使うことにした。Cocoa アプリからスクリプトを実行する前に、Thread.new { while 1; sleep 1; Thread.main.raise Interrupt if check_interrupt > 0; end }
という風にキー監視用のスレッドを走らせておく(check_interrupt
はキー入力をチェックして、command-ピリオドなら 1 を返す関数)。Thread.main.raise
がポイント。
ここで注意すべきは、Cocoa はデフォルトではマルチスレッド対応になっていないこと。このため、Ruby の Thread
を使う前に、NSThread
を使って1回ダミーのスレッドを立てておく必要がある。NSThread
の detachNewThreadSelector:toTarget:withObject:
を1回でも呼ぶと、以後はマルチスレッド対応になる。また、Ruby スクリプトを走らせていない時や、Ruby スクリプトからダイアログを出している時など、Cocoa 側で run loop が走っている時は、上の監視スレッドを止めておかないといけない。これにはグローバル変数と Mutex
を使う。
実はここに至るまでに、TCL と Python の組み込みを試してみたのだが、Ruby が一番楽な気がする。TCL は組み込み自体は簡単なのだが、言語の機能が高くないので、ちょっと込み入ったスクリプトを書こうとするとぐちゃぐちゃになってしまう。Python の組み込みは、ドキュメントが充実していてやるべきことは明快なのだが、コードの量が妙に多くなる。あと僕は「インデントの量がブロックレベルを表す」という Python の文法がどうにも辛い(組み込みには関係ないけど)。もちろんブロックレベルとインデントは対応させるべきだが、それを文法で強制されるのはしんどいんだな。