mruby で割り込みハンドラから呼ぶ処理をブロックで渡す際のやり方
Tickerはmbedでインターバルタイマを扱うクラスです。前に、Tickerの実装を、Ticker.new するときのブロックとして、タイマハンドラを渡したいけれどmrb_stateコンテキストをどう分けるのかわからないと書いたのですが、mruby-threadを参考に実装できました。
簡単にいうと、mrb_open しなおして別の vm を作り、タイマハンドラ側では、そのコンテキストで mrb_yiled() することで実現できるのですが、そのやり方が下の通りです。
typedef struct { // タイマハンドラ用コンテキストの構造体 int argc; // ブロックの引数 mrb_value* argv; struct RProc* proc; // ブロック本体 mrb_state* mrb; // コピーした vm } mrb_thread_context; class TickerThread { public: TickerThread(Ticker *t) { _ctx = NULL; _t = t; } ~TickerThread() { detach(); delete _t; } void attach(mrb_thread_context *ctx, float tick) { attach_us(ctx, (int)(tick * 1000000.0f)); } void attach_us(mrb_thread_context *ctx, int tick) { if (_ctx != NULL) detach(); _ctx = ctx; _t->attach_us(this, &TickerThread::tickerHandler, tick); } void detach() { if (_ctx != NULL) { if (_ctx->mrb != NULL) mrb_close(_ctx->mrb); if (_ctx->argv != NULL) free(_ctx->argv); free(_ctx); _ctx = NULL; _t->detach(); } } void tickerHandler(void) { // スレッド用VMのコンテキストでブロックをyieldする mrb_yield_argv(_ctx->mrb, mrb_obj_value(_ctx->proc), _ctx->argc, _ctx->argv); } private: Ticker *_t; mrb_thread_context *_ctx; }; mrb_thread_context * mrb_fork_context(mrb_state *mrb, mrb_value self) { int i, l; mrb_thread_context* context = (mrb_thread_context*) malloc(sizeof(mrb_thread_context)); context->mrb = mrb_open_allocf(mrb->allocf, mrb->allocf_ud); migrate_all_symbols(mrb, context->mrb); mrb_value gv = mrb_funcall(mrb, self, "global_variables", 0, NULL); l = RARRAY_LEN(gv); for (i = 0; i < l; i++) { mrb_int len; int ai = mrb_gc_arena_save(mrb); mrb_value k = mrb_ary_entry(gv, i); mrb_value o = mrb_gv_get(mrb, mrb_symbol(k)); if (is_safe_migratable_simple_value(mrb, o, context->mrb)) { const char *p = mrb_sym2name_len(mrb, mrb_symbol(k), &len); mrb_gv_set(context->mrb, mrb_intern_static(context->mrb, p, len), migrate_simple_value(mrb, o, context->mrb)); } mrb_gc_arena_restore(mrb, ai); } return context; } mrb_value mrb_mbed_tick_init(mrb_state *mrb, mrb_value self) { DATA_TYPE(self) = &mbed_tick_type; DATA_PTR(self) = new TickerThread(new Ticker()); return self; } mrb_value mrb_mbed_tick_attach(mrb_state *mrb, mrb_value self) { mrb_value proc = mrb_nil_value(); mrb_int argc; mrb_float tick; mrb_value* argv; mrb_get_args(mrb, "f&*", &tick, &proc, &argv, &argc); if (!mrb_nil_p(proc) && MRB_PROC_CFUNC_P(mrb_proc_ptr(proc))) { mrb_raise(mrb, E_RUNTIME_ERROR, "invalid parameter"); } // VM 作成 mrb_thread_context *context = mrb_fork_context(mrb, self); // ブロックをセット context->proc = mrb_proc_new(mrb, mrb_proc_ptr(proc)->body.irep); context->proc->target_class = context->mrb->object_class; context->argc = argc; context->argv = (mrb_value*)calloc(sizeof (mrb_value), context->argc); for (int i = 0; i < context->argc; i++) { context->argv[i] = migrate_simple_value(mrb, argv[i], context->mrb); } TickerThread *obj = static_cast<TickerThread*>(DATA_PTR(self)); obj->attach(context, tick); return self; }
Ticker#attach()メソッドの中で、mrb_fork_context()により新しいVMを作っています。変数等のマイグレーションは、ほぼmruby-threadの https://github.com/mattn/mruby-thread/blob/master/src/mrb_thread.c この実装のままです。グローバル変数のコピーは、ブロック内global_variablesすると :$1 シンボルとかが重複してるので、必要なのか理解できていません*1。
TickerThreadクラスでは、タイマハンドラにTickerThread::tickerHandler()を登録して、ハンドラ内で、作成したVMのmrb_stateでmrb_yield()によってブロックを実行します。
こんな感じで使います。
# 1secインターバルでLチカ led1 = DigitalOut.new LED_BLUE ticker = Ticker.new ticker.attach(1, led1) do |led| led.toggle end
今回の実装にあわせて、今まで、Mbedモジュールの下にmbedの各クラスを作っていましたが、クラスオブジェクトはスレッドに持ち込めないそうなので、トップレベル(?)に定義しなおしました。
また、mrubyでのThreadの実装については、今回実装したようにVMを作り直すのではなくて、GVL版というのがあるようです。mruby本体でサポートされて、RAMのオーバヘッドが少ないならこっちのほうがありがたいので、こっちも試してみたいと思います。
*1:これはmruby-threadも同じようになっていた