mruby には特殊変数である $!
が実装されていません。
また rescue
/ ensure
中に C 空間からエラーを直接取得する、CRuby にある rb_errinfo()
関数のようなものもありません。
mrb->exc
が利用できるだろうと考えたこともありますが、rescue
/ ensure
ブロックの中では仮想機械のスタックやマシンスタックに一時保存されて mrb->exc
は空っぽになってしまいます。
どのようにしたら実装できるかについて頭の中で考えていることを書き留めてみようと思います。
なおあくまでも脳内妄想であり、実際に試したわけでも現在取り組んでいるわけでもないことを明言しておきます。
やること① 特殊変数の仕組みを使えるようにする
まず、mruby には特殊変数のための仕組みそのものが実装されていません。
どういう事かと言うと、$!
どころか $0
だったり $~
だったりがまるっきり機能しません。
bin/mruby
でも $0
が使えるでしょと思ったそこのあなた、mruby だとそれはただの大域変数です。
このことは $0
に文字列を代入してみても、ps
コマンドでの表示が変更されないことを意味しています。
mruby-3.0.0 + mruby-process で実行してみるとこんな感じ。
% bin/mruby -e '$0 = "進行状況: 永遠の99%"; system("ps #{$$}")'
PID TT STAT TIME COMMAND
93979 23 S+ 0:00.01 bin/mruby -e $0 = "進行状況: 永遠の99%"; system("ps #{$$}")
Ruby-3.1 preview1 で試すと表示が変更されているのが確認できます。
% ruby31 -e '$0 = "進行状況: 永遠の99%"; system("ps #{$$}")'
PID TT STAT TIME COMMAND
94872 23 S+ 0:00.15 ruby31: 進行状況: 永遠の99% (ruby31)
mruby では alias
も出来ません。
% bin/mruby -e 'alias $ERROR_INFO $!'
-e:1:17: syntax error, unexpected global variable
なのでこのあたりの仕組みを丸ごと追加する必要があります。
ちなみに仮想機械の命令は存在こそしていますが、その先の実装がないため機能することはありません。
もっとも、構文解析で大域変数として処理されるので、命令自体が生成されることもありませぬ。
やること② エラー情報を保存・取得出来るようにする
すでに示してきましたが CRuby であれば、発生した例外は rescue
や ensure
内部で $!
を使うと取得できるようになっているし、C 空間からは直接 rb_errinfo()
で取得可能です。
mruby (3.0.0) では仮想機械のレジスタ上に例外オブジェクトが保存されますが、実行中にこれを取り出せません。
理由はどのレジスタに例外オブジェクトが置かれているかの情報がないためです。
なので取り出せるようにしなければなりません。
rescue/ensure
ブロックごとに例外オブジェクトを保存するレジスタはコンパイル時に決定されます。
この時の mruby-compiler はレジスタ番号と、rescue/ensure
ブロックの区間を把握しています。
そのため、このあたりの情報を irep に保存しておくことで解決できそうです。
それでどのように保存させるかについての問題が出てきます。
これについては例外ハンドラを利用できるだろうと考えています。
mruby-3.0.0 では例外・大域ジャンプが起きた場合の例外ハンドラ (rescue/ensure
ブロック) をコンパイル時に irep へと組み込んでおくようになっているので、これを拡張しましょう。
0 +1: catch handler type
1 +4: catch handler begin
5 +4: catch handler end
9 +4: catch handler target (rescue/ensureブロックの開始位置)
13 +4: catch handler finish <<< NEW! (rescue/ensureブロックの終了位置)
17 +2: error registor index <<< NEW!
19 ..: <END>
まあ見て分かるようにハンドラ一つあたり6バイト増えることになります。
RAM どころか ROM も少しでも減らしたい場合、これを削減するために mrb_irep::flags |= MRB_IREP_NO_ERRINFO
みたいなものを追加してもいいかもしれません。
ここまで見てきたのは Ruby 空間での rescue/ensure
ブロック内でのことです。
C 空間のことを考えた場合、C での mrb_rescue()
や mrb_ensure()
経由で使うためにこの例外ハンドラテーブルを利用できないことに気がつくでしょう。
ある時、構造体 mrb_callinfo
を使う方法を思いつきました。CINFO_ERRINFO
(仮) を導入します。
mrb_callinfo *ci = mrb->c->ci;
ci->cci = CINFO_ERRINFO; // <<< NEW!
ci->stack[0] = exception_object;
mrb_exc_keep(mrb, exc, userfunc, userdata)
関数 (仮) を追加して使えるようにすれば良さそうです。
userfunc()
が呼ばれた内部では、この時に保存された例外オブジェクトが見えるはずです。
副作用は、mrb_get_args()
関数は ci->cci == CINFO_ERRINFO
なフレームを上へ辿るようにする必要が出てくるし、他にも影響が出てくるでしょう。
何はともあれ、これでとりあえずはエラー情報の保存と取得ができそうです。
やること③ エラー情報を探せるようにする
実際にエラー情報を探索する場合、呼び出し情報 (callinfo; ci) を呼び出し元に辿っていきながら確認することになります。
ci->cci == CINFO_NONE
あるいは ci->cci == CINFO_SKIP
であれば、現在実行中の pc が例外ハンドラテーブルの rescue/ensure
ブロックの区間と一致しているかどうかを見ていきます。
一致すれば示されたレジスタの値が探し求めたエラー情報です。
ci->cci == CINFO_ERRINFO
であれば、ci->stack[0]
がエラー情報です。
ci を一番大本まで辿って何もなければエラー情報は nil
となります。
探索処理は結構重そうですが、そもそも頻繁に使うものではないので許容できると考えています。
最後に
これが実現できると、お試しコードで役に立つ rescue
修飾子で p $!
と書けます。
内部でエラーが起きるメソッド rescue p $!
また、引数なしの raise
で例外の再送が出来るようになります。
begin
ほげ
rescue
ふが
raise
end
ただし最初に述べたように、僕は実際の作業を行っていません。
すぐに使いたい人は是非とも挑戦して下さい。
別の方法だとしても歓迎します! 僕もたまに使いたくなるので!