|
|
||
「mruby で素数を数えるんだ - Tociyuki::Diary」で、引数がないときブロックからクロージャを作ったらうまく動かない原因を探すため、mruby のクロージャが生成されるまでを VM になったつもりで追いかけていました。
ブロックをコンパイルすると、OP_LAMBDA に irep 番号に OP_L_CAPTURE フラグ(値は2)を加えたコードが生成されます。下の場合ですと、 irep1 の 008 ステップがクロージャの生成指示部分です。
def prime_generator irep 1 nregs=7 nlocals=5 pools=0 syms=2 ... 007 OP_LOADSELF R5 008 OP_LAMBDA R6 I(2) 2 # irep(2) OP_L_CAPTURE 009 OP_SEND R5 'lambda' 0 010 OP_RETURN R5 lambda do irep 2 nregs=4 nlocals=2 pools=0 syms=3 000 OP_GETUPVAR R2 2 0 # n += 1 001 OP_LOADI R3 1 002 OP_LOADNIL R4 003 OP_ADD R2 '+' 2 004 OP_SETUPVAR R2 2 0 ...
VM の OP_LAMBDA の処理部分が読んでみると、OP_L_CAPTURE がついているときは、mrb_closure_new で処理をしています。mrb_closure_new の第二引数には、ブロック手続きが渡されます。上の場合ですと irep 2 を渡すわけです。
⇒ mruby/src/vm.c
1403 CASE(OP_LAMBDA) { 1404 /* A b c R(A) := lambda(SEQ[b],c) (b:c = 14:2) */ 1405 struct RProc *p; 1406 int c = GETARG_c(i); 1407 1408 if (c & OP_L_CAPTURE) { 1409 p = mrb_closure_new(mrb, mrb->irep[irep->idx+GETARG_b(i)]); 1410 } 1411 else { 1412 p = mrb_proc_new(mrb, mrb->irep[irep->idx+GETARG_b(i)]); 1413 } 1414 if (c & OP_L_STRICT) p->flags |= MRB_PROC_STRICT; 1415 regs[GETARG_A(i)] = mrb_obj_value(p); 1416 NEXT; 1417 }
mrb_closure_new は proc.c に定義があります。
⇒ mruby/src/proc.c
26 struct RProc * 27 mrb_closure_new(mrb_state *mrb, mrb_irep *irep) 28 { 29 struct RProc *p = mrb_proc_new(mrb, irep); 30 struct REnv *e; 31 32 if (!mrb->ci->env) { 33 e = mrb_obj_alloc(mrb, MRB_TT_ENV, (struct RClass *) mrb->ci->proc->env); 34 e->flags= (unsigned int)irep->nlocals; 35 e->mid = mrb->ci->mid; 36 e->cioff = mrb->ci - mrb->cibase; 37 e->stack = mrb->stack; 38 mrb->ci->env = e; 39 } 40 else { 41 e = mrb->ci->env; 42 } 43 p->env = e; 44 return p; 45 }
29行で生成した手続きオブジェクト p の env メンバを、33 行で生成した MRB_TT_ENV のタグがついたオブジェクトへ、43行で束縛しています。この env メンバを束縛しているオブジェクトがクロージャに結びついた環境になります。ですが、その中には、この段階ではまだ何もとりこまれていません。その代わりに、現在の環境の mrb->ci->env メンバを環境オブジェクトに束縛しています。
コードを追っていくと、生成した環境オブジェクトへの書き出しは OP_RETURN がおこなっています。
⇒ mruby/src/vm.c
945 L_RETURN: 946 if (mrb->ci->env) { 947 struct REnv *e = mrb->ci->env; 948 int len = (int)e->flags; 949 mrb_value *p = mrb_malloc(mrb, sizeof(mrb_value)*len); 950 951 e->cioff = -1; 952 memcpy(p, e->stack, sizeof(mrb_value)*len); 953 e->stack = p; 954 }
ここが書き出し部分です。なのですけど、不思議に感じるのは、952 行でスタックから書き出されるオブジェクト数は、このスタック中のローカル変数の個数ではなくて、proc.c の 34 行でセットしたブロックのローカル変数の個数になっていることです。この時点ではブロックはまだ実行されていないので当然スタック上にはブロックのローカル変数はありません。
当初は、コンパイル時にブロックの irep の nlocals はクロージャを生成する環境のローカル変数の個数をセットするのかと勘違いしてコード生成部のおかしな部分を探していたのですが、コンパイル結果を眺めていると、ブロック自身のローカル変数の個数が常に正しく nlocals に反映していることがだんだんわかってきました。つまり、コード生成部は正常なのだろうと判断せざるをえませんでした。
となると、おかしいのは、proc.c の 34 行ということになります。ここはクロージャにしたいブロックの nlocals ではなくて、現在スタックを使用中 irep の nlocals でなければなりません。うまくいかないのは、クロージャを作るメソッドのローカル変数の個数が、ブロックのローカル変数の個数よりも多い場合です。そのようなソースを書いて動かしてみると、セグメンテーションエラーが生じることも確認できました。では、それはどうやって取得できるのかだろうかと、vm.c を眺めていたら、OP_SEND のところに次のようなコードが入っていることに気がつきました。
⇒ mruby/src/vm.c
681 /* setup environment for calling method */ 682 proc = mrb->ci->proc = m; 683 irep = m->body.irep;
これが意味するところは、現在実行中の irep は mrb->ci->proc->body.irep で求めることができるということです。試しに、gdb で proc.c の 34 行にブレークポイントを置いて、このやりかたで nlocals を取得できるのかどうかチェックしてみたら大丈夫でした。これで、現在実行中の手続きの nlocals を取得する方法がわかりましたので、proc.c を書き直してみました。
#if 0
e->flags= (unsigned int)irep->nlocals;
#else
e->flags= (unsigned int)mrb->ci->proc->body.irep->nlocals;
#endif
動かなかった引数なしブロックからクロージャを作る場合も、この修正で動くようになりました。ということで、問題は当初考えていた「ひょっとして、ゼロ個の引数のときはうまくいかないのかもしれない」というのは勘違いで、もっと根本的なところがおかしかったという結論に辿り着きました。
懸念事項として、環境を書き出される proc の body が VM コードなら、これでいいのですが、クロージャへ環境を書き出される元が C 言語で記述されたメソッドの場合にどうなるのだろうかと疑問は残ります。そこは実装の仕様にかかわるところで、預かりしれないため、とりあえず、上の修正で pull request しておきました。
5月4日に mruby/mruby master に上のパッチがマージされました。私の懸念事項は考え過ぎだったのかもしれません。
mruby のインタープリタ Reit VM をじっくりと読んでいる最中です。
Riet VM のオブジェクト・コードの逆アセンブルリストは、bin/mruby に verbose オプションをつけると標準出力にパーサ出力の構文木出力に続いて表示されます。既存のオブジェクト・ファイルの逆アセンブルリストも、それで表示できます。最適化処理がないため、構文木からどのようにコード生成するのか、出力眺めるだけで手にとるようにわかりやすいです。
$ bin/mruby --verbose prime.rb
構文木は LISP の cons を使った連結リストになっていて、car、cdr がそのままソース中に記述されているので、LISP 慣れしている自分にとってはすごく src/codegen.c が読みやすくなっててうれしいことです。できれば、verbose の出力も括弧リストで表示してくれたら、Emacs の lisp-mode で追いかけやすくなったのにと残念でした。
コード読みには、GNU global の htags(1) で HTML を生成したものを使ってます。git リポジトリにタグファイルを作るのは避けて、アーカイブで /tmp にエクスポートし、そこでタグデータベースを作って HTML に変換しました。
$ cd mruby $ mkdir /tmp/mruby $ git archive --format=tar HEAD | (cd /tmp/mruby; tar xf -) $ pushd /tmp/mruby $ gtags $ htags -sanoTt 'mruby' $ mv HTML mruby $ tar cf - mruby | (cd $HOME/www; tar xf -) $ cd .. $ rm -fr mruby $ popd $ chromium $HOME/www/mruby/index.html
以下、読みながらの雑感です。読み取りミスで嘘を書いている可能性が高いので、その辺、斟酌してください。
lua_State * に対して mrb_state *。これはオマージュというものなのか……。mrb_get_args の書式
| 書式記号 | ポインタ | 意味 |
|---|---|---|
| o | mrb_value * | オブジェクト参照 |
| i | mrb_int * | 整数 int32_t |
| f | mrb_float * | 浮動小数点数 double |
| s | char**, size_t* | 文字列 |
| a | mrb_value ** | 配列 |
| b | mrb_value* | 引数渡しのProcインスタンス |
| & | mrb_value* | ブロック、Ruby の &proc相当 |
* | mrb_value**, size_t* | Ruby の *args相当 |