2008年07月01日

2008年07月01日:Ruby を Cocoa アプリケーションに組み込む

 引き続き(っていつの「続き」だ?)、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 みたいなスクリプトを実行してしまうと止める方法がなくなる。

 自前でキースキャンをやるには NSApplicationnextEventMatchingMask: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回ダミーのスレッドを立てておく必要がある。NSThreaddetachNewThreadSelector:toTarget:withObject: を1回でも呼ぶと、以後はマルチスレッド対応になる。また、Ruby スクリプトを走らせていない時や、Ruby スクリプトからダイアログを出している時など、Cocoa 側で run loop が走っている時は、上の監視スレッドを止めておかないといけない。これにはグローバル変数と Mutex を使う。

 実はここに至るまでに、TCL と Python の組み込みを試してみたのだが、Ruby が一番楽な気がする。TCL は組み込み自体は簡単なのだが、言語の機能が高くないので、ちょっと込み入ったスクリプトを書こうとするとぐちゃぐちゃになってしまう。Python の組み込みは、ドキュメントが充実していてやるべきことは明快なのだが、コードの量が妙に多くなる。あと僕は「インデントの量がブロックレベルを表す」という Python の文法がどうにも辛い(組み込みには関係ないけど)。もちろんブロックレベルとインデントは対応させるべきだが、それを文法で強制されるのはしんどいんだな。

Posted at 2008年07月01日 01:05:56
email.png