引き続き(っていつの「続き」だ?)、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 の文法がどうにも辛い(組み込みには関係ないけど)。もちろんブロックレベルとインデントは対応させるべきだが、それを文法で強制されるのはしんどいんだな。