mruby/c最近の変化点:mrblibの導入

Techc, Japanese, mruby/c

最近、mruby/cに大き目の更新があったので、ざっくり見てみる。

mrblibの導入

今までCで実装されていた組み込みメソッドの一部について、バイトコンパイルしたRubyコードをリンクする形式になった。
https://github.com/mrubyc/mrubyc/commit/c55d28039d066bb2fecdc63c905584ba11ca0445

これまで

組み込みのメソッドは全てCで書かれていた。
たとえば、Fixnumクラスのtimesメソッドの実装はこんな感じで行われていた。

static void c_fixnum_times(mrb_vm *vm, mrb_value v[], int argc)
{
  uint32_t code[2] = {
    MKOPCODE(OP_CALL) | MKARG_A(argc),
    MKOPCODE(OP_ABORT)
  };
  mrb_irep irep = {
    0,     // nlocals
    0,     // nregs
    0,     // rlen
    2,     // ilen
    0,     // plen
    (uint8_t *)code,   // iseq
    NULL,  // pools
    NULL,  // ptr_to_sym
    NULL,  // reps
  };

  // count of times
  int cnt = v[0].i;

  mrbc_push_callinfo(vm, 0);

  // adjust reg_top for reg[0]==Proc
  vm->current_regs += v - vm->regs + 1;

  int i;
  for( i=0 ; i<cnt ; i++ ){
    // set index
    mrbc_release( &v[2] );
    v[2].tt = MRB_TT_FIXNUM;
    v[2].i = i;

    // set OP_CALL irep
    vm->pc = 0;
    vm->pc_irep = &irep;

    // execute OP_CALL
    mrbc_vm_run(vm);
  }

  mrbc_pop_callinfo(vm);
}

}

ブロックはProcオブジェクトとして、timesメソッドの引き数として渡されている。
このProcオブジェクトを、mrbc_vm_rum()で実行したいが、そのままでは実行できないので、スタック上にirep構造体を作って、その中でOP_CALLを使ってProcオブジェクトに格納されているブロックのバイトコードを呼び出している。

mruby/cでは、mrubyには無いOP_ABORTという命令があるが、最初OP_STOPとの使い分けがよく分からなかった。
この実装を見ると、どうやらこのようにirepをVMに渡して応答をまつようなユースケースのためにあると思われる。

同様の実装が、RangeクラスとArrayクラスのeachメソッドに見られる。
この実装には、ブロックの処理の戻り値がSelfを上書きしてしまうという問題があった(おそらく)。
https://github.com/mrubyc/mrubyc/issues/55
直すのにもVMに何か状態を伝えるか、VMから処理結果に関する情報をもらう必要があって、どうしたものか?と考えていたが、新しい実装で解決している。

新しい実装

このようにちょっと力技感がある実装から、組み込みクラスのメソッドをRubyで書けるようになり、ブロックの処理も置き換えられた。
実体としてはこんな感じ。

mrblibというディレクトリが増えた。makeファイルでこんなことをしている。

SRC = array.rb numeric.rb object.rb range.rb
OUTPUT = ../src/mrblib.c
MRBC ?= mrbc


all: $(OUTPUT)

$(OUTPUT): $(SRC)
    cat $(SRC) > mrblib.rb
    $(MRBC) -E -Bmrblib_bytecode -o$(OUTPUT) mrblib.rb
    rm -f mrblib.rb

mrblib.rbに/mrbclib以下のrubyのコードをまとめて、バイトコンパイルして、その結果をsrc/mrblib.cにmrblib_bytecodeという名前の構造体として出力している。

例で挙げたtimesメソッドはnumeric.rbで以下のように実装されている。


class Fixnum

  # times
  def times
    i = 0
    while i < self
      yield i
      i += 1
    end
    self
  end

  
end

timesメソッドの最後にselfを返すようにしているので、selfがブロックの処理結果で上書きされるようなことも無くなっていることが期待できる。

バイトコンパイル後のmrblib.cの中身はこんな感じ。


#include <stdint.h>
extern const uint8_t mrblib_bytecode[];
const uint8_t
#if defined __GNUC__
__attribute__((aligned(4)))
#elif defined _MSC_VER
__declspec(align(4))
#endif
mrblib_bytecode[] = {
0x52,0x49,0x54,0x45,0x30,0x30,0x30,0x34,0xea,0xaa,0x00,0x00,0x04,0x76,0x4d,0x41,
0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x03,0xfb,0x30,0x30,
0x30,0x30,0x00,0x00,0x00,0x80,0x00,0x01,0x00,0x03,0x00,0x04,0x00,0x00,0x00,0x11,
0x00,0x80,0x00,0x05,0x01,0x00,0x00,0x05,0x00,0x80,0x00,0x43,0x00,0x80,0x00,0x45,
(snip)
};

このバイトコードをmruby/cのクラス初期化の最後に実行して、メソッドを追加している。

//================================================================
void mrbc_run_mrblib(void)
{
  extern const uint8_t mrblib_bytecode[];

  mrb_vm vm;
  mrbc_vm_open(&vm);
  mrbc_load_mrb(&vm, mrblib_bytecode);
  mrbc_vm_begin(&vm);
  mrbc_vm_run(&vm);
  mrbc_vm_end(&vm);
  //mrbc_vm_close(&vm);
}

//================================================================
// initialize

void mrbc_init_class(void)
{
  mrbc_init_class_object(0);
  mrbc_init_class_nil(0);
  mrbc_init_class_proc(0);
(snip)
  mrbc_init_class_hash(0);

  mrbc_run_mrblib();
}

最後のmrbc_run_mrblib()が新しく増えた部分で、この中で、mrblib_bytecodeが実行されている。

この実装によって、前述のブロックの戻り値の扱いの問題が解消されたことが確認できた。

まとめと雑感

mrblibによって組み込みクラスの拡張が容易になった。
環境毎のextensionの実装にも役立ちそう。

最初Cで書かれていたのもメモリの使用量を意識してのことだと思うので、この実装に切り替わった結果のメモリの使用量の変化が気になる。
Cのコードも削減できているから、そこまで劇的には増えてないのでは、と思われるが、増えてはいそう。
確認したい。

コンパイル済みであれば、make時にmrbcを呼ぶ必要は無いと思うのだけど、呼ばれている?ようなコメントも見かけたので、これも確認する。

mrbcで生成したバイトコードにmruby/cにとって不要な部分(ヘッダや、lv)があるので、make時にstripできたらよいのではと思う。必要なのはirep構造体だけであるはずなので。

リファレンスカウントがちゃんとカウントできているか、気になる・・・。

バイトコードの長さがmruby/cのバイナリサイズに直結するので、そこは減らしたい。
mrubyの最新にローカル変数名のテーブルの出力を端折る機能などあるので、使えるようにする方法を考える、など。(現状のmruby/cでは、mruby ver1.3のmrbcを利用することが前提となっている)
こんなPullRequestがマージされていたりする。
https://github.com/mruby/mruby/pull/4068

そもそも動いていないAVR環境でmruby/c動かす人は居ないと思うのだけど、たとえばAVRだと、PROGMENで修飾しないとグローバル変数に置いたstaticな配列もRAMに展開されてしまうので、ROMだけでなく、RAMにも影響がでてしまう。これは避けたい。
http://www.musashinodenpa.com/arduino/ref/index.php?f=0&pos=1830