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も同じようになっていた