2008年07月28日

2008年07月28日:Ruby の組み込み(つづき)

 組み込み Ruby の件の続き。どうやら、キー割り込みの監視用スレッドと実行本体を別々の rb_eval_string コールで実行したのでは安定して動作しないみたい。考えてみたら当然という気もするのだが、逆に動くこともあるのが不思議と言えば不思議。そこで、実行したい内容を「レシーバ・メソッド・引数(の配列)」にまとめて、「チェック用スレッドと目的のメソッド呼び出しを同時に実行し、終わったらチェック用スレッドを止める」という内容の Proc に渡すようにしてみた。

static int s_interrupt_var_initialized = 0; static VALUE s_interrupt_thread = Qnil; static VALUE s_interrupt_proc = Qnil; static VALUE s_interrupt_lock = Qnil; static VALUE s_interrupt_flag = Qfalse; /* rb_protect 用の補助関数 */ static VALUE s_Ruby_CallMethod(VALUE val) { void **ptr = (void **)val; VALUE receiver = (VALUE)ptr[0]; ID method_id = (ID)ptr[1]; VALUE args = (VALUE)ptr[2]; return rb_funcall(s_interrupt_proc, rb_intern("call"), 3, receiver, ID2SYM(method_id), args); } /* 割り込みチェック付きでメソッドを呼び出す */ VALUE Ruby_CallMethodWithInterrupt(VALUE receiver, ID method_id, VALUE args, int *status) { void *ptr[3]; VALUE retval; if (s_interrupt_var_initialized == 0) { rb_define_variable("$interrupt_thread", &s_interrupt_thread); rb_define_variable("$interrupt_proc", &s_interrupt_proc); rb_define_variable("$interrupt_lock", &s_interrupt_lock); rb_define_variable("$interrupt_flag", &s_interrupt_flag); rb_eval_string_protect("require 'thread'; $interrupt_lock = Mutex.new", status); if (*status != 0) return Qnil; s_interrupt_proc = rb_eval_string_protect( "Proc.new { |rec, method, args| ¥n" /* 監視用スレッドがすでに走っていれば止める */ " runflag = set_interrupt_flag(false) ¥n" /* 現在の監視用スレッドを退避 */ " thread_save = $interrupt_thread ¥n" /* 新しい監視用スレッドを作る */ " $interrupt_thread = Thread.new { ¥n" " while 1 ¥n" " 10.times { Thread.pass; Thread.stop if !get_interrupt_flag } ¥n" " if check_interrupt > 0 ¥n" /* "raise Interrupt" は Ruby1.8.6 ではエラーになる */ " Thread.main.raise Interrupt.new(nil) ¥n" " end ¥n" " end ¥n" " } ¥n" /* 割り込みチェックを開始する */ " set_interrupt_flag(true) ¥n" " begin ¥n" /* メソッド呼び出し本体 */ " rec.send(method, *args) ¥n" " ensure ¥n" /* 後始末 */ /* 監視用スレッドを止める */ " $interrupt_thread.kill ¥n" /* 以前の監視用スレッドを復帰 */ " $interrupt_thread = thread_save ¥n" /* 監視用スレッドが走っていたなら再開 */ " set_interrupt_flag(runflag) ¥n" " end ¥n" "}", status); if (*status != 0) return Qnil; s_interrupt_var_initialized = 1; } ptr[0] = (void *)receiver; ptr[1] = (void *)method_id; ptr[2] = (void *)args; retval = rb_protect(s_Ruby_CallMethod, (VALUE)ptr, status); return retval; }

 set_interrupt_flag, get_interrupt_flag は次のように実装している。

static VALUE s_SetInterruptFlag(VALUE self, VALUE val) { VALUE oldval; /* 値を true/false に揃える */ if (val != Qundef) val = ((val == Qfalse || val == Qnil) ? Qfalse : Qtrue); /* 同期制御 */ if (s_interrupt_lock != Qnil) rb_funcall(s_interrupt_lock, rb_intern("lock"), 0); oldval = s_interrupt_flag; if (val != Qundef) s_interrupt_flag = val; if (s_interrupt_lock != Qnil) rb_funcall(s_interrupt_lock, rb_intern("unlock"), 0); if (s_interrupt_thread != Qnil) { if (val == Qtrue) /* 監視用スレッドを再開する */ rb_funcall(s_interrupt_thread, rb_intern("wakeup"), 0); else if (val == Qfalse) /* 監視用スレッドが止まるまで待つ */ rb_eval_string("while $interrupt_thread.status == ¥"run¥"; end"); } return oldval; } static VALUE s_GetInterruptFlag(VALUE self) { return s_SetInterruptFlag(self, Qundef); }

 肝心の check_interrupt はだいたいこんな感じ。

static VALUE s_CheckInterrupt(VALUE self) { if (s_GetInterruptFlag(self) == Qfalse) return INT2NUM(-1); else { /* AppKit (Objective-C) code */ NSEvent *event = [NSApp nextEventMatchingMask: NSKeyDownMask untilDate: nil inMode: NSEventTrackingRunLoopMode dequeue: YES]; if (event != nil) { NSString *s = [event charactersIgnoringModifiers]; unsigned int flags = [event modifierFlags]; if ([s isEqualToString: @"."] && (flags & NSCommandKeyMask) != 0) return INT2NUM(1); } return INT2NUM(0); } }

 今のところ 10.4 (Ruby 1.8.2), 10.5 (Ruby 1.8.6) どちらでも動いているようです。まだ細かい問題はいろいろありそうだが、なんとか峠は越えたかな。

Posted at 2008年07月28日 23:48:55
email.png