mrubyでエラー発生時にバックトレースを表示する

2013-09-21

mrubyのスクリプト内でエラーが起きたときにエラーの起きた行番号が表示されなくて不便だなーと思っていたのだけど、出し方がわかったのでメモ。

文字列を実行するmrb_load_string()や、ファイルから読み込んで実行するmrb_load_file()を使うとエラー情報が生成されない。自分でコンテキストを生成して、さらにファイル名を指定してやることでエラー情報やバックトレースが表示される。

#include "mruby.h"
#include "mruby/array.h"
#include "mruby/compile.h"
#include "mruby/string.h"

void dump_error(mrb_state *mrb) {
mrb_print_error(mrb);
mrb->exc = 0;
}

void dostring(mrb_state *mrb, const char* str) {
int arena = mrb_gc_arena_save(mrb);
mrbc_context *cxt = mrbc_context_new(mrb);
mrbc_filename(mrb, cxt, "*interactive*");
mrb_load_string_cxt(mrb, str, cxt);
mrbc_context_free(mrb, cxt);
if (mrb->exc)
dump_error(mrb);
mrb_gc_arena_restore(mrb, arena);
}

void dofile(mrb_state *mrb, const char* filename) {
FILE* fp = fopen(filename, "r");
if (fp == NULL) {
fprintf(stderr, "Cannot open %s\n", filename);
return;
}

int arena = mrb_gc_arena_save(mrb);
mrbc_context *cxt = mrbc_context_new(mrb);
mrbc_filename(mrb, cxt, filename);
mrb_load_file_cxt(mrb, fp, cxt);
mrbc_context_free(mrb, cxt);
fclose(fp);
if (mrb->exc)
dump_error(mrb);
mrb_gc_arena_restore(mrb, arena);
}

void dofuncall(mrb_state *mrb, const char* funcname) {
int arena = mrb_gc_arena_save(mrb);
mrb_funcall(mrb, mrb_top_self(mrb), funcname, 0);
if (mrb->exc)
dump_error(mrb);
mrb_gc_arena_restore(mrb, arena);
}

#include <assert.h>
int main() {
mrb_state *mrb = mrb_open();
dostring(mrb,
"def func; yield; end\n"
"func { a = b }");
dofuncall(mrb, "func");
dofile(mrb, "foo.rb");
assert(mrb->arena_idx == 0);
mrb_close(mrb);
return 0;
}

実行結果:

trace:
[2] *interactive*:2:in Object.call
[1] *interactive*:1:in Object.func
[0] *interactive*:2
*interactive*:2: undefined method 'b' for main (NoMethodError)

Object.funcなどとなってしまうところが残念だな…。

注意点として、エラーが発生したときには一時オブジェクトをGCから守るためのarenaが変更されたまま戻ってきてしまい、これを続けているとRuntimeError : arena overflow errorが発生するので、C側からスクリプトを呼び出す時には必ずsave~`restore`してやる必要がある。

そうした場合には、例えば上記のdoXXXX()関数でスクリプトの実行結果の戻り値を返そうとしても、arenaを戻してしまっているのでその値が有効である保証がなくなるのでそういうことはできない。戻り値を参照するのであればarenaを戻す前にしないといけない。

DISABLE_STDIOで標準入出力をオフにしている場合は、mrb_get_backtrace()を呼び出すとバックトレースの情報がmrubyの配列として取得できるので、これを使って自前で出力すればよい:

extern "C" mrb_value mrb_get_backtrace(mrb_state*, mrb_value);

void show_backtrace(mrb_state *mrb) {
mrb_value exc = mrb_obj_value(mrb->exc);
mrb_value backtrace = mrb_get_backtrace(mrb, exc);
mrb_value s = mrb_funcall(mrb, exc, "inspect", 0);
fprintf(stderr, "%s\n", RSTRING_PTR(s));
for (mrb_int n = mrb_ary_len(mrb, backtrace), i = 0; i < n; ++i) {
mrb_value v = mrb_ary_ref(mrb, backtrace, i);
fprintf(stderr, "\t%s\n", RSTRING_PTR(v));
}
}

リリース時にエラー情報がいらない、という場合にはコンテキストにNULLを渡してやるか、コンテキストなし版の関数を呼び出してやればよい。